vectra-client 0.2.2 → 0.3.1
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 +85 -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 +81 -18
- data/docs/assets/style.css +806 -174
- data/docs/examples/index.md +46 -24
- data/docs/guides/monitoring.md +860 -0
- data/docs/guides/performance.md +200 -0
- data/docs/guides/runbooks/cache-issues.md +267 -0
- data/docs/guides/runbooks/high-error-rate.md +152 -0
- data/docs/guides/runbooks/high-latency.md +287 -0
- data/docs/guides/runbooks/pool-exhausted.md +216 -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/circuit_breaker.rb +336 -0
- data/lib/vectra/client.rb +2 -0
- data/lib/vectra/configuration.rb +6 -1
- data/lib/vectra/health_check.rb +254 -0
- data/lib/vectra/instrumentation/honeybadger.rb +128 -0
- data/lib/vectra/instrumentation/sentry.rb +117 -0
- data/lib/vectra/logging.rb +242 -0
- data/lib/vectra/pool.rb +256 -0
- data/lib/vectra/rate_limiter.rb +304 -0
- data/lib/vectra/streaming.rb +153 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +8 -0
- metadata +31 -1
data/lib/vectra/pool.rb
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module Vectra
|
|
6
|
+
# Connection pool with warmup support
|
|
7
|
+
#
|
|
8
|
+
# Provides connection pooling for database providers with configurable
|
|
9
|
+
# pool size, timeout, and connection warmup.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# pool = Vectra::Pool.new(size: 5, timeout: 5) { create_connection }
|
|
13
|
+
# pool.warmup(3) # Pre-create 3 connections
|
|
14
|
+
#
|
|
15
|
+
# pool.with_connection do |conn|
|
|
16
|
+
# conn.execute("SELECT 1")
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class Pool
|
|
20
|
+
class TimeoutError < Vectra::Error; end
|
|
21
|
+
class PoolExhaustedError < Vectra::Error; end
|
|
22
|
+
|
|
23
|
+
attr_reader :size, :timeout
|
|
24
|
+
|
|
25
|
+
# Initialize connection pool
|
|
26
|
+
#
|
|
27
|
+
# @param size [Integer] maximum pool size
|
|
28
|
+
# @param timeout [Integer] checkout timeout in seconds
|
|
29
|
+
# @yield connection factory block
|
|
30
|
+
def initialize(size:, timeout: 5, &block)
|
|
31
|
+
raise ArgumentError, "Connection factory block required" unless block_given?
|
|
32
|
+
|
|
33
|
+
@size = size
|
|
34
|
+
@timeout = timeout
|
|
35
|
+
@factory = block
|
|
36
|
+
@pool = Concurrent::Array.new
|
|
37
|
+
@checked_out = Concurrent::AtomicFixnum.new(0)
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
@condition = ConditionVariable.new
|
|
40
|
+
@shutdown = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Warmup the pool by pre-creating connections
|
|
44
|
+
#
|
|
45
|
+
# @param count [Integer] number of connections to create (default: pool size)
|
|
46
|
+
# @return [Integer] number of connections created
|
|
47
|
+
def warmup(count = nil)
|
|
48
|
+
count ||= size
|
|
49
|
+
count = [count, size].min
|
|
50
|
+
created = 0
|
|
51
|
+
|
|
52
|
+
count.times do
|
|
53
|
+
break if @pool.size >= size
|
|
54
|
+
|
|
55
|
+
conn = create_connection
|
|
56
|
+
if conn
|
|
57
|
+
@pool << conn
|
|
58
|
+
created += 1
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
created
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Execute block with a connection from the pool
|
|
66
|
+
#
|
|
67
|
+
# @yield [connection] the checked out connection
|
|
68
|
+
# @return [Object] result of the block
|
|
69
|
+
def with_connection
|
|
70
|
+
conn = checkout
|
|
71
|
+
begin
|
|
72
|
+
yield conn
|
|
73
|
+
ensure
|
|
74
|
+
checkin(conn)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Checkout a connection from the pool
|
|
79
|
+
#
|
|
80
|
+
# @return [Object] a connection
|
|
81
|
+
# @raise [TimeoutError] if checkout times out
|
|
82
|
+
# @raise [PoolExhaustedError] if pool is exhausted
|
|
83
|
+
def checkout
|
|
84
|
+
raise PoolExhaustedError, "Pool has been shutdown" if @shutdown
|
|
85
|
+
|
|
86
|
+
deadline = Time.now + timeout
|
|
87
|
+
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
loop do
|
|
90
|
+
# Try to get an existing connection
|
|
91
|
+
conn = @pool.pop
|
|
92
|
+
if conn
|
|
93
|
+
@checked_out.increment
|
|
94
|
+
return conn if healthy?(conn)
|
|
95
|
+
|
|
96
|
+
# Connection is unhealthy, discard and try again
|
|
97
|
+
close_connection(conn)
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Try to create a new connection if under limit
|
|
102
|
+
if @checked_out.value + @pool.size < size
|
|
103
|
+
conn = create_connection
|
|
104
|
+
if conn
|
|
105
|
+
@checked_out.increment
|
|
106
|
+
return conn
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Wait for a connection to be returned
|
|
111
|
+
remaining = deadline - Time.now
|
|
112
|
+
raise TimeoutError, "Connection checkout timed out after #{timeout}s" if remaining <= 0
|
|
113
|
+
|
|
114
|
+
@condition.wait(@mutex, remaining)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Return a connection to the pool
|
|
120
|
+
#
|
|
121
|
+
# @param connection [Object] connection to return
|
|
122
|
+
def checkin(connection)
|
|
123
|
+
return if @shutdown
|
|
124
|
+
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
@checked_out.decrement
|
|
127
|
+
if healthy?(connection) && @pool.size < size
|
|
128
|
+
@pool << connection
|
|
129
|
+
else
|
|
130
|
+
close_connection(connection)
|
|
131
|
+
end
|
|
132
|
+
@condition.signal
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Shutdown the pool, closing all connections
|
|
137
|
+
#
|
|
138
|
+
# @return [void]
|
|
139
|
+
def shutdown
|
|
140
|
+
@shutdown = true
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
while (conn = @pool.pop)
|
|
143
|
+
close_connection(conn)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Get pool statistics
|
|
149
|
+
#
|
|
150
|
+
# @return [Hash] pool stats
|
|
151
|
+
def stats
|
|
152
|
+
{
|
|
153
|
+
size: size,
|
|
154
|
+
available: @pool.size,
|
|
155
|
+
checked_out: @checked_out.value,
|
|
156
|
+
total_created: @pool.size + @checked_out.value,
|
|
157
|
+
shutdown: @shutdown
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check if pool is healthy (public method)
|
|
162
|
+
#
|
|
163
|
+
# @return [Boolean]
|
|
164
|
+
def pool_healthy?
|
|
165
|
+
!@shutdown && (@pool.size + @checked_out.value).positive?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# Internal health check for individual connections
|
|
171
|
+
def healthy?(conn)
|
|
172
|
+
return false if conn.nil?
|
|
173
|
+
return true unless conn.respond_to?(:status)
|
|
174
|
+
|
|
175
|
+
# For PG connections, check status. Otherwise assume healthy.
|
|
176
|
+
if defined?(PG::CONNECTION_OK)
|
|
177
|
+
conn.status == PG::CONNECTION_OK
|
|
178
|
+
else
|
|
179
|
+
# If PG not loaded, assume connection is healthy if it exists
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
rescue StandardError
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def create_connection
|
|
187
|
+
@factory.call
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
Vectra.configuration.logger&.error("Pool: Failed to create connection: #{e.message}")
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def close_connection(conn)
|
|
194
|
+
conn.close if conn.respond_to?(:close)
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
Vectra.configuration.logger&.warn("Pool: Error closing connection: #{e.message}")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Pooled connection module for pgvector
|
|
201
|
+
module PooledConnection
|
|
202
|
+
# Get a pooled connection
|
|
203
|
+
#
|
|
204
|
+
# @return [Pool] connection pool
|
|
205
|
+
def connection_pool
|
|
206
|
+
@connection_pool ||= create_pool
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Warmup the connection pool
|
|
210
|
+
#
|
|
211
|
+
# @param count [Integer] number of connections to pre-create
|
|
212
|
+
# @return [Integer] connections created
|
|
213
|
+
def warmup_pool(count = nil)
|
|
214
|
+
connection_pool.warmup(count)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Execute with pooled connection
|
|
218
|
+
#
|
|
219
|
+
# @yield [connection] database connection
|
|
220
|
+
def with_pooled_connection(&)
|
|
221
|
+
connection_pool.with_connection(&)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Shutdown the connection pool
|
|
225
|
+
def shutdown_pool
|
|
226
|
+
@connection_pool&.shutdown
|
|
227
|
+
@connection_pool = nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get pool statistics
|
|
231
|
+
#
|
|
232
|
+
# @return [Hash]
|
|
233
|
+
def pool_stats
|
|
234
|
+
return { status: "not_initialized" } unless @connection_pool
|
|
235
|
+
|
|
236
|
+
connection_pool.stats
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
def create_pool
|
|
242
|
+
pool_size = config.pool_size || 5
|
|
243
|
+
pool_timeout = config.pool_timeout || 5
|
|
244
|
+
|
|
245
|
+
Pool.new(size: pool_size, timeout: pool_timeout) do
|
|
246
|
+
create_raw_connection
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def create_raw_connection
|
|
251
|
+
require "pg"
|
|
252
|
+
conn_params = parse_connection_params
|
|
253
|
+
PG.connect(conn_params)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
# Proactive rate limiter using token bucket algorithm
|
|
5
|
+
#
|
|
6
|
+
# Throttles requests BEFORE sending to prevent rate limit errors from providers.
|
|
7
|
+
# Uses token bucket algorithm for smooth rate limiting with burst support.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# limiter = Vectra::RateLimiter.new(
|
|
11
|
+
# requests_per_second: 10,
|
|
12
|
+
# burst_size: 20
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# limiter.acquire do
|
|
16
|
+
# client.query(...)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example With client wrapper
|
|
20
|
+
# client = Vectra::RateLimitedClient.new(
|
|
21
|
+
# Vectra::Client.new(...),
|
|
22
|
+
# requests_per_second: 10
|
|
23
|
+
# )
|
|
24
|
+
# client.query(...) # Automatically rate limited
|
|
25
|
+
#
|
|
26
|
+
class RateLimiter
|
|
27
|
+
class RateLimitExceededError < Vectra::Error
|
|
28
|
+
attr_reader :wait_time
|
|
29
|
+
|
|
30
|
+
def initialize(wait_time:)
|
|
31
|
+
@wait_time = wait_time
|
|
32
|
+
super("Rate limit exceeded. Retry after #{wait_time.round(2)} seconds")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :requests_per_second, :burst_size
|
|
37
|
+
|
|
38
|
+
# Initialize rate limiter
|
|
39
|
+
#
|
|
40
|
+
# @param requests_per_second [Float] Sustained request rate
|
|
41
|
+
# @param burst_size [Integer] Maximum burst capacity (default: 2x RPS)
|
|
42
|
+
def initialize(requests_per_second:, burst_size: nil)
|
|
43
|
+
@requests_per_second = requests_per_second.to_f
|
|
44
|
+
@burst_size = burst_size || (requests_per_second * 2).to_i
|
|
45
|
+
@tokens = @burst_size.to_f
|
|
46
|
+
@last_refill = Time.now
|
|
47
|
+
@mutex = Mutex.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Acquire a token and execute block
|
|
51
|
+
#
|
|
52
|
+
# @param wait [Boolean] Wait for token if not available (default: true)
|
|
53
|
+
# @param timeout [Float] Maximum wait time in seconds (default: 30)
|
|
54
|
+
# @yield Block to execute after acquiring token
|
|
55
|
+
# @return [Object] Result of block
|
|
56
|
+
# @raise [RateLimitExceededError] If wait is false and no token available
|
|
57
|
+
def acquire(wait: true, timeout: 30, &)
|
|
58
|
+
acquired = try_acquire(wait: wait, timeout: timeout)
|
|
59
|
+
|
|
60
|
+
unless acquired
|
|
61
|
+
wait_time = time_until_token
|
|
62
|
+
raise RateLimitExceededError.new(wait_time: wait_time)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
yield
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Try to acquire a token without blocking
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean] true if token acquired
|
|
71
|
+
def try_acquire(wait: false, timeout: 30)
|
|
72
|
+
deadline = Time.now + timeout
|
|
73
|
+
|
|
74
|
+
loop do
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
refill_tokens
|
|
77
|
+
if @tokens >= 1
|
|
78
|
+
@tokens -= 1
|
|
79
|
+
return true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return false unless wait
|
|
84
|
+
return false if Time.now >= deadline
|
|
85
|
+
|
|
86
|
+
# Wait a bit before retrying
|
|
87
|
+
sleep([time_until_token, 0.1].min)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get current token count
|
|
92
|
+
#
|
|
93
|
+
# @return [Float]
|
|
94
|
+
def available_tokens
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
refill_tokens
|
|
97
|
+
@tokens
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get time until next token is available
|
|
102
|
+
#
|
|
103
|
+
# @return [Float] Seconds until next token
|
|
104
|
+
def time_until_token
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
refill_tokens
|
|
107
|
+
return 0 if @tokens >= 1
|
|
108
|
+
|
|
109
|
+
tokens_needed = 1 - @tokens
|
|
110
|
+
tokens_needed / @requests_per_second
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get rate limiter statistics
|
|
115
|
+
#
|
|
116
|
+
# @return [Hash]
|
|
117
|
+
def stats
|
|
118
|
+
@mutex.synchronize do
|
|
119
|
+
refill_tokens
|
|
120
|
+
{
|
|
121
|
+
requests_per_second: @requests_per_second,
|
|
122
|
+
burst_size: @burst_size,
|
|
123
|
+
available_tokens: @tokens.round(2),
|
|
124
|
+
time_until_token: @tokens >= 1 ? 0 : ((1 - @tokens) / @requests_per_second).round(3)
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Reset the rate limiter to full capacity
|
|
130
|
+
#
|
|
131
|
+
# @return [void]
|
|
132
|
+
def reset!
|
|
133
|
+
@mutex.synchronize do
|
|
134
|
+
@tokens = @burst_size.to_f
|
|
135
|
+
@last_refill = Time.now
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def refill_tokens
|
|
142
|
+
now = Time.now
|
|
143
|
+
elapsed = now - @last_refill
|
|
144
|
+
@last_refill = now
|
|
145
|
+
|
|
146
|
+
# Add tokens based on elapsed time
|
|
147
|
+
new_tokens = elapsed * @requests_per_second
|
|
148
|
+
@tokens = [@tokens + new_tokens, @burst_size.to_f].min
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Client wrapper with automatic rate limiting
|
|
153
|
+
#
|
|
154
|
+
# @example
|
|
155
|
+
# client = Vectra::RateLimitedClient.new(
|
|
156
|
+
# Vectra::Client.new(provider: :pinecone, ...),
|
|
157
|
+
# requests_per_second: 10,
|
|
158
|
+
# burst_size: 20
|
|
159
|
+
# )
|
|
160
|
+
#
|
|
161
|
+
# client.query(...) # Automatically waits if rate limited
|
|
162
|
+
#
|
|
163
|
+
class RateLimitedClient
|
|
164
|
+
RATE_LIMITED_METHODS = [:upsert, :query, :fetch, :update, :delete].freeze
|
|
165
|
+
|
|
166
|
+
attr_reader :client, :limiter
|
|
167
|
+
|
|
168
|
+
# Initialize rate limited client
|
|
169
|
+
#
|
|
170
|
+
# @param client [Client] The underlying Vectra client
|
|
171
|
+
# @param requests_per_second [Float] Request rate limit
|
|
172
|
+
# @param burst_size [Integer] Burst capacity
|
|
173
|
+
# @param wait [Boolean] Wait for rate limit (default: true)
|
|
174
|
+
def initialize(client, requests_per_second:, burst_size: nil, wait: true)
|
|
175
|
+
@client = client
|
|
176
|
+
@limiter = RateLimiter.new(
|
|
177
|
+
requests_per_second: requests_per_second,
|
|
178
|
+
burst_size: burst_size
|
|
179
|
+
)
|
|
180
|
+
@wait = wait
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Rate-limited upsert
|
|
184
|
+
def upsert(...)
|
|
185
|
+
with_rate_limit { client.upsert(...) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Rate-limited query
|
|
189
|
+
def query(...)
|
|
190
|
+
with_rate_limit { client.query(...) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Rate-limited fetch
|
|
194
|
+
def fetch(...)
|
|
195
|
+
with_rate_limit { client.fetch(...) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Rate-limited update
|
|
199
|
+
def update(...)
|
|
200
|
+
with_rate_limit { client.update(...) }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Rate-limited delete
|
|
204
|
+
def delete(...)
|
|
205
|
+
with_rate_limit { client.delete(...) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Pass through other methods without rate limiting
|
|
209
|
+
def method_missing(method, *, **, &)
|
|
210
|
+
if client.respond_to?(method)
|
|
211
|
+
client.public_send(method, *, **, &)
|
|
212
|
+
else
|
|
213
|
+
super
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def respond_to_missing?(method, include_private = false)
|
|
218
|
+
client.respond_to?(method, include_private) || super
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Get rate limiter stats
|
|
222
|
+
#
|
|
223
|
+
# @return [Hash]
|
|
224
|
+
def rate_limit_stats
|
|
225
|
+
limiter.stats
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def with_rate_limit(&)
|
|
231
|
+
limiter.acquire(wait: @wait, &)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Per-provider rate limiter registry
|
|
236
|
+
#
|
|
237
|
+
# @example
|
|
238
|
+
# # Configure rate limits per provider
|
|
239
|
+
# Vectra::RateLimiterRegistry.configure(:pinecone, requests_per_second: 100)
|
|
240
|
+
# Vectra::RateLimiterRegistry.configure(:qdrant, requests_per_second: 50)
|
|
241
|
+
#
|
|
242
|
+
# # Get limiter for provider
|
|
243
|
+
# limiter = Vectra::RateLimiterRegistry[:pinecone]
|
|
244
|
+
# limiter.acquire { ... }
|
|
245
|
+
#
|
|
246
|
+
module RateLimiterRegistry
|
|
247
|
+
class << self
|
|
248
|
+
# Configure rate limiter for provider
|
|
249
|
+
#
|
|
250
|
+
# @param provider [Symbol] Provider name
|
|
251
|
+
# @param requests_per_second [Float] Request rate
|
|
252
|
+
# @param burst_size [Integer] Burst capacity
|
|
253
|
+
# @return [RateLimiter]
|
|
254
|
+
def configure(provider, requests_per_second:, burst_size: nil)
|
|
255
|
+
limiters[provider.to_sym] = RateLimiter.new(
|
|
256
|
+
requests_per_second: requests_per_second,
|
|
257
|
+
burst_size: burst_size
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get limiter for provider
|
|
262
|
+
#
|
|
263
|
+
# @param provider [Symbol] Provider name
|
|
264
|
+
# @return [RateLimiter, nil]
|
|
265
|
+
def [](provider)
|
|
266
|
+
limiters[provider.to_sym]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Get all configured limiters
|
|
270
|
+
#
|
|
271
|
+
# @return [Hash<Symbol, RateLimiter>]
|
|
272
|
+
def all
|
|
273
|
+
limiters.dup
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Get stats for all limiters
|
|
277
|
+
#
|
|
278
|
+
# @return [Hash<Symbol, Hash>]
|
|
279
|
+
def stats
|
|
280
|
+
limiters.transform_values(&:stats)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Reset all limiters
|
|
284
|
+
#
|
|
285
|
+
# @return [void]
|
|
286
|
+
def reset_all!
|
|
287
|
+
limiters.each_value(&:reset!)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Clear all limiters
|
|
291
|
+
#
|
|
292
|
+
# @return [void]
|
|
293
|
+
def clear!
|
|
294
|
+
@limiters = {}
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def limiters
|
|
300
|
+
@limiters ||= {}
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
# Streaming query results for large datasets
|
|
5
|
+
#
|
|
6
|
+
# Provides lazy enumeration over query results with automatic pagination,
|
|
7
|
+
# reducing memory usage for large result sets.
|
|
8
|
+
#
|
|
9
|
+
# @example Stream through results
|
|
10
|
+
# stream = Vectra::Streaming.new(client)
|
|
11
|
+
# stream.query_each(index: 'my-index', vector: query_vec, total: 1000) do |match|
|
|
12
|
+
# process(match)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example As lazy enumerator
|
|
16
|
+
# results = stream.query_stream(index: 'my-index', vector: query_vec, total: 1000)
|
|
17
|
+
# results.take(50).each { |m| puts m.id }
|
|
18
|
+
#
|
|
19
|
+
class Streaming
|
|
20
|
+
DEFAULT_PAGE_SIZE = 100
|
|
21
|
+
|
|
22
|
+
attr_reader :client, :page_size
|
|
23
|
+
|
|
24
|
+
# Initialize streaming query handler
|
|
25
|
+
#
|
|
26
|
+
# @param client [Client] the Vectra client
|
|
27
|
+
# @param page_size [Integer] results per page (default: 100)
|
|
28
|
+
def initialize(client, page_size: DEFAULT_PAGE_SIZE)
|
|
29
|
+
@client = client
|
|
30
|
+
@page_size = [page_size, 1].max
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Stream query results with a block
|
|
34
|
+
#
|
|
35
|
+
# @param index [String] the index name
|
|
36
|
+
# @param vector [Array<Float>] query vector
|
|
37
|
+
# @param total [Integer] total results to fetch
|
|
38
|
+
# @param namespace [String, nil] optional namespace
|
|
39
|
+
# @param filter [Hash, nil] metadata filter
|
|
40
|
+
# @yield [Match] each match result
|
|
41
|
+
# @return [Integer] total results yielded
|
|
42
|
+
def query_each(index:, vector:, total:, namespace: nil, filter: nil, &block)
|
|
43
|
+
return 0 unless block_given?
|
|
44
|
+
|
|
45
|
+
count = 0
|
|
46
|
+
query_stream(index: index, vector: vector, total: total, namespace: namespace, filter: filter).each do |match|
|
|
47
|
+
block.call(match)
|
|
48
|
+
count += 1
|
|
49
|
+
end
|
|
50
|
+
count
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create a lazy enumerator for streaming results
|
|
54
|
+
#
|
|
55
|
+
# @param index [String] the index name
|
|
56
|
+
# @param vector [Array<Float>] query vector
|
|
57
|
+
# @param total [Integer] total results to fetch
|
|
58
|
+
# @param namespace [String, nil] optional namespace
|
|
59
|
+
# @param filter [Hash, nil] metadata filter
|
|
60
|
+
# @return [Enumerator::Lazy] lazy enumerator of results
|
|
61
|
+
def query_stream(index:, vector:, total:, namespace: nil, filter: nil)
|
|
62
|
+
Enumerator.new do |yielder|
|
|
63
|
+
fetched = 0
|
|
64
|
+
seen_ids = Set.new
|
|
65
|
+
|
|
66
|
+
while fetched < total
|
|
67
|
+
batch_size = [page_size, total - fetched].min
|
|
68
|
+
|
|
69
|
+
result = client.query(
|
|
70
|
+
index: index,
|
|
71
|
+
vector: vector,
|
|
72
|
+
top_k: batch_size,
|
|
73
|
+
namespace: namespace,
|
|
74
|
+
filter: filter,
|
|
75
|
+
include_metadata: true
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
break if result.empty?
|
|
79
|
+
|
|
80
|
+
result.each do |match|
|
|
81
|
+
# Skip duplicates (some providers may return overlapping results)
|
|
82
|
+
next if seen_ids.include?(match.id)
|
|
83
|
+
|
|
84
|
+
seen_ids.add(match.id)
|
|
85
|
+
yielder << match
|
|
86
|
+
fetched += 1
|
|
87
|
+
|
|
88
|
+
break if fetched >= total
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# If we got fewer results than requested, we've exhausted the index
|
|
92
|
+
break if result.size < batch_size
|
|
93
|
+
end
|
|
94
|
+
end.lazy
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Scan all vectors in an index (provider-dependent)
|
|
98
|
+
#
|
|
99
|
+
# @param index [String] the index name
|
|
100
|
+
# @param namespace [String, nil] optional namespace
|
|
101
|
+
# @param batch_size [Integer] IDs per batch
|
|
102
|
+
# @yield [Vector] each vector
|
|
103
|
+
# @return [Integer] total vectors scanned
|
|
104
|
+
# @note Not all providers support efficient scanning
|
|
105
|
+
def scan_all(index:, namespace: nil, batch_size: 1000)
|
|
106
|
+
return 0 unless block_given?
|
|
107
|
+
|
|
108
|
+
count = 0
|
|
109
|
+
offset = 0
|
|
110
|
+
|
|
111
|
+
loop do
|
|
112
|
+
# This is a simplified scan - actual implementation depends on provider
|
|
113
|
+
stats = client.stats(index: index, namespace: namespace)
|
|
114
|
+
total = stats[:total_vector_count] || 0
|
|
115
|
+
|
|
116
|
+
break if offset >= total
|
|
117
|
+
|
|
118
|
+
# Fetch IDs in batches (this is provider-specific)
|
|
119
|
+
# For now, we return what we can
|
|
120
|
+
break if offset.positive? # Only one iteration for basic implementation
|
|
121
|
+
|
|
122
|
+
offset += batch_size
|
|
123
|
+
count = total
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
count
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Streaming result wrapper with additional metadata
|
|
131
|
+
class StreamingResult
|
|
132
|
+
include Enumerable
|
|
133
|
+
|
|
134
|
+
attr_reader :enumerator, :metadata
|
|
135
|
+
|
|
136
|
+
def initialize(enumerator, metadata = {})
|
|
137
|
+
@enumerator = enumerator
|
|
138
|
+
@metadata = metadata
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def each(&)
|
|
142
|
+
enumerator.each(&)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def take(n)
|
|
146
|
+
enumerator.take(n)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def to_a
|
|
150
|
+
enumerator.to_a
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/vectra/version.rb
CHANGED