FAISS
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page target FAISS 1.15.1 (the faiss-cpu / faiss-gpu PyPI packages) with Python 3.12 and
NumPy. FAISS (Facebook AI Similarity Search) is not a database: it is a C++ library with Python bindings that
implements similarity-search and clustering algorithms over dense vectors, with no server process, no
network protocol and no persistence format beyond a single file. The DocsAssistant scenario uses it as a
local, disposable index alongside the pgvector-backed system of record: an IndexFlatIP ground truth, an
IndexHNSWFlat(768, 16) and an IndexIVFFlat with nlist = 64, persisted to docsassistant.faiss.
Architecture: Index, index_factory and the core operations
Every FAISS index is a subclass of the abstract Index type and shares the same small surface: it is
constructed either directly (faiss.IndexFlatIP(d)) or through index_factory, which parses a short string
describing a pipeline of transforms and a coarse/fine quantizer (for example "IVF64,Flat" or
"PCA256,IVF64,PQ16"). Indexes that need statistics about the data distribution (IVF*, PQ*) must be
train-ed on a representative sample before vectors can be added; flat indexes need no training.
import faiss
import numpy as np
d = 768 # matches nomic-embed-text
n = 10_000
rng = np.random.default_rng(seed=42)
vectors = rng.normal(size=(n, d)).astype("float32")
faiss.normalize_L2(vectors) # cosine via inner product on unit vectors
index = faiss.IndexFlatIP(d)
print(index.is_trained) # True -- Flat needs no training
index.add(vectors)
print(index.ntotal) # 10000
query = vectors[:3]
k = 5
distances, ids = index.search(query, k) # ids: row position, not a stable external id
By default add assigns sequential row ids 0..ntotal-1; IndexIDMap (or IndexIDMap2, which also supports
removal by id) wraps any index so add_with_ids can attach the caller’s own ids — in the DocsAssistant
scenario, the chunks.id primary key — instead of relying on insertion order:
inner = faiss.IndexFlatIP(d)
index = faiss.IndexIDMap(inner)
chunk_ids = np.arange(1000, 1000 + n, dtype="int64") # chunks.id values
index.add_with_ids(vectors, chunk_ids)
distances, ids = index.search(query, k) # ids are now real chunk_ids
search always returns exactly k neighbours (padded with -1 ids if ntotal < k). When the useful notion
is "every neighbour within a distance" rather than a fixed count, range_search takes a radius instead of a
k and returns a ragged result via lims/distances/labels arrays:
radius = 0.75 # inner-product threshold on unit-normalised vectors
lims, range_distances, range_ids = index.range_search(query, radius)
for i in range(len(query)):
hits = range_ids[lims[i]:lims[i + 1]]
print(f"query {i}: {len(hits)} neighbours within radius")
Persistence
An index is serialised to (and read back from) a single file with write_index / read_index — there is no
separate schema or metadata file, so anything beyond the raw vectors and ids must be stored elsewhere (see
What FAISS does not give you):
faiss.write_index(index, "docsassistant.faiss")
loaded = faiss.read_index("docsassistant.faiss")
assert loaded.ntotal == index.ntotal
CPU vs. GPU
The same index classes exist in CPU and GPU variants. faiss-cpu is the default install; faiss-gpu (or
faiss-gpu-cuvs for the cuVS-accelerated build) requires a CUDA-capable GPU and moves an index onto it with
index_cpu_to_gpu and back with index_gpu_to_cpu (a GPU index cannot be passed to write_index
directly). This is worthwhile for training and bulk search over large IVF/PQ indexes but rarely needed for
a corpus the size of DocsAssistant’s documentation chunks. The sketch below moves an empty IndexFlatIP to the
GPU, fills and queries it there, and copies it back to the CPU to persist it:
res = faiss.StandardGpuResources()
cpu_flat = faiss.IndexFlatIP(d)
gpu_index = faiss.index_cpu_to_gpu(res, 0, cpu_flat) # device 0
gpu_index.add(vectors) # a plain Flat index, so add() assigns ids 0..n-1
distances, ids = gpu_index.search(query, k)
cpu_index = faiss.index_gpu_to_cpu(gpu_index) # bring back for write_index
faiss.write_index(cpu_index, "docsassistant_flat.faiss")
See the official Getting started, Faiss indexes and Faiss on the GPU wiki pages.
Composite indexes
FAISS indexes compose: a factory string chains a vector transform, a coarse quantizer and (optionally) a codec into one object that trains and searches as a unit.
-
IndexPreTransform— applies a transform (typicallyPCAMatrix, via"PCA256,…"in a factory string) to every vector before it reaches the wrapped index, both onaddand onsearch. Useful to cut dimensionality before anIVF/PQstage; see Quantization & Compression for the recall trade-off. -
IndexRefineFlat— wraps a fast, approximate (and often quantized) index with an exact re-ranking pass: the approximate index over-fetchesk * refine_factorcandidates, thenIndexFlatL2/IndexFlatIPcomputes exact distances on just those candidates to reorder the final top-k. This recovers most of the recall lost to quantization at a fraction of exact search’s cost. -
IndexShards— splits a large collection across several sub-indexes (for example one per shard of a corpus too big to fit — or train efficiently — in one process) and fans a query out to all of them, merging results. It is a scale-out mechanism, not a durability one: there is still no replication, no network protocol and no coordinator.
# PCA to 256 dims, IVF with 64 lists, flat inner-product refinement
index = faiss.index_factory(768, "PCA256,IVF64,Flat,RFlat", faiss.METRIC_INNER_PRODUCT)
index.train(vectors)
index.add(vectors)
index.k_factor = 4 # RFlat: fetch 4x candidates before exact re-ranking
distances, ids = index.search(query, k)
Index-properties table
Approximate memory per vector for a d = 768-dimensional embedding (float32 baseline: 768 * 4 = 3072
bytes), ignoring per-index bookkeeping overhead:
| Index | Bytes / vector (d = 768) | Notes |
|---|---|---|
|
3072 B |
Exact, no training, linear scan. |
|
3072 B + small ID/list overhead |
Approximate via |
|
|
Same memory formula used throughout ANN Indexes: Flat, IVF, HNSW and Beyond; graph edges dominate the overhead over Flat. |
|
~16 B + list overhead |
See Quantization & Compression for the encode/decode arithmetic. |
|
256*4 = 1024 B + overhead |
Reduced dimensionality lowers both storage and distance-computation cost. |
Benchmarking Flat vs. IVF vs. HNSW
The snippet below builds the three DocsAssistant index types — the Flat (IndexFlatIP) ground truth, an
IVF64,Flat (IndexIVFFlat with nlist = 64) and an HNSW16 (IndexHNSWFlat(768, 16)) — over the same
synthetic 768-dimensional dataset, and prints build time, query time and recall@10 against the ground truth.
No output is reproduced here on purpose: the numbers depend entirely on the machine, the FAISS build (CPU/GPU,
SIMD level) and the data distribution, so run it on your own hardware and embeddings.
import time
import numpy as np
import faiss
d, n, n_queries, k = 768, 50_000, 200, 10
rng = np.random.default_rng(seed=7)
data = rng.normal(size=(n, d)).astype("float32")
faiss.normalize_L2(data)
queries = rng.normal(size=(n_queries, d)).astype("float32")
faiss.normalize_L2(queries)
def build_and_time(factory: str):
index = faiss.index_factory(d, factory, faiss.METRIC_INNER_PRODUCT)
t0 = time.perf_counter()
if not index.is_trained:
index.train(data)
index.add(data)
build_s = time.perf_counter() - t0
if factory.startswith("IVF"):
index.nprobe = 8 # visit 8 of the 64 lists (default is 1)
t0 = time.perf_counter()
_, ids = index.search(queries, k)
query_s = time.perf_counter() - t0
return index, ids, build_s, query_s
ground_truth, gt_ids, _, _ = build_and_time("Flat")
def recall_at_k(candidate_ids: np.ndarray) -> float:
hits = sum(
len(set(candidate_ids[i]) & set(gt_ids[i]))
for i in range(n_queries)
)
return hits / (n_queries * k)
for factory in ("Flat", "IVF64,Flat", "HNSW16"):
_, ids, build_s, query_s = build_and_time(factory)
recall = 1.0 if factory == "Flat" else recall_at_k(ids)
print(f"{factory:12s} build={build_s:6.3f}s query={query_s:6.4f}s recall@{k}={recall:.3f}")
See the official index factory and Faster search wiki pages.
The shape to expect — the same trade-offs described in ANN Indexes: Flat, IVF, HNSW and Beyond — is:
-
Flathas the cheapest build (no training, no graph) and the slowest queries, and its recall is 1.0 by construction because it is the ground truth. -
IVF64,Flatspends extra build time on k-means training, then answers queries faster thanFlatbecause it scans onlynprobeof the 64 lists; its recall rises towards 1.0 asnprobegrows and falls when a true neighbour sits in an unvisited list. -
HNSW16has the slowest build (it inserts every vector into a graph) and usually the fastest queries at high recall, tuned at query time withindex.hnsw.efSearch, at the cost of extra memory for the graph edges.
Synthetic Gaussian vectors are a pessimistic case for approximate indexes, because they have none of the cluster structure real embeddings have, so expect recall on real DocsAssistant embeddings to be higher at the same settings. Always re-measure on your own hardware and your own embedding distribution before trusting any number.
What FAISS does not give you
FAISS solves nearest-neighbour search and nothing else. It has no concept of:
-
Metadata / filtering — an index stores vectors and integer ids, not JSON payloads or columns; filtered search means maintaining a separate id → metadata mapping (a dict, a SQLite table, a pandas DataFrame) and joining results yourself, or pre/post-filtering candidate ids.
-
Transactions —
add/remove_ids/write_indexare not atomic together; a crash between updating the index and updating your metadata store leaves them inconsistent, with no rollback. -
Updates — most indexes (notably
IVF*) support removal only throughremove_ids, and in-place vector updates are commonly implemented as remove-then-re-add; there is noUPDATEprimitive. -
Access control, networking, replication, durability guarantees — there is no server, no auth, no client/server protocol; every process that needs the index loads the whole file into its own memory.
This gap is exactly why real systems that need vector search and metadata, transactions or concurrent
access either pair FAISS with an external store (as DocsAssistant keeps pgvector as the system of record and
uses FAISS only as a disposable side index, with IndexFlatIP as the exact ground truth to measure the ANN
indexes against) or embed vector search directly inside a database engine. sqlite-vss, an early SQLite
extension that embedded FAISS itself as its search backend, is the historical example of this path; it has
since been superseded by sqlite-vec, which does not vendor FAISS at all: its vec0 virtual tables run exact,
brute-force KNN inside SQLite (no ANN index as of 0.1.x), trading search speed on large collections for
transactional, single-file SQL semantics that FAISS itself does not provide (see
Vector Search in SQLite).
For a disposable, single-process store that does not need any of the above, LangChain’s FAISS vector store
wraps exactly this library plus a lightweight in-memory docstore for metadata:
from langchain_community.vectorstores import FAISS
from langchain_ollama import OllamaEmbeddings
embeddings = OllamaEmbeddings(model="nomic-embed-text")
store = FAISS.from_texts(
texts=["HNSW indexes...", "pgvector basics...", "RAG with Spring AI..."],
embedding=embeddings,
metadatas=[
{"source_path": "docs/vector-rag/hnsw.adoc", "component": "vector-rag"},
{"source_path": "docs/vector-rag/pgvector.adoc", "component": "vector-rag"},
{"source_path": "docs/spring-ai/rag.adoc", "component": "spring-ai"},
],
)
store.save_local("docsassistant_faiss_index")
reloaded = FAISS.load_local(
"docsassistant_faiss_index", embeddings, allow_dangerous_deserialization=True
)
hits = reloaded.similarity_search("how do I speed up similarity search", k=5)
See the official LangChain FAISS vector store page.
Workflow
In LangChain and Spring AI
LangChain
The FAISS vector store shown above is one of the concrete implementations behind LangChain’s generic
vector-store abstraction — construction, similarity search, persistence and retriever conversion all go
through the same interface documented in
Vector stores; swapping
FAISS for PGVector later is a one-line change to the constructor, not a rewrite of the surrounding agent
or retrieval code.
Spring AI
There is no dedicated Spring AI FAISS starter: FAISS ships no server and no client/server wire protocol, so
it does not fit Spring AI’s VectorStore auto-configuration model the way pgvector, Redis or Qdrant do. A
Spring application that wants FAISS specifically would need to shell out to Python or call it through a JNI
binding; in practice, projects on the JVM either use LangChain4j against a database-backed store or pick one
of Spring AI’s supported VectorStore implementations instead. See
VectorStore API for the
general abstraction and the list of stores it does support out of the box.
Related pages
-
ANN Indexes: Flat, IVF, HNSW and Beyond — the Flat/IVF/HNSW algorithms and parameters this page benchmarks and persists.
-
Quantization & Compression —
IndexPQ/IndexIVFPQand theIndexPreTransform/PCA arithmetic referenced above. -
Similarity Metrics & Semantic Search —
METRIC_L2/METRIC_INNER_PRODUCTalongside the other engines' metric names. -
Vector Indexing & HNSW — a server-based HNSW implementation to contrast with FAISS’s embedded one.
-
Choosing the Right Database — when an embedded library like FAISS is the right choice versus a purpose-built vector database.