vectra-client 0.3.3 → 0.4.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.
@@ -0,0 +1,340 @@
1
+ # Grafana Dashboard Setup for Vectra
2
+
3
+ Complete guide to set up a beautiful Grafana dashboard for monitoring Vectra vector database operations.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **Grafana Account** - Sign up at [grafana.com](https://grafana.com) or use self-hosted Grafana
8
+ 2. **Prometheus** - For metrics collection (or use Grafana Cloud's Prometheus)
9
+ 3. **Vectra with Instrumentation** - Enable metrics in your application
10
+
11
+ ## Quick Setup for Screenshots (3 minutes)
12
+
13
+ **Perfect for creating dashboard screenshots!**
14
+
15
+ ### Option 1: Use Demo Exporter (Easiest)
16
+
17
+ 1. **Start Prometheus Exporter:**
18
+ ```bash
19
+ ruby examples/prometheus-exporter.rb
20
+ ```
21
+ This generates demo metrics automatically.
22
+
23
+ 2. **Setup Grafana Cloud (or local):**
24
+ - Sign up at [grafana.com](https://grafana.com) (free tier available)
25
+ - Create a Prometheus data source pointing to `http://localhost:9394`
26
+ - Or use Grafana Cloud's built-in Prometheus
27
+
28
+ 3. **Import Dashboard:**
29
+ - Go to Dashboards → Import
30
+ - Upload `examples/grafana-dashboard.json`
31
+ - Select your Prometheus data source
32
+ - Click "Import"
33
+
34
+ 4. **Take Screenshots:**
35
+ - Dashboard will populate with demo data
36
+ - Wait 1-2 minutes for metrics to accumulate
37
+ - Use Grafana's built-in screenshot feature or browser screenshot
38
+
39
+ ### Option 2: Full Production Setup
40
+
41
+ ## Full Setup (5 minutes)
42
+
43
+ ### Step 1: Enable Vectra Metrics
44
+
45
+ Add to your Rails initializer or application:
46
+
47
+ ```ruby
48
+ # config/initializers/vectra_metrics.rb
49
+ require "prometheus/client"
50
+
51
+ module VectraMetrics
52
+ REGISTRY = Prometheus::Client.registry
53
+
54
+ REQUESTS_TOTAL = REGISTRY.counter(
55
+ :vectra_requests_total,
56
+ docstring: "Total Vectra requests",
57
+ labels: [:provider, :operation, :status]
58
+ )
59
+
60
+ REQUEST_DURATION = REGISTRY.histogram(
61
+ :vectra_request_duration_seconds,
62
+ docstring: "Request duration in seconds",
63
+ labels: [:provider, :operation],
64
+ buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
65
+ )
66
+
67
+ VECTORS_PROCESSED = REGISTRY.counter(
68
+ :vectra_vectors_processed_total,
69
+ docstring: "Total vectors processed",
70
+ labels: [:provider, :operation]
71
+ )
72
+
73
+ CACHE_HITS = REGISTRY.counter(
74
+ :vectra_cache_hits_total,
75
+ docstring: "Cache hit count"
76
+ )
77
+
78
+ CACHE_MISSES = REGISTRY.counter(
79
+ :vectra_cache_misses_total,
80
+ docstring: "Cache miss count"
81
+ )
82
+
83
+ ERRORS_TOTAL = REGISTRY.counter(
84
+ :vectra_errors_total,
85
+ docstring: "Total errors",
86
+ labels: [:provider, :error_type]
87
+ )
88
+
89
+ POOL_SIZE = REGISTRY.gauge(
90
+ :vectra_pool_connections,
91
+ docstring: "Connection pool size",
92
+ labels: [:state]
93
+ )
94
+ end
95
+
96
+ # Register instrumentation
97
+ Vectra::Instrumentation.register(:prometheus) do |event|
98
+ labels = {
99
+ provider: event[:provider],
100
+ operation: event[:operation]
101
+ }
102
+
103
+ status = event[:error] ? "error" : "success"
104
+ VectraMetrics::REQUESTS_TOTAL.increment(labels: labels.merge(status: status))
105
+
106
+ if event[:duration]
107
+ VectraMetrics::REQUEST_DURATION.observe(event[:duration], labels: labels)
108
+ end
109
+
110
+ if event[:metadata]&.dig(:vector_count)
111
+ VectraMetrics::VECTORS_PROCESSED.increment(
112
+ by: event[:metadata][:vector_count],
113
+ labels: labels
114
+ )
115
+ end
116
+
117
+ if event[:error]
118
+ VectraMetrics::ERRORS_TOTAL.increment(
119
+ labels: labels.merge(error_type: event[:error].class.name)
120
+ )
121
+ end
122
+ end
123
+ ```
124
+
125
+ ### Step 2: Expose Prometheus Endpoint
126
+
127
+ For Rails, add to `config/routes.rb`:
128
+
129
+ ```ruby
130
+ # config/routes.rb
131
+ require "rack/prometheus"
132
+
133
+ Rails.application.routes.draw do
134
+ # ... your routes ...
135
+
136
+ # Prometheus metrics endpoint
137
+ get "/metrics", to: proc { |env|
138
+ [
139
+ 200,
140
+ { "Content-Type" => "text/plain" },
141
+ [Prometheus::Client::Formats::Text.marshal(VectraMetrics::REGISTRY)]
142
+ ]
143
+ }
144
+ end
145
+ ```
146
+
147
+ Or use `rack-prometheus` gem:
148
+
149
+ ```ruby
150
+ # Gemfile
151
+ gem "rack-prometheus"
152
+
153
+ # config/application.rb
154
+ config.middleware.use Rack::Prometheus::Middleware
155
+ ```
156
+
157
+ ### Step 3: Configure Prometheus
158
+
159
+ Create `prometheus.yml`:
160
+
161
+ ```yaml
162
+ global:
163
+ scrape_interval: 15s
164
+ evaluation_interval: 15s
165
+
166
+ scrape_configs:
167
+ - job_name: "vectra"
168
+ static_configs:
169
+ - targets: ["localhost:3000"] # Your Rails app
170
+ metrics_path: "/metrics"
171
+ ```
172
+
173
+ ### Step 4: Import Dashboard to Grafana
174
+
175
+ 1. **Login to Grafana** (grafana.com or your instance)
176
+
177
+ 2. **Add Prometheus Data Source:**
178
+ - Go to Configuration → Data Sources
179
+ - Add Prometheus
180
+ - URL: `http://localhost:9090` (or your Prometheus URL)
181
+ - Click "Save & Test"
182
+
183
+ 3. **Import Dashboard:**
184
+ - Go to Dashboards → Import
185
+ - Click "Upload JSON file"
186
+ - Select `grafana-dashboard.json`
187
+ - Select your Prometheus data source
188
+ - Click "Import"
189
+
190
+ 4. **View Dashboard:**
191
+ - Dashboard will appear with all panels
192
+ - Run your Vectra demo to generate metrics
193
+ - Watch real-time data populate!
194
+
195
+ ## Dashboard Panels
196
+
197
+ The dashboard includes 12 panels:
198
+
199
+ ### Top Row (Stats)
200
+ 1. **Total Requests** - Requests per second
201
+ 2. **Error Rate** - Error percentage with color coding
202
+ 3. **P95 Latency** - 95th percentile latency
203
+ 4. **Cache Hit Ratio** - Cache performance
204
+
205
+ ### Middle Row (Time Series)
206
+ 5. **Request Rate by Operation** - Query, upsert, delete, etc.
207
+ 6. **Latency Distribution** - P50, P95, P99 percentiles
208
+ 7. **Vectors Processed** - Throughput by operation
209
+ 8. **Errors by Type** - Error breakdown
210
+
211
+ ### Bottom Row (Visualizations)
212
+ 9. **Connection Pool Status** - pgvector pool metrics
213
+ 10. **Request Rate by Provider** - Bar chart by provider
214
+ 11. **Operations Distribution** - Pie chart
215
+ 12. **Provider Distribution** - Pie chart
216
+
217
+ ## Generating Demo Data
218
+
219
+ Run the comprehensive demo to generate metrics:
220
+
221
+ ```bash
222
+ # Run demo (generates metrics)
223
+ bundle exec ruby examples/comprehensive_demo.rb
224
+
225
+ # Or run your application
226
+ rails server
227
+ ```
228
+
229
+ ## Screenshot Tips for Social Media
230
+
231
+ ### Best Panels for Screenshots
232
+
233
+ 1. **Request Rate by Operation** ⭐
234
+ - Shows activity over time
235
+ - Clean, professional look
236
+ - Perfect for Twitter/LinkedIn
237
+
238
+ 2. **Latency Distribution (P50, P95, P99)** ⭐
239
+ - Shows performance metrics
240
+ - Multiple lines = impressive
241
+ - Great for technical posts
242
+
243
+ 3. **Operations Distribution (Pie Chart)** ⭐
244
+ - Clean, colorful visualization
245
+ - Easy to understand
246
+ - Perfect for overview posts
247
+
248
+ 4. **Top Row Stats** ⭐
249
+ - 4 stat panels side-by-side
250
+ - Shows key metrics at a glance
251
+ - Great for hero images
252
+
253
+ ### Time Ranges for Screenshots
254
+
255
+ - **Last 15 minutes** - Shows recent activity (best for demos)
256
+ - **Last 1 hour** - Shows trends (good for posts)
257
+ - **Last 6 hours** - Shows daily patterns (for analysis posts)
258
+
259
+ ### Grafana Screenshot Features
260
+
261
+ 1. **Built-in Screenshot:**
262
+ - Click panel → Share → Direct link rendered image
263
+ - Or use browser screenshot (Cmd+Shift+4 on Mac)
264
+
265
+ 2. **Full Dashboard Screenshot:**
266
+ - Use browser developer tools
267
+ - Or Grafana's export feature
268
+
269
+ 3. **Panel Screenshots:**
270
+ - Right-click panel → Inspect
271
+ - Use browser screenshot tool
272
+
273
+ ### Customization for Better Screenshots
274
+
275
+ 1. **Change Theme:**
276
+ - Settings → Preferences → Theme
277
+ - Dark theme looks more professional
278
+
279
+ 2. **Adjust Time Range:**
280
+ - Use "Last 15 minutes" for demo screenshots
281
+ - Shows active data
282
+
283
+ 3. **Hide Legend (if needed):**
284
+ - Panel → Options → Legend → Hide
285
+ - Cleaner look for some panels
286
+
287
+ 4. **Add Title/Description:**
288
+ - Panel → Title → Add description
289
+ - Makes screenshots self-explanatory
290
+
291
+ ## Troubleshooting
292
+
293
+ ### No Data Showing
294
+
295
+ 1. **Check Prometheus is scraping:**
296
+ ```bash
297
+ curl http://localhost:9090/api/v1/targets
298
+ ```
299
+
300
+ 2. **Check metrics endpoint:**
301
+ ```bash
302
+ curl http://localhost:3000/metrics | grep vectra
303
+ ```
304
+
305
+ 3. **Verify instrumentation:**
306
+ ```ruby
307
+ # In Rails console
308
+ Vectra::Instrumentation.enabled? # Should be true
309
+ ```
310
+
311
+ ### Missing Metrics
312
+
313
+ - Ensure `config.instrumentation = true` in Vectra config
314
+ - Check Prometheus is scraping your app
315
+ - Verify labels match dashboard queries
316
+
317
+ ## Advanced: Grafana Cloud Setup
318
+
319
+ If using Grafana Cloud:
320
+
321
+ 1. **Create Prometheus data source:**
322
+ - Use Grafana Cloud's Prometheus URL
323
+ - Add API key for authentication
324
+
325
+ 2. **Push metrics:**
326
+ ```ruby
327
+ # Use prometheus/client_ruby with push gateway
328
+ require "prometheus/client/push"
329
+
330
+ Prometheus::Client::Push.new(
331
+ job: "vectra",
332
+ gateway: "https://prometheus-us-central1.grafana.net"
333
+ ).add(VectraMetrics::REGISTRY)
334
+ ```
335
+
336
+ ## Next Steps
337
+
338
+ - [Monitoring Guide](../docs/guides/monitoring.md) - Full monitoring setup
339
+ - [Performance Guide](../docs/guides/performance.md) - Optimization tips
340
+ - [Examples](../examples/) - More demo code
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Simple Prometheus Exporter for Vectra Demo
5
+ #
6
+ # This script exposes Prometheus metrics endpoint for Grafana dashboard.
7
+ # Run this alongside your demo to generate metrics.
8
+ #
9
+ # Prerequisites:
10
+ # gem install prometheus-client
11
+ #
12
+ # Usage:
13
+ # ruby examples/prometheus-exporter.rb
14
+ # # Then run: bundle exec ruby examples/comprehensive_demo.rb
15
+ #
16
+ # Access metrics:
17
+ # curl http://localhost:9394/metrics
18
+
19
+ begin
20
+ require "webrick"
21
+ require "prometheus/client"
22
+ require "json"
23
+ rescue LoadError => e
24
+ if e.message.include?("prometheus")
25
+ puts "❌ Error: prometheus-client gem not found"
26
+ puts
27
+ puts "Install it with:"
28
+ puts " gem install prometheus-client"
29
+ puts
30
+ puts "Or add to Gemfile:"
31
+ puts " gem 'prometheus-client'"
32
+ exit 1
33
+ else
34
+ raise
35
+ end
36
+ end
37
+
38
+ # Create Prometheus registry
39
+ REGISTRY = Prometheus::Client.registry
40
+
41
+ # Define metrics
42
+ REQUESTS_TOTAL = REGISTRY.counter(
43
+ :vectra_requests_total,
44
+ docstring: "Total Vectra requests",
45
+ labels: [:provider, :operation, :status]
46
+ )
47
+
48
+ REQUEST_DURATION = REGISTRY.histogram(
49
+ :vectra_request_duration_seconds,
50
+ docstring: "Request duration in seconds",
51
+ labels: [:provider, :operation],
52
+ buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
53
+ )
54
+
55
+ VECTORS_PROCESSED = REGISTRY.counter(
56
+ :vectra_vectors_processed_total,
57
+ docstring: "Total vectors processed",
58
+ labels: [:provider, :operation]
59
+ )
60
+
61
+ CACHE_HITS = REGISTRY.counter(
62
+ :vectra_cache_hits_total,
63
+ docstring: "Cache hit count"
64
+ )
65
+
66
+ CACHE_MISSES = REGISTRY.counter(
67
+ :vectra_cache_misses_total,
68
+ docstring: "Cache miss count"
69
+ )
70
+
71
+ ERRORS_TOTAL = REGISTRY.counter(
72
+ :vectra_errors_total,
73
+ docstring: "Total errors",
74
+ labels: [:provider, :error_type]
75
+ )
76
+
77
+ POOL_SIZE = REGISTRY.gauge(
78
+ :vectra_pool_connections,
79
+ docstring: "Connection pool size",
80
+ labels: [:state]
81
+ )
82
+
83
+ # Simulate metrics for demo (in production, these come from Vectra)
84
+ def generate_demo_metrics
85
+ providers = %w[pinecone qdrant pgvector]
86
+ operations = %w[query upsert fetch delete update]
87
+
88
+ loop do
89
+ provider = providers.sample
90
+ operation = operations.sample
91
+
92
+ # Simulate requests
93
+ REQUESTS_TOTAL.increment(
94
+ labels: {
95
+ provider: provider,
96
+ operation: operation,
97
+ status: rand > 0.05 ? "success" : "error"
98
+ }
99
+ )
100
+
101
+ # Simulate latency
102
+ duration = case operation
103
+ when "query"
104
+ rand(0.01..0.1)
105
+ when "upsert"
106
+ rand(0.05..0.3)
107
+ else
108
+ rand(0.01..0.2)
109
+ end
110
+
111
+ REQUEST_DURATION.observe(
112
+ duration,
113
+ labels: { provider: provider, operation: operation }
114
+ )
115
+
116
+ # Simulate vectors processed
117
+ if %w[upsert query].include?(operation)
118
+ VECTORS_PROCESSED.increment(
119
+ by: rand(1..100),
120
+ labels: { provider: provider, operation: operation }
121
+ )
122
+ end
123
+
124
+ # Simulate cache hits/misses
125
+ if rand > 0.3
126
+ CACHE_HITS.increment
127
+ else
128
+ CACHE_MISSES.increment
129
+ end
130
+
131
+ # Simulate errors
132
+ if rand < 0.05
133
+ ERRORS_TOTAL.increment(
134
+ labels: {
135
+ provider: provider,
136
+ error_type: %w[RateLimitError ServerError ConnectionError].sample
137
+ }
138
+ )
139
+ end
140
+
141
+ # Simulate pool metrics (pgvector)
142
+ if provider == "pgvector"
143
+ POOL_SIZE.set(
144
+ rand(3..8),
145
+ labels: { state: "available" }
146
+ )
147
+ POOL_SIZE.set(
148
+ rand(1..3),
149
+ labels: { state: "checked_out" }
150
+ )
151
+ end
152
+
153
+ sleep(rand(0.5..2.0))
154
+ end
155
+ end
156
+
157
+ # Start metrics generation in background
158
+ Thread.new { generate_demo_metrics }
159
+
160
+ # Create HTTP server
161
+ server = WEBrick::HTTPServer.new(Port: 9394)
162
+
163
+ server.mount_proc("/metrics") do |_req, res|
164
+ res.content_type = "text/plain; version=0.0.4"
165
+ res.body = Prometheus::Client::Formats::Text.marshal(REGISTRY)
166
+ end
167
+
168
+ server.mount_proc("/") do |_req, res|
169
+ res.content_type = "text/html"
170
+ res.body = <<~HTML
171
+ <!DOCTYPE html>
172
+ <html>
173
+ <head>
174
+ <title>Vectra Prometheus Exporter</title>
175
+ <style>
176
+ body { font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px; }
177
+ h1 { color: #05df72; }
178
+ .status { background: #f0f0f0; padding: 15px; border-radius: 5px; margin: 20px 0; }
179
+ code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; }
180
+ a { color: #05df72; text-decoration: none; }
181
+ a:hover { text-decoration: underline; }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <h1>🚀 Vectra Prometheus Exporter</h1>
186
+ <div class="status">
187
+ <p><strong>Status:</strong> Running</p>
188
+ <p><strong>Metrics Endpoint:</strong> <a href="/metrics"><code>/metrics</code></a></p>
189
+ <p><strong>Port:</strong> 9394</p>
190
+ </div>
191
+ <h2>Available Metrics</h2>
192
+ <ul>
193
+ <li><code>vectra_requests_total</code> - Total requests by provider/operation</li>
194
+ <li><code>vectra_request_duration_seconds</code> - Request latency histogram</li>
195
+ <li><code>vectra_vectors_processed_total</code> - Vectors processed</li>
196
+ <li><code>vectra_cache_hits_total</code> - Cache hits</li>
197
+ <li><code>vectra_cache_misses_total</code> - Cache misses</li>
198
+ <li><code>vectra_errors_total</code> - Error counts</li>
199
+ <li><code>vectra_pool_connections</code> - Connection pool status</li>
200
+ </ul>
201
+ <h2>Next Steps</h2>
202
+ <ol>
203
+ <li>Configure Prometheus to scrape <code>http://localhost:9394/metrics</code></li>
204
+ <li>Import Grafana dashboard from <code>examples/grafana-dashboard.json</code></li>
205
+ <li>Run your Vectra demo to generate real metrics</li>
206
+ </ol>
207
+ <p><a href="/metrics">View Metrics →</a></p>
208
+ </body>
209
+ </html>
210
+ HTML
211
+ end
212
+
213
+ trap("INT") { server.shutdown }
214
+ trap("TERM") { server.shutdown }
215
+
216
+ puts "=" * 60
217
+ puts "🚀 Vectra Prometheus Exporter"
218
+ puts "=" * 60
219
+ puts
220
+ puts "📊 Metrics endpoint: http://localhost:9394/metrics"
221
+ puts "🌐 Web interface: http://localhost:9394"
222
+ puts
223
+ puts "💡 This exporter generates demo metrics."
224
+ puts " In production, use Vectra instrumentation instead."
225
+ puts
226
+ puts "Press Ctrl+C to stop..."
227
+ puts
228
+
229
+ server.start
data/lib/vectra/batch.rb CHANGED
@@ -17,6 +17,17 @@ module Vectra
17
17
  # )
18
18
  # puts "Upserted: #{result[:upserted_count]}"
19
19
  #
20
+ # @example With progress tracking
21
+ # batch.upsert_async(
22
+ # index: 'docs',
23
+ # vectors: large_array,
24
+ # on_progress: ->(stats) {
25
+ # puts "Progress: #{stats[:percentage]}% (#{stats[:processed]}/#{stats[:total]})"
26
+ # puts " Chunk #{stats[:current_chunk] + 1}/#{stats[:total_chunks]}"
27
+ # puts " Success: #{stats[:success_count]}, Failed: #{stats[:failed_count]}"
28
+ # }
29
+ # )
30
+ #
20
31
  class Batch
21
32
  DEFAULT_CONCURRENCY = 4
22
33
  DEFAULT_CHUNK_SIZE = 100
@@ -38,12 +49,23 @@ module Vectra
38
49
  # @param vectors [Array<Hash>] vectors to upsert
39
50
  # @param namespace [String, nil] optional namespace
40
51
  # @param chunk_size [Integer] vectors per chunk (default: 100)
52
+ # @param on_progress [Proc, nil] optional callback called after each chunk completes
53
+ # Callback receives hash with: processed, total, percentage, current_chunk, total_chunks, success_count, failed_count
41
54
  # @return [Hash] aggregated result with :upserted_count, :chunks, :errors
42
- def upsert_async(index:, vectors:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE)
55
+ #
56
+ # @example With progress callback
57
+ # batch.upsert_async(
58
+ # index: 'docs',
59
+ # vectors: large_array,
60
+ # on_progress: ->(stats) {
61
+ # puts "Progress: #{stats[:percentage]}% (#{stats[:processed]}/#{stats[:total]})"
62
+ # }
63
+ # )
64
+ def upsert_async(index:, vectors:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE, on_progress: nil)
43
65
  chunks = vectors.each_slice(chunk_size).to_a
44
66
  return { upserted_count: 0, chunks: 0, errors: [] } if chunks.empty?
45
67
 
46
- results = process_chunks_concurrently(chunks) do |chunk|
68
+ results = process_chunks_concurrently(chunks, total_items: vectors.size, on_progress: on_progress) do |chunk|
47
69
  client.upsert(index: index, vectors: chunk, namespace: namespace)
48
70
  end
49
71
 
@@ -56,12 +78,14 @@ module Vectra
56
78
  # @param ids [Array<String>] IDs to delete
57
79
  # @param namespace [String, nil] optional namespace
58
80
  # @param chunk_size [Integer] IDs per chunk (default: 100)
81
+ # @param on_progress [Proc, nil] optional callback called after each chunk completes
82
+ # Callback receives hash with: processed, total, percentage, current_chunk, total_chunks, success_count, failed_count
59
83
  # @return [Hash] aggregated result
60
- def delete_async(index:, ids:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE)
84
+ def delete_async(index:, ids:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE, on_progress: nil)
61
85
  chunks = ids.each_slice(chunk_size).to_a
62
86
  return { deleted_count: 0, chunks: 0, errors: [] } if chunks.empty?
63
87
 
64
- results = process_chunks_concurrently(chunks) do |chunk|
88
+ results = process_chunks_concurrently(chunks, total_items: ids.size, on_progress: on_progress) do |chunk|
65
89
  client.delete(index: index, ids: chunk, namespace: namespace)
66
90
  end
67
91
 
@@ -74,12 +98,14 @@ module Vectra
74
98
  # @param ids [Array<String>] IDs to fetch
75
99
  # @param namespace [String, nil] optional namespace
76
100
  # @param chunk_size [Integer] IDs per chunk (default: 100)
101
+ # @param on_progress [Proc, nil] optional callback called after each chunk completes
102
+ # Callback receives hash with: processed, total, percentage, current_chunk, total_chunks, success_count, failed_count
77
103
  # @return [Hash<String, Vector>] merged results
78
- def fetch_async(index:, ids:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE)
104
+ def fetch_async(index:, ids:, namespace: nil, chunk_size: DEFAULT_CHUNK_SIZE, on_progress: nil)
79
105
  chunks = ids.each_slice(chunk_size).to_a
80
106
  return {} if chunks.empty?
81
107
 
82
- results = process_chunks_concurrently(chunks) do |chunk|
108
+ results = process_chunks_concurrently(chunks, total_items: ids.size, on_progress: on_progress) do |chunk|
83
109
  client.fetch(index: index, ids: chunk, namespace: namespace)
84
110
  end
85
111
 
@@ -88,15 +114,43 @@ module Vectra
88
114
 
89
115
  private
90
116
 
91
- def process_chunks_concurrently(chunks)
117
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength
118
+ def process_chunks_concurrently(chunks, total_items: nil, on_progress: nil)
92
119
  pool = Concurrent::FixedThreadPool.new(concurrency)
93
120
  futures = []
121
+ progress_mutex = Mutex.new
122
+ completed_count = Concurrent::AtomicFixnum.new(0)
123
+ success_count = Concurrent::AtomicFixnum.new(0)
124
+ failed_count = Concurrent::AtomicFixnum.new(0)
94
125
 
95
126
  chunks.each_with_index do |chunk, index|
96
127
  futures << Concurrent::Future.execute(executor: pool) do
97
- { index: index, result: yield(chunk), error: nil }
128
+ result = yield(chunk)
129
+ success_count.increment
130
+ { index: index, result: result, error: nil }
98
131
  rescue StandardError => e
132
+ failed_count.increment
99
133
  { index: index, result: nil, error: e }
134
+ ensure
135
+ # Call progress callback when chunk completes
136
+ if on_progress
137
+ completed = completed_count.increment
138
+ total_size = chunks.size * chunks.first.size
139
+ processed = [completed * chunks.first.size, total_items || total_size].min
140
+ percentage = total_items ? (processed.to_f / total_items * 100).round(2) : (completed.to_f / chunks.size * 100).round(2)
141
+
142
+ progress_mutex.synchronize do
143
+ on_progress.call(
144
+ processed: processed,
145
+ total: total_items || total_size,
146
+ percentage: percentage,
147
+ current_chunk: completed - 1,
148
+ total_chunks: chunks.size,
149
+ success_count: success_count.value,
150
+ failed_count: failed_count.value
151
+ )
152
+ end
153
+ end
100
154
  end
101
155
  end
102
156
 
@@ -107,6 +161,7 @@ module Vectra
107
161
 
108
162
  results.sort_by { |r| r[:index] }
109
163
  end
164
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockLength
110
165
 
111
166
  def aggregate_results(results, total_vectors)
112
167
  errors = results.select { |r| r[:error] }.map { |r| r[:error] }