vectra-client 1.1.1 → 1.1.3
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 +10 -28
- data/README.md +2 -0
- data/docs/_layouts/home.html +1 -1
- data/docs/_layouts/page.html +22 -0
- data/docs/api/cheatsheet.md +27 -0
- data/docs/api/methods.md +73 -0
- data/docs/api/overview.md +36 -5
- data/docs/assets/style.css +131 -0
- data/docs/guides/rails-integration.md +2 -0
- data/docs/search.json +26 -0
- data/lib/vectra/client.rb +166 -1
- data/lib/vectra/health_check.rb +3 -3
- data/lib/vectra/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83a312bb14b8f556d743e0072548a33ec0ac79c9d64768c78b18af3bc7ac6c6b
|
|
4
|
+
data.tar.gz: 0f0e859dd346e86f095a882e4cb7d47fd1ecf05aa968730b2b3e0d2ff7fa253b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccbc0520ce83f7d2f039a50c0a4b16a9fb59ce21956eb00d5f28270bf61e3ae6c81ab133b02b3ffd1662d29864f5b0ae9c04f60b8e61715f8cbb513b71d48c72
|
|
7
|
+
data.tar.gz: 38a9b1bbfc4db14005b88d1f29957806f888dc80886c147925fe9dec1643ca67e992aae71258108130ba33b317b999658df5a7756fffe92f0f91bb39b3ba88d1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,41 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [v1.1.
|
|
4
|
-
|
|
5
|
-
[Full Changelog](https://github.com/stokry/vectra/compare/v1.0.8...v1.1.0)
|
|
6
|
-
|
|
7
|
-
### 🎉 Major Feature: Middleware System
|
|
3
|
+
## [v1.1.3](https://github.com/stokry/vectra/tree/v1.1.3) (2026-01-20)
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
[Full Changelog](https://github.com/stokry/vectra/compare/v1.1.2...v1.1.3)
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
- Add `Client#validate!` for configuration/capability checks
|
|
8
|
+
- Add `Client#with_defaults` block helper for temporary default index/namespace
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
- **5 Built-in Middleware**:
|
|
15
|
-
- `Vectra::Middleware::Logging` - Structured logs with timing for all operations
|
|
16
|
-
- `Vectra::Middleware::Retry` - Automatic retry with exponential/linear backoff for transient errors
|
|
17
|
-
- `Vectra::Middleware::Instrumentation` - Hooks for metrics and APM integration
|
|
18
|
-
- `Vectra::Middleware::PIIRedaction` - Automatic PII redaction (email, phone, SSN, credit cards)
|
|
19
|
-
- `Vectra::Middleware::CostTracker` - Track API costs per operation with callbacks
|
|
20
|
-
- **Request/Response Objects** - Type-safe objects with metadata attachment
|
|
21
|
-
- **Extensible Framework** - Create custom middleware by extending `Vectra::Middleware::Base`
|
|
22
|
-
- **Global & Per-Client Middleware** - Apply middleware globally (`Client.use`) or per-instance (`new(middleware: [...])`)
|
|
10
|
+
## [v1.1.2](https://github.com/stokry/vectra/tree/v1.1.2) (2026-01-19)
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
[Full Changelog](https://github.com/stokry/vectra/compare/v1.1.1...v1.1.2)
|
|
25
13
|
|
|
26
|
-
|
|
27
|
-
- Middleware has complete visibility into all client operations
|
|
14
|
+
## [v1.1.1](https://github.com/stokry/vectra/tree/v1.1.1) (2026-01-15)
|
|
28
15
|
|
|
29
|
-
|
|
16
|
+
[Full Changelog](https://github.com/stokry/vectra/compare/v1.1.0...v1.1.1)
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
- Created `examples/middleware_demo.rb` demonstrating all 5 built-in middleware
|
|
33
|
-
- Full YARD documentation for all middleware classes
|
|
34
|
-
- Published [middleware guide](https://dev.to/stokry/rack-style-middleware-for-vector-databases-in-ruby-vectra-client-110-2jh3) on Dev.to
|
|
35
|
-
|
|
36
|
-
#### Migration Notes
|
|
18
|
+
## [v1.1.0](https://github.com/stokry/vectra/tree/v1.1.0) (2026-01-15)
|
|
37
19
|
|
|
38
|
-
|
|
20
|
+
[Full Changelog](https://github.com/stokry/vectra/compare/v1.0.8...v1.1.0)
|
|
39
21
|
|
|
40
22
|
## [v1.0.8](https://github.com/stokry/vectra/tree/v1.0.8) (2026-01-14)
|
|
41
23
|
|
data/README.md
CHANGED
|
@@ -226,6 +226,8 @@ This will:
|
|
|
226
226
|
- **Update the model** to include `ProductVector`
|
|
227
227
|
- **Append to `config/vectra.yml`** with index metadata (no API keys)
|
|
228
228
|
|
|
229
|
+
When `config/vectra.yml` contains exactly one entry, a plain `Vectra::Client.new` in that Rails app will automatically use that entry's `index` (and `namespace` if present) as its defaults, so you can usually omit `index:` when calling `upsert` / `query` / `text_search`.
|
|
230
|
+
|
|
229
231
|
### Complete Rails Guide
|
|
230
232
|
|
|
231
233
|
For a complete step-by-step guide including:
|
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.1.
|
|
58
|
+
<span class="tma-hero__badge">v1.1.1 — Hybrid Search, Rails Generator, Middleware & Text Search</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/_layouts/page.html
CHANGED
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
<span class="tma-nav__toggle-line"></span>
|
|
40
40
|
</button>
|
|
41
41
|
<ul class="tma-nav__menu" id="nav-menu">
|
|
42
|
+
<li class="tma-nav__search-wrapper">
|
|
43
|
+
<div class="tma-search">
|
|
44
|
+
<input type="search" id="search-input" class="tma-search__input" placeholder="Search docs..." aria-label="Search documentation">
|
|
45
|
+
<div id="search-results" class="tma-search__results"></div>
|
|
46
|
+
</div>
|
|
47
|
+
</li>
|
|
42
48
|
<li><a href="{{ site.baseurl }}/guides/getting-started" class="tma-nav__link">Getting Started</a></li>
|
|
43
49
|
<li><a href="{{ site.baseurl }}/guides/recipes" class="tma-nav__link">Recipes</a></li>
|
|
44
50
|
<li><a href="{{ site.baseurl }}/providers" class="tma-nav__link">Providers</a></li>
|
|
@@ -122,6 +128,9 @@
|
|
|
122
128
|
</div>
|
|
123
129
|
</footer>
|
|
124
130
|
|
|
131
|
+
<!-- Simple Jekyll Search -->
|
|
132
|
+
<script src="https://cdn.jsdelivr.net/npm/simple-jekyll-search@1.10.0/dest/simple-jekyll-search.min.js"></script>
|
|
133
|
+
|
|
125
134
|
<script>
|
|
126
135
|
document.addEventListener('DOMContentLoaded', function() {
|
|
127
136
|
const navToggle = document.querySelector('.tma-nav__toggle');
|
|
@@ -155,6 +164,19 @@
|
|
|
155
164
|
}
|
|
156
165
|
});
|
|
157
166
|
}
|
|
167
|
+
|
|
168
|
+
// Initialize search
|
|
169
|
+
if (typeof SimpleJekyllSearch !== 'undefined') {
|
|
170
|
+
SimpleJekyllSearch({
|
|
171
|
+
searchInput: document.getElementById('search-input'),
|
|
172
|
+
resultsContainer: document.getElementById('search-results'),
|
|
173
|
+
json: '{{ site.baseurl }}/search.json',
|
|
174
|
+
searchResultTemplate: '<div class="tma-search__result"><a href="{url}" class="tma-search__result-link"><span class="tma-search__result-title">{title}</span><span class="tma-search__result-excerpt">{excerpt}</span></a></div>',
|
|
175
|
+
noResultsText: '<div class="tma-search__no-results">No results found</div>',
|
|
176
|
+
fuzzy: true,
|
|
177
|
+
limit: 10
|
|
178
|
+
});
|
|
179
|
+
}
|
|
158
180
|
});
|
|
159
181
|
</script>
|
|
160
182
|
</body>
|
data/docs/api/cheatsheet.md
CHANGED
|
@@ -49,6 +49,25 @@ client.upsert(vectors: [...])
|
|
|
49
49
|
client.query(vector: query_embedding, top_k: 10)
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
In a Rails app with `config/vectra.yml` generated by `rails generate vectra:index`, if that YAML file contains only one entry, `Vectra::Client.new` will automatically use that entry's `index` (and `namespace`, if present) as defaults.
|
|
53
|
+
|
|
54
|
+
### Temporary defaults (block-scoped)
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
client.with_defaults(index: "products", namespace: "tenant-2") do |c|
|
|
58
|
+
c.upsert(vectors: [...])
|
|
59
|
+
c.query(vector: query_embedding, top_k: 10)
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Validate client config & capabilities
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
client.validate!
|
|
67
|
+
client.validate!(require_default_index: true)
|
|
68
|
+
client.validate!(features: [:text_search])
|
|
69
|
+
```
|
|
70
|
+
|
|
52
71
|
### Upsert
|
|
53
72
|
|
|
54
73
|
```ruby
|
|
@@ -193,6 +212,14 @@ else
|
|
|
193
212
|
end
|
|
194
213
|
```
|
|
195
214
|
|
|
215
|
+
For fast health checks you can temporarily lower the timeout:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
status = client.with_timeout(0.5) do |c|
|
|
219
|
+
c.ping
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
196
223
|
### Ping (with latency)
|
|
197
224
|
|
|
198
225
|
```ruby
|
data/docs/api/methods.md
CHANGED
|
@@ -36,6 +36,8 @@ client = Vectra::Client.new(
|
|
|
36
36
|
)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
In a Rails app that uses the `vectra:index` generator, if `config/vectra.yml` contains exactly one entry, `Vectra::Client.new` will automatically use that entry's `index` (and `namespace` if present) as its defaults. This allows you to omit `index:` in most calls (`upsert`, `query`, `text_search`, etc.).
|
|
40
|
+
|
|
39
41
|
---
|
|
40
42
|
|
|
41
43
|
### `client.upsert(index:, vectors:, namespace: nil)`
|
|
@@ -402,6 +404,77 @@ puts "Latency: #{status[:latency_ms]}ms"
|
|
|
402
404
|
|
|
403
405
|
---
|
|
404
406
|
|
|
407
|
+
### `client.with_timeout(seconds) { ... }`
|
|
408
|
+
|
|
409
|
+
Temporarily override the client's request timeout inside a block.
|
|
410
|
+
|
|
411
|
+
**Parameters:**
|
|
412
|
+
- `seconds` (Float) - Temporary timeout in seconds
|
|
413
|
+
|
|
414
|
+
**Returns:** Block result
|
|
415
|
+
|
|
416
|
+
**Example (fast health check in Rails controller):**
|
|
417
|
+
```ruby
|
|
418
|
+
status = client.with_timeout(0.5) do |c|
|
|
419
|
+
c.ping
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
render json: status, status: status[:healthy] ? :ok : :service_unavailable
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
After the block finishes (even if it raises), the previous `config.timeout` value is restored.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### `client.validate!(require_default_index: false, require_default_namespace: false, features: [])`
|
|
430
|
+
|
|
431
|
+
Validate the client configuration and (optionally) your defaults and provider feature support.
|
|
432
|
+
|
|
433
|
+
This is useful in boot-time checks (Rails initializers), health endpoints, and CI.
|
|
434
|
+
|
|
435
|
+
**Parameters:**
|
|
436
|
+
- `require_default_index` (Boolean) - Require `client.default_index` to be set
|
|
437
|
+
- `require_default_namespace` (Boolean) - Require `client.default_namespace` to be set
|
|
438
|
+
- `features` (Array<Symbol> or Symbol) - Provider features (methods) that must be supported (e.g. `:text_search`)
|
|
439
|
+
|
|
440
|
+
**Returns:** `Vectra::Client` (self)
|
|
441
|
+
|
|
442
|
+
**Raises:** `Vectra::ConfigurationError` when validation fails
|
|
443
|
+
|
|
444
|
+
**Example:**
|
|
445
|
+
```ruby
|
|
446
|
+
# Ensure client is configured
|
|
447
|
+
client.validate!
|
|
448
|
+
|
|
449
|
+
# Ensure calls can omit index:
|
|
450
|
+
client.validate!(require_default_index: true)
|
|
451
|
+
|
|
452
|
+
# Ensure provider supports text_search:
|
|
453
|
+
client.validate!(features: [:text_search])
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
### `client.with_defaults(index: ..., namespace: ...) { ... }`
|
|
459
|
+
|
|
460
|
+
Temporarily override the client's **default index and/or namespace** inside a block.
|
|
461
|
+
|
|
462
|
+
Unlike `with_index_and_namespace`, this helper accepts keyword arguments and only overrides what you pass.
|
|
463
|
+
|
|
464
|
+
**Returns:** Block result
|
|
465
|
+
|
|
466
|
+
**Example:**
|
|
467
|
+
```ruby
|
|
468
|
+
client.with_defaults(index: "products", namespace: "tenant-2") do |c|
|
|
469
|
+
c.upsert(vectors: [...]) # uses products/tenant-2
|
|
470
|
+
c.query(vector: embedding, top_k: 10)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Previous defaults are restored after the block.
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
405
478
|
### `client.health_check`
|
|
406
479
|
|
|
407
480
|
Detailed health check with provider-specific information.
|
data/docs/api/overview.md
CHANGED
|
@@ -10,15 +10,15 @@ permalink: /api/overview/
|
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
12
|
client = Vectra::Client.new(
|
|
13
|
-
provider: :pinecone, # Required: :pinecone, :qdrant, :weaviate, :pgvector
|
|
13
|
+
provider: :pinecone, # Required: :pinecone, :qdrant, :weaviate, :pgvector, :memory
|
|
14
14
|
api_key: 'your-api-key', # Required for cloud providers
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
port: 6333, # For self-hosted providers
|
|
18
|
-
environment: 'us-west-4' # For Pinecone
|
|
15
|
+
index: 'my-index', # Optional default index
|
|
16
|
+
namespace: 'tenant-1' # Optional default namespace
|
|
19
17
|
)
|
|
20
18
|
```
|
|
21
19
|
|
|
20
|
+
In Rails, if you use the `vectra:index` generator and `config/vectra.yml` contains exactly one entry, a plain `Vectra::Client.new` will automatically pick that entry's `index` (and `namespace` if present) as defaults.
|
|
21
|
+
|
|
22
22
|
## Core Methods
|
|
23
23
|
|
|
24
24
|
### `upsert(vectors:)`
|
|
@@ -138,6 +138,31 @@ results = client.hybrid_search(
|
|
|
138
138
|
|
|
139
139
|
**Provider Support:** Qdrant ✅, Weaviate ✅, pgvector ✅, Pinecone ⚠️
|
|
140
140
|
|
|
141
|
+
### `text_search(index:, text:, top_k:)`
|
|
142
|
+
|
|
143
|
+
Text-only search (keyword search without requiring embeddings).
|
|
144
|
+
|
|
145
|
+
**Parameters:**
|
|
146
|
+
- `index` (String) - Index/collection name (uses client's default index when omitted)
|
|
147
|
+
- `text` (String) - Text query for keyword search
|
|
148
|
+
- `top_k` (Integer) - Number of results (default: 10)
|
|
149
|
+
- `namespace` (String, optional) - Namespace
|
|
150
|
+
- `filter` (Hash, optional) - Metadata filter
|
|
151
|
+
- `include_values` (Boolean) - Include vector values (default: false)
|
|
152
|
+
- `include_metadata` (Boolean) - Include metadata (default: true)
|
|
153
|
+
|
|
154
|
+
**Example:**
|
|
155
|
+
```ruby
|
|
156
|
+
results = client.text_search(
|
|
157
|
+
index: 'products',
|
|
158
|
+
text: 'iPhone 15 Pro',
|
|
159
|
+
top_k: 10,
|
|
160
|
+
filter: { category: 'electronics' }
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Provider Support:** Qdrant ✅ (BM25), Weaviate ✅ (BM25), pgvector ✅ (PostgreSQL full-text), Memory ✅, Pinecone ❌
|
|
165
|
+
|
|
141
166
|
### `healthy?`
|
|
142
167
|
|
|
143
168
|
Quick health check - returns true if provider connection is healthy.
|
|
@@ -151,6 +176,12 @@ if client.healthy?
|
|
|
151
176
|
end
|
|
152
177
|
```
|
|
153
178
|
|
|
179
|
+
You can also run faster checks with a temporary timeout:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
fast_ok = client.with_timeout(0.5) { |c| c.healthy? }
|
|
183
|
+
```
|
|
184
|
+
|
|
154
185
|
### `ping`
|
|
155
186
|
|
|
156
187
|
Ping provider and get connection health status with latency.
|
data/docs/assets/style.css
CHANGED
|
@@ -165,6 +165,118 @@ body {
|
|
|
165
165
|
border-color: var(--tma-color-border-hover);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
.tma-nav__search-wrapper {
|
|
169
|
+
position: relative;
|
|
170
|
+
margin-right: var(--tma-spacing-md);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tma-search {
|
|
174
|
+
position: relative;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.tma-search__input {
|
|
178
|
+
width: 240px;
|
|
179
|
+
padding: var(--tma-spacing-sm) var(--tma-spacing-md);
|
|
180
|
+
padding-left: 2.5rem;
|
|
181
|
+
background: var(--tma-color-bg-tertiary);
|
|
182
|
+
border: 1px solid var(--tma-color-border);
|
|
183
|
+
border-radius: var(--tma-radius-sm);
|
|
184
|
+
color: var(--tma-color-text-primary);
|
|
185
|
+
font-size: 0.9rem;
|
|
186
|
+
font-family: var(--tma-font-family-primary);
|
|
187
|
+
transition: all var(--tma-transition-fast);
|
|
188
|
+
outline: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.tma-search__input::placeholder {
|
|
192
|
+
color: var(--tma-color-text-muted);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.tma-search__input:focus {
|
|
196
|
+
width: 320px;
|
|
197
|
+
border-color: var(--tma-color-accent-primary);
|
|
198
|
+
background: var(--tma-color-bg-elevated);
|
|
199
|
+
box-shadow: 0 0 0 3px var(--tma-color-accent-muted);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.tma-search {
|
|
203
|
+
position: relative;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.tma-search::before {
|
|
207
|
+
content: '🔍';
|
|
208
|
+
position: absolute;
|
|
209
|
+
left: var(--tma-spacing-md);
|
|
210
|
+
top: 50%;
|
|
211
|
+
transform: translateY(-50%);
|
|
212
|
+
color: var(--tma-color-text-muted);
|
|
213
|
+
pointer-events: none;
|
|
214
|
+
z-index: 1;
|
|
215
|
+
font-size: 0.9rem;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.tma-search__results {
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: calc(100% + var(--tma-spacing-xs));
|
|
221
|
+
left: 0;
|
|
222
|
+
right: 0;
|
|
223
|
+
max-width: 500px;
|
|
224
|
+
max-height: 400px;
|
|
225
|
+
overflow-y: auto;
|
|
226
|
+
background: var(--tma-color-bg-elevated);
|
|
227
|
+
border: 1px solid var(--tma-color-border);
|
|
228
|
+
border-radius: var(--tma-radius-md);
|
|
229
|
+
box-shadow: var(--tma-shadow-lg);
|
|
230
|
+
z-index: 1000;
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.tma-search__results:not(:empty) {
|
|
235
|
+
display: block;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.tma-search__result {
|
|
239
|
+
border-bottom: 1px solid var(--tma-color-border);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.tma-search__result:last-child {
|
|
243
|
+
border-bottom: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.tma-search__result-link {
|
|
247
|
+
display: block;
|
|
248
|
+
padding: var(--tma-spacing-md);
|
|
249
|
+
text-decoration: none;
|
|
250
|
+
color: var(--tma-color-text-primary);
|
|
251
|
+
transition: background var(--tma-transition-fast);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.tma-search__result-link:hover {
|
|
255
|
+
background: var(--tma-color-bg-hover);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.tma-search__result-title {
|
|
259
|
+
display: block;
|
|
260
|
+
font-weight: 600;
|
|
261
|
+
font-size: 0.95rem;
|
|
262
|
+
color: var(--tma-color-text-primary);
|
|
263
|
+
margin-bottom: var(--tma-spacing-xs);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.tma-search__result-excerpt {
|
|
267
|
+
display: block;
|
|
268
|
+
font-size: 0.85rem;
|
|
269
|
+
color: var(--tma-color-text-secondary);
|
|
270
|
+
line-height: 1.5;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.tma-search__no-results {
|
|
274
|
+
padding: var(--tma-spacing-lg);
|
|
275
|
+
text-align: center;
|
|
276
|
+
color: var(--tma-color-text-muted);
|
|
277
|
+
font-size: 0.9rem;
|
|
278
|
+
}
|
|
279
|
+
|
|
168
280
|
.tma-nav__toggle {
|
|
169
281
|
display: none;
|
|
170
282
|
flex-direction: column;
|
|
@@ -1261,6 +1373,25 @@ code {
|
|
|
1261
1373
|
justify-content: center;
|
|
1262
1374
|
}
|
|
1263
1375
|
|
|
1376
|
+
.tma-nav__search-wrapper {
|
|
1377
|
+
width: 100%;
|
|
1378
|
+
margin-right: 0;
|
|
1379
|
+
margin-bottom: var(--tma-spacing-md);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.tma-search__input {
|
|
1383
|
+
width: 100%;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.tma-search__input:focus {
|
|
1387
|
+
width: 100%;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
.tma-search__results {
|
|
1391
|
+
max-width: 100%;
|
|
1392
|
+
right: 0;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1264
1395
|
.tma-features,
|
|
1265
1396
|
.tma-providers {
|
|
1266
1397
|
padding: var(--tma-spacing-xl) var(--tma-spacing-md);
|
|
@@ -99,6 +99,8 @@ This will:
|
|
|
99
99
|
- Update `app/models/product.rb` to include the concern
|
|
100
100
|
- Add configuration to `config/vectra.yml`
|
|
101
101
|
|
|
102
|
+
When `config/vectra.yml` contains exactly one entry, a plain `Vectra::Client.new` in this Rails app will automatically use that entry's `index` (and `namespace` if present) as its defaults. That means you can usually omit `index:` when calling `upsert`, `query`, `hybrid_search`, or `text_search`.
|
|
103
|
+
|
|
102
104
|
### Run Migrations
|
|
103
105
|
|
|
104
106
|
```bash
|
data/docs/search.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: null
|
|
3
|
+
---
|
|
4
|
+
[
|
|
5
|
+
{% assign first = true %}
|
|
6
|
+
{% for page in site.pages %}
|
|
7
|
+
{% unless page.url == '/' or page.url == '/search.json' or page.url contains '/assets/' or page.url contains '/404' or page.url contains '/feed' or page.url contains '/sitemap' or page.url contains '/robots' or page.url contains '/index.html' or page.url contains '/index.md' %}
|
|
8
|
+
{% unless first %},{% endunless %}
|
|
9
|
+
{
|
|
10
|
+
"title": {{ page.title | default: page.url | jsonify }},
|
|
11
|
+
"url": {{ page.url | jsonify }},
|
|
12
|
+
"excerpt": {{ page.content | strip_html | truncatewords: 30 | default: "" | jsonify }}
|
|
13
|
+
}
|
|
14
|
+
{% assign first = false %}
|
|
15
|
+
{% endunless %}
|
|
16
|
+
{% endfor %}
|
|
17
|
+
{% for post in site.posts %}
|
|
18
|
+
{% unless first %},{% endunless %}
|
|
19
|
+
{
|
|
20
|
+
"title": {{ post.title | jsonify }},
|
|
21
|
+
"url": {{ post.url | jsonify }},
|
|
22
|
+
"excerpt": {{ post.content | strip_html | truncatewords: 30 | default: "" | jsonify }}
|
|
23
|
+
}
|
|
24
|
+
{% assign first = false %}
|
|
25
|
+
{% endfor %}
|
|
26
|
+
]
|
data/lib/vectra/client.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
3
5
|
# Ensure HealthCheck is loaded before Client
|
|
4
6
|
require_relative "health_check" unless defined?(Vectra::HealthCheck)
|
|
5
7
|
require_relative "configuration" unless defined?(Vectra::Configuration)
|
|
@@ -42,6 +44,9 @@ module Vectra
|
|
|
42
44
|
|
|
43
45
|
attr_reader :config, :provider, :default_index, :default_namespace
|
|
44
46
|
|
|
47
|
+
DEFAULT_UNSET = Object.new.freeze
|
|
48
|
+
private_constant :DEFAULT_UNSET
|
|
49
|
+
|
|
45
50
|
class << self
|
|
46
51
|
# Get the global middleware stack
|
|
47
52
|
#
|
|
@@ -89,6 +94,7 @@ module Vectra
|
|
|
89
94
|
@provider = build_provider
|
|
90
95
|
@default_index = options[:index]
|
|
91
96
|
@default_namespace = options[:namespace]
|
|
97
|
+
apply_rails_vectra_defaults! if @default_index.nil? && @default_namespace.nil?
|
|
92
98
|
@middleware = build_middleware_stack(options[:middleware])
|
|
93
99
|
end
|
|
94
100
|
|
|
@@ -618,6 +624,43 @@ module Vectra
|
|
|
618
624
|
result
|
|
619
625
|
end
|
|
620
626
|
|
|
627
|
+
# Validate the client configuration and provider capabilities.
|
|
628
|
+
#
|
|
629
|
+
# This is a convenience wrapper around `config.validate!` that can also
|
|
630
|
+
# validate your defaults and provider feature support.
|
|
631
|
+
#
|
|
632
|
+
# @param require_default_index [Boolean] require default index to be set
|
|
633
|
+
# @param require_default_namespace [Boolean] require default namespace to be set
|
|
634
|
+
# @param features [Array<Symbol>, Symbol] provider features (methods) required, e.g. :text_search
|
|
635
|
+
# @return [true]
|
|
636
|
+
# @raise [ConfigurationError] when client is misconfigured
|
|
637
|
+
#
|
|
638
|
+
# @example Basic validation
|
|
639
|
+
# client.validate! # => client
|
|
640
|
+
#
|
|
641
|
+
# @example Ensure you can call methods without passing index:
|
|
642
|
+
# client.validate!(require_default_index: true)
|
|
643
|
+
#
|
|
644
|
+
# @example Ensure provider supports text search:
|
|
645
|
+
# client.validate!(features: [:text_search])
|
|
646
|
+
#
|
|
647
|
+
def validate!(require_default_index: false, require_default_namespace: false, features: [])
|
|
648
|
+
errors = []
|
|
649
|
+
|
|
650
|
+
append_client_config_errors(errors)
|
|
651
|
+
append_client_provider_errors(errors)
|
|
652
|
+
append_client_default_errors(
|
|
653
|
+
errors,
|
|
654
|
+
require_default_index: require_default_index,
|
|
655
|
+
require_default_namespace: require_default_namespace
|
|
656
|
+
)
|
|
657
|
+
append_client_feature_errors(errors, features: features)
|
|
658
|
+
|
|
659
|
+
raise ConfigurationError, client_validation_message(errors) if errors.any?
|
|
660
|
+
|
|
661
|
+
self
|
|
662
|
+
end
|
|
663
|
+
|
|
621
664
|
# Chainable query builder
|
|
622
665
|
#
|
|
623
666
|
# @api public
|
|
@@ -745,6 +788,90 @@ module Vectra
|
|
|
745
788
|
Middleware::Stack.new(@provider, all_middleware)
|
|
746
789
|
end
|
|
747
790
|
|
|
791
|
+
def apply_rails_vectra_defaults!
|
|
792
|
+
return unless rails_root_available?
|
|
793
|
+
|
|
794
|
+
entry = load_single_vectra_entry
|
|
795
|
+
return unless entry
|
|
796
|
+
|
|
797
|
+
apply_vectra_defaults_from(entry)
|
|
798
|
+
rescue StandardError => e
|
|
799
|
+
log_error("Failed to infer default index/namespace from config/vectra.yml", e)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def rails_root_available?
|
|
803
|
+
defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def vectra_config_path
|
|
807
|
+
File.join(Rails.root.to_s, "config", "vectra.yml")
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def load_single_vectra_entry
|
|
811
|
+
path = vectra_config_path
|
|
812
|
+
return unless File.exist?(path)
|
|
813
|
+
|
|
814
|
+
raw = File.read(path)
|
|
815
|
+
data = YAML.safe_load(raw, permitted_classes: [], aliases: true) || {}
|
|
816
|
+
return unless data.is_a?(Hash) && data.size == 1
|
|
817
|
+
|
|
818
|
+
data.values.first || {}
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def apply_vectra_defaults_from(entry)
|
|
822
|
+
index = entry["index"] || entry[:index]
|
|
823
|
+
namespace = entry["namespace"] || entry[:namespace]
|
|
824
|
+
|
|
825
|
+
@default_index = index if @default_index.nil? && index.is_a?(String) && !index.empty?
|
|
826
|
+
@default_namespace = namespace if @default_namespace.nil? && namespace.is_a?(String) && !namespace.empty?
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def append_client_config_errors(errors)
|
|
830
|
+
config.validate!
|
|
831
|
+
rescue ConfigurationError, UnsupportedProviderError => e
|
|
832
|
+
errors << e.message
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def append_client_provider_errors(errors)
|
|
836
|
+
errors << "Provider is not initialized" if provider.nil?
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def append_client_default_errors(errors, require_default_index:, require_default_namespace:)
|
|
840
|
+
append_default_index_error(errors) if require_default_index
|
|
841
|
+
append_default_namespace_error(errors) if require_default_namespace
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def append_default_index_error(errors)
|
|
845
|
+
if default_index.nil? || (default_index.respond_to?(:empty?) && default_index.empty?)
|
|
846
|
+
errors << "Default index is not set (pass `index:` to Vectra::Client.new, or set it via config/vectra.yml in Rails)"
|
|
847
|
+
elsif !default_index.is_a?(String)
|
|
848
|
+
errors << "Default index must be a String"
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def append_default_namespace_error(errors)
|
|
853
|
+
if default_namespace.nil? || (default_namespace.respond_to?(:empty?) && default_namespace.empty?)
|
|
854
|
+
errors << "Default namespace is not set (pass `namespace:` to Vectra::Client.new, or set it via config/vectra.yml in Rails)"
|
|
855
|
+
elsif !default_namespace.is_a?(String)
|
|
856
|
+
errors << "Default namespace must be a String"
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def append_client_feature_errors(errors, features:)
|
|
861
|
+
return if provider.nil?
|
|
862
|
+
|
|
863
|
+
Array(features).compact.each do |feature|
|
|
864
|
+
method = feature.to_sym
|
|
865
|
+
next if provider.respond_to?(method)
|
|
866
|
+
|
|
867
|
+
errors << "Provider does not support `#{method}`"
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def client_validation_message(errors)
|
|
872
|
+
"Client validation failed:\n- #{errors.join("\n- ")}"
|
|
873
|
+
end
|
|
874
|
+
|
|
748
875
|
def validate_index!(index)
|
|
749
876
|
raise ValidationError, "Index name cannot be nil" if index.nil?
|
|
750
877
|
raise ValidationError, "Index name must be a string" unless index.is_a?(String)
|
|
@@ -811,6 +938,22 @@ module Vectra
|
|
|
811
938
|
config.logger.debug("[Vectra] #{data.inspect}") if data
|
|
812
939
|
end
|
|
813
940
|
|
|
941
|
+
# Temporarily override request timeout within a block.
|
|
942
|
+
#
|
|
943
|
+
# This updates the client's configuration timeout for the duration
|
|
944
|
+
# of the block and then restores the previous value.
|
|
945
|
+
#
|
|
946
|
+
# @param seconds [Float] temporary timeout in seconds
|
|
947
|
+
# @yield [Client] yields self with overridden timeout
|
|
948
|
+
# @return [Object] block result
|
|
949
|
+
def with_timeout(seconds)
|
|
950
|
+
previous = config.timeout
|
|
951
|
+
config.timeout = seconds
|
|
952
|
+
yield self
|
|
953
|
+
ensure
|
|
954
|
+
config.timeout = previous
|
|
955
|
+
end
|
|
956
|
+
|
|
814
957
|
# Temporarily override default index within a block.
|
|
815
958
|
#
|
|
816
959
|
# @param index [String] temporary index name
|
|
@@ -851,7 +994,29 @@ module Vectra
|
|
|
851
994
|
end
|
|
852
995
|
end
|
|
853
996
|
|
|
854
|
-
|
|
997
|
+
# Temporarily override default index and/or namespace within a block.
|
|
998
|
+
#
|
|
999
|
+
# Unlike `with_index_and_namespace`, this method accepts keyword arguments
|
|
1000
|
+
# and only overrides the values you pass.
|
|
1001
|
+
#
|
|
1002
|
+
# @param index [String, nil] temporary index name (omit to keep current)
|
|
1003
|
+
# @param namespace [String, nil] temporary namespace (omit to keep current)
|
|
1004
|
+
# @yield [Client] yields self with overridden defaults
|
|
1005
|
+
# @return [Object] block result
|
|
1006
|
+
def with_defaults(index: DEFAULT_UNSET, namespace: DEFAULT_UNSET)
|
|
1007
|
+
previous_index = @default_index
|
|
1008
|
+
previous_namespace = @default_namespace
|
|
1009
|
+
|
|
1010
|
+
@default_index = index unless index.equal?(DEFAULT_UNSET)
|
|
1011
|
+
@default_namespace = namespace unless namespace.equal?(DEFAULT_UNSET)
|
|
1012
|
+
|
|
1013
|
+
yield self
|
|
1014
|
+
ensure
|
|
1015
|
+
@default_index = previous_index
|
|
1016
|
+
@default_namespace = previous_namespace
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
public :with_index, :with_namespace, :with_index_and_namespace, :with_defaults, :with_timeout
|
|
855
1020
|
end
|
|
856
1021
|
# rubocop:enable Metrics/ClassLength
|
|
857
1022
|
end
|
data/lib/vectra/health_check.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Vectra
|
|
|
31
31
|
|
|
32
32
|
# For health checks we bypass client middleware and call the provider
|
|
33
33
|
# directly to avoid interference from custom stacks.
|
|
34
|
-
indexes =
|
|
34
|
+
indexes = healthcheck_with_timeout(timeout) { provider.list_indexes }
|
|
35
35
|
index_name = index || indexes.first&.dig(:name)
|
|
36
36
|
|
|
37
37
|
result = base_result(start_time, indexes)
|
|
@@ -53,7 +53,7 @@ module Vectra
|
|
|
53
53
|
|
|
54
54
|
private
|
|
55
55
|
|
|
56
|
-
def
|
|
56
|
+
def healthcheck_with_timeout(seconds, &)
|
|
57
57
|
Timeout.timeout(seconds, &)
|
|
58
58
|
rescue Timeout::Error
|
|
59
59
|
raise Vectra::TimeoutError, "Health check timed out after #{seconds}s"
|
|
@@ -72,7 +72,7 @@ module Vectra
|
|
|
72
72
|
def add_index_stats(result, index_name, include_stats, timeout)
|
|
73
73
|
return unless include_stats && index_name
|
|
74
74
|
|
|
75
|
-
stats =
|
|
75
|
+
stats = healthcheck_with_timeout(timeout) { provider.stats(index: index_name) }
|
|
76
76
|
result[:index] = index_name
|
|
77
77
|
result[:stats] = {
|
|
78
78
|
vector_count: stats[:total_vector_count],
|
data/lib/vectra/version.rb
CHANGED
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.1.
|
|
4
|
+
version: 1.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mijo Kristo
|
|
@@ -289,6 +289,7 @@ files:
|
|
|
289
289
|
- docs/providers/qdrant.md
|
|
290
290
|
- docs/providers/selection.md
|
|
291
291
|
- docs/providers/weaviate.md
|
|
292
|
+
- docs/search.json
|
|
292
293
|
- examples/GRAFANA_QUICKSTART.md
|
|
293
294
|
- examples/README.md
|
|
294
295
|
- examples/active_record_demo.rb
|