Qdrant - Vector Database
Points, Vectors, Payloads
2023 год. ChatGPT вышел и все хотят RAG (Retrieval-Augmented Generation). Семантический поиск по документам - embedding документа -> вектор -> поиск ближайших соседей -> ответ GPT. Компании хранят миллионы документов. PostgreSQL с pgvector работает до ~1 миллиона векторов. Дальше нужна специализированная векторная БД. Qdrant - система хранения и поиска по векторам. Понять её структуру означает понять, как работает семантический поиск в продакшене.
- **RAG pipeline**: каждый документ -> embedding -> Point в Qdrant. Поиск: query embedding -> nearest neighbors -> context для LLM
- **Recommendation**: пользователь лайкнул фильм -> embedding фильма -> поиск похожих векторов -> рекомендации
- **Code search**: каждая функция -> code embedding -> Qdrant. Запрос: "найти функцию сортировки" -> семантический поиск
- **E-commerce**: товар -> image embedding -> похожие товары по внешнему виду
Векторные базы данных: от word2vec до Qdrant
В 2013 году Томас Миколов из Google опубликовал word2vec - нейросетевую технику создания векторных представлений слов. Впервые семантически близкие слова оказывались близко в векторном пространстве. В 2017 году трансформеры ("Attention Is All You Need") создали предпосылки для dense embeddings любых данных. К 2021 году поиск ближайших соседей (ANN) стал критически важным для RAG систем и семантического поиска. Qdrant основан в 2021 году командой из России как open-source векторная БД на Rust. К 2023 году конкуренция: Pinecone (managed), Weaviate (GraphQL), Chroma (embedded), Milvus (Go). Qdrant выделяется производительностью HNSW реализации и гибкостью payload фильтров.
Предварительные знания
Структура точки: ID + вектор + payload
**Point** - основная единица данных в Qdrant. Каждая точка состоит из трёх частей: уникального идентификатора, вектора (embedding) и произвольного payload (метаданные).
**ID: integer vs UUID.** Integer ID проще и чуть быстрее (нет строкового хеширования). UUID удобен если уже есть UUID-идентификаторы из другой БД. Главное - **ID уникален в пределах коллекции**.
**Payload - schema-free JSON.** Нет фиксированной схемы. Разные точки могут иметь разные поля. Значения могут быть: строки, числа, булевы, массивы, вложенные объекты, `null`. Для фильтрации по полю нужен payload index (урок 8).
Точка 1 имеет payload `{category: 'science'}`, точка 2 - `{topic: 'physics', year: 2024}`. Это нормально для Qdrant?
Upsert: добавление и обновление точек
**Upsert** (update + insert) - основная операция записи в Qdrant. Если точка с таким ID существует - обновляется полностью. Если нет - создаётся. Нет отдельных INSERT и UPDATE.
**`wait: true`** - ждать подтверждения, что данные записаны в WAL. По умолчанию `false` (fire-and-forget). Для production upsert используйте `wait: true`, чтобы знать, что данные приняты.
**Генерация ID:** если нет натурального integer ID, генерируйте UUID: `crypto.randomUUID()` в Node.js 18+. UUID v4 - стандартный выбор. Qdrant принимает оба формата.
код вызвали upsert с точкой id=42, которая уже существует. Что произойдёт?
Операции с payload
**Payload можно обновлять независимо от вектора.** Это важно: повторная генерация embedding дорогая ($$$), а метаданные часто меняются. Qdrant поддерживает частичное обновление payload.
**setPayload vs overwritePayload:** `setPayload` - как `Object.assign()`, добавляет/обновляет поля, остальные остаются. `overwritePayload` - как `obj = {}`, сначала очищает, потом устанавливает. Используйте `setPayload` в 90% случаев.
У точки payload: `{title: 'Old', views: 100}`. код вызвали `setPayload({views: 200, tags: ['ai']})`. Что в payload теперь?
CRUD операции с точками
**Получение, обновление и удаление** отдельных точек - стандартные операции для работы с данными.
**`with_vector: false`** - всегда указывайте если вектор не нужен. Векторы занимают ~6KB каждый (1536 × 4 байта). Для 1000 точек - это 6MB лишних данных в ответе.
Хранить полный текст документа в payload Qdrant
Хранить в payload только метаданные и короткий preview/chunk. Полный текст - в основной БД (PostgreSQL), ссылаясь по ID
Payload загружается при каждом поиске. Если хранить 10KB текста в каждой точке и делать 100 RPS - это 1MB/запрос только на payload. Плюс: Qdrant не оптимизирован для хранения больших текстов, это задача PostgreSQL/S3
Нужно пройти по всем 10M точкам коллекции для экспорта. Какой метод использовать?
Ключевые идеи
- **Point = ID (int/UUID) + vector (float[]) + payload (JSON).** Payload schema-free - разные точки могут иметь разные поля
- **Upsert** - идемпотентная операция: создаёт или полностью заменяет точку. `wait: true` для подтверждения записи
- **Batch upsert:** 64-256 точек за раз, 4 параллельных потока - оптимальная скорость загрузки
- **`setPayload`** обновляет отдельные поля (как merge), **`overwritePayload`** заменяет всё. Первый используется в 90% случаев
- **`with_vector: false`** при retrieve/scroll - экономия 6KB на точку. **scroll()** - для полного обхода коллекции
Что дальше
Данные загружены. Пора искать!
- Первый поиск — search API, метрики расстояния, score threshold - как делать запросы
- Payload индексы и фильтрация — Как фильтровать результаты поиска по payload без ухудшения recall
Вопросы для размышления
- Стоит ли хранить полный текст документа в payload Qdrant? Какие альтернативы?
- Если в проекте 1M документов и нужно добавить поле 'language' всем точкам - как это сделать без перегрузки данных?
- Как организовать multi-chunk indexing: один большой документ → много chunks → много points?