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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f1fd06b5874c1bc1da1244fb05321cda4a4759e234d9175159c5a6ba7ed8d40
4
- data.tar.gz: 9d69e983f4ef5ed4d6bdd58e7a003e06045f3570ba1b3329587f82bcf07902a3
3
+ metadata.gz: 83a312bb14b8f556d743e0072548a33ec0ac79c9d64768c78b18af3bc7ac6c6b
4
+ data.tar.gz: 0f0e859dd346e86f095a882e4cb7d47fd1ecf05aa968730b2b3e0d2ff7fa253b
5
5
  SHA512:
6
- metadata.gz: 7c1911470f96d83470dd98cdc6bb3e6c438a11e71b7317265388d22ae4c3792cea3e595661c927a42587b6008fc891940d511815932b3da478bfeb056ccf8c30
7
- data.tar.gz: a3a17643736f8b9a19b92c81a87ce8749a4e55798085e474ef1bc2f13ab9f8c688b2bc7537467a851e59a31cf06390444a064a9d525cff8efb175dbae1de7c7c
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.0](https://github.com/stokry/vectra/tree/v1.1.0) (2026-01-15)
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
- This release introduces a **Rack-style middleware system** for all vector database operations.
5
+ [Full Changelog](https://github.com/stokry/vectra/compare/v1.1.2...v1.1.3)
10
6
 
11
- #### Added
7
+ - Add `Client#validate!` for configuration/capability checks
8
+ - Add `Client#with_defaults` block helper for temporary default index/namespace
12
9
 
13
- - **Middleware Stack** - All client operations now route through a composable middleware pipeline
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
- #### Changed
12
+ [Full Changelog](https://github.com/stokry/vectra/compare/v1.1.1...v1.1.2)
25
13
 
26
- - All client operations (`upsert`, `query`, `fetch`, `update`, `delete`, `stats`, `list_indexes`, etc.) now route through middleware stack for consistency
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
- #### Documentation
16
+ [Full Changelog](https://github.com/stokry/vectra/compare/v1.1.0...v1.1.1)
30
17
 
31
- - Added comprehensive middleware section to README
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
- No breaking changes. Middleware is opt-in - existing code works without modification.
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:
@@ -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.0 — Hybrid Search, Rails Generator & Middleware</span>
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>
@@ -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>
@@ -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
- index_name: 'my-index', # Optional, provider-dependent
16
- host: 'localhost', # For self-hosted providers
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.
@@ -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
- public :with_index, :with_namespace, :with_index_and_namespace
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
@@ -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 = with_timeout(timeout) { provider.list_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 with_timeout(seconds, &)
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 = with_timeout(timeout) { provider.stats(index: index_name) }
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],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vectra
4
- VERSION = "1.1.1"
4
+ VERSION = "1.1.3"
5
5
  end
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.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