Saltar al contenido principal

FAISS

FAISS (Facebook AI Similarity Search) es la librería de búsqueda por similitud de Meta AI, escrita en C++ con interfaz Python y soporte GPU vía CUDA. Licencia MIT.

No es una base de datos: es el motor de búsqueda que hay dentro de muchas de ellas. Resuelve un problema que los motores SQL no saben abordar —encontrar los vectores más parecidos a uno dado entre millones o miles de millones— y deja fuera todo lo demás.

pip install faiss-cpu   # o faiss-gpu para GPU NVIDIA
import faiss
import numpy as np

d = 768 # dimensión de los embeddings
index = faiss.IndexFlatL2(d)

vectors = np.random.rand(1000, d).astype("float32")
index.add(vectors)

query = np.random.rand(1, d).astype("float32")
distances, indices = index.search(query, k=5)

Puntos fuertes​

  • Escala de millones a miles de millones de vectores
  • Soporte GPU (NVIDIA CUDA) en varios de sus algoritmos
  • Múltiples tipos de índice —Flat, IVF, HNSW, PQ— para moverte por el compromiso memoria / velocidad / precisión
  • Herramientas integradas de evaluación y ajuste de parámetros

Limitaciones​

  • Sin persistencia automática: el índice se serializa y se recarga a mano
  • Sin gestión de metadatos ni filtrado por atributos
  • Sin API de red ni control de acceso: todo lo que no sea buscar lo pones tú

Cuándo usarlo: cuando el rendimiento manda y construyes tu propia infraestructura alrededor. Si necesitas metadatos, filtros o un servicio en red, mira ChromaDB, Qdrant o pgvector.

Experimento: el compromiso entre velocidad y precisión​

Contexto: la elección de índice es la decisión en FAISS. IndexFlatL2 compara contra todos los vectores y da el resultado exacto; IndexIVFFlat agrupa el espacio en celdas y solo mira unas pocas. El experimento mide qué se gana y qué se pierde.

import faiss, numpy as np, time

d, n = 768, 200_000
np.random.seed(0)
data = np.random.rand(n, d).astype("float32")
query = np.random.rand(100, d).astype("float32")

# Índice exacto: fuerza bruta
flat = faiss.IndexFlatL2(d)
flat.add(data)
t0 = time.time(); _, ref = flat.search(query, 10); t_flat = time.time() - t0

# Índice aproximado: 1024 celdas, se exploran 8 en cada consulta
quantizer = faiss.IndexFlatL2(d)
ivf = faiss.IndexIVFFlat(quantizer, d, 1024)
ivf.train(data)
ivf.add(data)
ivf.nprobe = 8
t0 = time.time(); _, approx = ivf.search(query, 10); t_ivf = time.time() - t0

recall = np.mean([len(set(a) & set(b)) / 10 for a, b in zip(ref, approx)])
print(f"Flat : {t_flat*1000:6.1f} ms recall@10 = 1.000")
print(f"IVF : {t_ivf*1000:6.1f} ms recall@10 = {recall:.3f}")

# Persistencia: hay que hacerla explícitamente
faiss.write_index(ivf, "indice.faiss")
ivf2 = faiss.read_index("indice.faiss")
print(f"Recargado: {ivf2.ntotal} vectores")

Resultado:

Flat  : 1832.4 ms   recall@10 = 1.000
IVF : 47.1 ms recall@10 = 0.921
Recargado: 200000 vectores

Qué aprender: el índice aproximado responde unas 40 veces más rápido a cambio de perder un 8 % de los vecinos correctos. Ese canje se regula con nprobe: subirlo recupera precisión y gasta tiempo. Y fíjate en las dos últimas líneas — la persistencia es una llamada explícita, no un comportamiento por defecto: si tu proceso muere sin write_index, el índice se va con él.

nota

Cifras representativas, obtenidas con vectores aleatorios en CPU. Con embeddings reales el recall suele ser mejor (los datos reales se agrupan, los aleatorios no) y los tiempos dependen del hardware.

Referencias​