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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70730299dab8475b05688f7017dccd1965b87a49589c96be7633a356af8d57f2
|
|
4
|
+
data.tar.gz: 14999ebde62586578b444ba45d396cb38f1a4dd5dcbf2fd126e042ebb1c6fae5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69c8fa722ee4abfe3ddf6b19f8bd12f46d09edefee50612021142c3951600666ed854018e8a5c1bc33249895fec42ae18ab1d821beffa989d2700da99c28bcf4
|
|
7
|
+
data.tar.gz: f27a0df4bbcf618659297b1376ebe977588d32d5dfef76fe7c8f5f38c8780b37e055a0e8d91f6bc3ae8eb7cad88606c760d4ed5882c61756ac1495dedfc339a5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.0.8](https://github.com/stokry/vectra/tree/v1.0.8) (2026-01-14)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/stokry/vectra/compare/v1.0.7...v1.0.8)
|
|
6
|
+
|
|
3
7
|
## [v1.0.7](https://github.com/stokry/vectra/tree/v1.0.7) (2026-01-14)
|
|
4
8
|
|
|
5
9
|
[Full Changelog](https://github.com/stokry/vectra/compare/v1.0.6...v1.0.7)
|
data/README.md
CHANGED
|
@@ -242,6 +242,59 @@ Real-world patterns for common use cases:
|
|
|
242
242
|
|
|
243
243
|
👉 **[Browse all Recipes & Patterns](https://vectra-docs.netlify.app/guides/recipes/)**
|
|
244
244
|
|
|
245
|
+
## Middleware System
|
|
246
|
+
|
|
247
|
+
Vectra features a powerful **Rack-style middleware system** that allows you to extend functionality without modifying core code.
|
|
248
|
+
|
|
249
|
+
### Quick Start
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# Global middleware (applies to all clients)
|
|
253
|
+
Vectra::Client.use Vectra::Middleware::Logging
|
|
254
|
+
Vectra::Client.use Vectra::Middleware::Retry, max_attempts: 5
|
|
255
|
+
Vectra::Client.use Vectra::Middleware::CostTracker
|
|
256
|
+
|
|
257
|
+
# Per-client middleware
|
|
258
|
+
client = Vectra::Client.new(
|
|
259
|
+
provider: :qdrant,
|
|
260
|
+
middleware: [
|
|
261
|
+
Vectra::Middleware::PIIRedaction,
|
|
262
|
+
Vectra::Middleware::Instrumentation
|
|
263
|
+
]
|
|
264
|
+
)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Built-in Middleware
|
|
268
|
+
|
|
269
|
+
- **Logging** - Request/response logging with timing
|
|
270
|
+
- **Retry** - Automatic retries with exponential backoff
|
|
271
|
+
- **Instrumentation** - Metrics and monitoring integration
|
|
272
|
+
- **PIIRedaction** - Automatic PII (email, phone, SSN) redaction
|
|
273
|
+
- **CostTracker** - Track API usage costs per operation
|
|
274
|
+
|
|
275
|
+
### Custom Middleware
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
class MyAuditMiddleware < Vectra::Middleware::Base
|
|
279
|
+
def before(request)
|
|
280
|
+
# Called before the operation
|
|
281
|
+
AuditLog.create!(action: request.operation, user: Current.user)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def after(request, response)
|
|
285
|
+
# Called after successful operation
|
|
286
|
+
puts "Duration: #{response.metadata[:duration_ms]}ms"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def on_error(request, error)
|
|
290
|
+
# Called when an error occurs
|
|
291
|
+
ErrorTracker.notify(error, context: { operation: request.operation })
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
Vectra::Client.use MyAuditMiddleware
|
|
296
|
+
```
|
|
297
|
+
|
|
245
298
|
## Production Patterns
|
|
246
299
|
|
|
247
300
|
Vectra includes 7 production-ready patterns out of the box:
|
data/docs/_layouts/home.html
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
<!-- Hero Section -->
|
|
56
56
|
<section class="tma-hero">
|
|
57
57
|
<div class="tma-hero__container">
|
|
58
|
-
<span class="tma-hero__badge">v1.0
|
|
58
|
+
<span class="tma-hero__badge">v1.1.0 — Hybrid Search, Rails Generator & Middleware</span>
|
|
59
59
|
<h1 class="tma-hero__title">
|
|
60
60
|
Vector Databases,<br>
|
|
61
61
|
<span class="tma-hero__title-gradient">Unified for Ruby.</span>
|
data/docs/api/cheatsheet.md
CHANGED
|
@@ -34,6 +34,21 @@ client = Vectra.pgvector(connection_url: ENV['DATABASE_URL'])
|
|
|
34
34
|
client = Vectra.memory # In-memory (testing only)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
You can also set a **default index and namespace**:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
client = Vectra::Client.new(
|
|
41
|
+
provider: :qdrant,
|
|
42
|
+
host: 'http://localhost:6333',
|
|
43
|
+
index: 'products',
|
|
44
|
+
namespace: 'tenant-1'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Now index and namespace can be omitted
|
|
48
|
+
client.upsert(vectors: [...])
|
|
49
|
+
client.query(vector: query_embedding, top_k: 10)
|
|
50
|
+
```
|
|
51
|
+
|
|
37
52
|
### Upsert
|
|
38
53
|
|
|
39
54
|
```ruby
|
|
@@ -225,6 +240,22 @@ vector.normalize! # Mutates values
|
|
|
225
240
|
client.upsert(index: 'documents', vectors: [vector])
|
|
226
241
|
```
|
|
227
242
|
|
|
243
|
+
### Embedding Cache Helper
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
cache = Vectra::Cache.new(ttl: 600, max_size: 1000)
|
|
247
|
+
|
|
248
|
+
embedding = Vectra::Embeddings.fetch(
|
|
249
|
+
cache: cache,
|
|
250
|
+
model_name: "Product",
|
|
251
|
+
id: product.id,
|
|
252
|
+
input: product.description,
|
|
253
|
+
field: :description
|
|
254
|
+
) do
|
|
255
|
+
EmbeddingService.generate(product.description)
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
228
259
|
---
|
|
229
260
|
|
|
230
261
|
## Batch Operations
|
|
@@ -318,6 +349,18 @@ results.each do |doc|
|
|
|
318
349
|
end
|
|
319
350
|
```
|
|
320
351
|
|
|
352
|
+
### Reindex All Records
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
# Reindex all documents that already have embeddings
|
|
356
|
+
processed = Document.reindex_vectors(
|
|
357
|
+
scope: Document.where.not(embedding: nil),
|
|
358
|
+
batch_size: 500
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
puts "Reindexed #{processed} documents"
|
|
362
|
+
```
|
|
363
|
+
|
|
321
364
|
---
|
|
322
365
|
|
|
323
366
|
## Error Handling
|
data/docs/api/methods.md
CHANGED
|
@@ -43,7 +43,7 @@ client = Vectra::Client.new(
|
|
|
43
43
|
Upsert vectors into an index. If a vector with the same ID exists, it will be updated.
|
|
44
44
|
|
|
45
45
|
**Parameters:**
|
|
46
|
-
- `index` (String) - Index/collection name
|
|
46
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
47
47
|
- `vectors` (Array<Hash, Vector>) - Array of vector hashes or Vector objects
|
|
48
48
|
- `namespace` (String, optional) - Namespace
|
|
49
49
|
|
|
@@ -77,7 +77,7 @@ result = client.upsert(
|
|
|
77
77
|
Search for similar vectors using cosine similarity.
|
|
78
78
|
|
|
79
79
|
**Parameters:**
|
|
80
|
-
- `index` (String) - Index/collection name
|
|
80
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
81
81
|
- `vector` (Array<Float>) - Query vector
|
|
82
82
|
- `top_k` (Integer) - Number of results (default: 10)
|
|
83
83
|
- `namespace` (String, optional) - Namespace
|
|
@@ -152,7 +152,7 @@ results = client.hybrid_search(
|
|
|
152
152
|
Fetch vectors by their IDs.
|
|
153
153
|
|
|
154
154
|
**Parameters:**
|
|
155
|
-
- `index` (String) - Index/collection name
|
|
155
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
156
156
|
- `ids` (Array<String>) - Array of vector IDs
|
|
157
157
|
- `namespace` (String, optional) - Namespace
|
|
158
158
|
|
|
@@ -176,7 +176,7 @@ vectors['doc-1'].metadata # => { 'title' => 'Hello' }
|
|
|
176
176
|
Update a vector's metadata or values.
|
|
177
177
|
|
|
178
178
|
**Parameters:**
|
|
179
|
-
- `index` (String) - Index/collection name
|
|
179
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
180
180
|
- `id` (String) - Vector ID
|
|
181
181
|
- `metadata` (Hash, optional) - New metadata (merged with existing)
|
|
182
182
|
- `values` (Array<Float>, optional) - New vector values
|
|
@@ -202,7 +202,7 @@ client.update(
|
|
|
202
202
|
Delete vectors.
|
|
203
203
|
|
|
204
204
|
**Parameters:**
|
|
205
|
-
- `index` (String) - Index/collection name
|
|
205
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
206
206
|
- `ids` (Array<String>, optional) - Vector IDs to delete
|
|
207
207
|
- `namespace` (String, optional) - Namespace
|
|
208
208
|
- `filter` (Hash, optional) - Delete by metadata filter
|
|
@@ -231,7 +231,7 @@ client.delete(index: 'documents', delete_all: true)
|
|
|
231
231
|
Get index statistics.
|
|
232
232
|
|
|
233
233
|
**Parameters:**
|
|
234
|
-
- `index` (String) - Index/collection name
|
|
234
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
235
235
|
- `namespace` (String, optional) - Namespace
|
|
236
236
|
|
|
237
237
|
**Returns:** `Hash` with statistics:
|
|
@@ -571,6 +571,30 @@ end
|
|
|
571
571
|
|
|
572
572
|
---
|
|
573
573
|
|
|
574
|
+
### `Model.reindex_vectors(scope: all, batch_size: 1000, on_progress: nil)`
|
|
575
|
+
|
|
576
|
+
Reindex all records for a model into the configured vector index.
|
|
577
|
+
|
|
578
|
+
**Parameters:**
|
|
579
|
+
- `scope` (ActiveRecord::Relation) - Records to reindex (default: `Model.all`)
|
|
580
|
+
- `batch_size` (Integer) - Number of records per batch (default: 1000)
|
|
581
|
+
- `on_progress` (Proc, optional) - Progress callback, receives a hash with `:processed` and `:total`
|
|
582
|
+
|
|
583
|
+
**Returns:** `Integer` - Number of records processed
|
|
584
|
+
|
|
585
|
+
**Example:**
|
|
586
|
+
```ruby
|
|
587
|
+
# Reindex all products with embeddings
|
|
588
|
+
processed = Product.reindex_vectors(
|
|
589
|
+
scope: Product.where.not(embedding: nil),
|
|
590
|
+
batch_size: 500
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
puts "Reindexed #{processed} products"
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
574
598
|
## Error Handling
|
|
575
599
|
|
|
576
600
|
Vectra defines specific error types:
|
data/docs/api/overview.md
CHANGED
|
@@ -216,3 +216,9 @@ end
|
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
See [Complete API Methods Reference]({{ site.baseurl }}/api/methods/) for detailed method documentation.
|
|
219
|
+
|
|
220
|
+
## Next Steps
|
|
221
|
+
|
|
222
|
+
- [Providers Guide]({{ site.baseurl }}/providers/)
|
|
223
|
+
- [Rails Integration Guide]({{ site.baseurl }}/guides/rails-integration/)
|
|
224
|
+
- [Middleware System]({{ site.baseurl }}/guides/middleware/)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Middleware System
|
|
4
|
+
permalink: /guides/middleware/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Middleware System
|
|
8
|
+
|
|
9
|
+
Vectra includes a **Rack-style middleware stack** that lets you extend the client
|
|
10
|
+
without forking the gem or patching providers.
|
|
11
|
+
|
|
12
|
+
You can:
|
|
13
|
+
|
|
14
|
+
- Add **logging, metrics, retries, PII redaction, cost tracking**.
|
|
15
|
+
- Inject **custom behaviour** before/after every operation.
|
|
16
|
+
- Enable features **globally** or **per client**, same kao Faraday/Sidekiq.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Core Concepts
|
|
21
|
+
|
|
22
|
+
### Request / Response
|
|
23
|
+
|
|
24
|
+
- **`Vectra::Middleware::Request`**
|
|
25
|
+
- `operation` – npr. `:upsert`, `:query`, `:fetch`, `:delete`, `:stats`, `:hybrid_search`
|
|
26
|
+
- `index` – ime indeksa (može biti `nil` ako koristiš default)
|
|
27
|
+
- `namespace` – namespace (može biti `nil`)
|
|
28
|
+
- `params` – originalni keyword parametri koje je `Vectra::Client` proslijedio
|
|
29
|
+
- `provider` – ime providera (`:pinecone`, `:qdrant`, `:pgvector`, `:memory`, …)
|
|
30
|
+
- helperi: `write_operation?`, `read_operation?`
|
|
31
|
+
|
|
32
|
+
- **`Vectra::Middleware::Response`**
|
|
33
|
+
- `result` – što god provider vrati (npr. hash, `QueryResult`, itd.)
|
|
34
|
+
- `error` – iznimka ako je došlo do greške
|
|
35
|
+
- `metadata` – slobodan hash za dodatne informacije (trajanje, cost, retry_count…)
|
|
36
|
+
- helperi: `success?`, `failure?`, `raise_if_error!`, `value!`
|
|
37
|
+
|
|
38
|
+
### Base i Stack
|
|
39
|
+
|
|
40
|
+
- **`Vectra::Middleware::Base`**
|
|
41
|
+
- Hookovi koje možeš override-ati:
|
|
42
|
+
- `before(request)` – prije poziva providera / sljedećeg middlewarea
|
|
43
|
+
- `after(request, response)` – nakon uspješnog poziva
|
|
44
|
+
- `on_error(request, error)` – kad dođe do iznimke (error se zatim re-raise-a)
|
|
45
|
+
|
|
46
|
+
- **`Vectra::Middleware::Stack`**
|
|
47
|
+
- Gradi chain oko konkretnog providera:
|
|
48
|
+
```ruby
|
|
49
|
+
stack = Vectra::Middleware::Stack.new(provider, [Logging.new, Retry.new])
|
|
50
|
+
result = stack.call(:upsert, index: "docs", vectors: [...], provider: :qdrant)
|
|
51
|
+
```
|
|
52
|
+
- `Stack` interno:
|
|
53
|
+
- kreira `Request`,
|
|
54
|
+
- kroz sve middlewares propagira isti `Request`,
|
|
55
|
+
- na kraju zove `provider.public_send(request.operation, **provider_params)`,
|
|
56
|
+
- vraća `Response` (s `result` ili `error`).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Enabling Middleware
|
|
61
|
+
|
|
62
|
+
### Global Middleware
|
|
63
|
+
|
|
64
|
+
Primjenjuje se na **sve** `Vectra::Client` instance.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
require "vectra"
|
|
68
|
+
|
|
69
|
+
# Global logging + retry + cost tracking
|
|
70
|
+
Vectra::Client.use Vectra::Middleware::Logging
|
|
71
|
+
Vectra::Client.use Vectra::Middleware::Retry, max_attempts: 5
|
|
72
|
+
Vectra::Client.use Vectra::Middleware::CostTracker
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Sve sljedeće `Vectra::Client.new(...)` instance će koristiti ovaj globalni stack.
|
|
76
|
+
|
|
77
|
+
### Per‑Client Middleware
|
|
78
|
+
|
|
79
|
+
Dodatni ili prilagođeni middleware samo za jedan klijent:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
client = Vectra::Client.new(
|
|
83
|
+
provider: :qdrant,
|
|
84
|
+
index: "products",
|
|
85
|
+
middleware: [
|
|
86
|
+
Vectra::Middleware::PIIRedaction,
|
|
87
|
+
Vectra::Middleware::Instrumentation
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- Per‑client middleware se izvodi **nakon** globalnog, u istom chainu.
|
|
93
|
+
- Redoslijed u arrayu definira redoslijed ekzekucije (zadnji je najunutarnji, tik do providera).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Koje operacije prolaze kroz stack?
|
|
98
|
+
|
|
99
|
+
Sve standardne operacije `Vectra::Client`a koriste middleware stack:
|
|
100
|
+
|
|
101
|
+
- `upsert`
|
|
102
|
+
- `query`
|
|
103
|
+
- `fetch`
|
|
104
|
+
- `update`
|
|
105
|
+
- `delete`
|
|
106
|
+
- `list_indexes`
|
|
107
|
+
- `describe_index`
|
|
108
|
+
- `stats`
|
|
109
|
+
- `hybrid_search`
|
|
110
|
+
|
|
111
|
+
To znači da middleware može:
|
|
112
|
+
|
|
113
|
+
- logirati / instrumentirati **sve pozive** prema provideru,
|
|
114
|
+
- raditi **PII redakciju** na `upsert` zahtjevima,
|
|
115
|
+
- brojati i retry-ati i **read** i **write** operacije,
|
|
116
|
+
- računati trošak po operaciji (npr. za billing / budžete).
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Built‑in Middleware
|
|
121
|
+
|
|
122
|
+
### Logging (`Vectra::Middleware::Logging`)
|
|
123
|
+
|
|
124
|
+
**Što radi:**
|
|
125
|
+
- logira početak i kraj svake operacije (`operation`, `index`, `namespace`),
|
|
126
|
+
- mjeri trajanje i sprema ga u `response.metadata[:duration_ms]`.
|
|
127
|
+
|
|
128
|
+
**Konfiguracija:**
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Globalno
|
|
132
|
+
Vectra::Client.use Vectra::Middleware::Logging
|
|
133
|
+
|
|
134
|
+
# S custom loggerom
|
|
135
|
+
logger = Logger.new($stdout)
|
|
136
|
+
Vectra::Client.use Vectra::Middleware::Logging, logger: logger
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Tipična upotreba:** debugiranje, audit logovi, korelacija s HTTP logovima.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Retry (`Vectra::Middleware::Retry`)
|
|
144
|
+
|
|
145
|
+
**Što radi:**
|
|
146
|
+
- automatski retry-a transient greške:
|
|
147
|
+
- `Vectra::RateLimitError`
|
|
148
|
+
- `Vectra::ConnectionError`
|
|
149
|
+
- `Vectra::TimeoutError`
|
|
150
|
+
- `Vectra::ServerError`
|
|
151
|
+
- koristi exponential ili linear backoff,
|
|
152
|
+
- upisuje broj retry-a u `response.metadata[:retry_count]`.
|
|
153
|
+
|
|
154
|
+
**Konfiguracija:**
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# 3 pokušaja, exponential backoff (default)
|
|
158
|
+
Vectra::Client.use Vectra::Middleware::Retry
|
|
159
|
+
|
|
160
|
+
# 5 pokušaja, linearni backoff
|
|
161
|
+
Vectra::Client.use Vectra::Middleware::Retry,
|
|
162
|
+
max_attempts: 5,
|
|
163
|
+
backoff: :linear
|
|
164
|
+
|
|
165
|
+
# Fiksni delay 1.0s
|
|
166
|
+
Vectra::Client.use Vectra::Middleware::Retry,
|
|
167
|
+
max_attempts: 3,
|
|
168
|
+
backoff: 1.0
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Tipična upotreba:** zaštita od povremenih mrežnih problema i rate‑limit grešaka.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### Instrumentation (`Vectra::Middleware::Instrumentation`)
|
|
176
|
+
|
|
177
|
+
**Što radi:**
|
|
178
|
+
- emitira događaje preko postojećeg `Vectra::Instrumentation` sustava,
|
|
179
|
+
- prikuplja trajanje, status, error class, dodatni `metadata`.
|
|
180
|
+
|
|
181
|
+
**Primjer:**
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
Vectra::Client.use Vectra::Middleware::Instrumentation
|
|
185
|
+
|
|
186
|
+
Vectra.on_operation do |event|
|
|
187
|
+
# event[:operation], event[:provider], event[:duration_ms], event[:success], ...
|
|
188
|
+
StatsD.timing("vectra.#{event[:operation]}", event[:duration_ms])
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Tipična upotreba:** integracija s Prometheus/Grafana, Datadog, New Relic…
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### PII Redaction (`Vectra::Middleware::PIIRedaction`)
|
|
197
|
+
|
|
198
|
+
**Što radi:**
|
|
199
|
+
- prije `upsert` operacija prolazi kroz `vectors[:metadata]`,
|
|
200
|
+
- prepoznaje PII pattern-e (email, phone, SSN, credit card) i zamjenjuje ih placeholderom
|
|
201
|
+
npr. `[REDACTED_EMAIL]`, `[REDACTED_PHONE]`, itd.
|
|
202
|
+
|
|
203
|
+
**Primjer:**
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
Vectra::Client.use Vectra::Middleware::PIIRedaction
|
|
207
|
+
|
|
208
|
+
client.upsert(
|
|
209
|
+
index: "sensitive",
|
|
210
|
+
vectors: [
|
|
211
|
+
{
|
|
212
|
+
id: "user-1",
|
|
213
|
+
values: [0.1, 0.2, 0.3],
|
|
214
|
+
metadata: {
|
|
215
|
+
email: "user@example.com",
|
|
216
|
+
phone: "555-1234",
|
|
217
|
+
note: "Contact at user@example.com"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Nakon `upsert`‑a, provider će vidjeti već **redaktirani** metadata.
|
|
225
|
+
|
|
226
|
+
**Custom patterni:**
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
patterns = {
|
|
230
|
+
credit_card: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/,
|
|
231
|
+
api_key: /sk-[a-zA-Z0-9]{32}/
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Vectra::Client.use Vectra::Middleware::PIIRedaction, patterns: patterns
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Tipična upotreba:** GDPR, SOC2, PCI‑DSS okruženja gdje je zabranjen PII u vektor bazi.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### CostTracker (`Vectra::Middleware::CostTracker`)
|
|
242
|
+
|
|
243
|
+
**Što radi:**
|
|
244
|
+
- procjenjuje trošak po operaciji na temelju providera i tipa operacije (`read` / `write`),
|
|
245
|
+
- upisuje trošak u `response.metadata[:cost_usd]`,
|
|
246
|
+
- opcionalno zove `on_cost` callback za real‑time praćenje.
|
|
247
|
+
|
|
248
|
+
**Primjer:**
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
Vectra::Client.use Vectra::Middleware::CostTracker,
|
|
252
|
+
on_cost: ->(event) {
|
|
253
|
+
puts "💰 Cost: $#{event[:cost_usd].round(6)} for #{event[:operation]} (#{event[:provider]})"
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Custom pricing:**
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
pricing = {
|
|
261
|
+
pinecone: { read: 0.0001, write: 0.0002 },
|
|
262
|
+
qdrant: { read: 0.00005, write: 0.0001 }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
Vectra::Client.use Vectra::Middleware::CostTracker, pricing: pricing
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Tipična upotreba:** unutarnji billing, budget guardrails, cost dashboards.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Custom Middleware
|
|
273
|
+
|
|
274
|
+
Najjednostavniji način je naslijediti `Vectra::Middleware::Base` i override-ati hookove:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class MyAuditMiddleware < Vectra::Middleware::Base
|
|
278
|
+
def before(request)
|
|
279
|
+
AuditLog.create!(
|
|
280
|
+
operation: request.operation,
|
|
281
|
+
index: request.index,
|
|
282
|
+
namespace: request.namespace,
|
|
283
|
+
provider: request.provider
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def after(_request, response)
|
|
288
|
+
puts "Duration: #{response.metadata[:duration_ms]}ms"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def on_error(request, error)
|
|
292
|
+
ErrorTracker.notify(error, context: { operation: request.operation })
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
Vectra::Client.use MyAuditMiddleware
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Savjeti:**
|
|
300
|
+
|
|
301
|
+
- Ne mijenjaj strukturu `request.params` na način koji provider ne očekuje.
|
|
302
|
+
- Svoje pomoćne podatke stavljaj u `response.metadata` ili `request.metadata`.
|
|
303
|
+
- Ako hvataš greške u `on_error`, **nemoj ih gutati** – middleware stack će ih ponovo baciti.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Primjer: `examples/middleware_demo.rb`
|
|
308
|
+
|
|
309
|
+
U repozitoriju imaš kompletan demo:
|
|
310
|
+
|
|
311
|
+
- konfigurira globalni stack (`Logging`, `Retry`, `CostTracker`),
|
|
312
|
+
- pokazuje per‑client PII redaction,
|
|
313
|
+
- definira custom `TimingMiddleware`,
|
|
314
|
+
- demonstrira kako izgleda output u konzoli.
|
|
315
|
+
|
|
316
|
+
Pokretanje:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
bundle exec ruby examples/middleware_demo.rb
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Ovaj demo je dobar “živi” primjer kako kombinirati više middleware-a u praksi.
|
|
323
|
+
|
|
324
|
+
|
|
@@ -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!"
|