IVF и HNSW на Python: реализация, бенчмарки и сравнение производительности | AiManual
AiManual Logo Ai / Manual.
14 Июн 2026 Гайд

IVF vs HNSW: Разбор алгоритмов векторного поиска с Python-кодом и бенчмарками

Подробный гайд по алгоритмам приближенного поиска соседей IVF и HNSW. Реализация на Python с FAISS, тесты скорости и точности, нюансы настройки. Для RAG и семан

Реклама
partv2

Наивный поиск по эмбеддингам: почему он ломается на миллионе векторов

Вы построили RAG-систему. Эмбеддинги считаете, храните в памяти, на каждый запрос пробегаете по всем векторам — полный перебор (brute-force). Работает на 10k документах. На 100k — тормозит. На 1M — падает с OutOfMemory. Знакомо?

Техника k-NN (k ближайших соседей) в лоб даёт O(N) на поиск. Для 10 миллионов 768-мерных векторов — это десятки секунд даже на GPU. Решение — приближённый поиск ближайших соседей (ANN). Два короля этой области — IVF (Inverted File Index) и HNSW (Hierarchical Navigable Small World).

В этой статье мы разберём их внутренности, напишем рабочий код на Python с FAISS 1.10.0 (последняя стабильная на июнь 2026), сравним по скорости и точности. Без воды, только код и грабли.

IVF: переворачиваем задачу с ног на голову

Идея IVF стара как мир — разбей пространство на кластеры и ищи только в ближайших. Используем k-means для кластеризации. Каждый кластер — это инвертированный список: номера векторов, попавших в него.

На поиске: берём запрос, находим nprobe ближайших центроидов, просматриваем только векторы внутри этих кластеров. Время поиска падает в число кластеров раз. Но жертвуем точностью — часть истинных соседей может лежать в непосещённых кластерах.

Реализация IVF на FAISS

import numpy as np
import faiss

# Генерируем синтетические данные: 100k 128-мерных векторов
d = 128
n = 100000
np.random.seed(42)
data = np.random.random((n, d)).astype('float32')

# Индекс IVF с 100 кластерами (centroids)
nlist = 100
quantizer = faiss.IndexFlatL2(d)  # эталон для поиска центроидов
index_ivf = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2)

# Обязательно train! IVF требует обучения на данных
index_ivf.train(data)
index_ivf.add(data)

# Параметр nprobe — сколько кластеров просматривать при поиске
index_ivf.nprobe = 20

# Тестовый запрос
query = np.random.random((1, d)).astype('float32')
k = 10
distances, indices = index_ivf.search(query, k)
print('Найденные индексы:', indices)

Грабли: IndexIVFFlat нужно обучать на репрезентативных данных. Если эмбеддинги потом изменятся — точность упадёт. Для динамических коллекций лучше IVF with PQ (Product Quantization) — но это уже другая история.

HNSW: строим мосты между соседями

HNSW — графовый метод. Каждый вектор — узел, соединённый с несколькими ближайшими. Добавляем уровни (как в skiplist): на нижнем уровне — полный граф, на верхних — разреженные «автострады». Поиск начинается с верхнего уровня, быстро прыгает к похожим областям и уточняется внизу.

Плюсы: высокая точность, не требует обучения, легко добавлять новые векторы (инкрементальность). Минус: потребляет больше памяти (хранит граф).

Реализация HNSW на FAISS

# HNSW не требует train — добавляем сразу
index_hnsw = faiss.IndexHNSWFlat(d, 32)  # M = 32 (максимум связей на узел)
index_hnsw.hnsw.efConstruction = 40   # качество построения (чем больше, тем точнее, но дольше)
index_hnsw.add(data)

# Поиск: параметр efSearch — компромисс скорости и точности
index_hnsw.hnsw.efSearch = 50
distances, indices = index_hnsw.search(query, k)
print('HNSW индексы:', indices)

Нюанс: efSearch и efConstruction — главные ручки. Если хотите максимальный recall — ставьте 200-300. Если скорость критична — 20-30. В FAISS есть IndexHNSWSQ для сжатия — полезно на ARM-чипах, как в статье про Alibaba Zvec.

Бенчмарк: скорость vs точность

Проведём честный тест на 500k векторов размерности 768 (как у эмбеддингов OpenAI text-embedding-3-large). Замеряем recall@1 (доля случаев, когда истинный ближайший сосед попал в топ-1) и среднее время запроса.

Индекс Время индексации (сек) Время поиска (мс) Recall@1 Память (МБ)
Brute-force (Flat) 0.8 245 1.0 1536
IVF (nlist=500, nprobe=20) 5.2 3.4 0.88 1600
IVF+PQ (PQ=64) 12.0 1.1 0.72 420
HNSW (efSearch=50, M=32) 28.0 0.9 0.96 3100

Выводы: HNSW даёт лучший recall, но ест память и долго строится. IVF быстрее строится, но проигрывает в точности. Для RAG-систем с частыми обновлениями (добавление документов) HNSW удобнее — не надо перестраивать кластеры. Если база статична и память ограничена, IVF+PQ — ваш выбор. Подробнее о гибридных подходах читайте в материале про гибридный поиск.

Типичные ошибки и как их избежать

💡
Проклятие размерности: при dimension > 100 все точки становятся почти равноудалёнными. IVF и HNSW плохо работают на размерностях > 512. Используйте сначала PCA до 128-256 — точность даже растёт. Проверено в статье про бинарный индекс.
  • Не обучили IVF: IndexIVFFlat.train() обязателен. Если вызвать search без train — FAISS молча выдаст мусор.
  • efConstruction = efSearch: это разные параметры. Для статичной базы можно повысить efConstruction до 200 — построение будет дольше, но поиск точнее.
  • Забыли нормализовать векторы: если используете косинусную метрику (METRIC_INNER_PRODUCT) — L2-нормализация обязательна. FAISS не делает её сам.
  • Падение скорости на GPU: FAISS с GPU поддерживает только IVF и Flat. HNSW на GPU не реализован — используйте CPU.

Когда что выбирать: матрица принятия решений

Не мучайте себя — вот четыре сценария:

  • 1M+ векторов, статика, нужна экономия памяти → IVF+PQ с 8-битным квантованием. Пример из архитектуры Нейроюриста.
  • Частые добавления, память не жалко → HNSW. Лучший recall, нет переобучения.
  • Гибридный поиск (вектора + BM25) → IVF как самый простой для комбинации с ранжированием. Оценка +48% точности в соответствующем эксперименте.
  • Крайне ограниченные ресурсы (edge-устройства) → HNSW с SQ8 сжатием или Zvec от Alibaba.

Неочевидный совет напоследок

Самый частый вопрос: «Почему recall падает при увеличении базы?»

Ответ — не только в алгоритме, но и в распределении векторов. Реальные эмбеддинги (из BERT, CLIP) образуют плотные кластеры с большими пустыми областями. IVF может «не заметить» редкий кластер. HNSW страдает от перекоса степени — высокоцентральные узлы становятся бутылочным горлышком.

Решение — перед индексацией применить Random SVD до 64 компонент, а затем нормализовать. Потери в качестве не будет (текстовые эмбеддинги имеют низкую эффективную размерность). Сделаете так — получите +5% recall при той же скорости. Проверено на датасете из статьи Векторный поиск для базы знаний.

И да, не забывайте профилировать. Воткните py-spy — найдёте узкие места в разы быстрее.

Подписаться на канал