Source code for indexers.faiss_indexer

import sys
from typing import Any, Dict, List, Tuple

import faiss
import numpy as np

from .base import BaseIndexer


[docs] class FaissIndexer(BaseIndexer): def __init__(self, dimension: int, index_type: str = "Flat"): """ index_type can be: - "Flat": IndexFlatL2 - "IVF": IndexIVFFlat (parameterized during build) - "HNSW": IndexHNSWFlat - "SQ8": Scalar Quantizer (8-bit) - "PQ": Product Quantizer - Or any FAISS factory string (e.g., "IVF100,PQ8", "OPQ16,PQ16") """ super().__init__(f"FAISS-{index_type.upper()}", dimension) self.index_type = index_type.upper() self.metadata = [] self.dimension = dimension self.index = None if self.index_type == "FLAT": self.index = faiss.IndexFlatL2(dimension) elif self.index_type == "HNSW": self.index = faiss.IndexHNSWFlat(dimension, 32) elif self.index_type == "SQ8": self.index = faiss.index_factory(dimension, "SQ8") elif self.index_type == "PQ": # Deferred initialization for PQ to adjust parameters based on data size pass elif self.index_type != "IVF": # Fallback to factory string self.index = faiss.index_factory(dimension, index_type)
[docs] def build_index(self, embeddings: List[List[float]], metadata: List[Dict[str, Any]]) -> None: vectors = np.array(embeddings, dtype=np.float32) n = len(vectors) if self.index_type == "IVF": # Dynamically determine nlist based on data size nlist = max(1, min(100, n // 4)) quantizer = faiss.IndexFlatL2(self.dimension) self.index = faiss.IndexIVFFlat(quantizer, self.dimension, nlist) self.index.train(vectors) elif self.index_type == "PQ" and self.index is None: # Dynamically determine PQ parameters # m: number of sub-vectors m = 8 if self.dimension % 8 == 0 else 4 # nbits: number of bits per sub-vector. # Default is 8 (256 clusters), but we need n >= 2^nbits nbits = 8 if n < 256: nbits = int(np.floor(np.log2(n))) if n > 1 else 1 # Standard PQ factory might not like very low nbits, # but let's try to keep it at least 4 if possible or fallback to Flat nbits = max(1, nbits) if nbits >= 4: self.index = faiss.index_factory(self.dimension, f"PQ{m}x{nbits}") else: # Too little data for PQ, fallback to Flat self.index = faiss.IndexFlatL2(self.dimension) if not self.index.is_trained: self.index.train(vectors) elif self.index is not None and not self.index.is_trained: self.index.train(vectors) if self.index is not None: self.index.add(vectors) self.metadata.extend(metadata)
[docs] def save_index(self, path: str): """Save index to disk.""" faiss.write_index(self.index, path)
[docs] def load_index(self, path: str): """Load index from disk.""" self.index = faiss.read_index(path) self.dimension = self.index.d
[docs] def search( self, query_embedding: List[float], top_k: int = 5 ) -> List[Tuple[Dict[str, Any], float]]: if self.index is None: return [] query_vector = np.array([query_embedding], dtype=np.float32) distances, indices = self.index.search(query_vector, top_k) results = [] for dist, idx in zip(distances[0], indices[0]): if idx != -1 and idx < len(self.metadata): results.append((self.metadata[idx], float(dist))) return results
[docs] def get_size(self) -> int: if self.index is None: return 0 # For quantized indexes, ntotal * code_size gives better estimation try: codes = getattr(self.index, "codes", None) if codes is not None: return codes.nbytes + sys.getsizeof(self.metadata) except Exception: pass # Fallback to general estimation vectors_size = self.index.ntotal * self.dimension * 4 return vectors_size + sys.getsizeof(self.metadata)