MAATRIX / Блог / Qdrant против Chroma: что выгоднее и когда

Qdrant против Chroma: что выгоднее и когда

Qdrant против Chroma: что выгоднее и когда

MAATRIX

Обе базы хранят эмбеддинги и ищут ближайших соседей, и на первых десятках тысяч фрагментов разницы вы не почувствуете. Она вылезает позже: в счёте за сервер, в поведении фильтров и в вопросе «как снять бэкап, не останавливая сервис». Разберём, что выгоднее — Qdrant или Chroma, — с расчётами, конфигами и скриптом переезда.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Разное происхождение: библиотека против сервера

Различие не в скорости, а в происхождении. Chroma родилась встраиваемой: строчка import chromadb — и хранилище живёт внутри вашего Python-процесса, складывая данные в SQLite и файлы индекса hnswlib рядом. Серверный режим появился позже как обёртка над той же схемой. В версии 1.0 (апрель 2025) ядро переписали с Python на Rust, но модель осталась: одна нода, один процесс, отдельный индекс в памяти на каждую коллекцию.

Qdrant с первого релиза — сетевой сервер на Rust со своим форматом хранения, сегментами, оптимизаторами, шардированием и репликацией. Встраиваемого режима у него нет: локальный режим клиента — отдельная упрощённая реализация, а не тот же движок.

Отсюда растёт всё остальное:

КритерийChromaQdrant
Природавстраиваемая библиотека, доросшая до серверасетевой сервер с первого дня
Язык ядраRust с версии 1.0, обвязка на PythonRust
Порты по умолчанию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 и число сегментов.

КоллекцияВекторы во float32Chroma: нужно RAMQdrant с 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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.