Обзор векторных баз данных: Chroma, FAISS, Qdrant — хранение и поиск векторов
Embeddings бесполезны, если вы не можете быстро искать среди миллионов векторов.
В предыдущей теме мы научились превращать текст в векторы. Теперь нужно понять, как их хранить и искать. Можно ли хранить embeddings в обычном списке и перебирать все векторы при поиске? Для 10 документов — да. Для 100 000 — нет: линейный поиск будет слишком медленным.
Векторная база данных — это специализированное хранилище, оптимизированное для быстрого поиска ближайших векторов (nearest neighbor search). Она использует индексные структуры (HNSW, IVF, Flat), чтобы находить похожие векторы за миллисекунды даже среди миллионов записей.
В RAG векторная БД — это «память» системы: здесь хранятся все чанки документов в виде embeddings.
ChromaDB — самый простой вариант для начала. Это встраиваемая (embedded) база данных — она работает внутри вашего Python-процесса, не требует отдельного сервера.
pip install chromadbКоллекция в Chroma — это аналог таблицы в реляционной БД. В одну коллекцию вы добавляете документы с их embeddings.
import chromadb
from chromadb.utils import embedding_functions
# Создаём клиент — документы сохраняются на диск
client = chromadb.PersistentClient(path="./chroma_db")
# Создаём embedding-функцию
embedder = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-small"
)
# Создаём или получаем коллекцию
collection = client.get_or_create_collection(
name="hr_documents",
embedding_function=embedder,
metadata={"hnsw:space": "cosine"} # Метрика сравнения
)
# Добавляем документы
collection.add(
documents=[
"Отпуск оформляется через HR-портал за 2 недели",
"Больничный лист нужно отнести в бухгалтерию",
"Удалённая работа — до 3 дней в неделю"
],
ids=["hr_001", "hr_002", "hr_003"],
metadatas=[
{"category": "отпуск", "department": "HR"},
{"category": "больничный", "department": "бухгалтерия"},
{"category": "удалёнка", "department": "HR"}
]
)Каждый документ должен иметь уникальный id. Поле metadatas — опциональное, позволяет хранить дополнительную информацию (категорию, дату, автора), которую потом можно использовать для фильтрации.
results = collection.query(
query_texts=["Как получить отпуск?"],
n_results=2,
where={"department": "HR"} # Фильтр по метаданным
)
for doc, metadata, distance in zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0]
):
print(f"Документ: {doc}")
print(f"Метаданные: {metadata}")
print(f"Расстояние: {distance:.4f}")
print("---")ChromaDB автоматически создаёт embedding запроса через embedding_function и ищет ближайшие документы. Поле distances содержит расстояние до каждого найденного документа — чем меньше, тем лучше.
Chroma поддерживает фильтрацию при поиске:
# Только документы категории "отпуск"
results = collection.query(
query_texts=["Когда можно уйти в отпуск?"],
n_results=1,
where={"category": "отпуск"}
)
# Комбинированный фильтр
results = collection.query(
query_texts=["Документы HR"],
n_results=5,
where={
"$and": [
{"department": "HR"},
{"category": {"$ne": "удалёнка"}}
]
}
)Операторы: $eq (равно), $ne (не равно), $gt (больше), $lt (меньше), $and, $or.
FAISS (Facebook AI Similarity Search) — библиотека от Meta для эффективного поиска векторов. В отличие от Chroma, это не полноценная БД с метаданными, а специализированный индекс.
pip install faiss-cpu
# или для GPU:
# pip install faiss-gpuIndexFlatL2 — простой плоский индекс с поиском по евклидову расстоянию. Точный (находит абсолютный ближайший), но медленный на больших данных:
import faiss
import numpy as np
# Размерность должна совпадать с embedding-моделью
dimension = 384
# Создаём плоский индекс (евклидово расстояние)
index = faiss.IndexFlatL2(dimension)
# Добавляем векторы
embeddings = np.random.random((100, dimension)).astype("float32")
index.add(embeddings)
print(f"Количество векторов в индексе: {index.ntotal}")IndexFlatIP — поиск по скалярному произведению (inner product). Подходит для нормализованных векторов (эквивалентно косинусному сходству):
# Индекс по скалярному произведению
index_ip = faiss.IndexFlatIP(dimension)
# Нормализуем векторы (обязательно для IP)
faiss.normalize_L2(embeddings)
index_ip.add(embeddings)IndexIVFFlat — индекс с инвертированным файлом (Inverted File with Flat storage). Быстрее Flat на больших данных, но приближённый поиск (может пропустить ближайший):
# IVF-индекс
n_clusters = 10 # Количество кластеров (квантизация)
quantizer = faiss.IndexFlatL2(dimension)
index_ivf = faiss.IndexIVFFlat(quantizer, dimension, n_clusters)
# IVF нужно обучить на данных перед добавлением
index_ivf.train(embeddings)
index_ivf.add(embeddings)
# Для поиска нужно задать nprobe (сколько кластеров проверять)
index_ivf.nprobe = 3
# Поиск
query = np.random.random((1, dimension)).astype("float32")
distances, indices = index_ivf.search(query, k=5)Больше nprobe — точнее, но медленнее. Меньше — быстрее, но менее точно.
# Поиск k ближайших
k = 3
distances, indices = index.search(query, k)
print(f"Индексы ближайших: {indices[0]}")
print(f"Расстояния: {distances[0]}")FAISS возвращает индексы векторов в массиве — вам нужно самостоятельно связать их с оригинальными документами (например, хранить список документов параллельно).
# Сохраняем индекс на диск
faiss.write_index(index, "my_index.faiss")
# Загружаем
loaded_index = faiss.read_index("my_index.faiss")Qdrant — более мощная система, поддерживает как локальный режим, так и облачный. Удобна для продакшена.
pip install qdrant-clientfrom qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# Локальный клиент (in-memory)
client = QdrantClient(":memory:")
# Создаём коллекцию
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(size=384, distance=Distance.COSINE)
)
# Добавляем точки
points = [
PointStruct(
id=1,
vector=[0.1] * 384, # Ваш embedding
payload={"text": "Отпуск через HR-портал", "category": "HR"}
),
PointStruct(
id=2,
vector=[0.2] * 384,
payload={"text": "Больничный в бухгалтерию", "category": "finance"}
)
]
client.upsert(collection_name="documents", points=points)from qdrant_client.models import Filter, FieldCondition, MatchValue
# Поиск с фильтром
results = client.search(
collection_name="documents",
query_vector=[0.15] * 384, # Embedding вопроса
query_filter=Filter(
must=[FieldCondition(key="category", match=MatchValue(value="HR"))]
),
limit=2
)
for hit in results:
print(f"ID: {hit.id}, Score: {hit.score}, Payload: {hit.payload}")score в Qdrant — это схожесть (чем больше, тем лучше), в отличие от Chroma/FAISS, где возвращается расстояние (чем меньше, тем лучше).
| Характеристика | ChromaDB | FAISS | Qdrant |
|---|---|---|---|
| Сложность настройки | Минимальная | Средняя | Средняя |
| Встроенные embeddings | Да | Нет | Нет |
| Метаданные и фильтрация | Да | Нет (только ID) | Да |
| Облачный режим | Нет (только локально) | Нет | Да |
| Масштабируемость | Средняя (до ~1M) | Высокая (до ~1B) | Высокая |
| Для прототипа | Отлично | Хорошо | Хорошо |
| Для продакшена | Средне | Средне | Отлично |
ChromaDB — лучший выбор для прототипа и небольших проектов. Прост в установке, есть встроенная embedding-функция, удобные метаданные. Если у вас до 100 000 документов и не нужен облачный режим — Chroma идеален.
FAISS — когда нужна максимальная скорость поиска на очень больших объёмах (миллионы векторов). Но нужно самостоятельно управлять метаданными и embedding-логикой.
Qdrant — для продакшен-систем с облачным развёртыванием. Поддержка фильтрации, масштабирования, мониторинга. Если ваш RAG будет работать с тысячами пользователей — Qdrant даст нужную инфраструктуру.
Вот полный пример индексации и поиска с использованием ChromaDB:
import chromadb
from chromadb.utils import embedding_functions
# 1. Инициализация
client = chromadb.PersistentClient(path="./rag_store")
embedder = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-small"
)
collection = client.get_or_create_collection(
name="company_docs",
embedding_function=embedder
)
# 2. Индексация документов
documents = [
"Политика компании по удалённой работе обновлена в январе 2026",
"Процедура оформления отпуска: заявление за 2 недели через HR-портал",
"Инструкция по охране труда: носить каску на производстве обязательно",
"Порядок сдачи больничных: лист нетрудоспособности в бухгалтерию за 3 дня"
]
collection.add(
documents=documents,
ids=[f"doc_{i}" for i in range(len(documents))]
)
# 3. Поиск
query = "Что делать если я заболел?"
results = collection.query(query_texts=[query], n_results=2)
for doc, dist in zip(results["documents"][0], results["distances"][0]):
print(f"[dist={dist:.4f}] {doc}")Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.