vectlite 0.1.11 → 0.1.12

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
@@ -46,6 +46,7 @@ db.close()
46
46
  - **Dense vectors** -- cosine similarity with automatic HNSW indexing for large collections
47
47
  - **Sparse vectors** -- BM25-scored inverted index for keyword retrieval
48
48
  - **Hybrid search** -- dense + sparse fusion with linear or RRF strategies
49
+ - **Vector quantization** -- scalar (int8, 4x), binary (32x), and product quantization (PQ) with 2-stage rescoring
49
50
  - **Rich metadata** -- string, number, boolean, null, array, and nested object values
50
51
  - **Crash-safe WAL** -- writes land in a write-ahead log first, then checkpoint with `compact()`
51
52
  - **Transactions** -- atomic batched writes with `db.transaction()`
@@ -183,6 +184,33 @@ console.log(outcome.stats.used_ann) // true
183
184
  console.log(outcome.results[0].explain) // Detailed scoring breakdown
184
185
  ```
185
186
 
187
+ ### Vector Quantization
188
+
189
+ 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.
190
+
191
+ ```js
192
+ // Scalar quantization (int8) -- 4x memory reduction, minimal recall loss
193
+ db.enableQuantization('scalar')
194
+
195
+ // Binary quantization -- 32x memory reduction, best for normalized embeddings
196
+ db.enableQuantization('binary', JSON.stringify({ rescoreMultiplier: 10 }))
197
+
198
+ // Product quantization -- configurable compression for very large datasets
199
+ db.enableQuantization('product', JSON.stringify({ numSubVectors: 16, numCentroids: 256 }))
200
+
201
+ // Search works exactly the same -- quantization accelerates it transparently
202
+ const results = db.search(queryEmbedding, { k: 10 })
203
+
204
+ // Check quantization status
205
+ console.log(db.isQuantized) // true
206
+ console.log(db.quantizationMethod) // "scalar", "binary", or "product"
207
+
208
+ // Disable quantization
209
+ db.disableQuantization()
210
+ ```
211
+
212
+ Quantization parameters persist across reopens in a `.vdb.quant` sidecar file. The quantized index auto-rebuilds on inserts and upserts.
213
+
186
214
  ## Database Methods Reference
187
215
 
188
216
  ### Write Methods
@@ -212,6 +240,15 @@ console.log(outcome.results[0].explain) // Detailed scoring breakdown
212
240
  | `db.path` | Database file path (property) |
213
241
  | `db.readOnly` | Whether the database is read-only (property) |
214
242
 
243
+ ### Quantization Methods
244
+
245
+ | Method | Description |
246
+ |---|---|
247
+ | `db.enableQuantization(method, optionsJson)` | Enable quantization (`'scalar'`, `'binary'`, or `'product'`) |
248
+ | `db.disableQuantization()` | Disable quantization and remove persisted parameters |
249
+ | `db.isQuantized` | Whether quantization is enabled (property) |
250
+ | `db.quantizationMethod` | Active method name or `null` (property) |
251
+
215
252
  ### Maintenance Methods
216
253
 
217
254
  | Method | Description |
package/native/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vectlite-node"
3
- version = "0.1.11"
3
+ version = "0.1.12"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  description = "Node.js bindings for vectlite."
package/native/src/lib.rs CHANGED
@@ -6,6 +6,10 @@ use napi::Error as NapiError;
6
6
  use napi::bindgen_prelude::*;
7
7
  use napi_derive::napi;
8
8
  use serde_json::{Map, Number, Value, json};
9
+ use vectlite::quantization::{
10
+ BinaryQuantizationConfig, ProductQuantizationConfig, QuantizationConfig,
11
+ ScalarQuantizationConfig,
12
+ };
9
13
  use vectlite::{
10
14
  Database as CoreDatabase, FusionStrategy, HybridSearchOptions, Metadata, MetadataFilter,
11
15
  MetadataValue, NamedVectors, Record, SearchOutcome, SearchResult, SparseVector,
@@ -348,6 +352,58 @@ impl NativeDatabase {
348
352
  database.compact().map_err(to_napi_error)
349
353
  }
350
354
 
355
+ // -------------------------------------------------------------------
356
+ // Quantization
357
+ // -------------------------------------------------------------------
358
+
359
+ /// Enable quantization on the database.
360
+ /// `method`: "scalar", "binary", or "product"
361
+ /// `options_json`: JSON with optional keys: rescore_multiplier, num_sub_vectors, num_centroids, training_iterations
362
+ #[napi(js_name = "enableQuantization")]
363
+ pub fn enable_quantization(
364
+ &self,
365
+ method: Option<String>,
366
+ options_json: Option<String>,
367
+ ) -> Result<()> {
368
+ let method = method.as_deref().unwrap_or("scalar");
369
+ let (rescore_multiplier, num_sub_vectors, num_centroids, training_iterations) =
370
+ parse_quantization_options(options_json.as_deref())?;
371
+ let config = build_quantization_config(
372
+ method,
373
+ rescore_multiplier,
374
+ num_sub_vectors,
375
+ num_centroids,
376
+ training_iterations,
377
+ )?;
378
+ let mut database = self.write_open()?;
379
+ database.enable_quantization(config).map_err(to_napi_error)
380
+ }
381
+
382
+ /// Disable quantization and remove persisted parameters.
383
+ #[napi(js_name = "disableQuantization")]
384
+ pub fn disable_quantization(&self) -> Result<()> {
385
+ let mut database = self.write_open()?;
386
+ database.disable_quantization().map_err(to_napi_error)
387
+ }
388
+
389
+ /// Returns true if quantization is enabled.
390
+ #[napi(getter, js_name = "isQuantized")]
391
+ pub fn is_quantized(&self) -> Result<bool> {
392
+ let database = self.read()?;
393
+ Ok(database.is_quantized())
394
+ }
395
+
396
+ /// Returns the quantization method name if enabled, else null.
397
+ #[napi(getter, js_name = "quantizationMethod")]
398
+ pub fn quantization_method(&self) -> Result<Option<String>> {
399
+ let database = self.read()?;
400
+ Ok(database.quantization_config().map(|config| match config {
401
+ QuantizationConfig::Scalar(_) => "scalar".to_owned(),
402
+ QuantizationConfig::Binary(_) => "binary".to_owned(),
403
+ QuantizationConfig::Product(_) => "product".to_owned(),
404
+ }))
405
+ }
406
+
351
407
  #[napi]
352
408
  pub fn snapshot(&self, dest: String) -> Result<()> {
353
409
  let database = self.read()?;
@@ -1315,3 +1371,70 @@ fn to_napi_error(error: vectlite::VectLiteError) -> NapiError {
1315
1371
  fn closed_database_error() -> vectlite::VectLiteError {
1316
1372
  vectlite::VectLiteError::InvalidFormat("database is closed".to_owned())
1317
1373
  }
1374
+
1375
+ fn parse_quantization_options(
1376
+ options_json: Option<&str>,
1377
+ ) -> Result<(Option<usize>, Option<usize>, Option<usize>, Option<usize>)> {
1378
+ let Some(json_str) = options_json else {
1379
+ return Ok((None, None, None, None));
1380
+ };
1381
+ let value: Value = serde_json::from_str(json_str)
1382
+ .map_err(|e| err(format!("invalid quantization options JSON: {e}")))?;
1383
+ let obj = value
1384
+ .as_object()
1385
+ .ok_or_else(|| err("quantization options must be a JSON object"))?;
1386
+
1387
+ let rescore_multiplier = obj
1388
+ .get("rescoreMultiplier")
1389
+ .or_else(|| obj.get("rescore_multiplier"))
1390
+ .and_then(|v| v.as_u64())
1391
+ .map(|v| v as usize);
1392
+ let num_sub_vectors = obj
1393
+ .get("numSubVectors")
1394
+ .or_else(|| obj.get("num_sub_vectors"))
1395
+ .and_then(|v| v.as_u64())
1396
+ .map(|v| v as usize);
1397
+ let num_centroids = obj
1398
+ .get("numCentroids")
1399
+ .or_else(|| obj.get("num_centroids"))
1400
+ .and_then(|v| v.as_u64())
1401
+ .map(|v| v as usize);
1402
+ let training_iterations = obj
1403
+ .get("trainingIterations")
1404
+ .or_else(|| obj.get("training_iterations"))
1405
+ .and_then(|v| v.as_u64())
1406
+ .map(|v| v as usize);
1407
+
1408
+ Ok((
1409
+ rescore_multiplier,
1410
+ num_sub_vectors,
1411
+ num_centroids,
1412
+ training_iterations,
1413
+ ))
1414
+ }
1415
+
1416
+ fn build_quantization_config(
1417
+ method: &str,
1418
+ rescore_multiplier: Option<usize>,
1419
+ num_sub_vectors: Option<usize>,
1420
+ num_centroids: Option<usize>,
1421
+ training_iterations: Option<usize>,
1422
+ ) -> Result<QuantizationConfig> {
1423
+ match method {
1424
+ "scalar" | "int8" => Ok(QuantizationConfig::Scalar(ScalarQuantizationConfig {
1425
+ rescore_multiplier: rescore_multiplier.unwrap_or(5),
1426
+ })),
1427
+ "binary" => Ok(QuantizationConfig::Binary(BinaryQuantizationConfig {
1428
+ rescore_multiplier: rescore_multiplier.unwrap_or(10),
1429
+ })),
1430
+ "product" | "pq" => Ok(QuantizationConfig::Product(ProductQuantizationConfig {
1431
+ num_sub_vectors: num_sub_vectors.unwrap_or(16),
1432
+ num_centroids: num_centroids.unwrap_or(256),
1433
+ training_iterations: training_iterations.unwrap_or(20),
1434
+ rescore_multiplier: rescore_multiplier.unwrap_or(10),
1435
+ })),
1436
+ other => Err(err(format!(
1437
+ "unknown quantization method '{other}'. Expected: 'scalar', 'binary', or 'product'"
1438
+ ))),
1439
+ }
1440
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vectlite-core"
3
- version = "0.1.11"
3
+ version = "0.1.12"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  description = "Core storage engine for vectlite."