vectlite 0.9.0 → 0.9.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.
package/README.md CHANGED
@@ -138,6 +138,10 @@ products.upsert('p1', embedding, { name: 'Widget', price: 9.99 })
138
138
 
139
139
  const logs = store.openOrCreateCollection('logs', 128)
140
140
  console.log(store.collections()) // ["logs", "products"]
141
+
142
+ products.close()
143
+ logs.close()
144
+ store.close()
141
145
  ```
142
146
 
143
147
  ### Transactions
@@ -231,20 +235,22 @@ db.dropIndex('score')
231
235
 
232
236
  ### Vector Quantization
233
237
 
234
- Reduce memory usage and accelerate search with quantized vectors. All methods use a 2-stage pipeline: fast quantized candidate selection followed by exact float32 rescoring.
238
+ Reduce in-memory candidate-index usage and accelerate search with quantized vectors. All methods use a 2-stage pipeline: fast quantized candidate selection followed by exact float32 rescoring.
235
239
 
236
240
  ```js
237
- // Scalar quantization (int8) -- 4x memory reduction, minimal recall loss
241
+ // Scalar quantization (int8) -- smaller in-memory candidate index, minimal recall loss
238
242
  db.enableQuantization('scalar')
239
243
 
240
- // Binary quantization -- 32x memory reduction, best for normalized embeddings
241
- db.enableQuantization('binary', JSON.stringify({ rescoreMultiplier: 10 }))
244
+ // Binary quantization -- smallest in-memory candidate index, best for normalized embeddings
245
+ db.enableQuantization('binary', { rescoreMultiplier: 10 })
242
246
 
243
- // Product quantization -- configurable compression for very large datasets
244
- db.enableQuantization('product', JSON.stringify({ numSubVectors: 16, numCentroids: 256 }))
247
+ // Product quantization -- "pq" and "product" are accepted case-insensitively
248
+ console.log(db.validNumSubVectors()) // valid PQ partitions for this dimension
249
+ db.enableQuantization('pq', { numSubVectors: 16, numCentroids: 256 })
245
250
 
246
251
  // Search works exactly the same -- quantization accelerates it transparently
247
252
  const results = db.search(queryEmbedding, { k: 10 })
253
+ const sameResults = db.search({ query: queryEmbedding, k: 10 })
248
254
 
249
255
  // Check quantization status
250
256
  console.log(db.isQuantized) // true
@@ -254,7 +260,11 @@ console.log(db.quantizationMethod) // "scalar", "binary", or "product"
254
260
  db.disableQuantization()
255
261
  ```
256
262
 
257
- Quantization parameters persist across reopens in a `.vdb.quant` sidecar file. The quantized index auto-rebuilds on inserts and upserts.
263
+ `rescoreMultiplier` controls the number of quantized candidates rescored with exact float32 scoring: `k * rescoreMultiplier`, capped at the collection size. Increase it to trade latency for recall.
264
+
265
+ For PQ, `numSubVectors` must divide the database dimension. If omitted, Vectlite chooses a compatible default; use `db.validNumSubVectors()` to inspect all valid values.
266
+
267
+ Quantization does not shrink the `.vdb` file on disk. Vectlite keeps the original float32 vectors for exact rescoring and stores quantization parameters in a `.vdb.quant` sidecar file, so total disk footprint can increase slightly. The quantized index auto-rebuilds on inserts and upserts.
258
268
 
259
269
  ### Multi-Vector / ColBERT Search
260
270
 
@@ -263,14 +273,12 @@ Store token-level embeddings (ColBERT, ColPali) and search with MaxSim late inte
263
273
  ```js
264
274
  // Upsert with per-token ColBERT embeddings
265
275
  db.upsertMultiVectors('doc1', denseVector,
266
- JSON.stringify({ colbert: [tokenVec1, tokenVec2] }),
267
- JSON.stringify({ metadata: { source: 'paper' } })
276
+ { colbert: [tokenVec1, tokenVec2] },
277
+ { metadata: { source: 'paper' } }
268
278
  )
269
279
 
270
280
  // MaxSim search
271
- const results = JSON.parse(
272
- db.searchMultiVector('colbert', JSON.stringify(queryTokenVectors))
273
- )
281
+ const results = db.searchMultiVector('colbert', queryTokenVectors)
274
282
 
