awesome-everything EN
↑ Обратно к восхождению

Data engineering

Векторный поиск: треугольник recall–latency–память за RAG

Суть Семантический поиск ранжирует эмбеддинги по расстоянию, но точный kNN — это O(N·d) и умирает на масштабе. ANN-индексы вроде HNSW покупают скорость ценой recall — а неверный ef-параметр тихо роняет recall, хотя запрос всё равно возвращает десять строк.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

На демо RAG отвечал идеально. В проде поддержка говорит, что бот «не находит» документы, которые очевидно есть. Ничего не падает — каждый запрос возвращает свои десять чанков, ранжированных, быстро. Подвох: команда выкатила HNSW-индекс, но так и не подняла hnsw.ef_search с дефолтных 40, а потом прикрутила фильтр WHERE tenant_id = ?. Recall тихо упал до ~30%. Система уверенно возвращала не те десять строк, и ни один лог, ни метрика, ни исключение об этом не сказали.

Эмбеддинги — это точки; поиск — это расстояние

Модель эмбеддингов превращает текст (или картинку) в вектор фиксированной длины — 768 измерений у многих открытых моделей, 1536 у OpenAI text-embedding-3-small, 3072 у большой. Семантически близкие входы оказываются рядом в этом пространстве, поэтому «найти релевантные документы» превращается в «найти ближайшие векторы к вектору запроса». Близость — это метрика: косинусная схожесть (угол), скалярное/inner product или L2 (евклидово) расстояние. Для нормализованных эмбеддингов косинус и dot ранжируют одинаково, поэтому inner product используют как более дешёвый эквивалент.

Наивный ответ — точный k-nearest-neighbor: сравнить запрос с каждым хранимым вектором и взять top k. Это O(N·d) на запрос — при 10M векторов × 1536 измерений это ~15 миллиардов умножений-сложений на один поиск. Нормально для 50k строк, фатально для 50M. Поэтому brute-force ORDER BY embedding <-> $1 LIMIT 10 без индекса корректен, но не выкатываем дальше пары сотен тысяч строк.

ANN: меняем recall на скорость

Approximate nearest neighbor (ANN) индексы отказываются от гарантии найти истинный top-k в обмен на сублинейный поиск. Метрика качества — recall@k: из истинного top-10 сколько индекс реально вернул? Recall 1.0 — идеально; 0.9 — один из каждых десяти «лучших» результатов потерян. Инсайт сеньора: recall — это регулятор настройки, а не фиксированное свойство, и, что критично, запрос на recall 0.3 выглядит идентично запросу на recall 1.0: то же число строк, тот же профиль latency, то же отсутствие ошибок. Поймать это можно только измеряя recall против точного бейзлайна.

Доминирующий индекс — HNSW (Hierarchical Navigable Small World). Он строит слоистый граф: разреженные дальние связи наверху для быстрых прыжков по пространству, плотные короткие связи внизу для точности. Запрос входит сверху, жадно идёт к вектору запроса и спускается. Три ручки управляют треугольником:

  • M — связей на узел (дефолт pgvector 16). Выше M → лучше recall и больше, плотнее граф. Сам граф HNSW занимает 2–5× размера сырых векторов в RAM, и M — главный рычаг на это.
  • ef_construction — размер пула кандидатов при построении (дефолт 64). Выше → лучше качество графа и recall, но время сборки растёт примерно линейно; 400 против 200 — примерно в 2× дольше.
  • ef_search — размер пула кандидатов в момент запроса (дефолт 40 в pgvector). Это runtime-регулятор recall/latency. Подъём со 100 до 500 может сдвинуть recall с ~85% до ~98% ценой роста с ~1мс до ~5мс на запрос.
ИндексПамятьRecallСборка / записиВыбирай, когда
HNSWВысокая (2–5× сырых)Наивысший, легче добитьсяМедленная сборка, дешёвые инкрементальные вставкиДефолт для RAG; данные меняются; recall важен
IVFFlat~1× сырыхХороший, дрейфует при измененияхБыстрая сборка, нужны данные заранееБольшой статичный набор, RAM в обрез
IVF-PQНаименьшая (сжатие ~7×)Ниже (~70%), восстановимСборка + обучение codebook100M+ векторов не влезают в RAM

IVF и IVF-PQ: кластеризуем, потом квантуем

Другое семейство — IVF (inverted file). Он k-means-кластеризует все векторы в lists бакетов; запрос сканирует только probes ближайших бакетов, а не всё. В pgvector это ivfflat с lists при сборке и ivfflat.probes в запросе (дефолт 1 — слишком мало для хорошего recall). Память IVF близка к 1× сырых векторов, но есть острый край: кластеры фиксированы при сборке, поэтому по мере вставки новых данных центроиды устаревают и recall дрейфует, пока не переиндексируешь. У HNSW такого дрейфа нет — вставки в граф константной стоимости и держат качество.

