@faiss-node/native 0.1.4
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/.env.example +5 -0
- package/0.1.1 +0 -0
- package/API.md +416 -0
- package/DOCUMENTATION.md +203 -0
- package/LICENSE +21 -0
- package/QUICK_PUBLISH.md +48 -0
- package/README.md +551 -0
- package/binding.gyp +90 -0
- package/docs/README.md +31 -0
- package/package.json +69 -0
- package/src/cpp/faiss_index.cpp +300 -0
- package/src/cpp/faiss_index.h +99 -0
- package/src/cpp/napi_bindings.cpp +969 -0
- package/src/js/index.js +349 -0
- package/src/js/types.d.ts +36 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +30 -0
package/src/js/index.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Try to load the native module (path may vary based on build system)
|
|
2
|
+
let FaissIndexWrapper;
|
|
3
|
+
try {
|
|
4
|
+
// node-gyp build path
|
|
5
|
+
FaissIndexWrapper = require('../../build/Release/faiss_node.node').FaissIndexWrapper;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
try {
|
|
8
|
+
// CMake build path
|
|
9
|
+
FaissIndexWrapper = require('../../build/faiss_node.node').FaissIndexWrapper;
|
|
10
|
+
} catch (e2) {
|
|
11
|
+
throw new Error('Native module not found. Run "npm run build" first.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* FaissIndex - High-level JavaScript API for FAISS vector similarity search
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const index = new FaissIndex({ type: 'FLAT_L2', dims: 128 });
|
|
20
|
+
* await index.add(vectors);
|
|
21
|
+
* const results = await index.search(query, 10);
|
|
22
|
+
*/
|
|
23
|
+
class FaissIndex {
|
|
24
|
+
/**
|
|
25
|
+
* Create a new FAISS index
|
|
26
|
+
* @param {Object} config - Configuration object
|
|
27
|
+
* @param {string} config.type - Index type ('FLAT_L2', 'IVF_FLAT', or 'HNSW')
|
|
28
|
+
* @param {number} config.dims - Vector dimensions (required)
|
|
29
|
+
*/
|
|
30
|
+
constructor(config) {
|
|
31
|
+
if (!config || typeof config !== 'object') {
|
|
32
|
+
throw new TypeError('Expected config object');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate index type
|
|
36
|
+
const validTypes = ['FLAT_L2', 'IVF_FLAT', 'HNSW'];
|
|
37
|
+
if (config.type && !validTypes.includes(config.type)) {
|
|
38
|
+
throw new Error(`Index type '${config.type}' not supported. Supported types: ${validTypes.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!Number.isInteger(config.dims) || config.dims <= 0) {
|
|
42
|
+
throw new TypeError('dims must be a positive integer');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build config object for native wrapper
|
|
46
|
+
const nativeConfig = { dims: config.dims };
|
|
47
|
+
if (config.type) {
|
|
48
|
+
nativeConfig.type = config.type;
|
|
49
|
+
}
|
|
50
|
+
if (config.nlist !== undefined) {
|
|
51
|
+
nativeConfig.nlist = config.nlist;
|
|
52
|
+
}
|
|
53
|
+
if (config.nprobe !== undefined) {
|
|
54
|
+
nativeConfig.nprobe = config.nprobe;
|
|
55
|
+
}
|
|
56
|
+
if (config.M !== undefined) {
|
|
57
|
+
nativeConfig.M = config.M;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._native = new FaissIndexWrapper(nativeConfig);
|
|
61
|
+
this._dims = config.dims;
|
|
62
|
+
this._type = config.type || 'FLAT_L2';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add vectors to the index
|
|
67
|
+
* @param {Float32Array} vectors - Single vector or batch of vectors
|
|
68
|
+
* @param {Float32Array} [ids] - Optional IDs for vectors (reserved for future use)
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
*/
|
|
71
|
+
async add(vectors, ids) {
|
|
72
|
+
if (!(vectors instanceof Float32Array)) {
|
|
73
|
+
throw new TypeError('vectors must be a Float32Array');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (vectors.length === 0) {
|
|
77
|
+
throw new Error('Cannot add empty vector array');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (vectors.length % this._dims !== 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Vector length (${vectors.length}) must be a multiple of dimensions (${this._dims})`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Async operation using background worker
|
|
87
|
+
try {
|
|
88
|
+
await this._native.add(vectors);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new Error(`Failed to add vectors: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Train the index (required for IVF indexes before adding vectors)
|
|
96
|
+
* @param {Float32Array} vectors - Training vectors
|
|
97
|
+
* @returns {Promise<void>}
|
|
98
|
+
*/
|
|
99
|
+
async train(vectors) {
|
|
100
|
+
if (!(vectors instanceof Float32Array)) {
|
|
101
|
+
throw new TypeError('vectors must be a Float32Array');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (vectors.length === 0) {
|
|
105
|
+
throw new Error('Cannot train with empty vector array');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (vectors.length % this._dims !== 0) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Vector length (${vectors.length}) must be a multiple of dimensions (${this._dims})`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!this._native) {
|
|
115
|
+
throw new Error('Index has been disposed');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await this._native.train(vectors);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new Error(`Failed to train index: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Set nprobe for IVF indexes (number of clusters to search)
|
|
127
|
+
* @param {number} nprobe - Number of clusters to probe
|
|
128
|
+
*/
|
|
129
|
+
setNprobe(nprobe) {
|
|
130
|
+
if (!Number.isInteger(nprobe) || nprobe <= 0) {
|
|
131
|
+
throw new TypeError('nprobe must be a positive integer');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!this._native) {
|
|
135
|
+
throw new Error('Index has been disposed');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
this._native.setNprobe(nprobe);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error(`Failed to set nprobe: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Search for k nearest neighbors (single query)
|
|
147
|
+
* @param {Float32Array} query - Query vector
|
|
148
|
+
* @param {number} k - Number of neighbors to return
|
|
149
|
+
* @returns {Promise<{distances: Float32Array, labels: Int32Array}>}
|
|
150
|
+
*/
|
|
151
|
+
async search(query, k) {
|
|
152
|
+
if (!(query instanceof Float32Array)) {
|
|
153
|
+
throw new TypeError('query must be a Float32Array');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (query.length !== this._dims) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Query vector length (${query.length}) must match index dimensions (${this._dims})`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!Number.isInteger(k) || k <= 0) {
|
|
163
|
+
throw new TypeError('k must be a positive integer');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Async operation using background worker
|
|
167
|
+
try {
|
|
168
|
+
const results = await this._native.search(query, k);
|
|
169
|
+
return {
|
|
170
|
+
distances: results.distances,
|
|
171
|
+
labels: results.labels
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Batch search for k nearest neighbors (multiple queries)
|
|
180
|
+
* @param {Float32Array} queries - Query vectors (nQueries * dims elements)
|
|
181
|
+
* @param {number} k - Number of neighbors to return per query
|
|
182
|
+
* @returns {Promise<{distances: Float32Array, labels: Int32Array, nq: number, k: number}>}
|
|
183
|
+
*/
|
|
184
|
+
async searchBatch(queries, k) {
|
|
185
|
+
if (!this._native) {
|
|
186
|
+
throw new Error('Index has been disposed');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!(queries instanceof Float32Array)) {
|
|
190
|
+
throw new TypeError('queries must be a Float32Array');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (queries.length === 0) {
|
|
194
|
+
throw new Error('Queries array cannot be empty');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (queries.length % this._dims !== 0) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Queries array length (${queries.length}) must be a multiple of index dimensions (${this._dims})`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!Number.isInteger(k) || k <= 0) {
|
|
204
|
+
throw new TypeError('k must be a positive integer');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const results = await this._native.searchBatch(queries, k);
|
|
209
|
+
return {
|
|
210
|
+
distances: results.distances,
|
|
211
|
+
labels: results.labels,
|
|
212
|
+
nq: results.nq,
|
|
213
|
+
k: results.k
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(`Batch search failed: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get index statistics
|
|
222
|
+
* @returns {Object} Statistics object
|
|
223
|
+
*/
|
|
224
|
+
getStats() {
|
|
225
|
+
try {
|
|
226
|
+
return this._native.getStats();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new Error(`Failed to get stats: ${error.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Explicitly dispose of the index (optional, automatic on GC)
|
|
234
|
+
*/
|
|
235
|
+
dispose() {
|
|
236
|
+
if (this._native) {
|
|
237
|
+
try {
|
|
238
|
+
this._native.dispose();
|
|
239
|
+
this._native = null;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Ignore errors on dispose
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Save index to disk
|
|
248
|
+
* @param {string} filename - Path to save the index
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async save(filename) {
|
|
252
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
253
|
+
throw new TypeError('filename must be a non-empty string');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await this._native.save(filename);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new Error(`Failed to save index: ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Serialize index to buffer
|
|
265
|
+
* @returns {Promise<Buffer>}
|
|
266
|
+
*/
|
|
267
|
+
async toBuffer() {
|
|
268
|
+
try {
|
|
269
|
+
return await this._native.toBuffer();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new Error(`Failed to serialize index: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Merge vectors from another index into this index
|
|
277
|
+
* @param {FaissIndex} otherIndex - Another FaissIndex to merge from
|
|
278
|
+
* @returns {Promise<void>}
|
|
279
|
+
*/
|
|
280
|
+
async mergeFrom(otherIndex) {
|
|
281
|
+
if (!this._native) {
|
|
282
|
+
throw new Error('Index has been disposed');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!otherIndex || !otherIndex._native) {
|
|
286
|
+
throw new TypeError('otherIndex must be a valid FaissIndex');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (otherIndex._dims !== this._dims) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Merging index must have the same dimensions. Got ${otherIndex._dims}, expected ${this._dims}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
await this._native.mergeFrom(otherIndex._native);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
throw new Error(`Failed to merge index: ${error.message}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Load index from disk (static method)
|
|
304
|
+
* @param {string} filename - Path to load the index from
|
|
305
|
+
* @returns {Promise<FaissIndex>}
|
|
306
|
+
*/
|
|
307
|
+
static async load(filename) {
|
|
308
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
309
|
+
throw new TypeError('filename must be a non-empty string');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const native = FaissIndexWrapper.load(filename);
|
|
314
|
+
const stats = native.getStats();
|
|
315
|
+
const index = Object.create(FaissIndex.prototype);
|
|
316
|
+
index._native = native;
|
|
317
|
+
index._dims = stats.dims;
|
|
318
|
+
index._type = stats.type;
|
|
319
|
+
return index;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw new Error(`Failed to load index: ${error.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Deserialize index from buffer (static method)
|
|
327
|
+
* @param {Buffer} buffer - Buffer containing serialized index
|
|
328
|
+
* @returns {Promise<FaissIndex>}
|
|
329
|
+
*/
|
|
330
|
+
static async fromBuffer(buffer) {
|
|
331
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
332
|
+
throw new TypeError('buffer must be a Node.js Buffer');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const native = FaissIndexWrapper.fromBuffer(buffer);
|
|
337
|
+
const stats = native.getStats();
|
|
338
|
+
const index = Object.create(FaissIndex.prototype);
|
|
339
|
+
index._native = native;
|
|
340
|
+
index._dims = stats.dims;
|
|
341
|
+
index._type = stats.type;
|
|
342
|
+
return index;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw new Error(`Failed to deserialize index: ${error.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { FaissIndex };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for faiss-node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface FaissIndexConfig {
|
|
6
|
+
type: 'FLAT_L2' | 'IVF_FLAT' | 'HNSW';
|
|
7
|
+
dims: number;
|
|
8
|
+
nlist?: number;
|
|
9
|
+
nprobe?: number;
|
|
10
|
+
M?: number;
|
|
11
|
+
efConstruction?: number;
|
|
12
|
+
efSearch?: number;
|
|
13
|
+
metric?: 'l2' | 'ip';
|
|
14
|
+
threads?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SearchResults {
|
|
18
|
+
distances: Float32Array;
|
|
19
|
+
labels: Int32Array;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IndexStats {
|
|
23
|
+
ntotal: number;
|
|
24
|
+
dims: number;
|
|
25
|
+
isTrained: boolean;
|
|
26
|
+
type: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare class FaissIndex {
|
|
30
|
+
constructor(config: FaissIndexConfig);
|
|
31
|
+
|
|
32
|
+
add(vectors: Float32Array, ids?: Int32Array): Promise<void>;
|
|
33
|
+
search(query: Float32Array, k: number): Promise<SearchResults>;
|
|
34
|
+
getStats(): IndexStats;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"checkJs": false,
|
|
8
|
+
"declaration": false,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"strict": false,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"noEmit": true
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src/js/**/*"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"node_modules",
|
|
21
|
+
"build",
|
|
22
|
+
"test",
|
|
23
|
+
"docs"
|
|
24
|
+
]
|
|
25
|
+
}
|
package/typedoc.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://typedoc.org/schema.json",
|
|
3
|
+
"entryPoints": [
|
|
4
|
+
"src/js/index.js"
|
|
5
|
+
],
|
|
6
|
+
"entryPointStrategy": "expand",
|
|
7
|
+
"out": "docs/api",
|
|
8
|
+
"theme": "default",
|
|
9
|
+
"name": "@faiss-node/native API Documentation",
|
|
10
|
+
"readme": "README.md",
|
|
11
|
+
"includeVersion": true,
|
|
12
|
+
"excludePrivate": true,
|
|
13
|
+
"excludeProtected": true,
|
|
14
|
+
"excludeExternals": true,
|
|
15
|
+
"excludeInternal": true,
|
|
16
|
+
"categorizeByGroup": true,
|
|
17
|
+
"categoryOrder": [
|
|
18
|
+
"Classes",
|
|
19
|
+
"Interfaces",
|
|
20
|
+
"Types"
|
|
21
|
+
],
|
|
22
|
+
"gitRevision": "main",
|
|
23
|
+
"gitRemote": "origin",
|
|
24
|
+
"githubPages": true,
|
|
25
|
+
"validation": {
|
|
26
|
+
"invalidLink": true,
|
|
27
|
+
"notDocumented": false
|
|
28
|
+
},
|
|
29
|
+
"plugin": []
|
|
30
|
+
}
|