Skip to content

Commit 5a46aeb

Browse files
AnupamAnupam
authored andcommitted
Add advanced index types and release tooling
1 parent ce0b33d commit 5a46aeb

10 files changed

Lines changed: 647 additions & 93 deletions

File tree

.github/dependabot.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
version: 2
2+
3+
updates:
4+
- package-ecosystem: "npm"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
open-pull-requests-limit: 5
9+
10+
- package-ecosystem: "npm"
11+
directory: "/test-install"
12+
schedule:
13+
interval: "weekly"
14+
open-pull-requests-limit: 2
15+
16+
- package-ecosystem: "github-actions"
17+
directory: "/"
18+
schedule:
19+
interval: "weekly"
20+
21+
- package-ecosystem: "docker"
22+
directory: "/"
23+
schedule:
24+
interval: "weekly"

.github/workflows/codeql.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
schedule:
9+
- cron: '20 4 * * 1'
10+
11+
permissions:
12+
actions: read
13+
contents: read
14+
security-events: write
15+
16+
jobs:
17+
analyze:
18+
name: Analyze (${{ matrix.language }})
19+
runs-on: ubuntu-latest
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
include:
24+
- language: javascript-typescript
25+
build-mode: none
26+
- language: c-cpp
27+
build-mode: manual
28+
29+
steps:
30+
- name: Checkout code
31+
uses: actions/checkout@v4
32+
33+
- name: Setup Node.js
34+
if: matrix.language == 'c-cpp'
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: 20
38+
cache: 'npm'
39+
40+
- name: Initialize CodeQL
41+
uses: github/codeql-action/init@v4
42+
with:
43+
languages: ${{ matrix.language }}
44+
build-mode: ${{ matrix.build-mode }}
45+
46+
- name: Install native dependencies
47+
if: matrix.language == 'c-cpp'
48+
run: |
49+
sudo apt-get update
50+
sudo apt-get install -y cmake libopenblas-dev libomp-dev build-essential
51+
git clone https://github.com/facebookresearch/faiss.git /tmp/faiss
52+
cd /tmp/faiss
53+
cmake -B build \
54+
-DFAISS_ENABLE_GPU=OFF \
55+
-DFAISS_ENABLE_PYTHON=OFF \
56+
-DBUILD_TESTING=OFF \
57+
-DCMAKE_INSTALL_PREFIX=/usr/local \
58+
-DCMAKE_CXX_FLAGS="-fopenmp" \
59+
-DCMAKE_C_FLAGS="-fopenmp"
60+
cmake --build build -j"$(nproc)"
61+
sudo cmake --install build
62+
sudo ldconfig
63+
64+
- name: Install npm dependencies
65+
if: matrix.language == 'c-cpp'
66+
run: npm ci --ignore-scripts
67+
68+
- name: Build native addon
69+
if: matrix.language == 'c-cpp'
70+
run: |
71+
export CPPFLAGS="-I/usr/local/include"
72+
export CXXFLAGS="-I/usr/local/include"
73+
export LDFLAGS="-L/usr/local/lib"
74+
npm run build
75+
76+
- name: Analyze
77+
uses: github/codeql-action/analyze@v4
78+
with:
79+
category: /language:${{ matrix.language }}

Doxyfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Generated by Anupam Maurya
33

44
PROJECT_NAME = "@faiss-node/native"
5-
PROJECT_NUMBER = "0.1.2"
5+
PROJECT_NUMBER = "0.1.10"
66
PROJECT_BRIEF = "High-performance Node.js native bindings for Facebook FAISS - the fastest vector similarity search library. Supports FLAT_L2, IVF_FLAT, and HNSW index types with async operations, persistence, and batch search."
77
PROJECT_LOGO =
88
OUTPUT_DIRECTORY = docs

