vectlite 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vectlite contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Node Binding
2
+
3
+ The Node binding now exists in-repo and builds from source.
4
+
5
+ Current state:
6
+
7
+ - Rust addon implemented with `napi-rs`
8
+ - JavaScript wrapper and TypeScript declarations included
9
+ - local smoke test available in `bindings/node/tests`
10
+ - npm package can be published as a source-build package
11
+ - installing the npm package requires a working Rust toolchain on the target machine
12
+
13
+ ## Local Build
14
+
15
+ From the repository root:
16
+
17
+ ```bash
18
+ cd bindings/node
19
+ npm run build
20
+ ```
21
+
22
+ This compiles the Rust addon and writes `bindings/node/vectlite.node`.
23
+
24
+ ## Local Test
25
+
26
+ ```bash
27
+ cd bindings/node
28
+ npm test
29
+ ```
30
+
31
+ ## npm Package Model
32
+
33
+ The npm package is set up as a source-build package:
34
+
35
+ - `prepack` stages a self-contained native crate plus the core Rust crate
36
+ - `install` compiles the addon with Cargo on the target machine
37
+ - the published tarball does not ship prebuilt binaries yet
38
+
39
+ That keeps one source of truth for the Rust core, but it means `npm install vectlite` requires:
40
+
41
+ - Node 18+
42
+ - Rust/Cargo installed
43
+ - registry/network access to fetch Rust crates during the build
44
+
45
+ ## Usage
46
+
47
+ ```js
48
+ const { open, sparseTerms } = require('./index.js')
49
+
50
+ const db = open('knowledge.vdb', { dimension: 384 })
51
+ db.upsert('doc1', embedding, { source: 'notes', title: 'Auth Guide' })
52
+
53
+ const results = db.search(queryEmbedding, {
54
+ k: 5,
55
+ sparse: sparseTerms('auth guide'),
56
+ filter: { source: 'notes' },
57
+ })
58
+ ```
59
+
60
+ ## Scope
61
+
62
+ The initial Node surface covers the core database and store operations:
63
+
64
+ - `open`, `openStore`, `restore`
65
+ - `insert`, `upsert`, `get`, `delete`
66
+ - batch writes and bulk ingest
67
+ - snapshots, backup, compact, flush
68
+ - namespaces and collections
69
+ - dense, sparse, and hybrid search
70
+ - search stats and text helpers
71
+
72
+ Not yet included:
73
+
74
+ - JS callback rerank hooks
75
+ - prebuilt binaries
package/index.d.ts ADDED
@@ -0,0 +1,182 @@
1
+ export type MetadataValue =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | MetadataValue[]
7
+ | { [key: string]: MetadataValue }
8
+
9
+ export type Metadata = { [key: string]: MetadataValue }
10
+ export type SparseVector = { [term: string]: number }
11
+ export type NamedVectors = { [name: string]: number[] }
12
+ export type Filter = { [key: string]: unknown }
13
+
14
+ export interface Record {
15
+ namespace: string
16
+ id: string
17
+ vector: number[]
18
+ vectors: NamedVectors
19
+ sparse: SparseVector
20
+ metadata: Metadata
21
+ }
22
+
23
+ export interface SearchTimings {
24
+ dense_us: number
25
+ sparse_us: number
26
+ fusion_us: number
27
+ total_us: number
28
+ }
29
+
30
+ export interface SearchStats {
31
+ used_ann: boolean
32
+ ann_candidate_count: number
33
+ exact_fallback: boolean
34
+ considered_count: number
35
+ fetch_k: number
36
+ mmr_applied: boolean
37
+ sparse_candidate_count: number
38
+ ann_loaded_from_disk: boolean
39
+ wal_entries_replayed: number
40
+ fusion: string
41
+ rerank_applied: boolean
42
+ rerank_count: number
43
+ timings: SearchTimings
44
+ }
45
+
46
+ export interface ExplainDetails {
47
+ fusion?: string
48
+ dense_score?: number
49
+ sparse_score?: number
50
+ matched_terms?: string[]
51
+ vector_name?: string | null
52
+ dense_rank?: number | null
53
+ sparse_rank?: number | null
54
+ bm25_term_scores?: { [term: string]: number }
55
+ }
56
+
57
+ export interface SearchResult {
58
+ namespace: string
59
+ id: string
60
+ score: number
61
+ dense_score: number
62
+ sparse_score: number
63
+ vector_name: string | null
64
+ matched_terms: string[]
65
+ dense_rank: number | null
66
+ sparse_rank: number | null
67
+ metadata: Metadata
68
+ explain?: ExplainDetails
69
+ }
70
+
71
+ export interface SearchResponse {
72
+ results: SearchResult[]
73
+ stats: SearchStats
74
+ }
75
+
76
+ export interface WriteOptions {
77
+ namespace?: string | null
78
+ sparse?: SparseVector | null
79
+ vectors?: NamedVectors | null
80
+ }
81
+
82
+ export interface BulkIngestOptions {
83
+ namespace?: string | null
84
+ batchSize?: number
85
+ }
86
+
87
+ export interface SearchOptions {
88
+ k?: number
89
+ filter?: Filter | null
90
+ namespace?: string | null
91
+ allNamespaces?: boolean
92
+ sparse?: SparseVector | null
93
+ denseWeight?: number
94
+ sparseWeight?: number
95
+ fetchK?: number
96
+ mmrLambda?: number | null
97
+ vectorName?: string | null
98
+ fusion?: 'linear' | 'rrf'
99
+ rrfK?: number
100
+ explain?: boolean
101
+ queryVectors?: { [name: string]: number[] } | null
102
+ vectorWeights?: { [name: string]: number } | null
103
+ }
104
+
105
+ export interface OpenOptions {
106
+ dimension?: number | null
107
+ readOnly?: boolean
108
+ }
109
+
110
+ export class VectLiteError extends Error {}
111
+
112
+ export class Transaction {
113
+ count(): number
114
+ insert(id: string, vector: number[], metadata?: Metadata | null, options?: WriteOptions): void
115
+ upsert(id: string, vector: number[], metadata?: Metadata | null, options?: WriteOptions): void
116
+ insertMany(records: Record[], options?: { namespace?: string | null }): number
117
+ upsertMany(records: Record[], options?: { namespace?: string | null }): number
118
+ delete(id: string, options?: { namespace?: string | null }): boolean
119
+ deleteMany(ids: string[], options?: { namespace?: string | null }): number
120
+ commit(): void
121
+ rollback(): void
122
+ }
123
+
124
+ export class Database {
125
+ readonly path: string
126
+ readonly walPath: string
127
+ readonly dimension: number
128
+ readonly readOnly: boolean
129
+
130
+ count(): number
131
+ namespaces(): string[]
132
+ transaction(): Transaction
133
+ insert(id: string, vector: number[], metadata?: Metadata | null, options?: WriteOptions): void
134
+ upsert(id: string, vector: number[], metadata?: Metadata | null, options?: WriteOptions): void
135
+ insertMany(records: Record[], options?: { namespace?: string | null }): number
136
+ upsertMany(records: Record[], options?: { namespace?: string | null }): number
137
+ bulkIngest(records: Record[], options?: BulkIngestOptions): number
138
+ get(id: string, options?: { namespace?: string | null }): Record | null
139
+ delete(id: string, options?: { namespace?: string | null }): boolean
140
+ deleteMany(ids: string[], options?: { namespace?: string | null }): number
141
+ flush(): void
142
+ compact(): void
143
+ snapshot(dest: string): void
144
+ backup(dest: string): void
145
+ search(query?: number[] | null, options?: SearchOptions): SearchResult[]
146
+ searchWithStats(query?: number[] | null, options?: SearchOptions): SearchResponse
147
+ }
148
+
149
+ export class Store {
150
+ readonly root: string
151
+ createCollection(name: string, dimension: number): Database
152
+ openCollection(name: string): Database
153
+ openOrCreateCollection(name: string, dimension: number): Database
154
+ openCollectionReadOnly(name: string): Database
155
+ dropCollection(name: string): boolean
156
+ collections(): string[]
157
+ }
158
+
159
+ export function open(path: string, options?: OpenOptions): Database
160
+ export function openStore(root: string): Store
161
+ export function restore(source: string, dest: string): Database
162
+ export function sparseTerms(text: string): SparseVector
163
+ export function upsertText(
164
+ db: Database,
165
+ id: string,
166
+ text: string,
167
+ embed: (text: string) => ArrayLike<number>,
168
+ metadata?: Metadata | null,
169
+ options?: WriteOptions,
170
+ ): void
171
+ export function searchText(
172
+ db: Database,
173
+ text: string,
174
+ embed: (text: string) => ArrayLike<number>,
175
+ options?: SearchOptions,
176
+ ): SearchResult[]
177
+ export function searchTextWithStats(
178
+ db: Database,
179
+ text: string,
180
+ embed: (text: string) => ArrayLike<number>,
181
+ options?: SearchOptions,
182
+ ): SearchResponse
package/index.js ADDED
@@ -0,0 +1,284 @@
1
+ 'use strict'
2
+
3
+ const native = require('./vectlite.node')
4
+
5
+ const TOKEN_RE = /[a-z0-9]+/g
6
+
7
+ class VectLiteError extends Error {
8
+ constructor(message, cause) {
9
+ super(message)
10
+ this.name = 'VectLiteError'
11
+ if (cause !== undefined) {
12
+ this.cause = cause
13
+ }
14
+ }
15
+ }
16
+
17
+ function wrapError(fn) {
18
+ try {
19
+ return fn()
20
+ } catch (error) {
21
+ if (error instanceof VectLiteError) {
22
+ throw error
23
+ }
24
+ throw new VectLiteError(error?.message ?? String(error), error)
25
+ }
26
+ }
27
+
28
+ function encode(value) {
29
+ return value == null ? null : JSON.stringify(value)
30
+ }
31
+
32
+ function decode(value) {
33
+ return value == null ? null : JSON.parse(value)
34
+ }
35
+
36
+ function asArray(values) {
37
+ return Array.from(values)
38
+ }
39
+
40
+ function normalizeWriteOptions(options = {}) {
41
+ return {
42
+ namespace: options.namespace ?? null,
43
+ sparse: options.sparse ?? null,
44
+ vectors: options.vectors ?? null,
45
+ }
46
+ }
47
+
48
+ function sparseTerms(text) {
49
+ const counts = {}
50
+ const tokens = String(text).toLowerCase().match(TOKEN_RE) ?? []
51
+ if (tokens.length === 0) {
52
+ return counts
53
+ }
54
+
55
+ const total = tokens.length
56
+ for (const token of tokens) {
57
+ counts[token] = (counts[token] ?? 0) + (1 / total)
58
+ }
59
+ return counts
60
+ }
61
+
62
+ class Transaction {
63
+ constructor(nativeTx) {
64
+ this._native = nativeTx
65
+ }
66
+
67
+ count() {
68
+ return wrapError(() => this._native.count())
69
+ }
70
+
71
+ insert(id, vector, metadata = null, options = {}) {
72
+ const { namespace, sparse, vectors } = normalizeWriteOptions(options)
73
+ return wrapError(() =>
74
+ this._native.insert(id, asArray(vector), encode(metadata), namespace, encode(sparse), encode(vectors)),
75
+ )
76
+ }
77
+
78
+ upsert(id, vector, metadata = null, options = {}) {
79
+ const { namespace, sparse, vectors } = normalizeWriteOptions(options)
80
+ return wrapError(() =>
81
+ this._native.upsert(id, asArray(vector), encode(metadata), namespace, encode(sparse), encode(vectors)),
82
+ )
83
+ }
84
+
85
+ insertMany(records, options = {}) {
86
+ return wrapError(() => this._native.insertMany(encode(records), options.namespace ?? null))
87
+ }
88
+
89
+ upsertMany(records, options = {}) {
90
+ return wrapError(() => this._native.upsertMany(encode(records), options.namespace ?? null))
91
+ }
92
+
93
+ delete(id, options = {}) {
94
+ return wrapError(() => this._native.delete(id, options.namespace ?? null))
95
+ }
96
+
97
+ deleteMany(ids, options = {}) {
98
+ return wrapError(() => this._native.deleteMany(ids, options.namespace ?? null))
99
+ }
100
+
101
+ commit() {
102
+ return wrapError(() => this._native.commit())
103
+ }
104
+
105
+ rollback() {
106
+ return wrapError(() => this._native.rollback())
107
+ }
108
+ }
109
+
110
+ class Database {
111
+ constructor(nativeDb) {
112
+ this._native = nativeDb
113
+ }
114
+
115
+ get path() {
116
+ return wrapError(() => this._native.path)
117
+ }
118
+
119
+ get walPath() {
120
+ return wrapError(() => this._native.walPath)
121
+ }
122
+
123
+ get dimension() {
124
+ return wrapError(() => this._native.dimension)
125
+ }
126
+
127
+ get readOnly() {
128
+ return wrapError(() => this._native.readOnly)
129
+ }
130
+
131
+ count() {
132
+ return wrapError(() => this._native.count())
133
+ }
134
+
135
+ namespaces() {
136
+ return wrapError(() => this._native.namespaces())
137
+ }
138
+
139
+ transaction() {
140
+ return wrapError(() => new Transaction(this._native.transaction()))
141
+ }
142
+
143
+ insert(id, vector, metadata = null, options = {}) {
144
+ const { namespace, sparse, vectors } = normalizeWriteOptions(options)
145
+ return wrapError(() =>
146
+ this._native.insert(id, asArray(vector), encode(metadata), namespace, encode(sparse), encode(vectors)),
147
+ )
148
+ }
149
+
150
+ upsert(id, vector, metadata = null, options = {}) {
151
+ const { namespace, sparse, vectors } = normalizeWriteOptions(options)
152
+ return wrapError(() =>
153
+ this._native.upsert(id, asArray(vector), encode(metadata), namespace, encode(sparse), encode(vectors)),
154
+ )
155
+ }
156
+
157
+ insertMany(records, options = {}) {
158
+ return wrapError(() => this._native.insertMany(encode(records), options.namespace ?? null))
159
+ }
160
+
161
+ upsertMany(records, options = {}) {
162
+ return wrapError(() => this._native.upsertMany(encode(records), options.namespace ?? null))
163
+ }
164
+
165
+ bulkIngest(records, options = {}) {
166
+ return wrapError(() =>
167
+ this._native.bulkIngest(encode(records), options.namespace ?? null, options.batchSize ?? 10_000),
168
+ )
169
+ }
170
+
171
+ get(id, options = {}) {
172
+ return wrapError(() => decode(this._native.get(id, options.namespace ?? null)))
173
+ }
174
+
175
+ delete(id, options = {}) {
176
+ return wrapError(() => this._native.delete(id, options.namespace ?? null))
177
+ }
178
+
179
+ deleteMany(ids, options = {}) {
180
+ return wrapError(() => this._native.deleteMany(ids, options.namespace ?? null))
181
+ }
182
+
183
+ flush() {
184
+ return wrapError(() => this._native.flush())
185
+ }
186
+
187
+ compact() {
188
+ return wrapError(() => this._native.compact())
189
+ }
190
+
191
+ snapshot(dest) {
192
+ return wrapError(() => this._native.snapshot(dest))
193
+ }
194
+
195
+ backup(dest) {
196
+ return wrapError(() => this._native.backup(dest))
197
+ }
198
+
199
+ search(query = null, options = {}) {
200
+ return wrapError(() => decode(this._native.search(query == null ? null : asArray(query), encode(options))))
201
+ }
202
+
203
+ searchWithStats(query = null, options = {}) {
204
+ return wrapError(() =>
205
+ decode(this._native.searchWithStats(query == null ? null : asArray(query), encode(options))),
206
+ )
207
+ }
208
+ }
209
+
210
+ class Store {
211
+ constructor(nativeStore) {
212
+ this._native = nativeStore
213
+ }
214
+
215
+ get root() {
216
+ return wrapError(() => this._native.root)
217
+ }
218
+
219
+ createCollection(name, dimension) {
220
+ return wrapError(() => new Database(this._native.createCollection(name, dimension)))
221
+ }
222
+
223
+ openCollection(name) {
224
+ return wrapError(() => new Database(this._native.openCollection(name)))
225
+ }
226
+
227
+ openOrCreateCollection(name, dimension) {
228
+ return wrapError(() => new Database(this._native.openOrCreateCollection(name, dimension)))
229
+ }
230
+
231
+ openCollectionReadOnly(name) {
232
+ return wrapError(() => new Database(this._native.openCollectionReadOnly(name)))
233
+ }
234
+
235
+ dropCollection(name) {
236
+ return wrapError(() => this._native.dropCollection(name))
237
+ }
238
+
239
+ collections() {
240
+ return wrapError(() => this._native.collections())
241
+ }
242
+ }
243
+
244
+ function open(path, options = {}) {
245
+ return wrapError(() => new Database(native.open(path, options.dimension ?? null, options.readOnly ?? false)))
246
+ }
247
+
248
+ function openStore(root) {
249
+ return wrapError(() => new Store(native.openStore(root)))
250
+ }
251
+
252
+ function restore(source, dest) {
253
+ return wrapError(() => new Database(native.restore(source, dest)))
254
+ }
255
+
256
+ function upsertText(db, id, text, embed, metadata = null, options = {}) {
257
+ const payload = { ...(metadata ?? {}) }
258
+ if (payload.text === undefined) {
259
+ payload.text = text
260
+ }
261
+ return db.upsert(id, asArray(embed(text)), payload, { ...options, sparse: sparseTerms(text) })
262
+ }
263
+
264
+ function searchText(db, text, embed, options = {}) {
265
+ return db.search(asArray(embed(text)), { ...options, sparse: sparseTerms(text) })
266
+ }
267
+
268
+ function searchTextWithStats(db, text, embed, options = {}) {
269
+ return db.searchWithStats(asArray(embed(text)), { ...options, sparse: sparseTerms(text) })
270
+ }
271
+
272
+ module.exports = {
273
+ Database,
274
+ Store,
275
+ Transaction,
276
+ VectLiteError,
277
+ open,
278
+ openStore,
279
+ restore,
280
+ sparseTerms,
281
+ upsertText,
282
+ searchText,
283
+ searchTextWithStats,
284
+ }
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "vectlite-node"
3
+ version = "0.1.3"
4
+ edition = "2024"
5
+ license = "MIT"
6
+ description = "Node.js bindings for vectlite."
7
+ homepage = "https://github.com/mcsedition-hub/vectlite"
8
+ repository = "https://github.com/mcsedition-hub/vectlite"
9
+ documentation = "https://github.com/mcsedition-hub/vectlite/tree/main/bindings/node"
10
+ build = "build.rs"
11
+
12
+ [lib]
13
+ name = "vectlite_node"
14
+ crate-type = ["cdylib"]
15
+
16
+ [dependencies]
17
+ napi = { version = "3.5.2", default-features = false, features = ["napi8"] }
18
+ napi-derive = "3.3.0"
19
+ serde_json = "1.0"
20
+ vectlite = { package = "vectlite-core", path = "./vectlite-core" }
21
+
22
+ [build-dependencies]
23
+ napi-build = "2.2.3"
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ napi_build::setup();
3
+ }