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 +37 -0
- package/native/Cargo.toml +1 -1
- package/native/src/lib.rs +123 -0
- package/native/vectlite-core/Cargo.toml +1 -1
- package/native/vectlite-core/src/lib.rs +421 -7
- package/native/vectlite-core/src/quantization.rs +1087 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/vectlite.node +0 -0
- package/prebuilds/darwin-x64/vectlite.node +0 -0
- package/prebuilds/linux-x64-gnu/vectlite.node +0 -0
- package/prebuilds/win32-x64-msvc/vectlite.node +0 -0
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
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
|
+
}
|