vectra-client 0.2.2 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +77 -37
- data/CHANGELOG.md +49 -6
- data/README.md +56 -203
- data/docs/Gemfile +0 -1
- data/docs/_config.yml +19 -2
- data/docs/_layouts/default.html +6 -6
- data/docs/_layouts/home.html +183 -29
- data/docs/_layouts/page.html +80 -18
- data/docs/assets/style.css +806 -174
- data/docs/examples/index.md +46 -24
- data/docs/guides/performance.md +200 -0
- data/docs/index.md +22 -38
- data/docs/providers/index.md +58 -39
- data/lib/vectra/batch.rb +148 -0
- data/lib/vectra/cache.rb +261 -0
- data/lib/vectra/configuration.rb +6 -1
- data/lib/vectra/pool.rb +256 -0
- data/lib/vectra/streaming.rb +153 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +4 -0
- metadata +20 -1
data/docs/examples/index.md
CHANGED
|
@@ -6,27 +6,49 @@ permalink: /examples/
|
|
|
6
6
|
|
|
7
7
|
# Code Examples
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
9
|
+
Practical examples to get started with Vectra.
|
|
10
|
+
|
|
11
|
+
## Quick Examples
|
|
12
|
+
|
|
13
|
+
<div class="tma-comparison-grid">
|
|
14
|
+
<div class="tma-comparison-card">
|
|
15
|
+
<h4>Basic Usage</h4>
|
|
16
|
+
<p>Simple searches and CRUD operations</p>
|
|
17
|
+
<a href="{{ site.baseurl }}/examples/basic-usage/">View Guide →</a>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="tma-comparison-card">
|
|
20
|
+
<h4>Rails Integration</h4>
|
|
21
|
+
<p>ActiveRecord integration with has_vector</p>
|
|
22
|
+
<a href="{{ site.baseurl }}/providers/pgvector/">View Guide →</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
## Provider Examples
|
|
27
|
+
|
|
28
|
+
<div class="tma-comparison-grid">
|
|
29
|
+
<div class="tma-comparison-card">
|
|
30
|
+
<h4>Pinecone</h4>
|
|
31
|
+
<p>Managed cloud vector database</p>
|
|
32
|
+
<a href="{{ site.baseurl }}/providers/pinecone/">View Guide →</a>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="tma-comparison-card">
|
|
35
|
+
<h4>Qdrant</h4>
|
|
36
|
+
<p>Open source, self-hosted</p>
|
|
37
|
+
<a href="{{ site.baseurl }}/providers/qdrant/">View Guide →</a>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="tma-comparison-card">
|
|
40
|
+
<h4>Weaviate</h4>
|
|
41
|
+
<p>Semantic search with GraphQL</p>
|
|
42
|
+
<a href="{{ site.baseurl }}/providers/weaviate/">View Guide →</a>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="tma-comparison-card">
|
|
45
|
+
<h4>pgvector</h4>
|
|
46
|
+
<p>PostgreSQL with vector support</p>
|
|
47
|
+
<a href="{{ site.baseurl }}/providers/pgvector/">View Guide →</a>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
## More Resources
|
|
52
|
+
|
|
53
|
+
- [GitHub Examples](https://github.com/stokry/vectra/tree/main/examples) - Full example files
|
|
54
|
+
- [Integration Tests](https://github.com/stokry/vectra/tree/main/spec/integration) - Real-world test cases
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Performance & Optimization
|
|
4
|
+
permalink: /guides/performance/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Performance & Optimization
|
|
8
|
+
|
|
9
|
+
Vectra provides several performance optimization features for high-throughput applications.
|
|
10
|
+
|
|
11
|
+
## Async Batch Operations
|
|
12
|
+
|
|
13
|
+
Process large vector sets concurrently with automatic chunking:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require 'vectra'
|
|
17
|
+
|
|
18
|
+
client = Vectra::Client.new(provider: :pinecone, api_key: ENV['PINECONE_API_KEY'])
|
|
19
|
+
|
|
20
|
+
# Create a batch processor with 4 concurrent workers
|
|
21
|
+
batch = Vectra::Batch.new(client, concurrency: 4)
|
|
22
|
+
|
|
23
|
+
# Async upsert with automatic chunking
|
|
24
|
+
vectors = 10_000.times.map { |i| { id: "vec_#{i}", values: Array.new(384) { rand } } }
|
|
25
|
+
|
|
26
|
+
result = batch.upsert_async(
|
|
27
|
+
index: 'my-index',
|
|
28
|
+
vectors: vectors,
|
|
29
|
+
chunk_size: 100
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
puts "Upserted: #{result[:upserted_count]} vectors in #{result[:chunks]} chunks"
|
|
33
|
+
puts "Errors: #{result[:errors].size}" if result[:errors].any?
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Batch Delete
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
ids = 1000.times.map { |i| "vec_#{i}" }
|
|
40
|
+
|
|
41
|
+
result = batch.delete_async(
|
|
42
|
+
index: 'my-index',
|
|
43
|
+
ids: ids,
|
|
44
|
+
chunk_size: 100
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Batch Fetch
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
ids = ['vec_1', 'vec_2', 'vec_3']
|
|
52
|
+
|
|
53
|
+
vectors = batch.fetch_async(
|
|
54
|
+
index: 'my-index',
|
|
55
|
+
ids: ids,
|
|
56
|
+
chunk_size: 50
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Streaming Results
|
|
61
|
+
|
|
62
|
+
For large query result sets, use streaming to reduce memory usage:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
stream = Vectra::Streaming.new(client, page_size: 100)
|
|
66
|
+
|
|
67
|
+
# Stream with a block
|
|
68
|
+
stream.query_each(
|
|
69
|
+
index: 'my-index',
|
|
70
|
+
vector: query_vector,
|
|
71
|
+
total: 1000
|
|
72
|
+
) do |match|
|
|
73
|
+
process_match(match)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Or use lazy enumerator
|
|
77
|
+
results = stream.query_stream(
|
|
78
|
+
index: 'my-index',
|
|
79
|
+
vector: query_vector,
|
|
80
|
+
total: 1000
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Only fetches what you need
|
|
84
|
+
results.take(50).each { |m| puts m.id }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Caching Layer
|
|
88
|
+
|
|
89
|
+
Cache frequently queried vectors to reduce database load:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Create cache with 5-minute TTL
|
|
93
|
+
cache = Vectra::Cache.new(ttl: 300, max_size: 1000)
|
|
94
|
+
|
|
95
|
+
# Wrap client with caching
|
|
96
|
+
cached_client = Vectra::CachedClient.new(client, cache: cache)
|
|
97
|
+
|
|
98
|
+
# First query hits the database
|
|
99
|
+
result1 = cached_client.query(index: 'idx', vector: vec, top_k: 10)
|
|
100
|
+
|
|
101
|
+
# Second identical query returns cached result
|
|
102
|
+
result2 = cached_client.query(index: 'idx', vector: vec, top_k: 10)
|
|
103
|
+
|
|
104
|
+
# Invalidate cache when data changes
|
|
105
|
+
cached_client.invalidate_index('idx')
|
|
106
|
+
|
|
107
|
+
# Clear all cache
|
|
108
|
+
cached_client.clear_cache
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Cache Statistics
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
stats = cache.stats
|
|
115
|
+
puts "Cache size: #{stats[:size]}/#{stats[:max_size]}"
|
|
116
|
+
puts "TTL: #{stats[:ttl]} seconds"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Connection Pooling (pgvector)
|
|
120
|
+
|
|
121
|
+
For pgvector, use connection pooling with warmup:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# Configure pool size
|
|
125
|
+
Vectra.configure do |config|
|
|
126
|
+
config.provider = :pgvector
|
|
127
|
+
config.host = ENV['DATABASE_URL']
|
|
128
|
+
config.pool_size = 10
|
|
129
|
+
config.pool_timeout = 5
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
client = Vectra::Client.new
|
|
133
|
+
|
|
134
|
+
# Warmup connections at startup
|
|
135
|
+
client.provider.warmup_pool(5)
|
|
136
|
+
|
|
137
|
+
# Check pool stats
|
|
138
|
+
stats = client.provider.pool_stats
|
|
139
|
+
puts "Available connections: #{stats[:available]}"
|
|
140
|
+
puts "Checked out: #{stats[:checked_out]}"
|
|
141
|
+
|
|
142
|
+
# Shutdown pool when done
|
|
143
|
+
client.provider.shutdown_pool
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Configuration Options
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Vectra.configure do |config|
|
|
150
|
+
# Provider settings
|
|
151
|
+
config.provider = :pinecone
|
|
152
|
+
config.api_key = ENV['PINECONE_API_KEY']
|
|
153
|
+
|
|
154
|
+
# Timeouts
|
|
155
|
+
config.timeout = 30
|
|
156
|
+
config.open_timeout = 10
|
|
157
|
+
|
|
158
|
+
# Retry settings
|
|
159
|
+
config.max_retries = 3
|
|
160
|
+
config.retry_delay = 1
|
|
161
|
+
|
|
162
|
+
# Batch operations
|
|
163
|
+
config.batch_size = 100
|
|
164
|
+
config.async_concurrency = 4
|
|
165
|
+
|
|
166
|
+
# Connection pooling (pgvector)
|
|
167
|
+
config.pool_size = 10
|
|
168
|
+
config.pool_timeout = 5
|
|
169
|
+
|
|
170
|
+
# Caching
|
|
171
|
+
config.cache_enabled = true
|
|
172
|
+
config.cache_ttl = 300
|
|
173
|
+
config.cache_max_size = 1000
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Benchmarking
|
|
178
|
+
|
|
179
|
+
Run the included benchmarks:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Batch operations benchmark
|
|
183
|
+
bundle exec ruby benchmarks/batch_operations_benchmark.rb
|
|
184
|
+
|
|
185
|
+
# Connection pooling benchmark
|
|
186
|
+
bundle exec ruby benchmarks/connection_pooling_benchmark.rb
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Best Practices
|
|
190
|
+
|
|
191
|
+
1. **Batch Size**: Use batch sizes of 100-500 for optimal throughput
|
|
192
|
+
2. **Concurrency**: Set concurrency to 2-4x your CPU cores
|
|
193
|
+
3. **Connection Pool**: Size pool to expected concurrent requests + 20%
|
|
194
|
+
4. **Cache TTL**: Set TTL based on data freshness requirements
|
|
195
|
+
5. **Warmup**: Always warmup connections in production
|
|
196
|
+
|
|
197
|
+
## Next Steps
|
|
198
|
+
|
|
199
|
+
- [API Reference]({{ site.baseurl }}/api/overview)
|
|
200
|
+
- [Provider Guides]({{ site.baseurl }}/providers)
|
data/docs/index.md
CHANGED
|
@@ -3,51 +3,35 @@ layout: home
|
|
|
3
3
|
title: Vectra
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Welcome to Vectra Documentation
|
|
7
|
-
|
|
8
|
-
**Vectra** is a unified Ruby client for vector databases that allows you to write once and switch providers easily.
|
|
9
|
-
|
|
10
|
-
## Supported Vector Databases
|
|
11
|
-
|
|
12
|
-
- **Pinecone** - Managed vector database in the cloud
|
|
13
|
-
- **Qdrant** - Open-source vector database
|
|
14
|
-
- **Weaviate** - Open-source vector search engine
|
|
15
|
-
- **PostgreSQL with pgvector** - SQL database with vector support
|
|
16
|
-
|
|
17
|
-
## Quick Links
|
|
18
|
-
|
|
19
|
-
- [Installation Guide]({{ site.baseurl }}/guides/installation)
|
|
20
|
-
- [Getting Started]({{ site.baseurl }}/guides/getting-started)
|
|
21
|
-
- [API Reference]({{ site.baseurl }}/api/overview)
|
|
22
|
-
- [Examples]({{ site.baseurl }}/examples/basic-usage)
|
|
23
|
-
- [Contributing]({{ site.baseurl }}/community/contributing)
|
|
24
|
-
|
|
25
|
-
## Key Features
|
|
26
|
-
|
|
27
|
-
- 🔄 **Provider Agnostic** - Switch between different vector database providers with minimal code changes
|
|
28
|
-
- 🚀 **Easy Integration** - Works seamlessly with Rails and other Ruby frameworks
|
|
29
|
-
- 📊 **Vector Operations** - Create, search, update, and delete vectors
|
|
30
|
-
- 🔌 **Multiple Providers** - Support for leading vector database platforms
|
|
31
|
-
- 📈 **Instrumentation** - Built-in support for Datadog and New Relic monitoring
|
|
32
|
-
- 🗄️ **ActiveRecord Integration** - Native support for Rails models
|
|
33
|
-
|
|
34
|
-
## Get Started
|
|
35
|
-
|
|
36
6
|
```ruby
|
|
37
7
|
require 'vectra'
|
|
38
8
|
|
|
39
|
-
# Initialize
|
|
40
|
-
client = Vectra::Client.new(
|
|
9
|
+
# Initialize any provider with the same API
|
|
10
|
+
client = Vectra::Client.new(
|
|
11
|
+
provider: :pinecone, # or :qdrant, :weaviate, :pgvector
|
|
12
|
+
api_key: ENV['API_KEY'],
|
|
13
|
+
host: 'your-host.example.com'
|
|
14
|
+
)
|
|
41
15
|
|
|
42
|
-
#
|
|
16
|
+
# Store vectors with metadata
|
|
43
17
|
client.upsert(
|
|
44
18
|
vectors: [
|
|
45
|
-
{
|
|
19
|
+
{
|
|
20
|
+
id: 'doc-1',
|
|
21
|
+
values: [0.1, 0.2, 0.3, ...], # Your embedding
|
|
22
|
+
metadata: { title: 'Getting Started with AI' }
|
|
23
|
+
}
|
|
46
24
|
]
|
|
47
25
|
)
|
|
48
26
|
|
|
49
|
-
# Search
|
|
50
|
-
results = client.query(
|
|
51
|
-
|
|
27
|
+
# Search by similarity
|
|
28
|
+
results = client.query(
|
|
29
|
+
vector: [0.1, 0.2, 0.3, ...],
|
|
30
|
+
top_k: 10,
|
|
31
|
+
filter: { category: 'tutorials' }
|
|
32
|
+
)
|
|
52
33
|
|
|
53
|
-
|
|
34
|
+
results.each do |match|
|
|
35
|
+
puts "#{match['id']}: #{match['score']}"
|
|
36
|
+
end
|
|
37
|
+
```
|
data/docs/providers/index.md
CHANGED
|
@@ -6,57 +6,76 @@ permalink: /providers/
|
|
|
6
6
|
|
|
7
7
|
# Vector Database Providers
|
|
8
8
|
|
|
9
|
-
Vectra supports multiple vector database providers. Choose the one that best fits your needs
|
|
9
|
+
Vectra supports multiple vector database providers. Choose the one that best fits your needs.
|
|
10
10
|
|
|
11
11
|
## Supported Providers
|
|
12
12
|
|
|
13
|
-
| Provider | Type | Best For |
|
|
14
|
-
|
|
15
|
-
| **Pinecone**
|
|
16
|
-
| **Qdrant**
|
|
17
|
-
| **Weaviate**
|
|
18
|
-
| **
|
|
13
|
+
| Provider | Type | Best For |
|
|
14
|
+
|----------|------|----------|
|
|
15
|
+
| [**Pinecone**]({{ site.baseurl }}/providers/pinecone) | Managed Cloud | Production, Zero ops |
|
|
16
|
+
| [**Qdrant**]({{ site.baseurl }}/providers/qdrant) | Open Source | Self-hosted, Performance |
|
|
17
|
+
| [**Weaviate**]({{ site.baseurl }}/providers/weaviate) | Open Source | Semantic search, GraphQL |
|
|
18
|
+
| [**pgvector**]({{ site.baseurl }}/providers/pgvector) | PostgreSQL | SQL integration, ACID |
|
|
19
19
|
|
|
20
20
|
## Quick Comparison
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
22
|
+
<div class="tma-comparison-grid">
|
|
23
|
+
<div class="tma-comparison-card">
|
|
24
|
+
<h4>Pinecone</h4>
|
|
25
|
+
<ul>
|
|
26
|
+
<li class="pro">Fully managed service</li>
|
|
27
|
+
<li class="pro">Easy setup</li>
|
|
28
|
+
<li class="pro">Highly scalable</li>
|
|
29
|
+
<li class="con">Cloud only</li>
|
|
30
|
+
<li class="con">Paid service</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="tma-comparison-card">
|
|
34
|
+
<h4>Qdrant</h4>
|
|
35
|
+
<ul>
|
|
36
|
+
<li class="pro">Open source</li>
|
|
37
|
+
<li class="pro">Self-hosted option</li>
|
|
38
|
+
<li class="pro">High performance</li>
|
|
39
|
+
<li class="pro">Cloud option available</li>
|
|
40
|
+
<li class="con">More configuration</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="tma-comparison-card">
|
|
44
|
+
<h4>Weaviate</h4>
|
|
45
|
+
<ul>
|
|
46
|
+
<li class="pro">Open source</li>
|
|
47
|
+
<li class="pro">Semantic search</li>
|
|
48
|
+
<li class="pro">GraphQL API</li>
|
|
49
|
+
<li class="pro">Multi-model support</li>
|
|
50
|
+
<li class="con">More complex setup</li>
|
|
51
|
+
</ul>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="tma-comparison-card">
|
|
54
|
+
<h4>pgvector</h4>
|
|
55
|
+
<ul>
|
|
56
|
+
<li class="pro">SQL database</li>
|
|
57
|
+
<li class="pro">ACID transactions</li>
|
|
58
|
+
<li class="pro">Use existing Postgres</li>
|
|
59
|
+
<li class="pro">Very affordable</li>
|
|
60
|
+
<li class="con">Not vector-specialized</li>
|
|
61
|
+
</ul>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
49
64
|
|
|
50
65
|
## Switching Providers
|
|
51
66
|
|
|
52
67
|
One of Vectra's key features is easy provider switching:
|
|
53
68
|
|
|
54
69
|
```ruby
|
|
55
|
-
#
|
|
56
|
-
client = Vectra::Client.new(provider: :qdrant)
|
|
70
|
+
# Just change the provider - your code stays the same!
|
|
71
|
+
client = Vectra::Client.new(provider: :qdrant, host: 'localhost:6333')
|
|
57
72
|
|
|
58
|
-
# All
|
|
59
|
-
|
|
73
|
+
# All operations work identically
|
|
74
|
+
client.upsert(vectors: [...])
|
|
75
|
+
results = client.query(vector: [...], top_k: 5)
|
|
60
76
|
```
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
## Next Steps
|
|
79
|
+
|
|
80
|
+
- [Getting Started Guide]({{ site.baseurl }}/guides/getting-started)
|
|
81
|
+
- [API Reference]({{ site.baseurl }}/api/overview)
|
data/lib/vectra/batch.rb
ADDED
|
@@ -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
|