vectra-client 0.2.1 → 0.3.0

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +77 -37
  3. data/CHANGELOG.md +49 -6
  4. data/README.md +52 -393
  5. data/docs/Gemfile +9 -0
  6. data/docs/_config.yml +37 -0
  7. data/docs/_layouts/default.html +14 -0
  8. data/docs/_layouts/home.html +187 -0
  9. data/docs/_layouts/page.html +82 -0
  10. data/docs/_site/api/overview/index.html +145 -0
  11. data/docs/_site/assets/main.css +649 -0
  12. data/docs/_site/assets/main.css.map +1 -0
  13. data/docs/_site/assets/minima-social-icons.svg +33 -0
  14. data/docs/_site/assets/style.css +295 -0
  15. data/docs/_site/community/contributing/index.html +110 -0
  16. data/docs/_site/examples/basic-usage/index.html +117 -0
  17. data/docs/_site/examples/index.html +58 -0
  18. data/docs/_site/feed.xml +1 -0
  19. data/docs/_site/guides/getting-started/index.html +106 -0
  20. data/docs/_site/guides/installation/index.html +82 -0
  21. data/docs/_site/index.html +92 -0
  22. data/docs/_site/providers/index.html +119 -0
  23. data/docs/_site/providers/pgvector/index.html +155 -0
  24. data/docs/_site/providers/pinecone/index.html +121 -0
  25. data/docs/_site/providers/qdrant/index.html +124 -0
  26. data/docs/_site/providers/weaviate/index.html +123 -0
  27. data/docs/_site/robots.txt +1 -0
  28. data/docs/_site/sitemap.xml +39 -0
  29. data/docs/api/overview.md +126 -0
  30. data/docs/assets/style.css +927 -0
  31. data/docs/community/contributing.md +89 -0
  32. data/docs/examples/basic-usage.md +102 -0
  33. data/docs/examples/index.md +54 -0
  34. data/docs/guides/getting-started.md +90 -0
  35. data/docs/guides/installation.md +67 -0
  36. data/docs/guides/performance.md +200 -0
  37. data/docs/index.md +37 -0
  38. data/docs/providers/index.md +81 -0
  39. data/docs/providers/pgvector.md +95 -0
  40. data/docs/providers/pinecone.md +72 -0
  41. data/docs/providers/qdrant.md +73 -0
  42. data/docs/providers/weaviate.md +72 -0
  43. data/lib/vectra/batch.rb +148 -0
  44. data/lib/vectra/cache.rb +261 -0
  45. data/lib/vectra/configuration.rb +6 -1
  46. data/lib/vectra/pool.rb +256 -0
  47. data/lib/vectra/streaming.rb +153 -0
  48. data/lib/vectra/version.rb +1 -1
  49. data/lib/vectra.rb +4 -0
  50. data/netlify.toml +12 -0
  51. metadata +58 -5
  52. data/IMPLEMENTATION_GUIDE.md +0 -686
  53. data/NEW_FEATURES_v0.2.0.md +0 -459
  54. data/RELEASE_CHECKLIST_v0.2.0.md +0 -383
  55. data/USAGE_EXAMPLES.md +0 -787