275
283
  // Enable 2-bit quantization (~16x compression)
276
284
  db.enableMultiVectorQuantization('colbert')
@@ -403,7 +411,7 @@ before re-throwing.
403
411
  | Method | Description |
404
412
  |---|---|
405
413
  | `db.get(id, { namespace })` | Get a single record by id |
406
- | `db.search(query, options)` | Search and return a list of results |
414
+ | `db.search(query, options)` or `db.search({ query, ...options })` | Search and return a list of results |
407
415
  | `db.searchWithStats(query, options)` | Search with detailed performance stats |
408
416
  | `db.count({ namespace, filter })` | Count records, optionally scoped by namespace/filter |
409
417
  | `db.list({ namespace, filter, limit, offset })` | List records without issuing a vector query |
@@ -426,10 +434,14 @@ before re-throwing.
426
434
 
427
435
  | Method | Description |
428
436
  |---|---|
429
- | `db.enableQuantization(method, optionsJson)` | Enable quantization (`'scalar'`, `'binary'`, or `'product'`) |
437
+ | `db.enableQuantization(method, options)` | Enable quantization (`'scalar'`, `'binary'`, or `'pq'` / `'product'`) |
430
438
  | `db.disableQuantization()` | Disable quantization and remove persisted parameters |
431
439
  | `db.isQuantized` | Whether quantization is enabled (property) |
432
440
  | `db.quantizationMethod` | Active method name or `null` (property) |
441
+ | `db.validNumSubVectors()` | Valid PQ `numSubVectors` values for this database dimension |
442
+ | `db.enableMultiVectorQuantization(space, options)` | Enable 2-bit quantization for a multi-vector space |
443
+ | `db.disableMultiVectorQuantization(space)` | Disable multi-vector quantization for a space |
444
+ | `db.isMultiVectorQuantized(space)` | Whether multi-vector quantization is enabled for a space |
433
445
 
434
446
  ### Maintenance Methods
435
447
 
package/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export type MetadataValue =
9
9
  export type Metadata = { [key: string]: MetadataValue }
10
10
  export type SparseVector = { [term: string]: number }
11
11
  export type NamedVectors = { [name: string]: number[] }
12
+ export type MultiVectors = { [space: string]: number[][] }
12
13
  export type Filter = { [key: string]: unknown }
13
14
  export type TextEmbedding = ArrayLike<number>
14
15
  export type TextEmbeddingResult = TextEmbedding | Promise<TextEmbedding>
@@ -130,6 +131,46 @@ export interface SearchOptions {
130
131
  vectorWeights?: { [name: string]: number } | null
131
132
  }
132
133
 
134
+ export interface SearchRequest extends SearchOptions {
135
+ query?: number[] | null
136
+ }
137
+
138
+ export type QuantizationMethod = 'scalar' | 'int8' | 'binary' | 'product' | 'pq'
139
+ export interface QuantizationOptions {
140
+ rescoreMultiplier?: number
141
+ rescore_multiplier?: number
142
+ numSubVectors?: number
143
+ num_sub_vectors?: number
144
+ numCentroids?: number
145
+ num_centroids?: number
146
+ trainingIterations?: number
147
+ training_iterations?: number
148
+ }
149
+
150
+ export interface MultiVectorWriteOptions {
151
+ namespace?: string | null
152
+ metadata?: Metadata | null
153
+ }
154
+
155
+ export interface MultiVectorSearchOptions {
156
+ k?: number
157
+ filter?: Filter | null
158
+ namespace?: string | null
159
+ }
160
+
161
+ export interface MultiVectorSearchResult {
162
+ namespace: string
163
+ id: string
164
+ score: number
165
+ metadata: Metadata
166
+ }
167
+
168
+ export interface MultiVectorQuantizationOptions {
169
+ method?: 'two_bit'
170
+ rescoreMultiplier?: number
171
+ rescore_multiplier?: number
172
+ }
173
+
133
174
  export type DistanceMetric = 'cosine' | 'euclidean' | 'dotproduct' | 'manhattan' | 'l2' | 'dot' | 'ip' | 'l1'
134
175
 
