vectra-client 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 316a75b282cab4d293dfdb3ab9f7b2220a58078008a6887e7e47e7caf6172211
4
- data.tar.gz: d4a9c10c5e6862194e7fb028c254b19d63b0d2fb6fa5ef5f45b59b6a9da1a317
3
+ metadata.gz: '05778206f66d4e2ead830f5b7c5885f6336b46d36af696f3df5a56eb26163c5f'
4
+ data.tar.gz: 1ae5814cd10df006ecf5568f34e457400b072ecc8a06dbf85a4e4f5b94120fb9
5
5
  SHA512:
6
- metadata.gz: ca06a9e2a961f6130aaa06cf42caafd900ef9fb38dc902e42c3bccb649962abe3d3cd64a50af84605df273682013f0ae90476c5096f4a3efee1ac67d3d1dc672
7
- data.tar.gz: e1fb369da70479f0b3505be4c15d1b6ee7089d0f6c14bb6b58d1ae02c15b9cbb92bfb448e88697726a769d01d5ac7b9569f6085c04ee420e39e55b2ee754013c
6
+ metadata.gz: d25889a4626c9caa9edf7ccc285c849799de87cf38a8a7a6f56a49edb46da592b28bc0232960e2612fd0d9199e990c21bd5fe6b88f15351d8ff38c73bd7afc84
7
+ data.tar.gz: d4ef355089814ed4caf8c61e216b9554f23035df12be72a65c778b3230bd2a32f16ecf4c50de3bc33e40dc323f1aee83d7a891ad962bec105585c8bc6b30902b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.0.8](https://github.com/stokry/vectra/tree/v1.0.8) (2026-01-14)
4
+
5
+ [Full Changelog](https://github.com/stokry/vectra/compare/v1.0.7...v1.0.8)
6
+
3
7
  ## [v1.0.7](https://github.com/stokry/vectra/tree/v1.0.7) (2026-01-14)
4
8
 
5
9
  [Full Changelog](https://github.com/stokry/vectra/compare/v1.0.6...v1.0.7)
@@ -34,6 +34,21 @@ client = Vectra.pgvector(connection_url: ENV['DATABASE_URL'])
34
34
  client = Vectra.memory # In-memory (testing only)
35
35
  ```
36
36
 
37
+ You can also set a **default index and namespace**:
38
+
39
+ ```ruby
40
+ client = Vectra::Client.new(
41
+ provider: :qdrant,
42
+ host: 'http://localhost:6333',
43
+ index: 'products',
44
+ namespace: 'tenant-1'
45
+ )
46
+
47
+ # Now index and namespace can be omitted
48
+ client.upsert(vectors: [...])
49
+ client.query(vector: query_embedding, top_k: 10)
50
+ ```
51
+
37
52
  ### Upsert
38
53
 
39
54
  ```ruby
@@ -225,6 +240,22 @@ vector.normalize! # Mutates values
225
240
  client.upsert(index: 'documents', vectors: [vector])
226
241
  ```
227
242
 
243
+ ### Embedding Cache Helper
244
+
245
+ ```ruby
246
+ cache = Vectra::Cache.new(ttl: 600, max_size: 1000)
247
+
248
+ embedding = Vectra::Embeddings.fetch(
249
+ cache: cache,
250
+ model_name: "Product",
251
+ id: product.id,
252
+ input: product.description,
253
+ field: :description
254
+ ) do
255
+ EmbeddingService.generate(product.description)
256
+ end
257
+ ```
258
+
228
259
  ---
229
260
 
230
261
  ## Batch Operations
@@ -318,6 +349,18 @@ results.each do |doc|
318
349
  end
319
350
  ```
320
351
 
352
+ ### Reindex All Records
353
+
354
+ ```ruby
355
+ # Reindex all documents that already have embeddings
356
+ processed = Document.reindex_vectors(
357
+ scope: Document.where.not(embedding: nil),
358
+ batch_size: 500
359
+ )
360
+
361
+ puts "Reindexed #{processed} documents"
362
+ ```
363
+
321
364
  ---
322
365
 
323
366
  ## Error Handling
data/docs/api/methods.md CHANGED
@@ -43,7 +43,7 @@ client = Vectra::Client.new(
43
43
  Upsert vectors into an index. If a vector with the same ID exists, it will be updated.
44
44
 
45
45
  **Parameters:**
46
- - `index` (String) - Index/collection name
46
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
47
47
  - `vectors` (Array<Hash, Vector>) - Array of vector hashes or Vector objects
48
48
  - `namespace` (String, optional) - Namespace
49
49
 
@@ -77,7 +77,7 @@ result = client.upsert(
77
77
  Search for similar vectors using cosine similarity.
78
78
 
79
79
  **Parameters:**
80
- - `index` (String) - Index/collection name
80
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
81
81
  - `vector` (Array<Float>) - Query vector
82
82
  - `top_k` (Integer) - Number of results (default: 10)
83
83
  - `namespace` (String, optional) - Namespace
@@ -152,7 +152,7 @@ results = client.hybrid_search(
152
152
  Fetch vectors by their IDs.
153
153
 
154
154
  **Parameters:**
155
- - `index` (String) - Index/collection name
155
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
156
156
  - `ids` (Array<String>) - Array of vector IDs
157
157
  - `namespace` (String, optional) - Namespace
158
158
 
@@ -176,7 +176,7 @@ vectors['doc-1'].metadata # => { 'title' => 'Hello' }
176
176
  Update a vector's metadata or values.
177
177
 
178
178
  **Parameters:**
179
- - `index` (String) - Index/collection name
179
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
180
180
  - `id` (String) - Vector ID
181
181
  - `metadata` (Hash, optional) - New metadata (merged with existing)
182
182
  - `values` (Array<Float>, optional) - New vector values
@@ -202,7 +202,7 @@ client.update(
202
202
  Delete vectors.
203
203
 
204
204
  **Parameters:**
205
- - `index` (String) - Index/collection name
205
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
206
206
  - `ids` (Array<String>, optional) - Vector IDs to delete
207
207
  - `namespace` (String, optional) - Namespace
208
208
  - `filter` (Hash, optional) - Delete by metadata filter
@@ -231,7 +231,7 @@ client.delete(index: 'documents', delete_all: true)
231
231
  Get index statistics.
232
232
 
233
233
  **Parameters:**
234
- - `index` (String) - Index/collection name
234
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
235
235
  - `namespace` (String, optional) - Namespace
236
236
 
237
237
  **Returns:** `Hash` with statistics:
@@ -571,6 +571,30 @@ end
571
571
 
572
572
  ---
573
573
 
574
+ ### `Model.reindex_vectors(scope: all, batch_size: 1000, on_progress: nil)`
575
+
576
+ Reindex all records for a model into the configured vector index.
577
+
578
+ **Parameters:**
579
+ - `scope` (ActiveRecord::Relation) - Records to reindex (default: `Model.all`)
580
+ - `batch_size` (Integer) - Number of records per batch (default: 1000)
581
+ - `on_progress` (Proc, optional) - Progress callback, receives a hash with `:processed` and `:total`
582
+
583
+ **Returns:** `Integer` - Number of records processed
584
+
585
+ **Example:**
586
+ ```ruby
587
+ # Reindex all products with embeddings
588
+ processed = Product.reindex_vectors(
589
+ scope: Product.where.not(embedding: nil),
590
+ batch_size: 500
591
+ )
592
+
593
+ puts "Reindexed #{processed} products"
594
+ ```
595
+
596
+ ---
597
+
574
598
  ## Error Handling
575
599
 
576
600
  Vectra defines specific error types:
@@ -2,8 +2,9 @@
2
2
 
3
3
  require "active_support/concern"
4
4
 
5
- # Ensure Client and Providers are loaded (for Rails autoloading compatibility)
5
+ # Ensure Client and supporting classes are loaded (for Rails autoloading compatibility)
6
6
  require_relative "client" unless defined?(Vectra::Client)
7
+ require_relative "batch" unless defined?(Vectra::Batch)
7
8
 
8
9
  module Vectra
9
10
  # ActiveRecord integration for vector embeddings
@@ -26,6 +27,7 @@ module Vectra
26
27
  # # Search similar documents
27
28
  # results = Document.vector_search([0.1, 0.2, ...], limit: 10)
28
29
  #
30
+ # rubocop:disable Metrics/ModuleLength
29
31
  module ActiveRecord
30
32
  extend ActiveSupport::Concern
31
33
 
@@ -86,6 +88,54 @@ module Vectra
86
88
  end
87
89
  end
88
90
 
91
+ # Reindex all vectors for this model using current configuration.
92
+ #
93
+ # @param scope [ActiveRecord::Relation] records to reindex (default: all)
94
+ # @param batch_size [Integer] number of records per batch
95
+ # @param on_progress [Proc, nil] optional callback called after each batch
96
+ # Receives a hash with :processed and :total keys (and any other stats from Batch)
97
+ #
98
+ # @return [Integer] number of records processed
99
+ def reindex_vectors(scope: all, batch_size: 1_000, on_progress: nil)
100
+ config = _vectra_config
101
+ client = vectra_client
102
+ batch = Vectra::Batch.new(client)
103
+
104
+ processed = 0
105
+
106
+ scope.in_batches(of: batch_size).each do |relation|
107
+ records = relation.to_a
108
+
109
+ vectors = records.map do |record|
110
+ vector = record.send(config[:attribute])
111
+ next if vector.nil?
112
+
113
+ metadata = config[:metadata_fields].each_with_object({}) do |field, hash|
114
+ hash[field.to_s] = record.send(field) if record.respond_to?(field)
115
+ end
116
+
117
+ {
118
+ id: "#{config[:index]}_#{record.id}",
119
+ values: vector,
120
+ metadata: metadata
121
+ }
122
+ end.compact
123
+
124
+ next if vectors.empty?
125
+
126
+ batch.upsert_async(
127
+ index: config[:index],
128
+ vectors: vectors,
129
+ namespace: nil,
130
+ on_progress: on_progress
131
+ )
132
+
133
+ processed += vectors.size
134
+ end
135
+
136
+ processed
137
+ end
138
+
89
139
  # Search vectors
90
140
  #
91
141
  # @api private
@@ -195,4 +245,5 @@ module Vectra
195
245
  "#{self.class._vectra_config[:index]}_#{id}"
196
246
  end
197
247
  end
248
+ # rubocop:enable Metrics/ModuleLength
198
249
  end
data/lib/vectra/cache.rb CHANGED
@@ -258,4 +258,53 @@ module Vectra
258
258
  "#{index}:f:#{id}:#{namespace || 'default'}"
259
259
  end
260
260
  end
261
+
262
+ # Helper for caching embeddings based on model, record ID and input text.
263
+ #
264
+ # @example
265
+ # cache = Vectra::Cache.new(ttl: 600, max_size: 1000)
266
+ #
267
+ # embedding = Vectra::Embeddings.fetch(
268
+ # cache: cache,
269
+ # model_name: "Product",
270
+ # id: product.id,
271
+ # input: product.description,
272
+ # field: :description
273
+ # ) do
274
+ # EmbeddingService.generate(product.description)
275
+ # end
276
+ #
277
+ module Embeddings
278
+ module_function
279
+
280
+ # Build a stable cache key for an embedding.
281
+ #
282
+ # @param model_name [String] model class name (e.g. "Product")
283
+ # @param id [Integer, String] record ID
284
+ # @param input [String] raw input used for embedding
285
+ # @param field [Symbol, String, nil] optional field name
286
+ #
287
+ # @return [String] cache key
288
+ def cache_key(model_name:, id:, input:, field: nil)
289
+ field_part = field ? field.to_s : "default"
290
+ base = "#{model_name}:#{field_part}:#{id}:#{input}"
291
+ digest = Digest::SHA256.hexdigest(base)[0, 32]
292
+ "emb:#{model_name}:#{field_part}:#{digest}"
293
+ end
294
+
295
+ # Fetch an embedding from cache or compute and store it.
296
+ #
297
+ # @param cache [Vectra::Cache] cache instance
298
+ # @param model_name [String] model class name
299
+ # @param id [Integer, String] record ID
300
+ # @param input [String] input used for embedding
301
+ # @param field [Symbol, String, nil] optional field name
302
+ #
303
+ # @yield block that computes the embedding when not cached
304
+ # @return [Object] cached or computed embedding
305
+ def fetch(cache:, model_name:, id:, input:, field: nil, &block)
306
+ key = cache_key(model_name: model_name, id: id, input: input, field: field)
307
+ cache.fetch(key, &block)
308
+ end
309
+ end
261
310
  end
data/lib/vectra/client.rb CHANGED
@@ -40,7 +40,7 @@ module Vectra
40
40
  class Client
41
41
  include Vectra::HealthCheck
42
42
 
43
- attr_reader :config, :provider
43
+ attr_reader :config, :provider, :default_index, :default_namespace
44
44
 
45
45
  # Initialize a new Client
46
46
  #
@@ -49,17 +49,21 @@ module Vectra
49
49
  # @param environment [String, nil] environment/region
50
50
  # @param host [String, nil] custom host URL
51
51
  # @param options [Hash] additional options
52
+ # @option options [String] :index default index name
53
+ # @option options [String] :namespace default namespace
52
54
  def initialize(provider: nil, api_key: nil, environment: nil, host: nil, **options)
53
55
  @config = build_config(provider, api_key, environment, host, options)
54
56
  @config.validate!
55
57
  @provider = build_provider
58
+ @default_index = options[:index]
59
+ @default_namespace = options[:namespace]
56
60
  end
57
61
 
58
62
  # Upsert vectors into an index
59
63
  #
60
- # @param index [String] the index/collection name
61
64
  # @param vectors [Array<Hash, Vector>] vectors to upsert
62
- # @param namespace [String, nil] optional namespace (provider-specific)
65
+ # @param index [String, nil] the index/collection name (falls back to client's default)
66
+ # @param namespace [String, nil] optional namespace (provider-specific, falls back to client's default)
63
67
  # @return [Hash] upsert response with :upserted_count
64
68
  #
65
69
  # @example Upsert vectors
@@ -71,7 +75,9 @@ module Vectra
71
75
  # ]
72
76
  # )
73
77
  #
74
- def upsert(index:, vectors:, namespace: nil)
78
+ def upsert(vectors:, index: nil, namespace: nil)
79
+ index ||= default_index
80
+ namespace ||= default_namespace
75
81
  validate_index!(index)
76
82
  validate_vectors!(vectors)
77
83
 
@@ -130,6 +136,10 @@ module Vectra
130
136
  # Handle positional argument for index in non-builder case
131
137
  index = index_arg if index_arg && index.nil?
132
138
 
139
+ # Fall back to default index/namespace when not provided
140
+ index ||= default_index
141
+ namespace ||= default_namespace
142
+
133
143
  # Backwards-compatible path: perform query immediately
134
144
  validate_index!(index)
135
145
  validate_query_vector!(vector)
@@ -157,16 +167,18 @@ module Vectra
157
167
 
158
168
  # Fetch vectors by IDs
159
169
  #
160
- # @param index [String] the index/collection name
161
170
  # @param ids [Array<String>] vector IDs to fetch
162
- # @param namespace [String, nil] optional namespace
171
+ # @param index [String, nil] the index/collection name (falls back to client's default)
172
+ # @param namespace [String, nil] optional namespace (falls back to client's default)
163
173
  # @return [Hash<String, Vector>] hash of ID to Vector
164
174
  #
165
175
  # @example Fetch vectors
166
176
  # vectors = client.fetch(index: 'my-index', ids: ['vec1', 'vec2'])
167
177
  # vectors['vec1'].values # => [0.1, 0.2, 0.3]
168
178
  #
169
- def fetch(index:, ids:, namespace: nil)
179
+ def fetch(ids:, index: nil, namespace: nil)
180
+ index ||= default_index
181
+ namespace ||= default_namespace
170
182
  validate_index!(index)
171
183
  validate_ids!(ids)
172
184
 
@@ -182,8 +194,8 @@ module Vectra
182
194
 
183
195
  # Update a vector's metadata or values
184
196
  #
185
- # @param index [String] the index/collection name
186
197
  # @param id [String] vector ID
198
+ # @param index [String, nil] the index/collection name (falls back to client's default)
187
199
  # @param metadata [Hash, nil] new metadata (merged with existing)
188
200
  # @param values [Array<Float>, nil] new vector values
189
201
  # @param namespace [String, nil] optional namespace
@@ -196,7 +208,9 @@ module Vectra
196
208
  # metadata: { category: 'updated' }
197
209
  # )
198
210
  #
199
- def update(index:, id:, metadata: nil, values: nil, namespace: nil)
211
+ def update(id:, index: nil, metadata: nil, values: nil, namespace: nil)
212
+ index ||= default_index
213
+ namespace ||= default_namespace
200
214
  validate_index!(index)
201
215
  validate_id!(id)
202
216
 
@@ -236,7 +250,9 @@ module Vectra
236
250
  # @example Delete all
237
251
  # client.delete(index: 'my-index', delete_all: true)
238
252
  #
239
- def delete(index:, ids: nil, namespace: nil, filter: nil, delete_all: false)
253
+ def delete(index: nil, ids: nil, namespace: nil, filter: nil, delete_all: false)
254
+ index ||= default_index
255
+ namespace ||= default_namespace
240
256
  validate_index!(index)
241
257
 
242
258
  if ids.nil? && filter.nil? && !delete_all
@@ -280,7 +296,8 @@ module Vectra
280
296
  # info = client.describe_index(index: 'my-index')
281
297
  # puts info[:dimension]
282
298
  #
283
- def describe_index(index:)
299
+ def describe_index(index: nil)
300
+ index ||= default_index
284
301
  validate_index!(index)
285
302
  provider.describe_index(index: index)
286
303
  end
@@ -295,7 +312,9 @@ module Vectra
295
312
  # stats = client.stats(index: 'my-index')
296
313
  # puts "Total vectors: #{stats[:total_vector_count]}"
297
314
  #
298
- def stats(index:, namespace: nil)
315
+ def stats(index: nil, namespace: nil)
316
+ index ||= default_index
317
+ namespace ||= default_namespace
299
318
  validate_index!(index)
300
319
  provider.stats(index: index, namespace: namespace)
301
320
  end
@@ -359,7 +378,8 @@ module Vectra
359
378
  # namespaces = client.list_namespaces(index: 'documents')
360
379
  # namespaces.each { |ns| puts "Namespace: #{ns}" }
361
380
  #
362
- def list_namespaces(index:)
381
+ def list_namespaces(index: nil)
382
+ index ||= default_index
363
383
  validate_index!(index)
364
384
  stats_data = provider.stats(index: index)
365
385
  namespaces = stats_data[:namespaces] || {}
@@ -408,6 +428,8 @@ module Vectra
408
428
  #
409
429
  def hybrid_search(index:, vector:, text:, alpha: 0.5, top_k: 10, namespace: nil,
410
430
  filter: nil, include_values: false, include_metadata: true)
431
+ index ||= default_index
432
+ namespace ||= default_namespace
411
433
  validate_index!(index)
412
434
  validate_query_vector!(vector)
413
435
  raise ValidationError, "Text query cannot be nil or empty" if text.nil? || text.empty?
@@ -671,6 +693,48 @@ module Vectra
671
693
  config.logger.debug("[Vectra] #{message}")
672
694
  config.logger.debug("[Vectra] #{data.inspect}") if data
673
695
  end
696
+
697
+ # Temporarily override default index within a block.
698
+ #
699
+ # @param index [String] temporary index name
700
+ # @yield [Client] yields self with overridden index
701
+ # @return [Object] block result
702
+ def with_index(index)
703
+ previous = @default_index
704
+ @default_index = index
705
+ yield self
706
+ ensure
707
+ @default_index = previous
708
+ end
709
+
710
+ # Temporarily override default namespace within a block.
711
+ #
712
+ # @param namespace [String] temporary namespace
713
+ # @yield [Client] yields self with overridden namespace
714
+ # @return [Object] block result
715
+ def with_namespace(namespace)
716
+ previous = @default_namespace
717
+ @default_namespace = namespace
718
+ yield self
719
+ ensure
720
+ @default_namespace = previous
721
+ end
722
+
723
+ # Temporarily override both index and namespace within a block.
724
+ #
725
+ # @param index [String] temporary index name
726
+ # @param namespace [String] temporary namespace
727
+ # @yield [Client] yields self with overridden index and namespace
728
+ # @return [Object] block result
729
+ def with_index_and_namespace(index, namespace)
730
+ with_index(index) do
731
+ with_namespace(namespace) do
732
+ yield self
733
+ end
734
+ end
735
+ end
736
+
737
+ public :with_index, :with_namespace, :with_index_and_namespace
674
738
  end
675
739
  # rubocop:enable Metrics/ClassLength
676
740
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vectra
4
- VERSION = "1.0.7"
4
+ VERSION = "1.0.8"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vectra-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mijo Kristo