IVF-PQ добавляет product quantization: разбивает каждый вектор на под-векторы и заменяет каждый id ближайшего центроида из маленького codebook, сжимая 1536-мерный float32-вектор (6 КБ) до пары десятков байт. NVIDIA показала 360 ГиБ FP32-векторов, сжатых до 54 ГиБ, удерживая >95% recall на миллиарде векторов. Цена — потеря recall из-за ошибки квантования: IVF-PQ сам по себе часто оказывается около 70% от recall HNSW и требует rerank-шага (пересчитать top-кандидатов по полноточным расстояниям), чтобы восстановить. PQ — это инструмент, когда 100M+ векторов просто не влезают в RAM, а не дефолт.

Почему это работает

«Проклятие размерности» — вот почему это вообще сложно. В 1536 измерениях почти каждая пара случайных точек примерно равноудалена — расстояния концентрируются, и контраст между ближайшим соседом и сотым становится тонким. Именно это делает recall хрупким: чуть слишком жадный обход графа или слишком грубый квантователь не отличат настоящего соседа от near-miss, а приближённые методы отказываются от гарантии именно потому, что точное разделение стоит O(N·d).

Фильтрация и hybrid search: где ломается прод

Две продовые ловушки. Первая — фильтрация по метаданным плохо сочетается с ANN. pgvector делает post-filtering: обходит граф HNSW на ef_search кандидатов, потом применяет твой WHERE tenant_id = ?. Если фильтр оставляет 1% строк, а ef_search равен 40, можно остаться почти ни с чем — граф навигировал по крошечному, почти разорванному подграфу, и recall рушится. pgvector 0.8.0 добавил hnsw.iterative_scan (strict_order / relaxed_order), чтобы продолжать вытягивать кандидатов, пока фильтр не удовлетворён, что возвращает latency ради потерянного recall.

Вторая — чистый векторный поиск пропускает точные совпадения. Запрос по коду ошибки, SKU или имени функции — это лексическая потребность, а эмбеддинги размазывают её в «семантически близкое» — поэтому panic: nil pointer может оказаться ниже смутно связанного абзаца. Фикс — hybrid search: запускать BM25 (ключевые слова) и вектор параллельно и сливать через Reciprocal Rank Fusion (RRF = Σ 1/(k + rank), k≈60), который объединяет по позиции в ранге, так что две несовместимые шкалы оценок никогда не сравниваются напрямую. Команды сообщают о прыжке с ~65–78% до ~91% recall@10 за счёт добавления keyword-ноги.

Выбери лучший вариант

RAG-сервис над ~5M чанков получает непрерывную загрузку, требует высокого recall и имеет запас RAM. Выбери стратегию индекса.

Викторина

Твой поиск на HNSW возвращает десять ранжированных строк за 2мс, без ошибок — но ответы слегка неверны. Что проверить первым?

Викторина

Пользователи ищут точную строку ошибки 'ECONNREFUSED', и чистый векторный поиск хоронит совпадающий документ. Лучший фикс?

Расставь шаги по порядку

Расставь, как запрос HNSW находит ближайших соседей и как ты бы его настраивал:

  1. 1 Эмбеддишь запрос в то же векторное пространство, что и хранимые документы
  2. 2 Входишь в граф HNSW на верхнем разреженном слое для быстрых дальних прыжков
  3. 3 Жадно идёшь к вектору запроса, спускаясь в более плотные слои
  4. 4 Собираешь ef_search кандидатов на нижнем слое и возвращаешь top k по расстоянию
  5. 5 Меряешь recall@k против точного бейзлайна; поднимаешь ef_search, если recall низкий
Вспомните перед уходом
  1. 01
    Почему ANN-индекс с низким recall так опасен в проде и как его реально обнаружить?
  2. 02
    Когда тянуться к IVF-PQ вместо HNSW и какую цену принимаешь?
Итог

Векторный поиск превращает «найти релевантный контент» в «найти ближайшие эмбеддинги», ранжированные по косинусу, dot или L2-расстоянию. Точный kNN корректен, но O(N·d), поэтому дальше пары сотен тысяч строк переходишь на approximate nearest neighbor и принимаешь обмен recall на скорость. HNSW — дефолт: слоистый граф, настраиваемый M и ef_construction при сборке и ef_search в запросе, стоящий 2–5× сырых векторов в RAM, но дающий наивысший recall с наименьшим дрейфом при изменениях данных. IVF кластеризует, а IVF-PQ сжимает — последний для корпусов, слишком больших для RAM, ценой recall (≈70% от HNSW), который восстанавливаешь reranking. Дисциплина сеньора — это треугольник recall, latency и памяти, и одна привычка важнее всех: падения recall тихие — то же число строк и latency — поэтому меряешь recall@k против точного бейзлайна, поднимаешь ef_search осознанно, следишь за селективными фильтрами по метаданным, которые post-filtering уничтожает (используй iterative scan), и добавляешь hybrid BM25 + вектор с rank fusion, когда точные совпадения важны.

Продолжить восхождение ↑Vector search: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.