135
176
  export interface OpenOptions {
@@ -181,13 +222,27 @@ export class Database {
181
222
  createIndex(field: string, indexType: 'keyword' | 'numeric'): boolean
182
223
  dropIndex(field: string): boolean
183
224
  listIndexes(): Array<{ field: string; type: 'keyword' | 'numeric' }>
225
+ readonly isQuantized: boolean
226
+ readonly quantizationMethod: 'scalar' | 'binary' | 'product' | null
227
+ enableQuantization(method?: QuantizationMethod, options?: QuantizationOptions | string): void
228
+ disableQuantization(): void
229
+ validNumSubVectors(): number[]
230
+ upsertMultiVectors(id: string, vector: number[], multiVectors: MultiVectors, options?: MultiVectorWriteOptions): void
231
+ searchMultiVector(space: string, queryTokens: number[][], options?: MultiVectorSearchOptions): MultiVectorSearchResult[]
232
+ enableMultiVectorQuantization(space: string, options?: MultiVectorQuantizationOptions | string): void
233
+ disableMultiVectorQuantization(space: string): void
234
+ isMultiVectorQuantized(space: string): boolean
184
235
  flush(): void
185
236
  compact(): void
186
237
  snapshot(dest: string): void
187
238
  backup(dest: string): void
239
+ search(request: SearchRequest): SearchResult[]
188
240
  search(query?: number[] | null, options?: SearchOptions): SearchResult[]
241
+ searchWithStats(request: SearchRequest): SearchResponse
189
242
  searchWithStats(query?: number[] | null, options?: SearchOptions): SearchResponse
243
+ searchAsync(request: SearchRequest): Promise<SearchResult[]>
190
244
  searchAsync(query?: number[] | null, options?: SearchOptions): Promise<SearchResult[]>
245
+ searchWithStatsAsync(request: SearchRequest): Promise<SearchResponse>
191
246
  searchWithStatsAsync(query?: number[] | null, options?: SearchOptions): Promise<SearchResponse>
192
247
  flushAsync(): Promise<void>
193
248
  compactAsync(): Promise<void>
@@ -202,6 +257,7 @@ export class Store {
202
257
  openCollectionReadOnly(name: string): Database
203
258
  dropCollection(name: string): boolean
204
259
  collections(): string[]
260
+ close(): void
205
261
  }
206
262
 
207
263
  export function open(path: string, options?: OpenOptions): Database
package/index.js CHANGED
@@ -182,9 +182,61 @@ function withSearchSpan(query, options, fn) {
182
182
  }
183
183
 
184
184
  function asArray(values) {
185
+ if (
186
+ values != null &&
187
+ typeof values === 'object' &&
188
+ !Array.isArray(values) &&
189
+ !ArrayBuffer.isView(values) &&
190
+ typeof values[Symbol.iterator] !== 'function' &&
191
+ typeof values.length !== 'number'
192
+ ) {
193
+ throw new TypeError('vector must be an array-like or iterable of numbers')
194
+ }
185
195
  return Array.from(values)
186
196
  }
187
197
 
198
+ function encodeNativeOptions(value) {
199
+ return typeof value === 'string' ? value : encode(value)
200
+ }
201
+
202
+ const SEARCH_OPTION_KEYS = new Set([
203
+ 'query',
204
+ 'k',
205
+ 'filter',
206
+ 'namespace',
207
+ 'allNamespaces',
208
+ 'sparse',
209
+ 'denseWeight',
210
+ 'sparseWeight',
211
+ 'fetchK',
212
+ 'mmrLambda',
213
+ 'vectorName',
214
+ 'fusion',
215
+ 'rrfK',
216
+ 'truncateDim',
217
+ 'explain',
218
+ 'queryVectors',
219
+ 'vectorWeights',
220
+ ])
221
+
222
+ function isSearchRequestObject(value) {
223
+ return (
224
+ value != null &&
225
+ typeof value === 'object' &&
226
+ !Array.isArray(value) &&
227
+ !ArrayBuffer.isView(value) &&
228
+ [...SEARCH_OPTION_KEYS].some((key) => Object.prototype.hasOwnProperty.call(value, key))
229
+ )
230
+ }
231
+
232
+ function normalizeSearchArgs(query, options) {
233
+ if (isSearchRequestObject(query) && (options == null || Object.keys(options).length === 0)) {
234
+ const { query: normalizedQuery = null, ...normalizedOptions } = query
235
+ return { query: normalizedQuery, options: normalizedOptions }
236
+ }
237
+ return { query, options: options ?? {} }
238
+ }
239
+
188
240
  function isPromiseLike(value) {
189
241
  return value != null && typeof value.then === 'function'
190
242
  }
@@ -406,6 +458,52 @@ class Database {
406
458
  return wrapError(() => decode(this._native.listIndexes()))
407
459
  }
408
460
 
461
+ enableQuantization(method = 'scalar', options = {}) {
462
+ return wrapError(() => this._native.enableQuantization(method, encodeNativeOptions(options)))
463
+ }
464
+
465
+ disableQuantization() {
466
+ return wrapError(() => this._native.disableQuantization())
467
+ }
468
+
469
+ get isQuantized() {
470
+ return wrapError(() => this._native.isQuantized)
471
+ }
472
+
473
+ get quantizationMethod() {
474
+ return wrapError(() => this._native.quantizationMethod)
475
+ }
476
+
477
+ validNumSubVectors() {
478
+ return wrapError(() => this._native.validNumSubVectors())
479
+ }
480
+
481
+ upsertMultiVectors(id, vector, multiVectors, options = {}) {
482
+ return wrapError(() =>
483
+ this._native.upsertMultiVectors(id, asArray(vector), encode(multiVectors), encode(options)),
484
+ )
485
+ }
486
+
487
+ searchMultiVector(space, queryTokens, options = {}) {
488
+ return wrapError(() =>
489
+ decode(this._native.searchMultiVector(space, encode(queryTokens), encode(options))),
490
+ )
491
+ }
492
+
493
+ enableMultiVectorQuantization(space, options = {}) {
494
+ return wrapError(() =>
495
+ this._native.enableMultiVectorQuantization(space, encodeNativeOptions(options)),
496
+ )
497
+ }
498
+
499
+ disableMultiVectorQuantization(space) {
500
+ return wrapError(() => this._native.disableMultiVectorQuantization(space))
501
+ }
502
+
503
+ isMultiVectorQuantized(space) {
504
+ return wrapError(() => this._native.isMultiVectorQuantized(space))
505
+ }
506
+
409
507
  flush() {
410
508
  return wrapError(() => this._native.flush())
411
509
  }
@@ -423,31 +521,53 @@ class Database {
423
521
  }
424
522
 
425
523
  search(query = null, options = {}) {
426
- return withSearchSpan(query, options, () =>
427
- wrapError(() => decode(this._native.search(query == null ? null : asArray(query), encode(options)))),
524
+ const normalized = normalizeSearchArgs(query, options)
525
+ return withSearchSpan(normalized.query, normalized.options, () =>
526
+ wrapError(() =>
527
+ decode(
528
+ this._native.search(
529
+ normalized.query == null ? null : asArray(normalized.query),
530
+ encode(normalized.options),
531
+ ),
532
+ ),
533
+ ),
428
534
  )
429
535
  }
430
536
 
431
537
  searchWithStats(query = null, options = {}) {
432
- return withSearchSpan(query, options, () =>
538
+ const normalized = normalizeSearchArgs(query, options)
539
+ return withSearchSpan(normalized.query, normalized.options, () =>
433
540
  wrapError(() =>
434
- decode(this._native.searchWithStats(query == null ? null : asArray(query), encode(options))),
541
+ decode(
542
+ this._native.searchWithStats(
543
+ normalized.query == null ? null : asArray(normalized.query),
544
+ encode(normalized.options),
545
+ ),
546
+ ),
435
547
  ),
436
548
  )
437
549
  }
438
550
 
439
551
  searchAsync(query = null, options = {}) {
440
- return withSearchSpan(query, options, () =>
552
+ const normalized = normalizeSearchArgs(query, options)
553
+ return withSearchSpan(normalized.query, normalized.options, () =>
441
554
  wrapAsync(
442
- this._native.searchAsync(query == null ? null : asArray(query), encode(options)),
555
+ this._native.searchAsync(
556
+ normalized.query == null ? null : asArray(normalized.query),
557
+ encode(normalized.options),
558
+ ),
443
559
  ).then(decode),
444
560
  )
445
561
  }
446
562
 
447
563
  searchWithStatsAsync(query = null, options = {}) {
448
- return withSearchSpan(query, options, () =>
564
+ const normalized = normalizeSearchArgs(query, options)
565
+ return withSearchSpan(normalized.query, normalized.options, () =>
449
566
  wrapAsync(
450
- this._native.searchWithStatsAsync(query == null ? null : asArray(query), encode(options)),
567
+ this._native.searchWithStatsAsync(
568
+ normalized.query == null ? null : asArray(normalized.query),
569
+ encode(normalized.options),
570
+ ),
451
571
  ).then(decode),
452
572
  )
453
573
  }
@@ -499,6 +619,10 @@ class Store {
499
619
  collections() {
500
620
  return wrapError(() => this._native.collections())
501
621
  }
622
+
623
+ close() {
624
+ return wrapError(() => this._native.close())
625
+ }
502
626
  }
503
627
 
504
628
  function open(path, options = {}) {
package/native/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vectlite-node"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  description = "Node.js bindings for vectlite."
package/native/src/lib.rs CHANGED
@@ -9,6 +9,7 @@ use serde_json::{Map, Number, Value, json};
9
9
  use vectlite::quantization::{
10
10
  BinaryQuantizationConfig, MultiVectorQuantizationConfig, ProductQuantizationConfig,
11
11
  QuantizationConfig, ScalarQuantizationConfig, TwoBitQuantizationConfig,
12
+ default_product_num_sub_vectors,
12
13
  };
13
14
  use vectlite::{
14
15
  Database as CoreDatabase, DistanceMetric, FusionStrategy, HybridSearchOptions, Metadata,
@@ -109,6 +110,13 @@ impl NativeStore {
109
110
  pub fn collections(&self) -> Result<Vec<String>> {
110
111
  self.inner.collections().map_err(to_napi_error)
111
112
  }
113
+
114
+ /// Close the store. This is a no-op (the store holds no open file handles)
115
+ /// but is provided for symmetry with `Database.close()`.
116
+ #[napi]
117
+ pub fn close(&self) -> Result<()> {
118
+ Ok(())
119
+ }
112
120
  }
113
121
 
114
122
  #[napi]
@@ -303,9 +311,7 @@ impl NativeDatabase {
303
311
  let indexes = database.list_indexes();
304
312
  let arr: Vec<Value> = indexes
305
313
  .into_iter()
306
- .map(|(field, index_type)| {
307
- json!({ "field": field, "type": index_type.name() })
308
- })
314
+ .map(|(field, index_type)| json!({ "field": field, "type": index_type.name() }))
309
315
  .collect();
310
316
  serde_json::to_string(&arr).map_err(|e| err(format!("JSON serialize: {e}")))
311
317
  }
@@ -479,14 +485,15 @@ impl NativeDatabase {
479
485
  let method = method.as_deref().unwrap_or("scalar");
480
486
  let (rescore_multiplier, num_sub_vectors, num_centroids, training_iterations) =
481
487
  parse_quantization_options(options_json.as_deref())?;
488
+ let mut database = self.write_open()?;
482
489
  let config = build_quantization_config(
483
490
  method,
484
491
  rescore_multiplier,
485
492
  num_sub_vectors,
486
493
  num_centroids,
487
494
  training_iterations,
495
+ database.dimension(),
488
496
  )?;
489
- let mut database = self.write_open()?;
490
497
  database.enable_quantization(config).map_err(to_napi_error)
491
498
  }
492
499
 
@@ -515,6 +522,23 @@ impl NativeDatabase {
515
522
  }))
516
523
  }
517
524
 
525
+ /// Returns valid Product Quantization num_sub_vectors values for this database.
526
+ #[napi(js_name = "validNumSubVectors")]
527
+ pub fn valid_num_sub_vectors(&self) -> Result<Vec<u32>> {
528
+ let database = self.read()?;
529
+ database
530
+ .valid_num_sub_vectors()
531
+ .into_iter()
532
+ .map(|value| {
533
+ u32::try_from(value).map_err(|_| {
534
+ to_napi_error(vectlite::VectLiteError::InvalidFormat(
535
+ "num_sub_vectors value exceeds u32".to_owned(),
536
+ ))
537
+ })
538
+ })
539
+ .collect()
540
+ }
541
+
518
542
  // ---- Multi-vector / ColBERT-style late interaction ----
519
543
 
520
544
  /// Upsert a record with multi-vector token embeddings (ColBERT-style).
@@ -535,8 +559,9 @@ impl NativeDatabase {
535
559
  let mv = json_to_multi_vectors(&mv_value)?;
536
560
 
537
561
  let (metadata, namespace) = if let Some(opts) = options_json {
538
- let opts: Value = serde_json::from_str(&opts)
539
- .map_err(|e| to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string())))?;
562
+ let opts: Value = serde_json::from_str(&opts).map_err(|e| {
563
+ to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string()))
564
+ })?;
540
565
  let metadata = opts
541
566
  .get("metadata")
542
567
  .map(|v| json_to_metadata(v))
@@ -570,11 +595,11 @@ impl NativeDatabase {
570
595
  ) -> Result<String> {
571
596
  let qt_value: Value = serde_json::from_str(&query_tokens_json)
572
597
  .map_err(|e| to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string())))?;
573
- let qt_arr = qt_value
574
- .as_array()
575
- .ok_or_else(|| to_napi_error(vectlite::VectLiteError::InvalidFormat(
598
+ let qt_arr = qt_value.as_array().ok_or_else(|| {
599
+ to_napi_error(vectlite::VectLiteError::InvalidFormat(
576
600
  "query_tokens must be a JSON array of arrays".to_owned(),
577
- )))?;
601
+ ))
602
+ })?;
578
603
  let query_tokens: Vec<Vec<f32>> = qt_arr
579
604
  .iter()
580
605
  .map(|v| {
@@ -586,26 +611,22 @@ impl NativeDatabase {
586
611
  })?
587
612
  .iter()
588
613
  .map(|n| {
589
- n.as_f64()
590
- .map(|f| f as f32)
591
- .ok_or_else(|| {
592
- to_napi_error(vectlite::VectLiteError::InvalidFormat(
593
- "token values must be numbers".to_owned(),
594
- ))
595
- })
614
+ n.as_f64().map(|f| f as f32).ok_or_else(|| {
615
+ to_napi_error(vectlite::VectLiteError::InvalidFormat(
616
+ "token values must be numbers".to_owned(),
617
+ ))
618
+ })
596
619
  })
597
620
  .collect::<Result<Vec<f32>>>()
598
621
  })
599
622
  .collect::<Result<Vec<Vec<f32>>>>()?;
600
623
 
601
624
  let (top_k, filter, namespace) = if let Some(opts) = options_json {
602
- let opts: Value = serde_json::from_str(&opts)
603
- .map_err(|e| to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string())))?;
625
+ let opts: Value = serde_json::from_str(&opts).map_err(|e| {
626
+ to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string()))
627
+ })?;
604
628
  let top_k = opts.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
