Qdrant против Chroma: что выгоднее и когда
Обе базы хранят эмбеддинги и ищут ближайших соседей, и на первых десятках тысяч фрагментов разницы вы не почувствуете. Она вылезает позже: в счёте за сервер, в поведении фильтров и в вопросе «как снять бэкап, не останавливая сервис». Разберём, что выгоднее — Qdrant или Chroma, — с расчётами, конфигами и скриптом переезда.
Содержание
- Разное происхождение: библиотека против сервера
- Порог входа: сколько кода до первого поиска
- Деньги: во сколько обойдётся миллион векторов
- Фильтры по метаданным: где разница перестаёт быть вкусовой
- Эксплуатация: бэкап, авторизация, обновления, метрики
- Кому что брать и как переехать с Chroma на Qdrant
- Какой сервер под векторную базу брать в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Разное происхождение: библиотека против сервера
Различие не в скорости, а в происхождении. Chroma родилась встраиваемой: строчка import chromadb — и хранилище живёт внутри вашего Python-процесса, складывая данные в SQLite и файлы индекса hnswlib рядом. Серверный режим появился позже как обёртка над той же схемой. В версии 1.0 (апрель 2025) ядро переписали с Python на Rust, но модель осталась: одна нода, один процесс, отдельный индекс в памяти на каждую коллекцию.
Qdrant с первого релиза — сетевой сервер на Rust со своим форматом хранения, сегментами, оптимизаторами, шардированием и репликацией. Встраиваемого режима у него нет: локальный режим клиента — отдельная упрощённая реализация, а не тот же движок.
Отсюда растёт всё остальное:
| Критерий | Chroma | Qdrant |
|---|---|---|
| Природа | встраиваемая библиотека, доросшая до сервера | сетевой сервер с первого дня |
| Язык ядра | Rust с версии 1.0, обвязка на Python | Rust |
| Порты по умолчанию | 8000 (HTTP) | 6333 (HTTP), 6334 (gRPC), 6335 (кластер) |
| Веб-интерфейс в комплекте | нет | /dashboard на 6333 |
| Свой эмбеддер «из коробки» | да, ONNX MiniLM-L6-v2 | нет, опционально FastEmbed в клиенте |
| Квантование векторов | нет | скалярное int8, бинарное, продуктовое |
| Векторы и payload на диск | нет, индекс держится в памяти | on_disk, on_disk_payload, mmap |
| Шардирование и репликация | нет, одна нода | есть, консенсус на Raft |
| Авторизация | токен через переменные окружения | api-key, отдельный read-only ключ, JWT-роли |
Проверка, что сервис жив:
curl -s http://127.0.0.1:8000/api/v2/heartbeat
# {"nanosecond heartbeat":1772439012345678}
curl -s http://127.0.0.1:6333/
# {"title":"qdrant - vector search engine","version":"1.19.0","commit":"..."}
Порог входа: сколько кода до первого поиска
Здесь Chroma выигрывает с отрывом: рабочий пример — пять строк, и эмбеддинги она посчитает сама.
import chromadb
client = chromadb.PersistentClient(path="/var/lib/chroma")
col = client.get_or_create_collection("docs")
col.add(ids=["1", "2"], documents=["Аренда сервера в Лондоне", "Оплата криптой"])
print(col.query(query_texts=["сервер в UK"], n_results=2))
Ни модели, ни размерности, ни сервиса: при первом вызове Chroma скачает ONNX-версию all-MiniLM-L6-v2 в ~/.cache/chroma/onnx_models/ и посчитает 384-мерные векторы на CPU. Здесь же первая ловушка — модель по умолчанию англоязычная: на русском корпусе она проигрывает multilingual-e5-base и bge-m3. Про выбор эмбеддера — отдельный разбор.
Вторая ловушка встречает раньше, на старых системах:
RuntimeError: Your system has an unsupported version of sqlite3.
Chroma requires sqlite3 >= 3.35.0.
Chroma завязана на свежий SQLite и на Debian 11 ломается сразу. Обходят подменой модуля через pysqlite3-binary, правильнее — система поновее.
У Qdrant порог выше: вектор приносите сами, коллекцию заводите явно, размерность и метрику задаёте руками.
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://127.0.0.1:6333", api_key="СЕКРЕТ")
client.create_collection(
"docs",
vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE),
on_disk_payload=True,
)
client.upsert("docs", points=[
models.PointStruct(id=1, vector=vec, payload={"lang": "ru", "year": 2026}),
])
Ошибётесь с размерностью — получите отказ на уровне API, а не мусор в выдаче:
{"status":{"error":"Wrong input: Vector dimension error: expected dim: 768, got 384"}}
Chroma строга так же, но исключением клиента: chromadb.errors.InvalidDimensionException: Embedding dimension 384 does not match collection dimensionality 768. Размерность обе фиксируют намертво — сменить эмбеддер без пересоздания коллекции нельзя.
Qdrant тоже даёт «встраиваемый» режим — QdrantClient(":memory:") или QdrantClient(path="./qdrant_local") — без сервера. Но это не движок на Rust, а Python-реализация с полным перебором: для тестов, не для прода.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть QdrantДеньги: во сколько обойдётся миллион векторов
Здесь «что выгоднее» перестаёт быть абстракцией: вопрос упирается в тариф.
Базовая оценка одинакова для обеих. Документация Qdrant даёт формулу число векторов × размерность × 4 байта × 1,5: четыре байта — это float32, множитель закрывает граф HNSW и служебные структуры. Сам граф при стандартном m: 16 — порядка 128 байт на вектор, десятые доли гигабайта на миллион точек. Миллион фрагментов на модели с 768 измерениями: 1 000 000 × 768 × 4 ≈ 3 ГБ сырых векторов, с накладными — около 4,6 ГБ.
Chroma отсюда никуда не денется. Квантования нет, векторы лежат во float32, загруженный сегмент коллекции живёт в памяти целиком. Смягчение одно — политика вытеснения:
CHROMA_SEGMENT_CACHE_POLICY=LRU
CHROMA_MEMORY_LIMIT_BYTES=3000000000
Она позволяет не держать все коллекции сразу, но платой становится перезагрузка индекса с диска: первый запрос после вытеснения задумывается. Внутри одной большой коллекции не помогает.
У Qdrant три рычага. Скалярное квантование в int8 ужимает векторную часть вчетверо — те же 3 ГБ превращаются в 768 МБ, которые закрепляются в памяти, а оригиналы уезжают на диск:
quantization_config:
scalar:
type: int8
quantile: 0.99
always_ram: true
storage:
on_disk_payload: true
Второй рычаг — hnsw_config.on_disk: true: граф читается через mmap, критична скорость NVMe, а не объём RAM. Третий — on_disk у векторов. Честная цена: квантование теряет точность, а компенсирующий пересчёт по оригиналам (rescore: true) — это лишние чтения с диска, на SATA-SSD схема разочарует.
Расчёт по формуле, а не замер: реальные цифры сдвинут размер payload, m и число сегментов.
| Коллекция | Векторы во float32 | Chroma: нужно RAM | Qdrant с int8: нужно RAM |
|---|---|---|---|
| 100 тыс. × 384 | ~0,15 ГБ | 2 ГБ хватает | 2 ГБ хватает |
| 1 млн × 768 | ~3 ГБ | 8 ГБ | 4 ГБ |
| 5 млн × 1024 | ~20,5 ГБ | 32–64 ГБ | 8–16 ГБ |
Вывод денежный: до нескольких сотен тысяч векторов разница в стоимости сервера нулевая, выбирайте по удобству. От миллиона Qdrant позволяет жить на тарифе ступенью ниже — это и есть «выгоднее». Подробнее — в материале о том, сколько RAM нужно для Qdrant.
Фильтры по метаданным: где разница перестаёт быть вкусовой
Чистый поиск обе базы делают одинаково — это один и тот же HNSW. Разница появляется, когда добавляется условие: только документы этого клиента, только за 2026 год.
У Chroma метаданные живут в SQLite. Фильтр where превращается в набор разрешённых идентификаторов, который передаётся в hnswlib функцией-предикатом: обход идёт по всем узлам, а не прошедшие условие кандидаты отбрасываются.
col.query(
query_texts=["правила возврата"],
n_results=10,
where={"$and": [{"lang": {"$eq": "ru"}}, {"year": {"$gte": 2025}}]},
where_document={"$contains": "возврат"},
)
Пока фильтр отсекает малую часть коллекции, всё хорошо. Когда он оставляет доли процента, обход блуждает по отвергаемым узлам: просите десять результатов — получаете три, иногда со строкой в логе Number of requested results 10 is greater than number of elements in index 3, updating n_results = 3. Формально ошибки нет, но качество уже не то.
Qdrant решает это структурой индекса. Payload-индекс добавляет в граф HNSW связи внутри каждой группы значений, и поиск с фильтром остаётся поиском по графу. Плюс порог full_scan_threshold (в килобайтах, по умолчанию 10000): если подмножество мельче, движок переключается на полный перебор — для маленьких выборок это и быстрее, и точнее.
curl -X PUT http://127.0.0.1:6333/collections/docs/index \
-H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
-d '{"field_name":"tenant_id","field_schema":{"type":"keyword","is_tenant":true}}'
Флаг is_tenant: true заставляет Qdrant группировать точки одного арендатора рядом на диске.
Тут же вылезает ограничение Chroma. Изоляции клиентов в ней добиваются коллекцией на клиента, а каждая коллекция — свой индекс hnswlib, занимающий память отдельно. Двести клиентов по пятьдесят тысяч фрагментов дают двести индексов и расход выше, чем у одной коллекции на десять миллионов точек с полем tenant_id.
Эксплуатация: бэкап, авторизация, обновления, метрики
Этап, который в сравнениях пропускают, а он определяет, сколько вы будете спать.
Бэкап. Qdrant умеет снапшоты по API на живой базе: POST /collections/docs/snapshots кладёт файл в каталог снапшотов, полный снимок хранилища — POST /snapshots, восстановление — загрузкой файла обратно. У Chroma снапшот-API нет: бэкап — это копия каталога данных, файл chroma.sqlite3 плюс подкаталоги с UUID в именах, где лежат data_level0.bin, header.bin, length.bin, link_lists.bin. Копия на работающем сервисе консистентность не гарантирует: SQLite и файлы индекса пишутся независимо. Честный бэкап Chroma требует простоя.
Авторизация. У Qdrant ключ задаётся переменной и проверяется на всех эндпоинтах, второй ключ пускает только на чтение, дальше есть JWT-роли с ограничением по коллекциям.
QDRANT__SERVICE__API_KEY=<секрет>
QDRANT__SERVICE__READ_ONLY_API_KEY=<второй секрет>
У Chroma токен-авторизация тоже есть, но с миной: имена переменных и классов провайдеров менялись между ветками. Старые CHROMA_SERVER_AUTH_CREDENTIALS и chromadb.auth.token.TokenAuthServerProvider заменены на новые.
CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider
CHROMA_SERVER_AUTHN_CREDENTIALS=<токен>
CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=X-Chroma-Token
Скопируете конфиг из руководства двухлетней давности — сервер поднимется без единой жалобы и пустит всех подряд: неизвестные переменные он игнорирует. Проверять надо запросом без токена: отвечает данными — защиты нет. Порт наружу не открывайте: ufw deny 8000/tcp, ufw deny 6333/tcp, доступ через Nginx с TLS.
Обновления. Chroma 1.0 убрала эндпоинты /api/v1, и старый клиент против нового сервера получает:
{"error":"Unimplemented","message":"The v1 API is deprecated. Please use /v2 apis"}
Клиент и сервер придётся поднимать вместе, а переход с 0.4 на 0.5 требовал ещё и миграции данных. У Qdrant формат в пределах 1.x совместим, но обновляться надо по одной минорной версии и со снапшотом.
Наблюдаемость. Qdrant отдаёт /metrics в формате Prometheus — число точек, статусы коллекций, задержки. У Chroma эндпоинта метрик нет, телеметрия идёт экспортом в OpenTelemetry-коллектор. Диагностика тормозов — в статье про медленный поиск по векторам.
Кому что брать и как переехать с Chroma на Qdrant
Свожу к четырём ситуациям.
- Прототип, ноутбук, демо, до сотни тысяч фрагментов. Берите Chroma: отдельный сервис не нужен, встроенный эмбеддер экономит день.
- Внутренний сервис, 100 тысяч — миллион фрагментов, фильтры, бэкап без простоя. Берите Qdrant: здесь начинают болеть память, фильтры и снапшоты.
- Мультитенантный продукт, данные клиентов, SLA, больше миллиона точек. Qdrant без вариантов: изоляция по
tenant_id, роли, репликация, метрики. - Уже есть PostgreSQL и данных немного. Возможно, не нужна ни та, ни другая:
pgvectorдаёт векторный поиск в знакомой базе. Общая картина — в статье про векторную базу для RAG.
Минусы Qdrant называю прямо: ещё один сервис — ставить, обновлять, мониторить; эмбеддингов он не считает (FastEmbed — опция клиента, а не часть сервера); понятий больше — сегменты, оптимизаторы, payload-индексы, квантование. Для задачи «показать RAG через два часа» Chroma лучше.
Переезд обходится без пересчёта эмбеддингов, векторы просто перекладываются:
import uuid, chromadb
from qdrant_client import QdrantClient, models
src = chromadb.PersistentClient(path="/var/lib/chroma").get_collection("docs")
dst = QdrantClient(url="http://127.0.0.1:6333", api_key="СЕКРЕТ")
dst.create_collection(
"docs",
vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE),
on_disk_payload=True,
optimizers_config=models.OptimizersConfigDiff(indexing_threshold=0),
)
offset, limit = 0, 1000
while True:
b = src.get(limit=limit, offset=offset,
include=["embeddings", "documents", "metadatas"])
if not b["ids"]:
break
dst.upsert("docs", points=[
models.PointStruct(
id=str(uuid.uuid5(uuid.NAMESPACE_URL, i)),
vector=list(v),
payload={**(m or {}), "text": d},
)
for i, v, d, m in zip(b["ids"], b["embeddings"], b["documents"], b["metadatas"])
])
offset += limit
dst.update_collection("docs",
optimizer_config=models.OptimizersConfigDiff(indexing_threshold=20000))
Три грабли на этом скрипте. Идентификаторы: Qdrant принимает только беззнаковые целые и UUID, а в Chroma лежат строки вроде doc-17-chunk-3 — отсюда детерминированный uuid5. Метрика: по умолчанию Chroma считает l2, а не косинус, и если вы не указывали hnsw:space, переезд на Distance.COSINE меняет ранжирование. Индексация: indexing_threshold=0 на время заливки отключает построение HNSW, иначе индекс перестраивается на каждом батче; включить обратно обязательно. Заодно ждите ошибку вида Batch size 20000 exceeds maximum batch size 5461 — предел отдаёт client.get_max_batch_size().
Какой сервер под векторную базу брать в MAATRIX
Вывод сравнения ложится в тарифную ступеньку, и минимумы у двух баз разные.
Остаётесь на Chroma: 2 vCPU, 4 ГБ RAM, 40 ГБ NVMe. Хватает примерно на 300–400 тысяч фрагментов при 384 измерениях. Ограничение честное: сжимать векторы Chroma не умеет, потолок наступает быстро и лечится только следующим тарифом. И не считайте на этой машине эмбеддинги локальной моделью — sentence-transformers заберёт два-три гигабайта, а OOM-killer выберет между ним и базой не в вашу пользу.
Берёте Qdrant: те же 2 vCPU и 4 ГБ, но 60 ГБ NVMe. Со скалярным квантованием и on_disk_payload сюда помещается около миллиона векторов на 768 измерениях — тот самый выигрыш ступеньки. Диска нужно больше потому, что оригиналы векторов уезжают на него: RAM экономится, место расходуется.
Комфортный вариант для обеих: 4 vCPU, 8–16 ГБ RAM, 80–160 ГБ NVMe. Четыре ядра дают параллельные запросы и фоновую оптимизацию сегментов, которая на двух ядрах отбирает ресурсы у поиска. Диск считайте с коэффициентом 2,5–3 к объёму векторов: снапшот сопоставим по размеру с коллекцией.
Локация — Лондон (UK). Пока вы выбираете между базами, корпус придётся переиндексировать не один раз, а каждая переиндексация тянет либо веса модели с huggingface.co, либо API эмбеддингов OpenAI, Cohere или Voyage: с британской площадки и то, и другое работает напрямую, без прокси и региональных отказов. До европейских пользователей пинг — десятки миллисекунд, а в RAG-конвейере запрос ходит в базу не один раз за ответ. Плюс периметр GDPR. Франция равноценна, США логичнее при американском конвейере, Россия — когда данные обязаны по 152-ФЗ оставаться в стране.
Разворачивать вручную не обязательно: Qdrant есть в каталоге apps.maatrix.io и ставится автоматически при заказе сервера, на Ubuntu и на Debian. Адрес и ключи появятся в личном кабинете, в разделе «Доступ» — останется создать коллекцию и залить векторы. Пошаговый разбор — в статье про установку Qdrant на VPS. Оплата — картами российских банков, по СБП, криптовалютой или токеном MAAT.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть QdrantОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Что из них быстрее?
На чистом поиске без фильтров сопоставимо: алгоритм один, разницу определяют m и ef, а не вывеска. Расходятся они в двух местах — поиск с жёстким фильтром и расход памяти на больших коллекциях. Вендорским бенчмаркам не верьте, меряйте на своём корпусе.
Chroma съела всю память, процесс убит — что делать?
Подтвердите диагноз: journalctl -k | grep -i oom. Быстрый обход — CHROMA_SEGMENT_CACHE_POLICY=LRU вместе с CHROMA_MEMORY_LIMIT_BYTES, но внутри одной большой коллекции он не работает — остаётся только больше RAM. Структурное решение — Qdrant с квантованием int8.
Можно ли начать на Chroma и переехать потом?
Да, это разумная стратегия: эмбеддинги пересчитывать не нужно, переносятся сами векторы скриптом из шестого раздела. Заложите два момента — не завязывайтесь на произвольные строковые идентификаторы и сразу задайте metadata={"hnsw:space": "cosine"}, чтобы метрика после переезда совпала.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.