vectra-client 0.1.2 → 0.2.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 +23 -3
- data/CHANGELOG.md +23 -0
- data/IMPLEMENTATION_GUIDE.md +686 -0
- data/NEW_FEATURES_v0.2.0.md +459 -0
- data/RELEASE_CHECKLIST_v0.2.0.md +383 -0
- data/Rakefile +12 -0
- data/USAGE_EXAMPLES.md +787 -0
- data/benchmarks/batch_operations_benchmark.rb +117 -0
- data/benchmarks/connection_pooling_benchmark.rb +93 -0
- data/examples/active_record_demo.rb +227 -0
- data/examples/instrumentation_demo.rb +157 -0
- data/lib/generators/vectra/install_generator.rb +115 -0
- data/lib/generators/vectra/templates/enable_pgvector_extension.rb +11 -0
- data/lib/generators/vectra/templates/vectra.rb +79 -0
- data/lib/vectra/active_record.rb +195 -0
- data/lib/vectra/client.rb +60 -22
- data/lib/vectra/configuration.rb +6 -1
- data/lib/vectra/instrumentation/datadog.rb +82 -0
- data/lib/vectra/instrumentation/new_relic.rb +70 -0
- data/lib/vectra/instrumentation.rb +143 -0
- data/lib/vectra/providers/pgvector/connection.rb +5 -1
- data/lib/vectra/retry.rb +156 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +11 -0
- metadata +45 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
# Instrumentation and observability hooks
|
|
5
|
+
#
|
|
6
|
+
# Provides hooks for monitoring tools like New Relic, Datadog, and custom loggers.
|
|
7
|
+
# Records metrics for all vector operations including duration, result counts, and errors.
|
|
8
|
+
#
|
|
9
|
+
# @example Enable instrumentation
|
|
10
|
+
# Vectra.configure do |config|
|
|
11
|
+
# config.instrumentation = true
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Custom instrumentation handler
|
|
15
|
+
# Vectra.on_operation do |event|
|
|
16
|
+
# puts "#{event.operation} took #{event.duration}ms"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
module Instrumentation
|
|
20
|
+
# Event object passed to instrumentation handlers
|
|
21
|
+
#
|
|
22
|
+
# @attr_reader [Symbol] operation The operation type (:upsert, :query, :fetch, etc.)
|
|
23
|
+
# @attr_reader [Symbol] provider The provider name (:pinecone, :pgvector, etc.)
|
|
24
|
+
# @attr_reader [String] index The index/table name
|
|
25
|
+
# @attr_reader [Float] duration Duration in milliseconds
|
|
26
|
+
# @attr_reader [Hash] metadata Additional operation metadata
|
|
27
|
+
# @attr_reader [Exception, nil] error Exception if operation failed
|
|
28
|
+
class Event
|
|
29
|
+
attr_reader :operation, :provider, :index, :duration, :metadata, :error
|
|
30
|
+
|
|
31
|
+
def initialize(operation:, provider:, index:, duration:, metadata: {}, error: nil)
|
|
32
|
+
@operation = operation
|
|
33
|
+
@provider = provider
|
|
34
|
+
@index = index
|
|
35
|
+
@duration = duration
|
|
36
|
+
@metadata = metadata
|
|
37
|
+
@error = error
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if operation succeeded
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def success?
|
|
44
|
+
error.nil?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if operation failed
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def failure?
|
|
51
|
+
!success?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
# Register an instrumentation handler
|
|
57
|
+
#
|
|
58
|
+
# The block will be called for every vector operation with an Event object.
|
|
59
|
+
#
|
|
60
|
+
# @yield [event] The instrumentation event
|
|
61
|
+
# @yieldparam event [Event] Event details
|
|
62
|
+
#
|
|
63
|
+
# @example
|
|
64
|
+
# Vectra::Instrumentation.on_operation do |event|
|
|
65
|
+
# StatsD.timing("vectra.#{event.operation}", event.duration)
|
|
66
|
+
# StatsD.increment("vectra.#{event.operation}.#{event.success? ? 'success' : 'error'}")
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
def on_operation(&block)
|
|
70
|
+
handlers << block
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Instrument a vector operation
|
|
74
|
+
#
|
|
75
|
+
# @param operation [Symbol] Operation name
|
|
76
|
+
# @param provider [Symbol] Provider name
|
|
77
|
+
# @param index [String] Index name
|
|
78
|
+
# @param metadata [Hash] Additional metadata
|
|
79
|
+
# @yield The operation to instrument
|
|
80
|
+
# @return [Object] The result of the block
|
|
81
|
+
#
|
|
82
|
+
# @api private
|
|
83
|
+
def instrument(operation:, provider:, index:, metadata: {})
|
|
84
|
+
return yield unless enabled?
|
|
85
|
+
|
|
86
|
+
start_time = Time.now
|
|
87
|
+
error = nil
|
|
88
|
+
result = nil
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
result = yield
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
error = e
|
|
94
|
+
raise
|
|
95
|
+
ensure
|
|
96
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
|
97
|
+
|
|
98
|
+
event = Event.new(
|
|
99
|
+
operation: operation,
|
|
100
|
+
provider: provider,
|
|
101
|
+
index: index,
|
|
102
|
+
duration: duration,
|
|
103
|
+
metadata: metadata,
|
|
104
|
+
error: error
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
notify_handlers(event)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
result
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if instrumentation is enabled
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean]
|
|
116
|
+
def enabled?
|
|
117
|
+
Vectra.configuration.instrumentation
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Clear all handlers (useful for testing)
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
def clear_handlers!
|
|
124
|
+
@handlers = []
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def handlers
|
|
130
|
+
@handlers ||= []
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def notify_handlers(event)
|
|
134
|
+
handlers.each do |handler|
|
|
135
|
+
handler.call(event)
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
# Don't let instrumentation errors crash the app
|
|
138
|
+
warn "Vectra instrumentation handler error: #{e.message}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -5,6 +5,8 @@ module Vectra
|
|
|
5
5
|
class Pgvector < Base
|
|
6
6
|
# Connection management for pgvector provider
|
|
7
7
|
module Connection
|
|
8
|
+
include Vectra::Retry
|
|
9
|
+
|
|
8
10
|
private
|
|
9
11
|
|
|
10
12
|
# Get or create database connection
|
|
@@ -49,7 +51,9 @@ module Vectra
|
|
|
49
51
|
# Execute SQL with parameters
|
|
50
52
|
def execute(sql, params = [])
|
|
51
53
|
log_debug("Executing SQL", { sql: sql, params: params })
|
|
52
|
-
|
|
54
|
+
with_retry do
|
|
55
|
+
connection.exec_params(sql, params)
|
|
56
|
+
end
|
|
53
57
|
rescue PG::Error => e
|
|
54
58
|
handle_pg_error(e)
|
|
55
59
|
end
|
data/lib/vectra/retry.rb
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "connection_pool"
|
|
4
|
+
|
|
5
|
+
module Vectra
|
|
6
|
+
# Retry helper for transient errors
|
|
7
|
+
#
|
|
8
|
+
# Provides exponential backoff retry logic for database operations.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# include Vectra::Retry
|
|
12
|
+
#
|
|
13
|
+
# with_retry(max_attempts: 3) do
|
|
14
|
+
# connection.exec_params(sql, params)
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
module Retry
|
|
18
|
+
# Errors that should be retried
|
|
19
|
+
RETRYABLE_PG_ERRORS = [
|
|
20
|
+
"PG::ConnectionBad",
|
|
21
|
+
"PG::UnableToSend",
|
|
22
|
+
"PG::AdminShutdown",
|
|
23
|
+
"PG::CrashShutdown",
|
|
24
|
+
"PG::CannotConnectNow",
|
|
25
|
+
"PG::TooManyConnections",
|
|
26
|
+
"PG::SerializationFailure",
|
|
27
|
+
"PG::DeadlockDetected"
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# Execute block with retry logic
|
|
31
|
+
#
|
|
32
|
+
# @param max_attempts [Integer] Maximum number of attempts (default: from config)
|
|
33
|
+
# @param base_delay [Float] Initial delay in seconds (default: from config)
|
|
34
|
+
# @param max_delay [Float] Maximum delay in seconds (default: 30)
|
|
35
|
+
# @param backoff_factor [Float] Multiplier for each retry (default: 2)
|
|
36
|
+
# @param jitter [Boolean] Add randomness to delay (default: true)
|
|
37
|
+
# @yield The block to execute with retry logic
|
|
38
|
+
# @return [Object] The result of the block
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# result = with_retry(max_attempts: 5) do
|
|
42
|
+
# perform_database_operation
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
def with_retry(max_attempts: nil, base_delay: nil, max_delay: 30, backoff_factor: 2, jitter: true, &block)
|
|
46
|
+
max_attempts ||= config.max_retries
|
|
47
|
+
base_delay ||= config.retry_delay
|
|
48
|
+
|
|
49
|
+
attempt = 0
|
|
50
|
+
last_error = nil
|
|
51
|
+
|
|
52
|
+
loop do
|
|
53
|
+
attempt += 1
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
return block.call
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
last_error = e
|
|
59
|
+
|
|
60
|
+
# Don't retry if not retryable or out of attempts
|
|
61
|
+
should_retry = retryable_error?(e) && attempt < max_attempts
|
|
62
|
+
|
|
63
|
+
unless should_retry
|
|
64
|
+
log_error("Operation failed after #{attempt} attempts", e)
|
|
65
|
+
raise
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Calculate delay with exponential backoff
|
|
69
|
+
delay = calculate_delay(
|
|
70
|
+
attempt: attempt,
|
|
71
|
+
base_delay: base_delay,
|
|
72
|
+
max_delay: max_delay,
|
|
73
|
+
backoff_factor: backoff_factor,
|
|
74
|
+
jitter: jitter
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
log_retry(attempt, max_attempts, delay, e)
|
|
78
|
+
sleep(delay)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Check if error should be retried
|
|
86
|
+
#
|
|
87
|
+
# @param error [Exception] The error to check
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def retryable_error?(error)
|
|
90
|
+
error_class = error.class.name
|
|
91
|
+
|
|
92
|
+
# Check if it's a retryable PG error
|
|
93
|
+
return true if RETRYABLE_PG_ERRORS.include?(error_class)
|
|
94
|
+
|
|
95
|
+
# Check if it's a connection pool timeout
|
|
96
|
+
return true if error.instance_of?(::ConnectionPool::TimeoutError)
|
|
97
|
+
|
|
98
|
+
# Check message for specific patterns
|
|
99
|
+
error_message = error.message.downcase
|
|
100
|
+
error_message.include?("timeout") ||
|
|
101
|
+
error_message.include?("connection") ||
|
|
102
|
+
error_message.include?("temporary")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Calculate exponential backoff delay
|
|
106
|
+
#
|
|
107
|
+
# @param attempt [Integer] Current attempt number
|
|
108
|
+
# @param base_delay [Float] Base delay in seconds
|
|
109
|
+
# @param max_delay [Float] Maximum delay in seconds
|
|
110
|
+
# @param backoff_factor [Float] Multiplier for each retry
|
|
111
|
+
# @param jitter [Boolean] Add randomness
|
|
112
|
+
# @return [Float] Delay in seconds
|
|
113
|
+
def calculate_delay(attempt:, base_delay:, max_delay:, backoff_factor:, jitter:)
|
|
114
|
+
# Exponential backoff: base_delay * (backoff_factor ^ (attempt - 1))
|
|
115
|
+
delay = base_delay * (backoff_factor**(attempt - 1))
|
|
116
|
+
|
|
117
|
+
# Cap at max_delay
|
|
118
|
+
delay = [delay, max_delay].min
|
|
119
|
+
|
|
120
|
+
# Add jitter (±25% randomness)
|
|
121
|
+
if jitter
|
|
122
|
+
jitter_amount = delay * 0.25
|
|
123
|
+
delay += rand(-jitter_amount..jitter_amount)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
delay.clamp(base_delay, max_delay)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Log retry attempt
|
|
130
|
+
#
|
|
131
|
+
# @param attempt [Integer] Current attempt
|
|
132
|
+
# @param max_attempts [Integer] Maximum attempts
|
|
133
|
+
# @param delay [Float] Delay before next attempt
|
|
134
|
+
# @param error [Exception] The error that triggered retry
|
|
135
|
+
def log_retry(attempt, max_attempts, delay, error)
|
|
136
|
+
return unless config.logger
|
|
137
|
+
|
|
138
|
+
config.logger.warn(
|
|
139
|
+
"[Vectra] Retry attempt #{attempt}/#{max_attempts} after error: " \
|
|
140
|
+
"#{error.class} - #{error.message}. Waiting #{delay.round(2)}s..."
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Log final error
|
|
145
|
+
#
|
|
146
|
+
# @param message [String] Error message
|
|
147
|
+
# @param error [Exception] The error
|
|
148
|
+
def log_error(message, error)
|
|
149
|
+
return unless config.logger
|
|
150
|
+
|
|
151
|
+
config.logger.error(
|
|
152
|
+
"[Vectra] #{message}: #{error.class} - #{error.message}"
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/vectra/version.rb
CHANGED
data/lib/vectra.rb
CHANGED
|
@@ -5,6 +5,9 @@ require_relative "vectra/errors"
|
|
|
5
5
|
require_relative "vectra/configuration"
|
|
6
6
|
require_relative "vectra/vector"
|
|
7
7
|
require_relative "vectra/query_result"
|
|
8
|
+
require_relative "vectra/instrumentation"
|
|
9
|
+
require_relative "vectra/retry"
|
|
10
|
+
require_relative "vectra/active_record"
|
|
8
11
|
require_relative "vectra/providers/base"
|
|
9
12
|
require_relative "vectra/providers/pinecone"
|
|
10
13
|
require_relative "vectra/providers/qdrant"
|
|
@@ -53,6 +56,14 @@ require_relative "vectra/client"
|
|
|
53
56
|
#
|
|
54
57
|
module Vectra
|
|
55
58
|
class << self
|
|
59
|
+
# Register an instrumentation handler
|
|
60
|
+
#
|
|
61
|
+
# @yield [event] The instrumentation event
|
|
62
|
+
# @see Instrumentation.on_operation
|
|
63
|
+
def on_operation(&)
|
|
64
|
+
Instrumentation.on_operation(&)
|
|
65
|
+
end
|
|
66
|
+
|
|
56
67
|
# Create a new client with the given options
|
|
57
68
|
#
|
|
58
69
|
# @param options [Hash] client options
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vectra-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mijo Kristo
|
|
@@ -37,6 +37,34 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: activerecord
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: sqlite3
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.4'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.4'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
69
|
name: pg
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -179,14 +207,29 @@ files:
|
|
|
179
207
|
- CHANGELOG.md
|
|
180
208
|
- CODE_OF_CONDUCT.md
|
|
181
209
|
- CONTRIBUTING.md
|
|
210
|
+
- IMPLEMENTATION_GUIDE.md
|
|
182
211
|
- LICENSE
|
|
212
|
+
- NEW_FEATURES_v0.2.0.md
|
|
183
213
|
- README.md
|
|
214
|
+
- RELEASE_CHECKLIST_v0.2.0.md
|
|
184
215
|
- Rakefile
|
|
185
216
|
- SECURITY.md
|
|
217
|
+
- USAGE_EXAMPLES.md
|
|
218
|
+
- benchmarks/batch_operations_benchmark.rb
|
|
219
|
+
- benchmarks/connection_pooling_benchmark.rb
|
|
220
|
+
- examples/active_record_demo.rb
|
|
221
|
+
- examples/instrumentation_demo.rb
|
|
222
|
+
- lib/generators/vectra/install_generator.rb
|
|
223
|
+
- lib/generators/vectra/templates/enable_pgvector_extension.rb
|
|
224
|
+
- lib/generators/vectra/templates/vectra.rb
|
|
186
225
|
- lib/vectra.rb
|
|
226
|
+
- lib/vectra/active_record.rb
|
|
187
227
|
- lib/vectra/client.rb
|
|
188
228
|
- lib/vectra/configuration.rb
|
|
189
229
|
- lib/vectra/errors.rb
|
|
230
|
+
- lib/vectra/instrumentation.rb
|
|
231
|
+
- lib/vectra/instrumentation/datadog.rb
|
|
232
|
+
- lib/vectra/instrumentation/new_relic.rb
|
|
190
233
|
- lib/vectra/providers/base.rb
|
|
191
234
|
- lib/vectra/providers/pgvector.rb
|
|
192
235
|
- lib/vectra/providers/pgvector/connection.rb
|
|
@@ -196,6 +239,7 @@ files:
|
|
|
196
239
|
- lib/vectra/providers/qdrant.rb
|
|
197
240
|
- lib/vectra/providers/weaviate.rb
|
|
198
241
|
- lib/vectra/query_result.rb
|
|
242
|
+
- lib/vectra/retry.rb
|
|
199
243
|
- lib/vectra/vector.rb
|
|
200
244
|
- lib/vectra/version.rb
|
|
201
245
|
homepage: https://github.com/stokry/vectra
|