Векторный поиск и ANN-индексы
Сердце RAG — поиск ближайших соседей в пространстве эмбеддингов. На игрушечной базе можно перебрать все векторы честно, но на миллионах приходится жертвовать точностью ради скорости — этим занимаются ANN-индексы, и трейд-офф между recall и скоростью надо уметь объяснить на пальцах.
Интуиция
После того как тексты превращены в эмбеддинги, «найти похожее по смыслу» = «найти ближайшие точки в многомерном пространстве». Близость меряют тремя способами: косинусная близость (cosine similarity, угол между векторами), скалярное произведение (dot product) и евклидово расстояние. Звучит как три разных мира, но если векторы нормировать к единичной длине, все три метрики упорядочивают соседей практически одинаково — поэтому спор «косинус или евклид» на нормированных эмбеддингах почти беспредметен.
Точный поиск — сравнить запрос с каждым из N векторов размерности d:
Для 10 тысяч векторов — мгновенно. Для 100 миллионов при d = 1024 — уже сотни миллиардов операций на запрос. Не масштабируется.
Как это работает: ANN
ANN (Approximate Nearest Neighbors, приближённый поиск соседей) — сознательный размен: ищем не гарантированно ближайших, а «почти наверняка ближайших», зато на порядки быстрее. Качество индекса меряют метрикой recall@k — какая доля истинных топ-k соседей нашлась. Recall 0.95 значит: в среднем 19 из 20 настоящих соседей на месте.
- HNSW (Hierarchical Navigable Small World) — многослойный граф. Интуиция: верхние слои — редкие «хайвеи» для дальних прыжков по пространству, нижние — плотные «улицы» для точной доводки. Поиск жадно спускается сверху вниз: быстро долетаем в нужный район, потом аккуратно ищем дом. Быстрый и точный, но граф ест много памяти.
- IVF (Inverted File index) — база заранее разбивается на ячейки вокруг центроидов (кластеризация, привет k-means). При запросе ищем только в nprobe ближайших ячейках. Больше зондов — выше recall, но больше просмотренных точек.
- PQ (Product Quantization) — сжатие векторов: длинный вектор режется на части, каждая кодируется номером ближайшего эталона из маленького словаря. Память падает в десятки раз, точность немного страдает; часто комбинируется с IVF (IVF-PQ).
Отдельная боль — фильтры по метаданным («ищи только среди документов отдела X»). Наивно: найти топ-k, потом отфильтровать — но фильтр может выбить почти всех, и останется два результата из k. Поэтому зрелые движки поддерживают фильтрацию внутри обхода индекса (filter-aware search).
Что выбрать на практике
Выбор движка — вопрос масштаба и стека, а не моды:
- До ~1 млн векторов и уже есть Postgres — pgvector: одна база, транзакции, привычные бэкапы. Отдельная векторная БД не обязательна.
- Библиотека в процессе — FAISS / hnswlib: максимум контроля и скорости, но персистентность, репликация и фильтры — ваша забота.
- Выделенный сервис — Qdrant, Milvus, Weaviate и т.п.: когда нужны масштаб, фильтр-осведомлённый поиск и обновления «на живую».
Типичная ошибка дизайна — тащить выделенную векторную БД в проект на 50 тысяч документов «на вырост»: операционная сложность реальна уже сегодня, а масштаб, может, не наступит никогда.
- «Косинус или евклид?» — на нормированных векторах ранжирование почти совпадает; важнее, с какой метрикой обучалась модель эмбеддингов и построен индекс.
- «Объясните HNSW на пальцах» — многослойный граф: сверху «хайвеи» для дальних прыжков, снизу «улицы» для точной доводки; жадный спуск по слоям.
- «Что крутить в IVF, если recall низкий?» — увеличить nprobe (медленнее, полнее) или число/качество центроидов; проверить случай запросов у границ ячеек.
- «Как ANN уживается с фильтрами по метаданным?» — пост-фильтрация может оставить меньше k результатов; нужен движок с фильтрацией внутри обхода индекса.