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.
@@ -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