vectra-client 1.0.6 → 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 +4 -4
- data/CHANGELOG.md +8 -5
- data/README.md +13 -0
- data/docs/_layouts/default.html +1 -0
- data/docs/_layouts/home.html +45 -0
- data/docs/_layouts/page.html +1 -0
- data/docs/api/cheatsheet.md +387 -0
- data/docs/api/methods.md +632 -0
- data/docs/api/overview.md +1 -1
- data/docs/assets/style.css +50 -1
- data/docs/examples/index.md +0 -1
- data/docs/guides/faq.md +215 -0
- data/docs/guides/recipes.md +612 -0
- data/docs/guides/testing.md +211 -0
- data/docs/providers/selection.md +215 -0
- data/lib/vectra/active_record.rb +52 -1
- data/lib/vectra/batch.rb +69 -0
- data/lib/vectra/cache.rb +49 -0
- data/lib/vectra/client.rb +144 -12
- data/lib/vectra/version.rb +1 -1
- metadata +7 -1
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Recipes & Patterns
|
|
4
|
+
permalink: /guides/recipes/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Recipes & Patterns
|
|
8
|
+
|
|
9
|
+
Real-world patterns and recipes for common use cases with Vectra.
|
|
10
|
+
|
|
11
|
+
## E-Commerce: Semantic Product Search with Filters
|
|
12
|
+
|
|
13
|
+
**Use Case:** Build a product search that understands user intent and supports filtering by category, price, and availability.
|
|
14
|
+
|
|
15
|
+
### Model Setup
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# app/models/product.rb
|
|
19
|
+
class Product < ApplicationRecord
|
|
20
|
+
include Vectra::ActiveRecord
|
|
21
|
+
|
|
22
|
+
has_vector :embedding,
|
|
23
|
+
provider: :qdrant,
|
|
24
|
+
index: 'products',
|
|
25
|
+
dimension: 1536,
|
|
26
|
+
metadata_fields: [:name, :category, :price, :in_stock, :brand]
|
|
27
|
+
|
|
28
|
+
# Scope for filtering
|
|
29
|
+
scope :in_category, ->(cat) { where(category: cat) }
|
|
30
|
+
scope :in_price_range, ->(min, max) { where(price: min..max) }
|
|
31
|
+
scope :available, -> { where(in_stock: true) }
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Search Service
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# app/services/product_search_service.rb
|
|
39
|
+
class ProductSearchService
|
|
40
|
+
def self.search(query_text:, category: nil, min_price: nil, max_price: nil, limit: 10)
|
|
41
|
+
# Generate embedding from query
|
|
42
|
+
query_embedding = EmbeddingService.generate(query_text)
|
|
43
|
+
|
|
44
|
+
# Build filter
|
|
45
|
+
filter = {}
|
|
46
|
+
filter[:category] = category if category.present?
|
|
47
|
+
filter[:price] = { gte: min_price } if min_price
|
|
48
|
+
filter[:price] = { lte: max_price } if max_price
|
|
49
|
+
filter[:in_stock] = true # Always show only available products
|
|
50
|
+
|
|
51
|
+
# Perform vector search
|
|
52
|
+
results = Vectra::Client.new.query(
|
|
53
|
+
index: 'products',
|
|
54
|
+
vector: query_embedding,
|
|
55
|
+
top_k: limit,
|
|
56
|
+
filter: filter
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Map to Product records
|
|
60
|
+
product_ids = results.map(&:id)
|
|
61
|
+
Product.where(id: product_ids)
|
|
62
|
+
.order("array_position(ARRAY[?]::bigint[], id)", product_ids)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Controller
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# app/controllers/products_controller.rb
|
|
71
|
+
class ProductsController < ApplicationController
|
|
72
|
+
def search
|
|
73
|
+
@products = ProductSearchService.search(
|
|
74
|
+
query_text: params[:q],
|
|
75
|
+
category: params[:category],
|
|
76
|
+
min_price: params[:min_price],
|
|
77
|
+
max_price: params[:max_price],
|
|
78
|
+
limit: 20
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Usage
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Search for "wireless headphones under $100"
|
|
88
|
+
ProductSearchService.search(
|
|
89
|
+
query_text: "wireless headphones",
|
|
90
|
+
max_price: 100.0,
|
|
91
|
+
limit: 10
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Key Benefits:**
|
|
96
|
+
- Semantic understanding: "headphones" matches "earbuds", "earphones"
|
|
97
|
+
- Fast filtering with metadata
|
|
98
|
+
- Scales to millions of products
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Blog: Hybrid Search (Semantic + Keyword)
|
|
103
|
+
|
|
104
|
+
**Use Case:** Blog search that combines semantic understanding with exact keyword matching for best results.
|
|
105
|
+
|
|
106
|
+
### Model Setup
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# app/models/article.rb
|
|
110
|
+
class Article < ApplicationRecord
|
|
111
|
+
include Vectra::ActiveRecord
|
|
112
|
+
|
|
113
|
+
has_vector :embedding,
|
|
114
|
+
provider: :pinecone,
|
|
115
|
+
index: 'articles',
|
|
116
|
+
dimension: 1536,
|
|
117
|
+
metadata_fields: [:title, :author, :published_at, :tags]
|
|
118
|
+
|
|
119
|
+
# Text content for keyword search
|
|
120
|
+
def searchable_text
|
|
121
|
+
"#{title} #{body}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Hybrid Search Service
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# app/services/article_search_service.rb
|
|
130
|
+
class ArticleSearchService
|
|
131
|
+
def self.hybrid_search(query:, tags: nil, author: nil, limit: 10, alpha: 0.7)
|
|
132
|
+
client = Vectra::Client.new
|
|
133
|
+
|
|
134
|
+
# Generate embedding
|
|
135
|
+
query_embedding = EmbeddingService.generate(query)
|
|
136
|
+
|
|
137
|
+
# Build filter
|
|
138
|
+
filter = {}
|
|
139
|
+
filter[:tags] = tags if tags.present?
|
|
140
|
+
filter[:author] = author if author.present?
|
|
141
|
+
|
|
142
|
+
# Hybrid search: 70% semantic, 30% keyword
|
|
143
|
+
results = client.hybrid_search(
|
|
144
|
+
index: 'articles',
|
|
145
|
+
vector: query_embedding,
|
|
146
|
+
text: query,
|
|
147
|
+
alpha: alpha, # 0.7 = 70% semantic, 30% keyword
|
|
148
|
+
filter: filter,
|
|
149
|
+
top_k: limit
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Map to Article records
|
|
153
|
+
article_ids = results.map(&:id)
|
|
154
|
+
Article.where(id: article_ids)
|
|
155
|
+
.order("array_position(ARRAY[?]::bigint[], id)", article_ids)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Usage
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# Search with hybrid approach
|
|
164
|
+
ArticleSearchService.hybrid_search(
|
|
165
|
+
query: "ruby on rails performance optimization",
|
|
166
|
+
tags: ["ruby", "performance"],
|
|
167
|
+
alpha: 0.7 # Tune based on your content
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# More keyword-focused (for exact matches)
|
|
171
|
+
ArticleSearchService.hybrid_search(
|
|
172
|
+
query: "ActiveRecord query optimization",
|
|
173
|
+
alpha: 0.3 # 30% semantic, 70% keyword
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Key Benefits:**
|
|
178
|
+
- Catches semantic intent ("performance" → "speed", "optimization")
|
|
179
|
+
- Preserves exact keyword matches ("ActiveRecord" stays exact)
|
|
180
|
+
- Tunable with `alpha` parameter
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Multi-Tenant SaaS: Namespace Isolation
|
|
185
|
+
|
|
186
|
+
**Use Case:** Separate vector data per tenant while using a single index.
|
|
187
|
+
|
|
188
|
+
### Model Setup
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# app/models/document.rb
|
|
192
|
+
class Document < ApplicationRecord
|
|
193
|
+
include Vectra::ActiveRecord
|
|
194
|
+
|
|
195
|
+
belongs_to :tenant
|
|
196
|
+
|
|
197
|
+
has_vector :embedding,
|
|
198
|
+
provider: :qdrant,
|
|
199
|
+
index: 'documents',
|
|
200
|
+
dimension: 1536,
|
|
201
|
+
metadata_fields: [:tenant_id, :title, :category]
|
|
202
|
+
|
|
203
|
+
# Override namespace to use tenant_id
|
|
204
|
+
def vector_namespace
|
|
205
|
+
"tenant_#{tenant_id}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Tenant-Scoped Search
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# app/services/document_search_service.rb
|
|
214
|
+
class DocumentSearchService
|
|
215
|
+
def self.search_for_tenant(tenant:, query:, limit: 10)
|
|
216
|
+
query_embedding = EmbeddingService.generate(query)
|
|
217
|
+
|
|
218
|
+
client = Vectra::Client.new
|
|
219
|
+
results = client.query(
|
|
220
|
+
index: 'documents',
|
|
221
|
+
vector: query_embedding,
|
|
222
|
+
namespace: "tenant_#{tenant.id}",
|
|
223
|
+
top_k: limit,
|
|
224
|
+
filter: { tenant_id: tenant.id } # Double protection
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
document_ids = results.map(&:id)
|
|
228
|
+
tenant.documents.where(id: document_ids)
|
|
229
|
+
.order("array_position(ARRAY[?]::bigint[], id)", document_ids)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Usage
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
tenant = Tenant.find(1)
|
|
238
|
+
DocumentSearchService.search_for_tenant(
|
|
239
|
+
tenant: tenant,
|
|
240
|
+
query: "user documentation",
|
|
241
|
+
limit: 10
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Key Benefits:**
|
|
246
|
+
- Complete tenant isolation
|
|
247
|
+
- Single index, multiple namespaces
|
|
248
|
+
- Efficient resource usage
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## RAG Chatbot: Context Retrieval
|
|
253
|
+
|
|
254
|
+
**Use Case:** Retrieve relevant context chunks for a RAG (Retrieval-Augmented Generation) chatbot.
|
|
255
|
+
|
|
256
|
+
### Chunk Model
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# app/models/document_chunk.rb
|
|
260
|
+
class DocumentChunk < ApplicationRecord
|
|
261
|
+
include Vectra::ActiveRecord
|
|
262
|
+
|
|
263
|
+
belongs_to :document
|
|
264
|
+
|
|
265
|
+
has_vector :embedding,
|
|
266
|
+
provider: :weaviate,
|
|
267
|
+
index: 'document_chunks',
|
|
268
|
+
dimension: 1536,
|
|
269
|
+
metadata_fields: [:document_id, :chunk_index, :source_url]
|
|
270
|
+
|
|
271
|
+
# Store chunk text for context
|
|
272
|
+
def context_text
|
|
273
|
+
content
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### RAG Service
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# app/services/rag_service.rb
|
|
282
|
+
class RAGService
|
|
283
|
+
def self.retrieve_context(query:, document_ids: nil, top_k: 5)
|
|
284
|
+
query_embedding = EmbeddingService.generate(query)
|
|
285
|
+
|
|
286
|
+
client = Vectra::Client.new
|
|
287
|
+
|
|
288
|
+
# Build filter if searching specific documents
|
|
289
|
+
filter = {}
|
|
290
|
+
filter[:document_id] = document_ids if document_ids.present?
|
|
291
|
+
|
|
292
|
+
# Retrieve relevant chunks
|
|
293
|
+
results = client.query(
|
|
294
|
+
index: 'document_chunks',
|
|
295
|
+
vector: query_embedding,
|
|
296
|
+
top_k: top_k,
|
|
297
|
+
filter: filter,
|
|
298
|
+
include_metadata: true
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Build context from chunks
|
|
302
|
+
chunks = DocumentChunk.where(id: results.map(&:id))
|
|
303
|
+
context = chunks.map do |chunk|
|
|
304
|
+
{
|
|
305
|
+
text: chunk.context_text,
|
|
306
|
+
source: chunk.source_url,
|
|
307
|
+
score: results.find { |r| r.id == chunk.id.to_s }&.score
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Combine into single context string
|
|
312
|
+
context.map { |c| c[:text] }.join("\n\n")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def self.generate_response(query:, context:)
|
|
316
|
+
# Use your LLM (OpenAI, Anthropic, etc.)
|
|
317
|
+
# prompt = "Context: #{context}\n\nQuestion: #{query}\n\nAnswer:"
|
|
318
|
+
# LLMClient.complete(prompt)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Usage
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
# Retrieve context for a question
|
|
327
|
+
context = RAGService.retrieve_context(
|
|
328
|
+
query: "How do I configure authentication?",
|
|
329
|
+
document_ids: [1, 2, 3], # Optional: limit to specific docs
|
|
330
|
+
top_k: 5
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Generate response with context
|
|
334
|
+
response = RAGService.generate_response(
|
|
335
|
+
query: "How do I configure authentication?",
|
|
336
|
+
context: context
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Key Benefits:**
|
|
341
|
+
- Retrieves most relevant context chunks
|
|
342
|
+
- Supports document filtering
|
|
343
|
+
- Ready for LLM integration
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Zero-Downtime Provider Migration
|
|
348
|
+
|
|
349
|
+
**Use Case:** Migrate from one provider to another without downtime.
|
|
350
|
+
|
|
351
|
+
### Dual-Write Strategy
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
# app/services/vector_migration_service.rb
|
|
355
|
+
class VectorMigrationService
|
|
356
|
+
def self.migrate_provider(from_provider:, to_provider:, index:)
|
|
357
|
+
from_client = Vectra::Client.new(provider: from_provider)
|
|
358
|
+
to_client = Vectra::Client.new(provider: to_provider)
|
|
359
|
+
|
|
360
|
+
# Create index in new provider
|
|
361
|
+
to_client.provider.create_index(name: index, dimension: 1536)
|
|
362
|
+
|
|
363
|
+
# Batch migrate vectors
|
|
364
|
+
batch_size = 100
|
|
365
|
+
offset = 0
|
|
366
|
+
|
|
367
|
+
loop do
|
|
368
|
+
# Fetch batch from old provider (if supported)
|
|
369
|
+
# For most providers, you'll need to maintain a list of IDs
|
|
370
|
+
vector_ids = get_vector_ids(from_client, index, offset, batch_size)
|
|
371
|
+
break if vector_ids.empty?
|
|
372
|
+
|
|
373
|
+
# Fetch vectors
|
|
374
|
+
vectors = from_client.fetch(index: index, ids: vector_ids)
|
|
375
|
+
|
|
376
|
+
# Write to new provider
|
|
377
|
+
to_client.upsert(
|
|
378
|
+
index: index,
|
|
379
|
+
vectors: vectors.values.map do |v|
|
|
380
|
+
{
|
|
381
|
+
id: v.id,
|
|
382
|
+
values: v.values,
|
|
383
|
+
metadata: v.metadata
|
|
384
|
+
}
|
|
385
|
+
end
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
offset += batch_size
|
|
389
|
+
puts "Migrated #{offset} vectors..."
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
private
|
|
394
|
+
|
|
395
|
+
def self.get_vector_ids(client, index, offset, limit)
|
|
396
|
+
# Provider-specific: maintain your own ID list or use provider's list API
|
|
397
|
+
# This is a simplified example
|
|
398
|
+
[]
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Canary Deployment
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# config/initializers/vectra.rb
|
|
407
|
+
require 'vectra'
|
|
408
|
+
|
|
409
|
+
# Use feature flag for gradual migration
|
|
410
|
+
if ENV['VECTRA_USE_NEW_PROVIDER'] == 'true'
|
|
411
|
+
Vectra.configure do |config|
|
|
412
|
+
config.provider = :pinecone # New provider
|
|
413
|
+
config.api_key = ENV['PINECONE_API_KEY']
|
|
414
|
+
end
|
|
415
|
+
else
|
|
416
|
+
Vectra.configure do |config|
|
|
417
|
+
config.provider = :qdrant # Old provider
|
|
418
|
+
config.host = ENV['QDRANT_HOST']
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Dual-Write During Migration
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
# app/models/concerns/vector_dual_write.rb
|
|
427
|
+
module VectorDualWrite
|
|
428
|
+
extend ActiveSupport::Concern
|
|
429
|
+
|
|
430
|
+
included do
|
|
431
|
+
after_save :write_to_both_providers, if: :should_dual_write?
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
private
|
|
435
|
+
|
|
436
|
+
def write_to_both_providers
|
|
437
|
+
# Write to primary (new provider)
|
|
438
|
+
primary_client.upsert(...)
|
|
439
|
+
|
|
440
|
+
# Also write to secondary (old provider) during migration
|
|
441
|
+
if ENV['VECTRA_DUAL_WRITE'] == 'true'
|
|
442
|
+
secondary_client.upsert(...)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def should_dual_write?
|
|
447
|
+
ENV['VECTRA_DUAL_WRITE'] == 'true'
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Migration Steps:**
|
|
453
|
+
1. Enable dual-write: `VECTRA_DUAL_WRITE=true`
|
|
454
|
+
2. Migrate existing data: `VectorMigrationService.migrate_provider(...)`
|
|
455
|
+
3. Verify new provider works
|
|
456
|
+
4. Switch reads: `VECTRA_USE_NEW_PROVIDER=true`
|
|
457
|
+
5. Disable dual-write after verification
|
|
458
|
+
|
|
459
|
+
**Key Benefits:**
|
|
460
|
+
- Zero downtime
|
|
461
|
+
- Gradual rollout
|
|
462
|
+
- Easy rollback
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Recommendation Engine: Similar Items
|
|
467
|
+
|
|
468
|
+
**Use Case:** Find similar products/articles based on user behavior or item characteristics.
|
|
469
|
+
|
|
470
|
+
### Similarity Service
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# app/services/similarity_service.rb
|
|
474
|
+
class SimilarityService
|
|
475
|
+
def self.find_similar(item:, limit: 10, exclude_ids: [])
|
|
476
|
+
client = Vectra::Client.new
|
|
477
|
+
|
|
478
|
+
# Get item's embedding
|
|
479
|
+
embedding = item.embedding
|
|
480
|
+
return [] unless embedding.present?
|
|
481
|
+
|
|
482
|
+
# Find similar items
|
|
483
|
+
results = client.query(
|
|
484
|
+
index: item.class.table_name,
|
|
485
|
+
vector: embedding,
|
|
486
|
+
top_k: limit + exclude_ids.size, # Get extra to account for exclusions
|
|
487
|
+
filter: { id: { not_in: exclude_ids } } if exclude_ids.any?
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Map to records
|
|
491
|
+
item_ids = results.map(&:id).reject { |id| exclude_ids.include?(id) }
|
|
492
|
+
item.class.where(id: item_ids)
|
|
493
|
+
.order("array_position(ARRAY[?]::bigint[], id)", item_ids)
|
|
494
|
+
.limit(limit)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def self.find_similar_by_user(user:, limit: 10)
|
|
498
|
+
# Get user's average embedding from liked/viewed items
|
|
499
|
+
user_items = user.viewed_items.includes(:embedding)
|
|
500
|
+
embeddings = user_items.map(&:embedding).compact
|
|
501
|
+
|
|
502
|
+
return [] if embeddings.empty?
|
|
503
|
+
|
|
504
|
+
# Average user's preference vector
|
|
505
|
+
avg_embedding = embeddings.transpose.map { |vals| vals.sum / vals.size }
|
|
506
|
+
|
|
507
|
+
# Find similar items
|
|
508
|
+
client = Vectra::Client.new
|
|
509
|
+
results = client.query(
|
|
510
|
+
index: 'products',
|
|
511
|
+
vector: avg_embedding,
|
|
512
|
+
top_k: limit,
|
|
513
|
+
filter: { id: { not_in: user.viewed_item_ids } }
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
Product.where(id: results.map(&:id))
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Usage
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
# Find similar products
|
|
525
|
+
product = Product.find(1)
|
|
526
|
+
SimilarityService.find_similar(
|
|
527
|
+
item: product,
|
|
528
|
+
limit: 5,
|
|
529
|
+
exclude_ids: [product.id]
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Find recommendations based on user behavior
|
|
533
|
+
user = User.find(1)
|
|
534
|
+
SimilarityService.find_similar_by_user(user: user, limit: 10)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Key Benefits:**
|
|
538
|
+
- Personalized recommendations
|
|
539
|
+
- Fast similarity search
|
|
540
|
+
- Scales to millions of items
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Performance Tips
|
|
545
|
+
|
|
546
|
+
### 1. Batch Operations
|
|
547
|
+
|
|
548
|
+
Always use batch operations for multiple vectors:
|
|
549
|
+
|
|
550
|
+
```ruby
|
|
551
|
+
# ❌ Slow: Individual upserts
|
|
552
|
+
vectors.each { |v| client.upsert(vectors: [v]) }
|
|
553
|
+
|
|
554
|
+
# ✅ Fast: Batch upsert
|
|
555
|
+
client.upsert(vectors: vectors)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 2. Normalize Embeddings
|
|
559
|
+
|
|
560
|
+
Normalize embeddings for better cosine similarity:
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
embedding = EmbeddingService.generate(text)
|
|
564
|
+
normalized = Vectra::Vector.normalize(embedding)
|
|
565
|
+
client.upsert(vectors: [{ id: 'doc-1', values: normalized }])
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### 3. Use Metadata Filters
|
|
569
|
+
|
|
570
|
+
Filter in the vector database, not in Ruby:
|
|
571
|
+
|
|
572
|
+
```ruby
|
|
573
|
+
# ❌ Slow: Filter in Ruby
|
|
574
|
+
results = client.query(...)
|
|
575
|
+
filtered = results.select { |r| r.metadata[:category] == 'electronics' }
|
|
576
|
+
|
|
577
|
+
# ✅ Fast: Filter in database
|
|
578
|
+
results = client.query(
|
|
579
|
+
vector: embedding,
|
|
580
|
+
filter: { category: 'electronics' }
|
|
581
|
+
)
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### 4. Connection Pooling
|
|
585
|
+
|
|
586
|
+
For pgvector, use connection pooling:
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
Vectra.configure do |config|
|
|
590
|
+
config.provider = :pgvector
|
|
591
|
+
config.pool_size = 10 # Adjust based on your load
|
|
592
|
+
end
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 5. Caching Frequent Queries
|
|
596
|
+
|
|
597
|
+
Enable caching for repeated queries:
|
|
598
|
+
|
|
599
|
+
```ruby
|
|
600
|
+
Vectra.configure do |config|
|
|
601
|
+
config.cache_enabled = true
|
|
602
|
+
config.cache_ttl = 3600 # 1 hour
|
|
603
|
+
end
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Next Steps
|
|
609
|
+
|
|
610
|
+
- [Rails Integration Guide](/guides/rails-integration/) - Complete Rails setup
|
|
611
|
+
- [Performance Guide](/guides/performance/) - Optimization strategies
|
|
612
|
+
- [API Reference](/api/overview/) - Full API documentation
|