vectra-client 1.0.8 ā 1.1.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/CHANGELOG.md +37 -0
- data/README.md +75 -0
- data/docs/_layouts/home.html +1 -1
- data/docs/_layouts/page.html +7 -0
- data/docs/api/cheatsheet.md +17 -0
- data/docs/api/methods.md +45 -0
- data/docs/api/overview.md +6 -0
- data/docs/guides/middleware.md +324 -0
- data/docs/guides/roadmap.md +53 -0
- data/examples/middleware_demo.rb +103 -0
- data/lib/vectra/client.rb +132 -15
- 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/providers/memory.rb +56 -0
- data/lib/vectra/providers/pgvector.rb +50 -0
- data/lib/vectra/providers/qdrant.rb +39 -0
- data/lib/vectra/providers/weaviate.rb +64 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +9 -0
- metadata +13 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Middleware System Demo
|
|
5
|
+
#
|
|
6
|
+
# This script demonstrates the new middleware system in Vectra.
|
|
7
|
+
# Run with: ruby examples/middleware_demo.rb
|
|
8
|
+
|
|
9
|
+
require_relative "../lib/vectra"
|
|
10
|
+
|
|
11
|
+
# Configure global middleware
|
|
12
|
+
puts "šÆ Configuring global middleware..."
|
|
13
|
+
Vectra::Client.use Vectra::Middleware::Logging
|
|
14
|
+
Vectra::Client.use Vectra::Middleware::Retry, max_attempts: 3
|
|
15
|
+
Vectra::Client.use Vectra::Middleware::CostTracker, on_cost: ->(event) {
|
|
16
|
+
puts "š° Cost: $#{event[:cost_usd].round(6)} for #{event[:operation]}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Create client
|
|
20
|
+
puts "\nš¦ Creating client with Memory provider..."
|
|
21
|
+
client = Vectra::Client.new(
|
|
22
|
+
provider: :memory,
|
|
23
|
+
index: "demo"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Example 1: Upsert with middleware
|
|
27
|
+
puts "\nš Example 1: Upsert with middleware stack"
|
|
28
|
+
puts "=" * 50
|
|
29
|
+
client.upsert(
|
|
30
|
+
index: "demo",
|
|
31
|
+
vectors: [
|
|
32
|
+
{ id: "doc-1", values: [0.1, 0.2, 0.3], metadata: { title: "Ruby" } },
|
|
33
|
+
{ id: "doc-2", values: [0.4, 0.5, 0.6], metadata: { title: "Python" } }
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Example 2: Query with middleware
|
|
38
|
+
puts "\nš Example 2: Query with middleware stack"
|
|
39
|
+
puts "=" * 50
|
|
40
|
+
results = client.query(
|
|
41
|
+
index: "demo",
|
|
42
|
+
vector: [0.1, 0.2, 0.3],
|
|
43
|
+
top_k: 2
|
|
44
|
+
)
|
|
45
|
+
puts "Found #{results.size} results"
|
|
46
|
+
|
|
47
|
+
# Example 3: Per-client middleware
|
|
48
|
+
puts "\nšØ Example 3: Per-client middleware (PII Redaction)"
|
|
49
|
+
puts "=" * 50
|
|
50
|
+
pii_client = Vectra::Client.new(
|
|
51
|
+
provider: :memory,
|
|
52
|
+
index: "sensitive",
|
|
53
|
+
middleware: [Vectra::Middleware::PIIRedaction]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
pii_client.upsert(
|
|
57
|
+
index: "sensitive",
|
|
58
|
+
vectors: [
|
|
59
|
+
{
|
|
60
|
+
id: "user-1",
|
|
61
|
+
values: [0.1, 0.2, 0.3],
|
|
62
|
+
metadata: {
|
|
63
|
+
email: "user@example.com",
|
|
64
|
+
phone: "555-1234",
|
|
65
|
+
note: "Contact at user@example.com"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Fetch to see redacted data
|
|
72
|
+
fetched = pii_client.fetch(index: "sensitive", ids: ["user-1"])
|
|
73
|
+
puts "Original email: user@example.com"
|
|
74
|
+
puts "Redacted: #{fetched["user-1"].metadata[:email]}"
|
|
75
|
+
puts "Redacted note: #{fetched["user-1"].metadata[:note]}"
|
|
76
|
+
|
|
77
|
+
# Example 4: Custom middleware
|
|
78
|
+
puts "\nš ļø Example 4: Custom middleware"
|
|
79
|
+
puts "=" * 50
|
|
80
|
+
|
|
81
|
+
class TimingMiddleware < Vectra::Middleware::Base
|
|
82
|
+
def before(request)
|
|
83
|
+
puts "ā±ļø Starting #{request.operation}..."
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def after(request, response)
|
|
87
|
+
duration = response.metadata[:duration_ms] || 0
|
|
88
|
+
puts "ā
Completed in #{duration.round(2)}ms"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
custom_client = Vectra::Client.new(
|
|
93
|
+
provider: :memory,
|
|
94
|
+
index: "custom",
|
|
95
|
+
middleware: [TimingMiddleware]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
custom_client.upsert(
|
|
99
|
+
index: "custom",
|
|
100
|
+
vectors: [{ id: "test", values: [1, 2, 3] }]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
puts "\n⨠Demo complete!"
|
data/lib/vectra/client.rb
CHANGED
|
@@ -42,6 +42,37 @@ module Vectra
|
|
|
42
42
|
|
|
43
43
|
attr_reader :config, :provider, :default_index, :default_namespace
|
|
44
44
|
|
|
45
|
+
class << self
|
|
46
|
+
# Get the global middleware stack
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Array>] Array of [middleware_class, options] pairs
|
|
49
|
+
def middleware
|
|
50
|
+
@middleware ||= []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Add middleware to the global stack
|
|
54
|
+
#
|
|
55
|
+
# @param middleware_class [Class] Middleware class
|
|
56
|
+
# @param options [Hash] Options to pass to middleware constructor
|
|
57
|
+
#
|
|
58
|
+
# @example Add global logging middleware
|
|
59
|
+
# Vectra::Client.use Vectra::Middleware::Logging
|
|
60
|
+
#
|
|
61
|
+
# @example Add middleware with options
|
|
62
|
+
# Vectra::Client.use Vectra::Middleware::Retry, max_attempts: 5
|
|
63
|
+
#
|
|
64
|
+
def use(middleware_class, **options)
|
|
65
|
+
middleware << [middleware_class, options]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Clear all global middleware
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
def clear_middleware!
|
|
72
|
+
@middleware = []
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
45
76
|
# Initialize a new Client
|
|
46
77
|
#
|
|
47
78
|
# @param provider [Symbol, nil] provider name (:pinecone, :qdrant, :weaviate)
|
|
@@ -51,12 +82,14 @@ module Vectra
|
|
|
51
82
|
# @param options [Hash] additional options
|
|
52
83
|
# @option options [String] :index default index name
|
|
53
84
|
# @option options [String] :namespace default namespace
|
|
85
|
+
# @option options [Array<Class, Object>] :middleware instance-level middleware
|
|
54
86
|
def initialize(provider: nil, api_key: nil, environment: nil, host: nil, **options)
|
|
55
87
|
@config = build_config(provider, api_key, environment, host, options)
|
|
56
88
|
@config.validate!
|
|
57
89
|
@provider = build_provider
|
|
58
90
|
@default_index = options[:index]
|
|
59
91
|
@default_namespace = options[:namespace]
|
|
92
|
+
@middleware = build_middleware_stack(options[:middleware])
|
|
60
93
|
end
|
|
61
94
|
|
|
62
95
|
# Upsert vectors into an index
|
|
@@ -87,7 +120,7 @@ module Vectra
|
|
|
87
120
|
index: index,
|
|
88
121
|
metadata: { vector_count: vectors.size }
|
|
89
122
|
) do
|
|
90
|
-
|
|
123
|
+
@middleware.call(:upsert, index: index, vectors: vectors, namespace: namespace, provider: provider_name)
|
|
91
124
|
end
|
|
92
125
|
end
|
|
93
126
|
|
|
@@ -151,14 +184,16 @@ module Vectra
|
|
|
151
184
|
index: index,
|
|
152
185
|
metadata: { top_k: top_k }
|
|
153
186
|
) do
|
|
154
|
-
result =
|
|
187
|
+
result = @middleware.call(
|
|
188
|
+
:query,
|
|
155
189
|
index: index,
|
|
156
190
|
vector: vector,
|
|
157
191
|
top_k: top_k,
|
|
158
192
|
namespace: namespace,
|
|
159
193
|
filter: filter,
|
|
160
194
|
include_values: include_values,
|
|
161
|
-
include_metadata: include_metadata
|
|
195
|
+
include_metadata: include_metadata,
|
|
196
|
+
provider: provider_name
|
|
162
197
|
)
|
|
163
198
|
end
|
|
164
199
|
|
|
@@ -188,7 +223,7 @@ module Vectra
|
|
|
188
223
|
index: index,
|
|
189
224
|
metadata: { id_count: ids.size }
|
|
190
225
|
) do
|
|
191
|
-
|
|
226
|
+
@middleware.call(:fetch, index: index, ids: ids, namespace: namespace, provider: provider_name)
|
|
192
227
|
end
|
|
193
228
|
end
|
|
194
229
|
|
|
@@ -222,12 +257,14 @@ module Vectra
|
|
|
222
257
|
index: index,
|
|
223
258
|
metadata: { has_metadata: !metadata.nil?, has_values: !values.nil? }
|
|
224
259
|
) do
|
|
225
|
-
|
|
260
|
+
@middleware.call(
|
|
261
|
+
:update,
|
|
226
262
|
index: index,
|
|
227
263
|
id: id,
|
|
228
264
|
metadata: metadata,
|
|
229
265
|
values: values,
|
|
230
|
-
namespace: namespace
|
|
266
|
+
namespace: namespace,
|
|
267
|
+
provider: provider_name
|
|
231
268
|
)
|
|
232
269
|
end
|
|
233
270
|
end
|
|
@@ -265,12 +302,14 @@ module Vectra
|
|
|
265
302
|
index: index,
|
|
266
303
|
metadata: { id_count: ids&.size, delete_all: delete_all, has_filter: !filter.nil? }
|
|
267
304
|
) do
|
|
268
|
-
|
|
305
|
+
@middleware.call(
|
|
306
|
+
:delete,
|
|
269
307
|
index: index,
|
|
270
308
|
ids: ids,
|
|
271
309
|
namespace: namespace,
|
|
272
310
|
filter: filter,
|
|
273
|
-
delete_all: delete_all
|
|
311
|
+
delete_all: delete_all,
|
|
312
|
+
provider: provider_name
|
|
274
313
|
)
|
|
275
314
|
end
|
|
276
315
|
end
|
|
@@ -284,7 +323,7 @@ module Vectra
|
|
|
284
323
|
# indexes.each { |idx| puts idx[:name] }
|
|
285
324
|
#
|
|
286
325
|
def list_indexes
|
|
287
|
-
|
|
326
|
+
@middleware.call(:list_indexes, provider: provider_name)
|
|
288
327
|
end
|
|
289
328
|
|
|
290
329
|
# Describe an index
|
|
@@ -299,7 +338,7 @@ module Vectra
|
|
|
299
338
|
def describe_index(index: nil)
|
|
300
339
|
index ||= default_index
|
|
301
340
|
validate_index!(index)
|
|
302
|
-
|
|
341
|
+
@middleware.call(:describe_index, index: index, provider: provider_name)
|
|
303
342
|
end
|
|
304
343
|
|
|
305
344
|
# Get index statistics
|
|
@@ -316,7 +355,7 @@ module Vectra
|
|
|
316
355
|
index ||= default_index
|
|
317
356
|
namespace ||= default_namespace
|
|
318
357
|
validate_index!(index)
|
|
319
|
-
|
|
358
|
+
@middleware.call(:stats, index: index, namespace: namespace, provider: provider_name)
|
|
320
359
|
end
|
|
321
360
|
|
|
322
361
|
# Create a new index
|
|
@@ -342,7 +381,7 @@ module Vectra
|
|
|
342
381
|
index: name,
|
|
343
382
|
metadata: { dimension: dimension, metric: metric }
|
|
344
383
|
) do
|
|
345
|
-
|
|
384
|
+
@middleware.call(:create_index, name: name, dimension: dimension, metric: metric, provider: provider_name, **options)
|
|
346
385
|
end
|
|
347
386
|
end
|
|
348
387
|
|
|
@@ -365,7 +404,7 @@ module Vectra
|
|
|
365
404
|
provider: provider_name,
|
|
366
405
|
index: name
|
|
367
406
|
) do
|
|
368
|
-
|
|
407
|
+
@middleware.call(:delete_index, name: name, provider: provider_name)
|
|
369
408
|
end
|
|
370
409
|
end
|
|
371
410
|
|
|
@@ -440,7 +479,8 @@ module Vectra
|
|
|
440
479
|
"Hybrid search is not supported by #{provider_name} provider"
|
|
441
480
|
end
|
|
442
481
|
|
|
443
|
-
|
|
482
|
+
@middleware.call(
|
|
483
|
+
:hybrid_search,
|
|
444
484
|
index: index,
|
|
445
485
|
vector: vector,
|
|
446
486
|
text: text,
|
|
@@ -449,10 +489,72 @@ module Vectra
|
|
|
449
489
|
namespace: namespace,
|
|
450
490
|
filter: filter,
|
|
451
491
|
include_values: include_values,
|
|
452
|
-
include_metadata: include_metadata
|
|
492
|
+
include_metadata: include_metadata,
|
|
493
|
+
provider: provider_name
|
|
453
494
|
)
|
|
454
495
|
end
|
|
455
496
|
|
|
497
|
+
# Text-only search (keyword search without embeddings)
|
|
498
|
+
#
|
|
499
|
+
# Performs keyword/text search without requiring vector embeddings.
|
|
500
|
+
# Useful for exact matches, product names, function names, etc.
|
|
501
|
+
#
|
|
502
|
+
# @param index [String] the index/collection name
|
|
503
|
+
# @param text [String] text query for keyword search
|
|
504
|
+
# @param top_k [Integer] number of results to return (default: 10)
|
|
505
|
+
# @param namespace [String, nil] optional namespace
|
|
506
|
+
# @param filter [Hash, nil] metadata filter
|
|
507
|
+
# @param include_values [Boolean] include vector values in results
|
|
508
|
+
# @param include_metadata [Boolean] include metadata in results
|
|
509
|
+
# @return [QueryResult] search results
|
|
510
|
+
#
|
|
511
|
+
# @example Basic text search
|
|
512
|
+
# results = client.text_search(
|
|
513
|
+
# index: 'products',
|
|
514
|
+
# text: 'iPhone 15 Pro',
|
|
515
|
+
# top_k: 10
|
|
516
|
+
# )
|
|
517
|
+
#
|
|
518
|
+
# @example Text search with filter
|
|
519
|
+
# results = client.text_search(
|
|
520
|
+
# index: 'products',
|
|
521
|
+
# text: 'laptop',
|
|
522
|
+
# filter: { category: 'electronics', in_stock: true }
|
|
523
|
+
# )
|
|
524
|
+
#
|
|
525
|
+
# @raise [UnsupportedFeatureError] if provider doesn't support text search
|
|
526
|
+
def text_search(index:, text:, top_k: 10, namespace: nil, filter: nil,
|
|
527
|
+
include_values: false, include_metadata: true)
|
|
528
|
+
index ||= default_index
|
|
529
|
+
namespace ||= default_namespace
|
|
530
|
+
validate_index!(index)
|
|
531
|
+
raise ValidationError, "Text query cannot be nil or empty" if text.nil? || text.empty?
|
|
532
|
+
|
|
533
|
+
unless provider.respond_to?(:text_search)
|
|
534
|
+
raise UnsupportedFeatureError,
|
|
535
|
+
"Text search is not supported by #{provider_name} provider"
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
Instrumentation.instrument(
|
|
539
|
+
operation: :text_search,
|
|
540
|
+
provider: provider_name,
|
|
541
|
+
index: index,
|
|
542
|
+
metadata: { top_k: top_k }
|
|
543
|
+
) do
|
|
544
|
+
@middleware.call(
|
|
545
|
+
:text_search,
|
|
546
|
+
index: index,
|
|
547
|
+
text: text,
|
|
548
|
+
top_k: top_k,
|
|
549
|
+
namespace: namespace,
|
|
550
|
+
filter: filter,
|
|
551
|
+
include_values: include_values,
|
|
552
|
+
include_metadata: include_metadata,
|
|
553
|
+
provider: provider_name
|
|
554
|
+
)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
456
558
|
# Get the provider name
|
|
457
559
|
#
|
|
458
560
|
# @return [Symbol]
|
|
@@ -628,6 +730,21 @@ module Vectra
|
|
|
628
730
|
end
|
|
629
731
|
end
|
|
630
732
|
|
|
733
|
+
def build_middleware_stack(instance_middleware = nil)
|
|
734
|
+
# Combine class-level + instance-level middleware
|
|
735
|
+
all_middleware = self.class.middleware.map do |klass, opts|
|
|
736
|
+
klass.new(**opts)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
if instance_middleware
|
|
740
|
+
all_middleware += Array(instance_middleware).map do |mw|
|
|
741
|
+
mw.is_a?(Class) ? mw.new : mw
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
Middleware::Stack.new(@provider, all_middleware)
|
|
746
|
+
end
|
|
747
|
+
|
|
631
748
|
def validate_index!(index)
|
|
632
749
|
raise ValidationError, "Index name cannot be nil" if index.nil?
|
|
633
750
|
raise ValidationError, "Index name must be a string" unless index.is_a?(String)
|
data/lib/vectra/health_check.rb
CHANGED
|
@@ -29,7 +29,9 @@ module Vectra
|
|
|
29
29
|
def health_check(index: nil, include_stats: false, timeout: 5)
|
|
30
30
|
start_time = Time.now
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# For health checks we bypass client middleware and call the provider
|
|
33
|
+
# directly to avoid interference from custom stacks.
|
|
34
|
+
indexes = with_timeout(timeout) { provider.list_indexes }
|
|
33
35
|
index_name = index || indexes.first&.dig(:name)
|
|
34
36
|
|
|
35
37
|
result = base_result(start_time, indexes)
|
|
@@ -70,7 +72,7 @@ module Vectra
|
|
|
70
72
|
def add_index_stats(result, index_name, include_stats, timeout)
|
|
71
73
|
return unless include_stats && index_name
|
|
72
74
|
|
|
73
|
-
stats = with_timeout(timeout) { stats(index: index_name) }
|
|
75
|
+
stats = with_timeout(timeout) { provider.stats(index: index_name) }
|
|
74
76
|
result[:index] = index_name
|
|
75
77
|
result[:stats] = {
|
|
76
78
|
vector_count: stats[:total_vector_count],
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Base class for all middleware
|
|
6
|
+
#
|
|
7
|
+
# Middleware can hook into three lifecycle events:
|
|
8
|
+
# - before(request): Called before the next middleware/provider
|
|
9
|
+
# - after(request, response): Called after successful execution
|
|
10
|
+
# - on_error(request, error): Called when an error occurs
|
|
11
|
+
#
|
|
12
|
+
# @example Simple logging middleware
|
|
13
|
+
# class LoggingMiddleware < Vectra::Middleware::Base
|
|
14
|
+
# def before(request)
|
|
15
|
+
# puts "Starting #{request.operation}"
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# def after(request, response)
|
|
19
|
+
# puts "Completed #{request.operation}"
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Error handling middleware
|
|
24
|
+
# class ErrorHandlerMiddleware < Vectra::Middleware::Base
|
|
25
|
+
# def on_error(request, error)
|
|
26
|
+
# ErrorTracker.notify(error, context: { operation: request.operation })
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class Base
|
|
31
|
+
# Execute the middleware
|
|
32
|
+
#
|
|
33
|
+
# This is the main entry point called by the middleware stack.
|
|
34
|
+
# It handles the before/after/error lifecycle hooks.
|
|
35
|
+
#
|
|
36
|
+
# @param request [Request] The request object
|
|
37
|
+
# @param app [Proc] The next middleware in the chain
|
|
38
|
+
# @return [Response] The response object
|
|
39
|
+
def call(request, app)
|
|
40
|
+
# Before hook
|
|
41
|
+
before(request)
|
|
42
|
+
|
|
43
|
+
# Call next middleware
|
|
44
|
+
response = app.call(request)
|
|
45
|
+
|
|
46
|
+
# Check if response has an error
|
|
47
|
+
if response.error
|
|
48
|
+
on_error(request, response.error)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# After hook
|
|
52
|
+
after(request, response)
|
|
53
|
+
|
|
54
|
+
response
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
# Error handling hook (for exceptions raised directly)
|
|
57
|
+
on_error(request, e)
|
|
58
|
+
raise
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
protected
|
|
62
|
+
|
|
63
|
+
# Hook called before the next middleware
|
|
64
|
+
#
|
|
65
|
+
# Override this method to add logic before the operation executes.
|
|
66
|
+
#
|
|
67
|
+
# @param request [Request] The request object
|
|
68
|
+
# @return [void]
|
|
69
|
+
def before(request)
|
|
70
|
+
# Override in subclass
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Hook called after successful execution
|
|
74
|
+
#
|
|
75
|
+
# Override this method to add logic after the operation completes.
|
|
76
|
+
#
|
|
77
|
+
# @param request [Request] The request object
|
|
78
|
+
# @param response [Response] The response object
|
|
79
|
+
# @return [void]
|
|
80
|
+
def after(request, response)
|
|
81
|
+
# Override in subclass
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Hook called when an error occurs
|
|
85
|
+
#
|
|
86
|
+
# Override this method to add error handling logic.
|
|
87
|
+
# The error will be re-raised after this hook executes.
|
|
88
|
+
#
|
|
89
|
+
# @param request [Request] The request object
|
|
90
|
+
# @param error [Exception] The error that occurred
|
|
91
|
+
# @return [void]
|
|
92
|
+
def on_error(request, error)
|
|
93
|
+
# Override in subclass
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -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
|