@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.
@@ -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
+ }