vectra-client 1.0.7 → 1.1.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/CHANGELOG.md +4 -0
- data/README.md +53 -0
- data/docs/_layouts/home.html +1 -1
- data/docs/api/cheatsheet.md +43 -0
- data/docs/api/methods.md +30 -6
- data/docs/api/overview.md +6 -0
- data/docs/guides/middleware.md +324 -0
- data/examples/middleware_demo.rb +103 -0
- data/lib/vectra/active_record.rb +52 -1
- data/lib/vectra/cache.rb +49 -0
- data/lib/vectra/client.rb +148 -28
- data/lib/vectra/health_check.rb +4 -2
- data/lib/vectra/middleware/base.rb +97 -0
- data/lib/vectra/middleware/cost_tracker.rb +121 -0
- data/lib/vectra/middleware/instrumentation.rb +44 -0
- data/lib/vectra/middleware/logging.rb +62 -0
- data/lib/vectra/middleware/pii_redaction.rb +65 -0
- data/lib/vectra/middleware/request.rb +62 -0
- data/lib/vectra/middleware/response.rb +65 -0
- data/lib/vectra/middleware/retry.rb +103 -0
- data/lib/vectra/middleware/stack.rb +74 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +9 -0
- metadata +12 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Cost tracking middleware for monitoring API usage costs
|
|
6
|
+
#
|
|
7
|
+
# Tracks estimated costs per operation based on provider pricing.
|
|
8
|
+
# Costs are stored in response metadata and can be aggregated.
|
|
9
|
+
#
|
|
10
|
+
# @example With default pricing
|
|
11
|
+
# Vectra::Client.use Vectra::Middleware::CostTracker
|
|
12
|
+
#
|
|
13
|
+
# @example With custom pricing
|
|
14
|
+
# custom_pricing = {
|
|
15
|
+
# pinecone: { read: 0.0001, write: 0.0002 },
|
|
16
|
+
# qdrant: { read: 0.00005, write: 0.0001 }
|
|
17
|
+
# }
|
|
18
|
+
# Vectra::Client.use Vectra::Middleware::CostTracker, pricing: custom_pricing
|
|
19
|
+
#
|
|
20
|
+
# @example With cost callback
|
|
21
|
+
# Vectra::Client.use Vectra::Middleware::CostTracker, on_cost: ->(event) {
|
|
22
|
+
# puts "Cost: $#{event[:cost_usd]} for #{event[:operation]}"
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
class CostTracker < Base
|
|
26
|
+
# Default pricing per operation (in USD)
|
|
27
|
+
# These are estimated values - check provider pricing for actual costs
|
|
28
|
+
DEFAULT_PRICING = {
|
|
29
|
+
pinecone: { read: 0.0001, write: 0.0002 },
|
|
30
|
+
qdrant: { read: 0.00005, write: 0.0001 },
|
|
31
|
+
weaviate: { read: 0.00008, write: 0.00015 },
|
|
32
|
+
pgvector: { read: 0.0, write: 0.0 }, # Self-hosted, no API costs
|
|
33
|
+
memory: { read: 0.0, write: 0.0 } # In-memory, no costs
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# @param pricing [Hash] Custom pricing structure
|
|
37
|
+
# @param on_cost [Proc] Callback to invoke with cost events
|
|
38
|
+
def initialize(pricing: DEFAULT_PRICING, on_cost: nil)
|
|
39
|
+
super()
|
|
40
|
+
@pricing = pricing
|
|
41
|
+
@on_cost = on_cost
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def after(request, response)
|
|
45
|
+
return unless response.success?
|
|
46
|
+
|
|
47
|
+
provider = request.provider || :unknown
|
|
48
|
+
operation_type = write_operation?(request.operation) ? :write : :read
|
|
49
|
+
|
|
50
|
+
cost = calculate_cost(provider, operation_type, request)
|
|
51
|
+
response.metadata[:cost_usd] = cost
|
|
52
|
+
|
|
53
|
+
# Invoke callback if provided
|
|
54
|
+
return unless @on_cost
|
|
55
|
+
|
|
56
|
+
@on_cost.call(
|
|
57
|
+
operation: request.operation,
|
|
58
|
+
provider: provider,
|
|
59
|
+
index: request.index,
|
|
60
|
+
cost_usd: cost,
|
|
61
|
+
timestamp: Time.now
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Check if operation is a write operation
|
|
68
|
+
#
|
|
69
|
+
# @param operation [Symbol] The operation type
|
|
70
|
+
# @return [Boolean] true if write operation
|
|
71
|
+
def write_operation?(operation)
|
|
72
|
+
[:upsert, :delete, :update, :create_index, :delete_index].include?(operation)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Calculate cost for operation
|
|
76
|
+
#
|
|
77
|
+
# @param provider [Symbol] Provider name
|
|
78
|
+
# @param operation_type [Symbol] :read or :write
|
|
79
|
+
# @param request [Request] The request object
|
|
80
|
+
# @return [Float] Cost in USD
|
|
81
|
+
def calculate_cost(provider, operation_type, request)
|
|
82
|
+
rate = @pricing.dig(provider, operation_type) || 0.0
|
|
83
|
+
multiplier = operation_multiplier(request)
|
|
84
|
+
rate * multiplier
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Calculate multiplier for operation based on batch size
|
|
88
|
+
#
|
|
89
|
+
# @param request [Request] The request object
|
|
90
|
+
# @return [Integer, Float] Multiplier for the base rate
|
|
91
|
+
def operation_multiplier(request)
|
|
92
|
+
return 100 if delete_all?(request)
|
|
93
|
+
|
|
94
|
+
case request.operation
|
|
95
|
+
when :upsert
|
|
96
|
+
collection_size(request.params[:vectors])
|
|
97
|
+
when :fetch, :delete
|
|
98
|
+
collection_size(request.params[:ids])
|
|
99
|
+
else
|
|
100
|
+
1 # Includes :query and all other operations
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if request is a delete_all operation
|
|
105
|
+
#
|
|
106
|
+
# @param request [Request] The request object
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
def delete_all?(request)
|
|
109
|
+
request.operation == :delete && request.params[:delete_all]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Safely compute collection size with a default of 1
|
|
113
|
+
#
|
|
114
|
+
# @param collection [Enumerable, nil]
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
def collection_size(collection)
|
|
117
|
+
collection&.size || 1
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Instrumentation middleware for metrics and monitoring
|
|
6
|
+
#
|
|
7
|
+
# Emits instrumentation events for all operations, compatible with
|
|
8
|
+
# Vectra's existing instrumentation system.
|
|
9
|
+
#
|
|
10
|
+
# @example Enable instrumentation middleware
|
|
11
|
+
# Vectra::Client.use Vectra::Middleware::Instrumentation
|
|
12
|
+
#
|
|
13
|
+
# @example With custom event handler
|
|
14
|
+
# Vectra.on_operation do |event|
|
|
15
|
+
# puts "Operation: #{event[:operation]}, Duration: #{event[:duration_ms]}ms"
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Vectra::Client.use Vectra::Middleware::Instrumentation
|
|
19
|
+
#
|
|
20
|
+
class Instrumentation < Base
|
|
21
|
+
def call(request, app)
|
|
22
|
+
start_time = Time.now
|
|
23
|
+
|
|
24
|
+
response = app.call(request)
|
|
25
|
+
|
|
26
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
27
|
+
|
|
28
|
+
# Emit instrumentation event
|
|
29
|
+
Vectra::Instrumentation.instrument(
|
|
30
|
+
operation: request.operation,
|
|
31
|
+
provider: request.provider,
|
|
32
|
+
index: request.index,
|
|
33
|
+
namespace: request.namespace,
|
|
34
|
+
duration_ms: duration_ms,
|
|
35
|
+
success: response.success?,
|
|
36
|
+
error: response.error&.class&.name,
|
|
37
|
+
metadata: response.metadata
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
response
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Logging middleware for tracking operations
|
|
6
|
+
#
|
|
7
|
+
# Logs before and after each operation, including timing information.
|
|
8
|
+
#
|
|
9
|
+
# @example With default logger
|
|
10
|
+
# Vectra::Client.use Vectra::Middleware::Logging
|
|
11
|
+
#
|
|
12
|
+
# @example With custom logger
|
|
13
|
+
# logger = Logger.new($stdout)
|
|
14
|
+
# Vectra::Client.use Vectra::Middleware::Logging, logger: logger
|
|
15
|
+
#
|
|
16
|
+
# @example Per-client logging
|
|
17
|
+
# client = Vectra::Client.new(
|
|
18
|
+
# provider: :qdrant,
|
|
19
|
+
# middleware: [Vectra::Middleware::Logging]
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
class Logging < Base
|
|
23
|
+
def initialize(logger: nil)
|
|
24
|
+
super()
|
|
25
|
+
@logger = logger || Vectra.configuration.logger
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def before(request)
|
|
29
|
+
return unless @logger
|
|
30
|
+
|
|
31
|
+
@start_time = Time.now
|
|
32
|
+
@logger.info(
|
|
33
|
+
"[Vectra] #{request.operation.upcase} " \
|
|
34
|
+
"index=#{request.index} " \
|
|
35
|
+
"namespace=#{request.namespace || 'default'}"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def after(request, response)
|
|
40
|
+
return unless @logger
|
|
41
|
+
return unless @start_time
|
|
42
|
+
|
|
43
|
+
duration_ms = ((Time.now - @start_time) * 1000).round(2)
|
|
44
|
+
response.metadata[:duration_ms] = duration_ms
|
|
45
|
+
|
|
46
|
+
if response.success?
|
|
47
|
+
@logger.info("[Vectra] ✅ #{request.operation} completed in #{duration_ms}ms")
|
|
48
|
+
else
|
|
49
|
+
@logger.error("[Vectra] ❌ #{request.operation} failed: #{response.error.message}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def on_error(request, error)
|
|
54
|
+
return unless @logger
|
|
55
|
+
|
|
56
|
+
@logger.error(
|
|
57
|
+
"[Vectra] 💥 #{request.operation} exception: #{error.class} - #{error.message}"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# PII Redaction middleware for protecting sensitive data
|
|
6
|
+
#
|
|
7
|
+
# Automatically redacts Personally Identifiable Information (PII) from
|
|
8
|
+
# metadata before upserting to vector databases.
|
|
9
|
+
#
|
|
10
|
+
# @example With default patterns (email, phone, SSN)
|
|
11
|
+
# Vectra::Client.use Vectra::Middleware::PIIRedaction
|
|
12
|
+
#
|
|
13
|
+
# @example With custom patterns
|
|
14
|
+
# custom_patterns = {
|
|
15
|
+
# credit_card: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/,
|
|
16
|
+
# api_key: /sk-[a-zA-Z0-9]{32}/
|
|
17
|
+
# }
|
|
18
|
+
# Vectra::Client.use Vectra::Middleware::PIIRedaction, patterns: custom_patterns
|
|
19
|
+
#
|
|
20
|
+
class PIIRedaction < Base
|
|
21
|
+
# Default PII patterns
|
|
22
|
+
DEFAULT_PATTERNS = {
|
|
23
|
+
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
|
|
24
|
+
phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/,
|
|
25
|
+
ssn: /\b\d{3}-\d{2}-\d{4}\b/,
|
|
26
|
+
credit_card: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def initialize(patterns: DEFAULT_PATTERNS)
|
|
30
|
+
super()
|
|
31
|
+
@patterns = patterns
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def before(request)
|
|
35
|
+
return unless request.operation == :upsert
|
|
36
|
+
return unless request.params[:vectors]
|
|
37
|
+
|
|
38
|
+
# Redact PII from metadata in all vectors
|
|
39
|
+
request.params[:vectors].each do |vector|
|
|
40
|
+
next unless vector[:metadata]
|
|
41
|
+
|
|
42
|
+
vector[:metadata] = redact_metadata(vector[:metadata])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Redact PII from metadata hash
|
|
49
|
+
#
|
|
50
|
+
# @param metadata [Hash] Metadata to redact
|
|
51
|
+
# @return [Hash] Redacted metadata
|
|
52
|
+
def redact_metadata(metadata)
|
|
53
|
+
metadata.transform_values do |value|
|
|
54
|
+
next value unless value.is_a?(String)
|
|
55
|
+
|
|
56
|
+
redacted = value.dup
|
|
57
|
+
@patterns.each do |type, pattern|
|
|
58
|
+
redacted.gsub!(pattern, "[REDACTED_#{type.upcase}]")
|
|
59
|
+
end
|
|
60
|
+
redacted
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Request object passed through middleware chain
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# request = Request.new(
|
|
9
|
+
# operation: :upsert,
|
|
10
|
+
# index: 'products',
|
|
11
|
+
# namespace: 'prod',
|
|
12
|
+
# vectors: [{ id: 'doc-1', values: [0.1, 0.2, 0.3] }]
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# request.operation # => :upsert
|
|
16
|
+
# request.index # => 'products'
|
|
17
|
+
# request.namespace # => 'prod'
|
|
18
|
+
# request.metadata[:custom_key] = 'custom_value'
|
|
19
|
+
#
|
|
20
|
+
class Request
|
|
21
|
+
attr_accessor :operation, :index, :namespace, :params, :metadata
|
|
22
|
+
|
|
23
|
+
# @param operation [Symbol] The operation type (:upsert, :query, :delete, etc.)
|
|
24
|
+
# @param params [Hash] All parameters for the operation
|
|
25
|
+
def initialize(operation:, **params)
|
|
26
|
+
@operation = operation
|
|
27
|
+
@index = params[:index]
|
|
28
|
+
@namespace = params[:namespace]
|
|
29
|
+
@params = params
|
|
30
|
+
@metadata = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convert request back to hash for provider call
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] Parameters hash
|
|
36
|
+
def to_h
|
|
37
|
+
params
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get the provider from params
|
|
41
|
+
#
|
|
42
|
+
# @return [Symbol, nil] Provider name
|
|
43
|
+
def provider
|
|
44
|
+
params[:provider]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if this is a write operation
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def write_operation?
|
|
51
|
+
[:upsert, :delete, :update, :create_index, :delete_index].include?(operation)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if this is a read operation
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def read_operation?
|
|
58
|
+
[:query, :fetch, :list_indexes, :describe_index, :stats].include?(operation)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Response object returned through middleware chain
|
|
6
|
+
#
|
|
7
|
+
# @example Success response
|
|
8
|
+
# response = Response.new(result: { success: true })
|
|
9
|
+
# response.success? # => true
|
|
10
|
+
# response.result # => { success: true }
|
|
11
|
+
#
|
|
12
|
+
# @example Error response
|
|
13
|
+
# response = Response.new(error: StandardError.new('Failed'))
|
|
14
|
+
# response.failure? # => true
|
|
15
|
+
# response.error # => #<StandardError: Failed>
|
|
16
|
+
#
|
|
17
|
+
# @example With metadata
|
|
18
|
+
# response = Response.new(result: [])
|
|
19
|
+
# response.metadata[:duration_ms] = 45
|
|
20
|
+
# response.metadata[:cache_hit] = true
|
|
21
|
+
#
|
|
22
|
+
class Response
|
|
23
|
+
attr_accessor :result, :error, :metadata
|
|
24
|
+
|
|
25
|
+
# @param result [Object] The successful result
|
|
26
|
+
# @param error [Exception, nil] The error if failed
|
|
27
|
+
def initialize(result: nil, error: nil)
|
|
28
|
+
@result = result
|
|
29
|
+
@error = error
|
|
30
|
+
@metadata = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if the response was successful
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean] true if no error
|
|
36
|
+
def success?
|
|
37
|
+
error.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if the response failed
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean] true if error present
|
|
43
|
+
def failure?
|
|
44
|
+
!success?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raise error if present
|
|
48
|
+
#
|
|
49
|
+
# @raise [Exception] The stored error
|
|
50
|
+
# @return [void]
|
|
51
|
+
def raise_if_error!
|
|
52
|
+
raise error if error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get the result or raise error
|
|
56
|
+
#
|
|
57
|
+
# @return [Object] The result
|
|
58
|
+
# @raise [Exception] If error present
|
|
59
|
+
def value!
|
|
60
|
+
raise_if_error!
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Retry middleware for handling transient failures
|
|
6
|
+
#
|
|
7
|
+
# Automatically retries failed requests with configurable backoff strategy.
|
|
8
|
+
#
|
|
9
|
+
# @example With default settings (3 attempts, exponential backoff)
|
|
10
|
+
# Vectra::Client.use Vectra::Middleware::Retry
|
|
11
|
+
#
|
|
12
|
+
# @example With custom settings
|
|
13
|
+
# Vectra::Client.use Vectra::Middleware::Retry, max_attempts: 5, backoff: :linear
|
|
14
|
+
#
|
|
15
|
+
# @example Per-client retry
|
|
16
|
+
# client = Vectra::Client.new(
|
|
17
|
+
# provider: :pinecone,
|
|
18
|
+
# middleware: [[Vectra::Middleware::Retry, { max_attempts: 3 }]]
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
class Retry < Base
|
|
22
|
+
# @param max_attempts [Integer] Maximum number of attempts (default: 3)
|
|
23
|
+
# @param backoff [Symbol, Numeric] Backoff strategy (:exponential, :linear) or fixed delay
|
|
24
|
+
def initialize(max_attempts: 3, backoff: :exponential)
|
|
25
|
+
super()
|
|
26
|
+
@max_attempts = max_attempts
|
|
27
|
+
@backoff = backoff
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(request, app)
|
|
31
|
+
attempt = 0
|
|
32
|
+
last_error = nil
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
attempt += 1
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
response = app.call(request)
|
|
39
|
+
|
|
40
|
+
# If successful, return immediately
|
|
41
|
+
if response.success?
|
|
42
|
+
response.metadata[:retry_count] = attempt - 1
|
|
43
|
+
return response
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# If error is retryable and we haven't exceeded max attempts, retry
|
|
47
|
+
if response.error && retryable?(response.error) && attempt < @max_attempts
|
|
48
|
+
sleep(backoff_delay(attempt))
|
|
49
|
+
next
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Error is not retryable or max attempts reached, return response
|
|
53
|
+
response.metadata[:retry_count] = attempt - 1
|
|
54
|
+
return response
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
last_error = e
|
|
57
|
+
|
|
58
|
+
# If error is retryable and we haven't exceeded max attempts, retry
|
|
59
|
+
if retryable?(e) && attempt < @max_attempts
|
|
60
|
+
sleep(backoff_delay(attempt))
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Error is not retryable or max attempts reached, raise
|
|
65
|
+
raise
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Check if error is retryable
|
|
73
|
+
#
|
|
74
|
+
# @param error [Exception] The error to check
|
|
75
|
+
# @return [Boolean] true if error is retryable
|
|
76
|
+
def retryable?(error)
|
|
77
|
+
error.is_a?(Vectra::RateLimitError) ||
|
|
78
|
+
error.is_a?(Vectra::ConnectionError) ||
|
|
79
|
+
error.is_a?(Vectra::TimeoutError) ||
|
|
80
|
+
error.is_a?(Vectra::ServerError)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Calculate backoff delay
|
|
84
|
+
#
|
|
85
|
+
# @param attempt [Integer] Current attempt number
|
|
86
|
+
# @return [Float] Delay in seconds
|
|
87
|
+
def backoff_delay(attempt)
|
|
88
|
+
case @backoff
|
|
89
|
+
when :exponential
|
|
90
|
+
# 0.2s, 0.4s, 0.8s, 1.6s, ...
|
|
91
|
+
(2**(attempt - 1)) * 0.2
|
|
92
|
+
when :linear
|
|
93
|
+
# 0.5s, 1.0s, 1.5s, 2.0s, ...
|
|
94
|
+
attempt * 0.5
|
|
95
|
+
when Numeric
|
|
96
|
+
@backoff
|
|
97
|
+
else
|
|
98
|
+
1.0
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Middleware stack executor
|
|
6
|
+
#
|
|
7
|
+
# Builds and executes a chain of middleware around provider calls.
|
|
8
|
+
# Similar to Rack middleware, each middleware wraps the next one
|
|
9
|
+
# in the chain until reaching the actual provider.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# provider = Vectra::Providers::Memory.new
|
|
13
|
+
# middlewares = [LoggingMiddleware.new, RetryMiddleware.new]
|
|
14
|
+
# stack = Stack.new(provider, middlewares)
|
|
15
|
+
#
|
|
16
|
+
# result = stack.call(:upsert, index: 'test', vectors: [...])
|
|
17
|
+
#
|
|
18
|
+
class Stack
|
|
19
|
+
# @param provider [Vectra::Providers::Base] The actual provider
|
|
20
|
+
# @param middlewares [Array<Base>] Array of middleware instances
|
|
21
|
+
def initialize(provider, middlewares = [])
|
|
22
|
+
@provider = provider
|
|
23
|
+
@middlewares = middlewares
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute the middleware stack for an operation
|
|
27
|
+
#
|
|
28
|
+
# @param operation [Symbol] The operation to perform (:upsert, :query, etc.)
|
|
29
|
+
# @param params [Hash] The operation parameters
|
|
30
|
+
# @return [Object] The result from the provider
|
|
31
|
+
# @raise [Exception] Any error from middleware or provider
|
|
32
|
+
def call(operation, **params)
|
|
33
|
+
request = Request.new(operation: operation, **params)
|
|
34
|
+
|
|
35
|
+
# Build middleware chain
|
|
36
|
+
app = build_chain(request)
|
|
37
|
+
|
|
38
|
+
# Execute chain
|
|
39
|
+
response = app.call(request)
|
|
40
|
+
|
|
41
|
+
# Raise if error occurred
|
|
42
|
+
raise response.error if response.error
|
|
43
|
+
|
|
44
|
+
response.result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Build the middleware chain
|
|
50
|
+
#
|
|
51
|
+
# @param request [Request] The request object (unused here, but available)
|
|
52
|
+
# @return [Proc] The complete middleware chain
|
|
53
|
+
def build_chain(_request)
|
|
54
|
+
# Final app: actual provider call
|
|
55
|
+
final_app = lambda do |req|
|
|
56
|
+
# Remove middleware-specific params before calling provider
|
|
57
|
+
provider_params = req.to_h.except(:provider)
|
|
58
|
+
result = @provider.public_send(req.operation, **provider_params)
|
|
59
|
+
Response.new(result: result)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Response.new(error: e)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Wrap with middlewares in reverse order
|
|
65
|
+
# (last middleware in array is first to execute)
|
|
66
|
+
@middlewares.reverse.inject(final_app) do |next_app, middleware|
|
|
67
|
+
lambda do |req|
|
|
68
|
+
middleware.call(req, next_app)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/vectra/version.rb
CHANGED
data/lib/vectra.rb
CHANGED
|
@@ -18,6 +18,15 @@ require_relative "vectra/health_check"
|
|
|
18
18
|
require_relative "vectra/credential_rotation"
|
|
19
19
|
require_relative "vectra/audit_log"
|
|
20
20
|
require_relative "vectra/active_record"
|
|
21
|
+
require_relative "vectra/middleware/request"
|
|
22
|
+
require_relative "vectra/middleware/response"
|
|
23
|
+
require_relative "vectra/middleware/base"
|
|
24
|
+
require_relative "vectra/middleware/stack"
|
|
25
|
+
require_relative "vectra/middleware/logging"
|
|
26
|
+
require_relative "vectra/middleware/retry"
|
|
27
|
+
require_relative "vectra/middleware/instrumentation"
|
|
28
|
+
require_relative "vectra/middleware/pii_redaction"
|
|
29
|
+
require_relative "vectra/middleware/cost_tracker"
|
|
21
30
|
require_relative "vectra/providers/base"
|
|
22
31
|
require_relative "vectra/providers/pinecone"
|
|
23
32
|
require_relative "vectra/providers/qdrant"
|
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: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mijo Kristo
|
|
@@ -268,6 +268,7 @@ files:
|
|
|
268
268
|
- docs/guides/faq.md
|
|
269
269
|
- docs/guides/getting-started.md
|
|
270
270
|
- docs/guides/installation.md
|
|
271
|
+
- docs/guides/middleware.md
|
|
271
272
|
- docs/guides/monitoring.md
|
|
272
273
|
- docs/guides/performance.md
|
|
273
274
|
- docs/guides/rails-integration.md
|
|
@@ -294,6 +295,7 @@ files:
|
|
|
294
295
|
- examples/grafana-dashboard.json
|
|
295
296
|
- examples/grafana-setup.md
|
|
296
297
|
- examples/instrumentation_demo.rb
|
|
298
|
+
- examples/middleware_demo.rb
|
|
297
299
|
- examples/prometheus-exporter.rb
|
|
298
300
|
- lib/generators/vectra/index_generator.rb
|
|
299
301
|
- lib/generators/vectra/install_generator.rb
|
|
@@ -316,6 +318,15 @@ files:
|
|
|
316
318
|
- lib/vectra/instrumentation/new_relic.rb
|
|
317
319
|
- lib/vectra/instrumentation/sentry.rb
|
|
318
320
|
- lib/vectra/logging.rb
|
|
321
|
+
- lib/vectra/middleware/base.rb
|
|
322
|
+
- lib/vectra/middleware/cost_tracker.rb
|
|
323
|
+
- lib/vectra/middleware/instrumentation.rb
|
|
324
|
+
- lib/vectra/middleware/logging.rb
|
|
325
|
+
- lib/vectra/middleware/pii_redaction.rb
|
|
326
|
+
- lib/vectra/middleware/request.rb
|
|
327
|
+
- lib/vectra/middleware/response.rb
|
|
328
|
+
- lib/vectra/middleware/retry.rb
|
|
329
|
+
- lib/vectra/middleware/stack.rb
|
|
319
330
|
- lib/vectra/pool.rb
|
|
320
331
|
- lib/vectra/providers/base.rb
|
|
321
332
|
- lib/vectra/providers/memory.rb
|