vectlite 0.9.0 → 0.9.2
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 +26 -14
- package/index.d.ts +60 -0
- package/index.js +135 -8
- package/native/Cargo.toml +1 -1
- package/native/src/lib.rs +80 -47
- package/native/vectlite-core/Cargo.toml +1 -1
- package/native/vectlite-core/src/lib.rs +512 -152
- package/native/vectlite-core/src/quantization.rs +234 -49
- 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
|
@@ -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) --
|
|
241
|
+
// Scalar quantization (int8) -- smaller in-memory candidate index, minimal recall loss
|
|
238
242
|
db.enableQuantization('scalar')
|
|
239
243
|
|
|
240
|
-
// Binary quantization --
|
|
241
|
-
db.enableQuantization('binary',
|
|
244
|
+
// Binary quantization -- smallest in-memory candidate index, best for normalized embeddings
|
|
245
|
+
db.enableQuantization('binary', { rescoreMultiplier: 10 })
|
|
242
246
|
|
|
243
|
-
// Product quantization --
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
276
|
+
{ colbert: [tokenVec1, tokenVec2] },
|
|
277
|
+
{ metadata: { source: 'paper' } }
|
|
268
278
|
)
|
|
269
279
|
|
|
270
280
|
// MaxSim search
|
|
271
|
-
const results =
|
|
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,
|
|
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,31 @@ 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[]
|
|
240
|
+
search(query: number[], k: number): SearchResult[]
|
|
188
241
|
search(query?: number[] | null, options?: SearchOptions): SearchResult[]
|
|
242
|
+
searchWithStats(request: SearchRequest): SearchResponse
|
|
243
|
+
searchWithStats(query: number[], k: number): SearchResponse
|
|
189
244
|
searchWithStats(query?: number[] | null, options?: SearchOptions): SearchResponse
|
|
245
|
+
searchAsync(request: SearchRequest): Promise<SearchResult[]>
|
|
246
|
+
searchAsync(query: number[], k: number): Promise<SearchResult[]>
|
|
190
247
|
searchAsync(query?: number[] | null, options?: SearchOptions): Promise<SearchResult[]>
|
|
248
|
+
searchWithStatsAsync(request: SearchRequest): Promise<SearchResponse>
|
|
249
|
+
searchWithStatsAsync(query: number[], k: number): Promise<SearchResponse>
|
|
191
250
|
searchWithStatsAsync(query?: number[] | null, options?: SearchOptions): Promise<SearchResponse>
|
|
192
251
|
flushAsync(): Promise<void>
|
|
193
252
|
compactAsync(): Promise<void>
|
|
@@ -202,6 +261,7 @@ export class Store {
|
|
|
202
261
|
openCollectionReadOnly(name: string): Database
|
|
203
262
|
dropCollection(name: string): boolean
|
|
204
263
|
collections(): string[]
|
|
264
|
+
close(): void
|
|
205
265
|
}
|
|
206
266
|
|
|
207
267
|
export function open(path: string, options?: OpenOptions): Database
|
package/index.js
CHANGED
|
@@ -182,9 +182,64 @@ 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
|
+
if (typeof options === 'number') {
|
|
238
|
+
return { query, options: { k: options } }
|
|
239
|
+
}
|
|
240
|
+
return { query, options: options ?? {} }
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
function isPromiseLike(value) {
|
|
189
244
|
return value != null && typeof value.then === 'function'
|
|
190
245
|
}
|
|
@@ -406,6 +461,52 @@ class Database {
|
|
|
406
461
|
return wrapError(() => decode(this._native.listIndexes()))
|
|
407
462
|
}
|
|
408
463
|
|
|
464
|
+
enableQuantization(method = 'scalar', options = {}) {
|
|
465
|
+
return wrapError(() => this._native.enableQuantization(method, encodeNativeOptions(options)))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
disableQuantization() {
|
|
469
|
+
return wrapError(() => this._native.disableQuantization())
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
get isQuantized() {
|
|
473
|
+
return wrapError(() => this._native.isQuantized)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
get quantizationMethod() {
|
|
477
|
+
return wrapError(() => this._native.quantizationMethod)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
validNumSubVectors() {
|
|
481
|
+
return wrapError(() => this._native.validNumSubVectors())
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
upsertMultiVectors(id, vector, multiVectors, options = {}) {
|
|
485
|
+
return wrapError(() =>
|
|
486
|
+
this._native.upsertMultiVectors(id, asArray(vector), encode(multiVectors), encode(options)),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
searchMultiVector(space, queryTokens, options = {}) {
|
|
491
|
+
return wrapError(() =>
|
|
492
|
+
decode(this._native.searchMultiVector(space, encode(queryTokens), encode(options))),
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
enableMultiVectorQuantization(space, options = {}) {
|
|
497
|
+
return wrapError(() =>
|
|
498
|
+
this._native.enableMultiVectorQuantization(space, encodeNativeOptions(options)),
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
disableMultiVectorQuantization(space) {
|
|
503
|
+
return wrapError(() => this._native.disableMultiVectorQuantization(space))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
isMultiVectorQuantized(space) {
|
|
507
|
+
return wrapError(() => this._native.isMultiVectorQuantized(space))
|
|
508
|
+
}
|
|
509
|
+
|
|
409
510
|
flush() {
|
|
410
511
|
return wrapError(() => this._native.flush())
|
|
411
512
|
}
|
|
@@ -423,31 +524,53 @@ class Database {
|
|
|
423
524
|
}
|
|
424
525
|
|
|
425
526
|
search(query = null, options = {}) {
|
|
426
|
-
|
|
427
|
-
|
|
527
|
+
const normalized = normalizeSearchArgs(query, options)
|
|
528
|
+
return withSearchSpan(normalized.query, normalized.options, () =>
|
|
529
|
+
wrapError(() =>
|
|
530
|
+
decode(
|
|
531
|
+
this._native.search(
|
|
532
|
+
normalized.query == null ? null : asArray(normalized.query),
|
|
533
|
+
encode(normalized.options),
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
),
|
|
428
537
|
)
|
|
429
538
|
}
|
|
430
539
|
|
|
431
540
|
searchWithStats(query = null, options = {}) {
|
|
432
|
-
|
|
541
|
+
const normalized = normalizeSearchArgs(query, options)
|
|
542
|
+
return withSearchSpan(normalized.query, normalized.options, () =>
|
|
433
543
|
wrapError(() =>
|
|
434
|
-
decode(
|
|
544
|
+
decode(
|
|
545
|
+
this._native.searchWithStats(
|
|
546
|
+
normalized.query == null ? null : asArray(normalized.query),
|
|
547
|
+
encode(normalized.options),
|
|
548
|
+
),
|
|
549
|
+
),
|
|
435
550
|
),
|
|
436
551
|
)
|
|
437
552
|
}
|
|
438
553
|
|
|
439
554
|
searchAsync(query = null, options = {}) {
|
|
440
|
-
|
|
555
|
+
const normalized = normalizeSearchArgs(query, options)
|
|
556
|
+
return withSearchSpan(normalized.query, normalized.options, () =>
|
|
441
557
|
wrapAsync(
|
|
442
|
-
this._native.searchAsync(
|
|
558
|
+
this._native.searchAsync(
|
|
559
|
+
normalized.query == null ? null : asArray(normalized.query),
|
|
560
|
+
encode(normalized.options),
|
|
561
|
+
),
|
|
443
562
|
).then(decode),
|
|
444
563
|
)
|
|
445
564
|
}
|
|
446
565
|
|
|
447
566
|
searchWithStatsAsync(query = null, options = {}) {
|
|
448
|
-
|
|
567
|
+
const normalized = normalizeSearchArgs(query, options)
|
|
568
|
+
return withSearchSpan(normalized.query, normalized.options, () =>
|
|
449
569
|
wrapAsync(
|
|
450
|
-
this._native.searchWithStatsAsync(
|
|
570
|
+
this._native.searchWithStatsAsync(
|
|
571
|
+
normalized.query == null ? null : asArray(normalized.query),
|
|
572
|
+
encode(normalized.options),
|
|
573
|
+
),
|
|
451
574
|
).then(decode),
|
|
452
575
|
)
|
|
453
576
|
}
|
|
@@ -499,6 +622,10 @@ class Store {
|
|
|
499
622
|
collections() {
|
|
500
623
|
return wrapError(() => this._native.collections())
|
|
501
624
|
}
|
|
625
|
+
|
|
626
|
+
close() {
|
|
627
|
+
return wrapError(() => this._native.close())
|
|
628
|
+
}
|
|
502
629
|
}
|
|
503
630
|
|
|
504
631
|
function open(path, options = {}) {
|
package/native/Cargo.toml
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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!(
|
|
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
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
}
|