README.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ High-performance Node.js native bindings for [Facebook FAISS](https://github.com
1212

1313
- 🚀 **Async Operations** - Non-blocking Promise-based API that never blocks the event loop
1414
- 🔒 **Thread-Safe** - Mutex-protected concurrent operations for production workloads
15-
- 📦 **Multiple Index Types** - FLAT_L2, FLAT_IP (cosine similarity), IVF_FLAT, and HNSW with optimized defaults
15+
- 📦 **Multiple Index Types** - FLAT_L2, FLAT_IP, IVF_FLAT, HNSW, PQ, IVF_PQ, IVF_SQ, plus raw FAISS factory strings for advanced pipelines
1616
- 💾 **Persistence** - Save/load indexes to disk or serialize to buffers
1717
-**High Performance** - Direct C++ bindings with zero-copy data transfer
1818
- 📚 **TypeScript Support** - Full type definitions included
@@ -111,15 +111,20 @@ const index = new FaissIndex(config);
111111
```
112112

113113
**Parameters:**
114-
- `config.type` (string, optional): Index type - `'FLAT_L2'`, `'FLAT_IP'`, `'IVF_FLAT'`, or `'HNSW'` (default: `'FLAT_L2'`)
114+
- `config.type` (string, optional): Index type - `'FLAT_L2'`, `'FLAT_IP'`, `'IVF_FLAT'`, `'HNSW'`, `'PQ'`, `'IVF_PQ'`, or `'IVF_SQ'` (default: `'FLAT_L2'`)
115+
- `config.factory` (string, optional): Raw FAISS factory string for advanced pipelines such as OPQ, PCA, or PCAR
115116
- `config.dims` (number, required): Vector dimensions (must be positive integer)
116-
- `config.nlist` (number, optional): Number of clusters for IVF_FLAT (default: 100)
117-
- `config.nprobe` (number, optional): Clusters to search for IVF_FLAT (default: 10)
117+
- `config.metric` (string, optional): Distance metric - `'l2'` or `'ip'` for compatible index types and raw factory indexes
118+
- `config.nlist` (number, optional): Number of clusters for IVF_FLAT, IVF_PQ, or IVF_SQ (default: 100)
119+
- `config.nprobe` (number, optional): Clusters to search for IVF_FLAT, IVF_PQ, or IVF_SQ (default: 10)
118120
- `config.M` (number, optional): Connections per node for HNSW (default: 16)
119121
- `config.efConstruction` (number, optional): HNSW construction parameter (default: 200)
120122
- `config.efSearch` (number, optional): HNSW search parameter (default: 50)
123+
- `config.pqSegments` (number, optional): Number of PQ subquantizers for PQ and IVF_PQ
124+
- `config.pqBits` (number, optional): Bits per PQ code for PQ and IVF_PQ (default: 8)
125+
- `config.sqType` (string, optional): Scalar quantizer type for IVF_SQ (default: `'SQ8'`)
121126

122-
Use `nlist` and `nprobe` only with `IVF_FLAT`, and use `M`, `efConstruction`, and `efSearch` only with `HNSW`.
127+
Use `nlist` and `nprobe` only with `IVF_FLAT`, `IVF_PQ`, or `IVF_SQ`. Use `pqSegments` and `pqBits` only with `PQ` or `IVF_PQ`. Use `M`, `efConstruction`, and `efSearch` only with `HNSW`. Use `factory` by itself for advanced FAISS pipelines, because the topology is encoded directly in the factory string.
123128

124129
**Examples:**
125130

@@ -141,6 +146,36 @@ const ivfIndex = new FaissIndex({
141146
});
142147
await ivfIndex.train(trainingVectors); // Must train before adding vectors!
143148

149+
// PQ - Memory efficient quantization without IVF
150+
const pqIndex = new FaissIndex({
151+
type: 'PQ',
152+
dims: 768,
153+
pqSegments: 48,
154+
pqBits: 8
155+
});
156+
await pqIndex.train(trainingVectors);
157+
158+
// IVF_PQ - IVF coarse quantization plus PQ compression
159+
const ivfPqIndex = new FaissIndex({
160+
type: 'IVF_PQ',
161+
dims: 768,
162+
nlist: 100,
163+
nprobe: 10,
164+
pqSegments: 48,
165+
pqBits: 8
166+
});
167+
await ivfPqIndex.train(trainingVectors);
168+
169+
// IVF_SQ - IVF with scalar quantization
170+
const ivfSqIndex = new FaissIndex({
171+
type: 'IVF_SQ',
172+
dims: 768,
173+
nlist: 100,
174+
nprobe: 10,
175+
sqType: 'SQ8'
176+
});
177+
await ivfSqIndex.train(trainingVectors);
178+
144179
// HNSW - State-of-the-art approximate search (best for large datasets)
145180
const hnswIndex = new FaissIndex({
146181
type: 'HNSW',
@@ -149,6 +184,18 @@ const hnswIndex = new FaissIndex({
149184
efConstruction: 200, // Construction parameter
150185
efSearch: 50 // Search parameter (higher = more accurate, slower)
151186
});
187+
188+
// Advanced factory string - unlock FAISS preprocessing pipelines
189+
const customIndex = new FaissIndex({
190+
dims: 768,
191+
factory: 'PCA256,Flat',
192+
metric: 'l2'
193+
});
194+
await customIndex.train(trainingVectors);
195+
196+
// You can also pass OPQ / PCAR pipelines directly:
197+
// 'OPQ48_192,IVF100,PQ48'
198+
// 'PCAR256,IVF100,PQ48'
152199
```
153200

154201
### Methods

src/cpp/faiss_index.cpp

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
#include <faiss/IndexFlat.h>
77
#include <faiss/IndexHNSW.h>
88
#include <faiss/IndexIVF.h>
9+
#include <faiss/IndexIVFPQ.h>
10+
#include <faiss/IndexPQ.h>
11+
#include <faiss/IndexPreTransform.h>
12+
#include <faiss/IndexScalarQuantizer.h>
13+
#include <faiss/VectorTransform.h>
914
#include <faiss/index_factory.h>
1015
#include <faiss/index_io.h>
1116
#include <faiss/impl/io.h>
@@ -19,8 +24,95 @@
1924
#include "faiss_index.h"
2025
#include <stdexcept>
2126

22-
FaissIndexWrapper::FaissIndexWrapper(int dims, const std::string& indexDescription, int metric)
23-
: dims_(dims), disposed_(false) {
27+
namespace {
28+
29+
std::string MetricToString(faiss::MetricType metric) {
30+
return metric == faiss::METRIC_INNER_PRODUCT ? "ip" : "l2";
31+
}
32+
33+
std::string InferTransformLabel(const faiss::IndexPreTransform* pretransform) {
34+
if (pretransform == nullptr) {
35+
return "";
36+
}
37+
38+
for (const auto* transform : pretransform->chain) {
39+
if (dynamic_cast<const faiss::OPQMatrix*>(transform) != nullptr) {
40+
return "OPQ";
41+
}
42+
43+
const auto* pca = dynamic_cast<const faiss::PCAMatrix*>(transform);
44+
if (pca != nullptr) {
45+
return pca->random_rotation ? "PCAR" : "PCA";
46+
}
47+
}
48+
49+
return "PRETRANSFORM";
50+
}
51+
52+
std::string InferIndexType(const faiss::Index* index) {
53+
if (index == nullptr) {
54+
return "UNKNOWN";
55+
}
56+
57+
const auto* pretransform = dynamic_cast<const faiss::IndexPreTransform*>(index);
58+
if (pretransform != nullptr) {
59+
const std::string transformLabel = InferTransformLabel(pretransform);
60+
const std::string innerLabel = InferIndexType(pretransform->index);
61+
62+
if (transformLabel.empty()) {
63+
return innerLabel;
64+
}
65+
66+
if (innerLabel.empty() || innerLabel == "UNKNOWN" || innerLabel == "CUSTOM") {
67+
return transformLabel;
68+
}
69+
70+
return transformLabel + "_" + innerLabel;
71+
}
72+
73+
if (dynamic_cast<const faiss::IndexIVFPQ*>(index) != nullptr) {
74+
return "IVF_PQ";
75+
}
76+
77+
if (dynamic_cast<const faiss::IndexIVFScalarQuantizer*>(index) != nullptr) {
78+
return "IVF_SQ";
79+
}
80+
81+
if (dynamic_cast<const faiss::IndexPQ*>(index) != nullptr) {
82+
return "PQ";
83+
}
84+
85+
if (dynamic_cast<const faiss::IndexScalarQuantizer*>(index) != nullptr) {
86+
return "SQ";
87+
}
88+
89+
if (dynamic_cast<const faiss::IndexHNSW*>(index) != nullptr) {
90+
return "HNSW";
91+
}
92+
93+
if (dynamic_cast<const faiss::IndexIVF*>(index) != nullptr) {
94+
return "IVF_FLAT";
95+
}
96+
97+
if (dynamic_cast<const faiss::IndexFlat*>(index) != nullptr) {
98+
return index->metric_type == faiss::METRIC_INNER_PRODUCT ? "FLAT_IP" : "FLAT_L2";
99+
}
100+
101+
return "CUSTOM";
102+
}
103+
104+
} // namespace
105+
106+
FaissIndexWrapper::FaissIndexWrapper(
107+
int dims,
108+
const std::string& indexDescription,
109+
int metric,
110+
const std::string& typeLabel,
111+
const std::string& factoryDescription)
112+
: dims_(dims),
113+
disposed_(false),
114+
type_label_(typeLabel),
115+
factory_description_(factoryDescription.empty() ? indexDescription : factoryDescription) {
24116
if (dims <= 0) {
25117
throw std::invalid_argument("Dimensions must be positive");
26118
}
@@ -29,6 +121,10 @@ FaissIndexWrapper::FaissIndexWrapper(int dims, const std::string& indexDescripti
29121
// Examples: "Flat" -> IndexFlatL2, "IVF100,Flat" -> IndexIVFFlat, "HNSW32" -> IndexHNSW
30122
faiss::MetricType metricType = static_cast<faiss::MetricType>(metric);
31123
index_ = std::unique_ptr<faiss::Index>(faiss::index_factory(dims, indexDescription.c_str(), metricType));
124+
125+
if (type_label_.empty()) {
126+
type_label_ = InferIndexType(index_.get());
127+
}
32128
}
33129

34130
FaissIndexWrapper::FaissIndexWrapper(int dims)
@@ -189,20 +285,23 @@ std::string FaissIndexWrapper::GetIndexType() const {
189285
if (disposed_) {
190286
return "UNKNOWN";
191287
}
288+
return type_label_.empty() ? InferIndexType(index_.get()) : type_label_;
289+
}
192290

193-
if (dynamic_cast<faiss::IndexHNSW*>(index_.get()) != nullptr) {
194-
return "HNSW";
195-
}
196-
197-
if (dynamic_cast<faiss::IndexIVF*>(index_.get()) != nullptr) {
198-
return "IVF_FLAT";
291+
std::string FaissIndexWrapper::GetFactoryDescription() const {
292+
std::lock_guard<std::mutex> lock(mutex_);
293+
if (disposed_) {
294+
return "";
199295
}
296+
return factory_description_;
297+
}
200298

201-
if (dynamic_cast<faiss::IndexFlat*>(index_.get()) != nullptr) {
202-
return index_->metric_type == faiss::METRIC_INNER_PRODUCT ? "FLAT_IP" : "FLAT_L2";
299+
std::string FaissIndexWrapper::GetMetricName() const {
300+
std::lock_guard<std::mutex> lock(mutex_);
301+
if (disposed_) {
302+
return "l2";
203303
}
204-
205-
return "UNKNOWN";
304+
return MetricToString(index_->metric_type);
206305
}
207306

208307
void FaissIndexWrapper::Dispose() {
@@ -244,6 +343,8 @@ std::unique_ptr<FaissIndexWrapper> FaissIndexWrapper::Load(const std::string& fi
244343
auto wrapper = std::make_unique<FaissIndexWrapper>(loaded_index->d);
245344
wrapper->index_.reset(loaded_index);
246345
wrapper->dims_ = loaded_index->d;
346+
wrapper->type_label_ = InferIndexType(loaded_index);
347+
wrapper->factory_description_.clear();
247348

248349
return wrapper;
249350
} catch (const std::exception& e) {
@@ -287,6 +388,8 @@ std::unique_ptr<FaissIndexWrapper> FaissIndexWrapper::FromBuffer(const uint8_t*
287388
auto wrapper = std::make_unique<FaissIndexWrapper>(loaded_index->d);
288389
wrapper->index_.reset(loaded_index);
289390
wrapper->dims_ = loaded_index->d;
391+
wrapper->type_label_ = InferIndexType(loaded_index);
392+
wrapper->factory_description_.clear();
290393

291394
return wrapper;
292395
} catch (const std::exception& e) {

0 commit comments

Comments
 (0)