@@ -0,0 +1,95 @@
1
+ ---
2
+ layout: page
3
+ title: PostgreSQL with pgvector
4
+ permalink: /providers/pgvector/
5
+ ---
6
+
7
+ # PostgreSQL with pgvector Provider
8
+
9
+ [pgvector](https://github.com/pgvector/pgvector) is a PostgreSQL extension for vector data.
10
+
11
+ ## Setup
12
+
13
+ ### Prerequisites
14
+
15
+ ```bash
16
+ # Install PostgreSQL with pgvector extension
17
+ # macOS with Homebrew
18
+ brew install postgresql
19
+
20
+ # Enable pgvector extension
21
+ psql -d your_database -c "CREATE EXTENSION IF NOT EXISTS vector;"
22
+ ```
23
+
24
+ ### Connect with Vectra
25
+
26
+ ```ruby
27
+ client = Vectra::Client.new(
28
+ provider: :pgvector,
29
+ database: 'my_database',
30
+ host: 'localhost',
31
+ port: 5432,
32
+ user: 'postgres',
33
+ password: ENV['DB_PASSWORD']
34
+ )
35
+ ```
36
+
37
+ ## Features
38
+
39
+ - ✅ Upsert vectors
40
+ - ✅ Query/search
41
+ - ✅ Delete vectors
42
+ - ✅ SQL integration
43
+ - ✅ ACID transactions
44
+ - ✅ Complex queries
45
+ - ✅ Rails ActiveRecord integration
46
+
47
+ ## Example
48
+
49
+ ```ruby
50
+ # Initialize client
51
+ client = Vectra::Client.new(
52
+ provider: :pgvector,
53
+ database: 'vectors_db',
54
+ host: 'localhost'
55
+ )
56
+
57
+ # Upsert vectors
58
+ client.upsert(
59
+ vectors: [
60
+ { id: 'doc-1', values: [0.1, 0.2, 0.3], metadata: { title: 'Doc 1' } }
61
+ ]
62
+ )
63
+
64
+ # Search using cosine distance
65
+ results = client.query(vector: [0.1, 0.2, 0.3], top_k: 5)
66
+ ```
67
+
68
+ ## ActiveRecord Integration
69
+
70
+ ```ruby
71
+ class Document < ApplicationRecord
72
+ include Vectra::ActiveRecord
73
+
74
+ vector_search :embedding_vector
75
+ end
76
+
77
+ # Search
78
+ docs = Document.vector_search([0.1, 0.2, 0.3], limit: 10)
79
+ ```
80
+
81
+ ## Configuration Options
82
+
83
+ | Option | Type | Required | Description |
84
+ |--------|------|----------|-------------|
85
+ | `database` | String | Yes | Database name |
86
+ | `host` | String | Yes | PostgreSQL host |
87
+ | `port` | Integer | No | PostgreSQL port (default: 5432) |
88
+ | `user` | String | No | Database user |
89
+ | `password` | String | No | Database password |
90
+ | `schema` | String | No | Database schema |
91
+
92
+ ## Documentation
93
+
94
+ - [pgvector GitHub](https://github.com/pgvector/pgvector)
95
+ - [pgvector Docs](https://github.com/pgvector/pgvector#readme)
@@ -0,0 +1,72 @@
1
+ ---
2
+ layout: page
3
+ title: Pinecone
4
+ permalink: /providers/pinecone/
5
+ ---
6
+
7
+ # Pinecone Provider
8
+
9
+ [Pinecone](https://www.pinecone.io/) is a managed vector database in the cloud.
10
+
11
+ ## Setup
12
+
13
+ 1. Create a Pinecone account at https://www.pinecone.io/
14
+ 2. Create an index and get your API key
15
+ 3. Set up Vectra:
16
+
17
+ ```ruby
18
+ client = Vectra::Client.new(
19
+ provider: :pinecone,
20
+ api_key: ENV['PINECONE_API_KEY'],
21
+ index_name: 'my-index',
22
+ environment: 'us-west-4'
23
+ )
24
+ ```
25
+
26
+ ## Features
27
+
28
+ - ✅ Upsert vectors
29
+ - ✅ Query/search
30
+ - ✅ Delete vectors
31
+ - ✅ Fetch vectors by ID
32
+ - ✅ Index statistics
33
+ - ✅ Metadata filtering
34
+ - ✅ Namespace support
35
+
36
+ ## Example
37
+
38
+ ```ruby
39
+ # Initialize client
40
+ client = Vectra::Client.new(
41
+ provider: :pinecone,
42
+ api_key: ENV['PINECONE_API_KEY'],
43
+ environment: 'us-west-4'
44
+ )
45
+
46
+ # Upsert vectors
47
+ client.upsert(
48
+ vectors: [
49
+ { id: 'doc-1', values: [0.1, 0.2, 0.3], metadata: { title: 'Page 1' } },
50
+ { id: 'doc-2', values: [0.2, 0.3, 0.4], metadata: { title: 'Page 2' } }
51
+ ]
52
+ )
53
+
54
+ # Search
55
+ results = client.query(vector: [0.1, 0.2, 0.3], top_k: 5)
56
+ results.matches.each do |match|
57
+ puts "#{match['id']}: #{match['score']}"
58
+ end
59
+ ```
60
+
61
+ ## Configuration Options
62
+
63
+ | Option | Type | Required | Description |
64
+ |--------|------|----------|-------------|
65
+ | `api_key` | String | Yes | Your Pinecone API key |
66
+ | `environment` | String | Yes | Pinecone environment (e.g., 'us-west-4') |
67
+ | `index_name` | String | No | Index name (if not set globally) |
68
+
69
+ ## Documentation
70
+
71
+ - [Pinecone Docs](https://docs.pinecone.io/)
72
+ - [Pinecone API Reference](https://docs.pinecone.io/reference/api/)
@@ -0,0 +1,73 @@
1
+ ---
2
+ layout: page
3
+ title: Qdrant
4
+ permalink: /providers/qdrant/
5
+ ---
6
+
7
+ # Qdrant Provider
8
+
9
+ [Qdrant](https://qdrant.tech/) is an open-source vector search engine.
10
+
11
+ ## Setup
12
+
13
+ ### Local Installation
14
+
15
+ ```bash
16
+ docker run -p 6333:6333 qdrant/qdrant
17
+ ```
18
+
19
+ ### Connect with Vectra
20
+
21
+ ```ruby
22
+ client = Vectra::Client.new(
23
+ provider: :qdrant,
24
+ host: 'localhost',
25
+ port: 6333,
26
+ collection_name: 'my-collection'
27
+ )
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - ✅ Upsert vectors
33
+ - ✅ Query/search
34
+ - ✅ Delete vectors
35
+ - ✅ Fetch vectors by ID
36
+ - ✅ Collection management
37
+ - ✅ Metadata filtering
38
+ - ✅ Hybrid search
39
+
40
+ ## Example
41
+
42
+ ```ruby
43
+ # Initialize client
44
+ client = Vectra::Client.new(
45
+ provider: :qdrant,
46
+ host: 'localhost',
47
+ port: 6333
48
+ )
49
+
50
+ # Upsert vectors
51
+ client.upsert(
52
+ vectors: [
53
+ { id: 'doc-1', values: [0.1, 0.2, 0.3], metadata: { source: 'web' } }
54
+ ]
55
+ )
56
+
57
+ # Search
58
+ results = client.query(vector: [0.1, 0.2, 0.3], top_k: 10)
59
+ ```
60
+
61
+ ## Configuration Options
62
+
63
+ | Option | Type | Required | Description |
64
+ |--------|------|----------|-------------|
65
+ | `host` | String | Yes | Qdrant host address |
66
+ | `port` | Integer | Yes | Qdrant port (default: 6333) |
67
+ | `collection_name` | String | No | Collection name |
68
+ | `api_key` | String | No | API key if auth is enabled |
69
+
70
+ ## Documentation
71
+
72
+ - [Qdrant Docs](https://qdrant.tech/documentation/)
73
+ - [Qdrant API Reference](https://api.qdrant.tech/)
@@ -0,0 +1,72 @@
1
+ ---
2
+ layout: page
3
+ title: Weaviate
4
+ permalink: /providers/weaviate/
5
+ ---
6
+
7
+ # Weaviate Provider
8
+
9
+ [Weaviate](https://weaviate.io/) is an open-source vector search engine with semantic search capabilities.
10
+
11
+ ## Setup
12
+
13
+ ### Local Installation
14
+
15
+ ```bash
16
+ docker run -p 8080:8080 semitechnologies/weaviate:latest
17
+ ```
18
+
19
+ ### Connect with Vectra
20
+
21
+ ```ruby
22
+ client = Vectra::Client.new(
23
+ provider: :weaviate,
24
+ host: 'localhost',
25
+ port: 8080,
26
+ class_name: 'Document'
27
+ )
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - ✅ Upsert vectors
33
+ - ✅ Query/search
34
+ - ✅ Delete vectors
35
+ - ✅ Class management
36
+ - ✅ Metadata filtering
37
+ - ✅ Semantic search
38
+
39
+ ## Example
40
+
41
+ ```ruby
42
+ # Initialize client
43
+ client = Vectra::Client.new(
44
+ provider: :weaviate,
45
+ host: 'localhost',
46
+ port: 8080
47
+ )
48
+
49
+ # Upsert vectors
50
+ client.upsert(
51
+ vectors: [
52
+ { id: 'doc-1', values: [0.1, 0.2, 0.3], metadata: { category: 'news' } }
53
+ ]
54
+ )
55
+
56
+ # Search
57
+ results = client.query(vector: [0.1, 0.2, 0.3], top_k: 5)
58
+ ```
59
+
60
+ ## Configuration Options
61
+
62
+ | Option | Type | Required | Description |
63
+ |--------|------|----------|-------------|
64
+ | `host` | String | Yes | Weaviate host address |
65
+ | `port` | Integer | Yes | Weaviate port (default: 8080) |
66
+ | `class_name` | String | No | Class name for vectors |
67
+ | `api_key` | String | No | API key if auth is enabled |
68
+
69
+ ## Documentation
70
+
71
+ - [Weaviate Docs](https://weaviate.io/developers)
72
+ - [Weaviate API Reference](https://weaviate.io/developers/weaviate/api/rest)
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Vectra
6
+ # Batch operations with concurrent processing
7
+ #
8
+ # Provides async batch upsert capabilities with configurable concurrency
9
+ # and automatic chunking of large vector sets.
10
+ #
11
+ # @example Async batch upsert
12
+ # batch = Vectra::Batch.new(client, concurrency: 4)
13
+ # result = batch.upsert_async(
14
+ # index: 'my-index',
15
+ # vectors: large_vector_array,
16
+ # chunk_size: 100
17
+ # )
18
+ # puts "Upserted: #{result[:upserted_count]}"
19
+ #
20
+ class Batch
21
+ DEFAULT_CONCURRENCY = 4
22
+ DEFAULT_CHUNK_SIZE = 100
23
+
24
+ attr_reader :client, :concurrency
25
+
26
+ # Initialize a new Batch processor
27
+ #
28
+ # @param client [Client] the Vectra client
29
+ # @param concurrency [Integer] max concurrent requests (default: 4)
30
+ def initialize(client, concurrency: DEFAULT_CONCURRENCY)
31
+ @client = client
32
+ @concurrency = [concurrency, 1].max
33
+ end
34
+
35
+ # Perform async batch upsert with concurrent requests
36
+ #
37
+ # @param index [String] the index name
38
+ # @param vectors [Array<Hash>] vectors to upsert
39
+ # @param namespace [String, nil] optional namespace
40
+ # @param chunk_size [Integer] vectors per chunk (default: 100)
41
+ # @return [Hash] aggregated result with :upserted_count, :chunks, :errors
42
+ def upsert_async(index:, vectors:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE)
43
+ chunks = vectors.each_slice(chunk_size).to_a
44
+ return { upserted_count: 0, chunks: 0, errors: [] } if chunks.empty?
45
+
46
+ results = process_chunks_concurrently(chunks) do |chunk|
47
+ client.upsert(index: index, vectors: chunk, namespace: namespace)
48
+ end
49
+
50
+ aggregate_results(results, vectors.size)
51
+ end
52
+
53
+ # Perform async batch delete with concurrent requests
54
+ #
55
+ # @param index [String] the index name
56
+ # @param ids [Array<String>] IDs to delete
57
+ # @param namespace [String, nil] optional namespace
58
+ # @param chunk_size [Integer] IDs per chunk (default: 100)
59
+ # @return [Hash] aggregated result
60
+ def delete_async(index:, ids:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE)
61
+ chunks = ids.each_slice(chunk_size).to_a
62
+ return { deleted_count: 0, chunks: 0, errors: [] } if chunks.empty?
63
+
64
+ results = process_chunks_concurrently(chunks) do |chunk|
65
+ client.delete(index: index, ids: chunk, namespace: namespace)
66
+ end
67
+
68
+ aggregate_delete_results(results, ids.size)
69
+ end
70
+
71
+ # Perform async batch fetch with concurrent requests
72
+ #
73
+ # @param index [String] the index name
74
+ # @param ids [Array<String>] IDs to fetch
75
+ # @param namespace [String, nil] optional namespace
76
+ # @param chunk_size [Integer] IDs per chunk (default: 100)
77
+ # @return [Hash<String, Vector>] merged results
78
+ def fetch_async(index:, ids:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE)
79
+ chunks = ids.each_slice(chunk_size).to_a
80
+ return {} if chunks.empty?
81
+
82
+ results = process_chunks_concurrently(chunks) do |chunk|
83
+ client.fetch(index: index, ids: chunk, namespace: namespace)
84
+ end
85
+
86
+ merge_fetch_results(results)
87
+ end
88
+
89
+ private
90
+
91
+ def process_chunks_concurrently(chunks)
92
+ pool = Concurrent::FixedThreadPool.new(concurrency)
93
+ futures = []
94
+
95
+ chunks.each_with_index do |chunk, index|
96
+ futures << Concurrent::Future.execute(executor: pool) do
97
+ { index: index, result: yield(chunk), error: nil }
98
+ rescue StandardError => e
99
+ { index: index, result: nil, error: e }
100
+ end
101
+ end
102
+
103
+ # Wait for all futures and collect results
104
+ results = futures.map(&:value)
105
+ pool.shutdown
106
+ pool.wait_for_termination(30)
107
+
108
+ results.sort_by { |r| r[:index] }
109
+ end
110
+
111
+ def aggregate_results(results, total_vectors)
112
+ errors = results.select { |r| r[:error] }.map { |r| r[:error] }
113
+ successful = results.reject { |r| r[:error] }
114
+ upserted = successful.sum { |r| r.dig(:result, :upserted_count) || 0 }
115
+
116
+ {
117
+ upserted_count: upserted,
118
+ total_vectors: total_vectors,
119
+ chunks: results.size,
120
+ successful_chunks: successful.size,
121
+ errors: errors
122
+ }
123
+ end
124
+
125
+ def aggregate_delete_results(results, total_ids)
126
+ errors = results.select { |r| r[:error] }.map { |r| r[:error] }
127
+ successful = results.reject { |r| r[:error] }
128
+
129
+ {
130
+ deleted_count: total_ids - (errors.size * (total_ids / results.size.to_f).ceil),
131
+ total_ids: total_ids,
132
+ chunks: results.size,
133
+ successful_chunks: successful.size,
134
+ errors: errors
135
+ }
136
+ end
137
+
138
+ def merge_fetch_results(results)
139
+ merged = {}
140
+ results.each do |r|
141
+ next if r[:error] || r[:result].nil?
142
+
143
+ merged.merge!(r[:result])
144
+ end
145
+ merged
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Vectra
6
+ # Optional caching layer for frequently queried vectors
7
+ #
8
+ # Provides in-memory caching with TTL support for query results
9
+ # and fetched vectors to reduce database load.
10
+ #
11
+ # @example Basic usage
12
+ # cache = Vectra::Cache.new(ttl: 300, max_size: 1000)
13
+ # cached_client = Vectra::CachedClient.new(client, cache: cache)
14
+ #
15
+ # # First call hits the database
16
+ # result1 = cached_client.query(index: 'idx', vector: vec, top_k: 10)
17
+ #
18
+ # # Second call returns cached result
19
+ # result2 = cached_client.query(index: 'idx', vector: vec, top_k: 10)
20
+ #
21
+ class Cache
22
+ DEFAULT_TTL = 300 # 5 minutes
23
+ DEFAULT_MAX_SIZE = 1000
24
+
25
+ attr_reader :ttl, :max_size
26
+
27
+ # Initialize cache
28
+ #
29
+ # @param ttl [Integer] time-to-live in seconds (default: 300)
30
+ # @param max_size [Integer] maximum cache entries (default: 1000)
31
+ def initialize(ttl: DEFAULT_TTL, max_size: DEFAULT_MAX_SIZE)
32
+ @ttl = ttl
33
+ @max_size = max_size
34
+ @store = {}
35
+ @timestamps = {}
36
+ @mutex = Mutex.new
37
+ end
38
+
39
+ # Get value from cache
40
+ #
41
+ # @param key [String] cache key
42
+ # @return [Object, nil] cached value or nil if not found/expired
43
+ def get(key)
44
+ @mutex.synchronize do
45
+ return nil unless @store.key?(key)
46
+
47
+ if expired?(key)
48
+ delete_entry(key)
49
+ return nil
50
+ end
51
+
52
+ @store[key]
53
+ end
54
+ end
55
+
56
+ # Set value in cache
57
+ #
58
+ # @param key [String] cache key
59
+ # @param value [Object] value to cache
60
+ # @return [Object] the cached value
61
+ def set(key, value)
62
+ @mutex.synchronize do
63
+ evict_if_needed
64
+ @store[key] = value
65
+ @timestamps[key] = Time.now
66
+ value
67
+ end
68
+ end
69
+
70
+ # Get or set value with block
71
+ #
72
+ # @param key [String] cache key
73
+ # @yield block to compute value if not cached
74
+ # @return [Object] cached or computed value
75
+ def fetch(key)
76
+ cached = get(key)
77
+ return cached unless cached.nil?
78
+
79
+ value = yield
80
+ set(key, value)
81
+ value
82
+ end
83
+
84
+ # Delete entry from cache
85
+ #
86
+ # @param key [String] cache key
87
+ # @return [Object, nil] deleted value
88
+ def delete(key)
89
+ @mutex.synchronize { delete_entry(key) }
90
+ end
91
+
92
+ # Clear all cache entries
93
+ #
94
+ # @return [void]
95
+ def clear
96
+ @mutex.synchronize do
97
+ @store.clear
98
+ @timestamps.clear
99
+ end
100
+ end
101
+
102
+ # Get cache statistics
103
+ #
104
+ # @return [Hash] cache stats
105
+ def stats
106
+ @mutex.synchronize do
107
+ {
108
+ size: @store.size,
109
+ max_size: max_size,
110
+ ttl: ttl,
111
+ keys: @store.keys
112
+ }
113
+ end
114
+ end
115
+
116
+ # Check if key exists and is not expired
117
+ #
118
+ # @param key [String] cache key
119
+ # @return [Boolean]
120
+ def exist?(key)
121
+ @mutex.synchronize do
122
+ return false unless @store.key?(key)
123
+ return false if expired?(key)
124
+
125
+ true
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def expired?(key)
132
+ return true unless @timestamps.key?(key)
133
+
134
+ Time.now - @timestamps[key] > ttl
135
+ end
136
+
137
+ def delete_entry(key)
138
+ @timestamps.delete(key)
139
+ @store.delete(key)
140
+ end
141
+
142
+ def evict_if_needed
143
+ # Evict when at or above max_size to make room for new entry
144
+ return if @store.size < max_size
145
+
146
+ # Remove oldest entries (at least 10% of max_size to avoid frequent evictions)
147
+ entries_to_remove = [(max_size * 0.2).ceil, 1].max
148
+ oldest_keys = @timestamps.sort_by { |_, v| v }.first(entries_to_remove).map(&:first)
149
+ oldest_keys.each { |key| delete_entry(key) }
150
+ end
151
+ end
152
+
153
+ # Client wrapper with caching support
154
+ #
155
+ # Wraps a Vectra::Client to add transparent caching for query and fetch operations.
156
+ #
157
+ class CachedClient
158
+ attr_reader :client, :cache
159
+
160
+ # Initialize cached client
161
+ #
162
+ # @param client [Client] the underlying Vectra client
163
+ # @param cache [Cache] cache instance (creates default if nil)
164
+ # @param cache_queries [Boolean] whether to cache query results (default: true)
165
+ # @param cache_fetches [Boolean] whether to cache fetch results (default: true)
166
+ def initialize(client, cache: nil, cache_queries: true, cache_fetches: true)
167
+ @client = client
168
+ @cache = cache || Cache.new
169
+ @cache_queries = cache_queries
170
+ @cache_fetches = cache_fetches
171
+ end
172
+
173
+ # Query with caching
174
+ #
175
+ # @see Client#query
176
+ def query(index:, vector:, top_k: 10, namespace: nil, filter: nil, **options)
177
+ unless @cache_queries
178
+ return client.query(index: index, vector: vector, top_k: top_k,
179
+ namespace: namespace, filter: filter, **options)
180
+ end
181
+
182
+ key = query_cache_key(index, vector, top_k, namespace, filter)
183
+ cache.fetch(key) do
184
+ client.query(index: index, vector: vector, top_k: top_k,
185
+ namespace: namespace, filter: filter, **options)
186
+ end
187
+ end
188
+
189
+ # Fetch with caching
190
+ #
191
+ # @see Client#fetch
192
+ def fetch(index:, ids:, namespace: nil)
193
+ return client.fetch(index: index, ids: ids, namespace: namespace) unless @cache_fetches
194
+
195
+ # Check cache for each ID
196
+ results = {}
197
+ uncached_ids = []
198
+
199
+ ids.each do |id|
200
+ key = fetch_cache_key(index, id, namespace)
201
+ cached = cache.get(key)
202
+ if cached
203
+ results[id] = cached
204
+ else
205
+ uncached_ids << id
206
+ end
207
+ end
208
+
209
+ # Fetch uncached IDs
210
+ if uncached_ids.any?
211
+ fetched = client.fetch(index: index, ids: uncached_ids, namespace: namespace)
212
+ fetched.each do |id, vector|
213
+ key = fetch_cache_key(index, id, namespace)
214
+ cache.set(key, vector)
215
+ results[id] = vector
216
+ end
217
+ end
218
+
219
+ results
220
+ end
221
+
222
+ # Pass through other methods to underlying client
223
+ def method_missing(method, *, **, &)
224
+ if client.respond_to?(method)
225
+ client.public_send(method, *, **, &)
226
+ else
227
+ super
228
+ end
229
+ end
230
+
231
+ def respond_to_missing?(method, include_private = false)
232
+ client.respond_to?(method, include_private) || super
233
+ end
234
+
235
+ # Invalidate cache entries for an index
236
+ #
237
+ # @param index [String] index name
238
+ def invalidate_index(index)
239
+ cache.stats[:keys].each do |key|
240
+ cache.delete(key) if key.start_with?("#{index}:")
241
+ end
242
+ end
243
+
244
+ # Clear entire cache
245
+ def clear_cache
246
+ cache.clear
247
+ end
248
+
249
+ private
250
+
251
+ def query_cache_key(index, vector, top_k, namespace, filter)
252
+ vector_hash = Digest::MD5.hexdigest(vector.to_s)[0, 16]
253
+ filter_hash = filter ? Digest::MD5.hexdigest(filter.to_s)[0, 8] : "nofilter"
254
+ "#{index}:q:#{vector_hash}:#{top_k}:#{namespace || 'default'}:#{filter_hash}"
255
+ end
256
+
257
+ def fetch_cache_key(index, id, namespace)
258
+ "#{index}:f:#{id}:#{namespace || 'default'}"
259
+ end
260
+ end
261
+ end