605
- let filter = opts
606
- .get("filter")
607
- .map(|v| json_to_filter(v))
608
- .transpose()?;
629
+ let filter = opts.get("filter").map(|v| json_to_filter(v)).transpose()?;
609
630
  let namespace = opts
610
631
  .get("namespace")
611
632
  .and_then(|v| v.as_str())
@@ -650,8 +671,9 @@ impl NativeDatabase {
650
671
  options_json: Option<String>,
651
672
  ) -> Result<()> {
652
673
  let (method, rescore_multiplier) = if let Some(opts) = options_json {
653
- let opts: Value = serde_json::from_str(&opts)
654
- .map_err(|e| to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string())))?;
674
+ let opts: Value = serde_json::from_str(&opts).map_err(|e| {
675
+ to_napi_error(vectlite::VectLiteError::InvalidFormat(e.to_string()))
676
+ })?;
655
677
  let method = opts
656
678
  .get("method")
657
679
  .and_then(|v| v.as_str())
@@ -659,6 +681,7 @@ impl NativeDatabase {
659
681
  .to_string();
660
682
  let rescore = opts
661
683
  .get("rescoreMultiplier")
684
+ .or_else(|| opts.get("rescore_multiplier"))
662
685
  .and_then(|v| v.as_u64())
663
686
  .map(|v| v as usize);
664
687
  (method, rescore)
@@ -672,7 +695,9 @@ impl NativeDatabase {
672
695
  }),
673
696
  other => {
674
697
  return Err(to_napi_error(vectlite::VectLiteError::InvalidFormat(
675
- format!("unknown multi-vector quantization method: {other}. Supported: two_bit"),
698
+ format!(
699
+ "unknown multi-vector quantization method: {other}. Supported: two_bit"
700
+ ),
676
701
  )));
677
702
  }
678
703
  };
@@ -1622,9 +1647,7 @@ fn json_to_record(object: &Map<String, Value>, default_namespace: Option<&str>)
1622
1647
  .transpose()?
1623
1648
  .unwrap_or_default();
1624
1649
 
1625
- let ttl = object
1626
- .get("ttl")
1627
- .and_then(|v| v.as_f64());
1650
+ let ttl = object.get("ttl").and_then(|v| v.as_f64());
1628
1651
  let expires_at = ttl_to_expires_at(ttl)?;
1629
1652
 
1630
1653
  Ok(Record {
@@ -1910,9 +1933,7 @@ fn value_to_f32(value: &Value, label: &str) -> Result<f32> {
1910
1933
  fn ttl_to_expires_at(ttl: Option<f64>) -> Result<Option<f64>> {
1911
1934
  match ttl {
1912
1935
  None => Ok(None),
1913
- Some(t) if t < 0.0 || t.is_nan() => {
1914
- Err(err("ttl must be a non-negative finite number"))
1915
- }
1936
+ Some(t) if t < 0.0 || t.is_nan() => Err(err("ttl must be a non-negative finite number")),
1916
1937
  Some(t) => {
1917
1938
  let now = std::time::SystemTime::now()
1918
1939
  .duration_since(std::time::UNIX_EPOCH)
@@ -2004,22 +2025,34 @@ fn build_quantization_config(
2004
2025
  num_sub_vectors: Option<usize>,
2005
2026
  num_centroids: Option<usize>,
2006
2027
  training_iterations: Option<usize>,
2028
+ dimension: usize,
2007
2029
  ) -> Result<QuantizationConfig> {
2008
- match method {
2009
- "scalar" | "int8" => Ok(QuantizationConfig::Scalar(ScalarQuantizationConfig {
2010
- rescore_multiplier: rescore_multiplier.unwrap_or(5),
2011
- })),
2012
- "binary" => Ok(QuantizationConfig::Binary(BinaryQuantizationConfig {
2013
- rescore_multiplier: rescore_multiplier.unwrap_or(10),
2014
- })),
2015
- "product" | "pq" => Ok(QuantizationConfig::Product(ProductQuantizationConfig {
2016
- num_sub_vectors: num_sub_vectors.unwrap_or(16),
2017
- num_centroids: num_centroids.unwrap_or(256),
2018
- training_iterations: training_iterations.unwrap_or(20),
2019
- rescore_multiplier: rescore_multiplier.unwrap_or(10),
2020
- })),
2021
- other => Err(err(format!(
2022
- "unknown quantization method '{other}'. Expected: 'scalar', 'binary', or 'product'"
2030
+ let normalized = method.to_ascii_lowercase();
2031
+ match normalized.as_str() {
2032
+ "scalar" | "int8" => {
2033
+ let default = ScalarQuantizationConfig::default();
2034
+ Ok(QuantizationConfig::Scalar(ScalarQuantizationConfig {
2035
+ rescore_multiplier: rescore_multiplier.unwrap_or(default.rescore_multiplier),
2036
+ }))
2037
+ }
2038
+ "binary" => {
2039
+ let default = BinaryQuantizationConfig::default();
2040
+ Ok(QuantizationConfig::Binary(BinaryQuantizationConfig {
2041
+ rescore_multiplier: rescore_multiplier.unwrap_or(default.rescore_multiplier),
2042
+ }))
2043
+ }
2044
+ "product" | "pq" => {
2045
+ let default = ProductQuantizationConfig::default();
2046
+ Ok(QuantizationConfig::Product(ProductQuantizationConfig {
2047
+ num_sub_vectors: num_sub_vectors
2048
+ .unwrap_or_else(|| default_product_num_sub_vectors(dimension)),
2049
+ num_centroids: num_centroids.unwrap_or(default.num_centroids),
2050
+ training_iterations: training_iterations.unwrap_or(default.training_iterations),
2051
+ rescore_multiplier: rescore_multiplier.unwrap_or(default.rescore_multiplier),
2052
+ }))
2053
+ }
2054
+ _ => Err(err(format!(
2055
+ "unknown quantization method '{method}'. Expected: 'scalar', 'binary', or 'pq' (alias: 'product')"
2023
2056
  ))),
2024
2057
  }
2025
2058
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vectlite-core"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  description = "Core storage engine for vectlite."