中文翻译暂不可用,显示俄语原文。
Как работает HNSW (Hierarchical Navigable Small World) алгоритм внутренне?
Краткий тезис
HNSW (Hierarchical Navigable Small World) — это алгоритм приближённого поиска ближайших соседей (ANN), основанный на многослойном графе. Верхние слои содержат редкие связи (long jumps), позволяющие быстро перемещаться в нужную область пространства, а нижние слои — плотные связи для точного поиска. Поиск начинается с самого верхнего слоя и последовательно спускается вниз, выполняя на каждом уровне жадный обход графа. Это даёт логарифмическую сложность поиска и высокую точность, что делает HNSW стандартом для векторных БД (FAISS, Qdrant, Weaviate).
1. Термин: Приближённый поиск ближайших соседей (ANN)
Приближённый поиск ближайших соседей (Approximate Nearest Neighbor, ANN) — это класс алгоритмов, которые находят не гарантированно точных, а «достаточно близких» соседей, жертвуя точностью ради скорости. В отличие от точного поиска (kNN), ANN обрабатывает миллиарды векторов за миллисекунды.
Зачем нужен ANN в RAG
- Векторные БД хранят миллионы эмбеддингов (например, 768-мерных от text-embedding-3-small)
- Точный поиск (полный перебор) требует O(N * D) операций — неприемлемо для реального времени
- ANN даёт O(log N) или O(sqrt(N)) — приемлемо для интерактивных систем
HNSW — один из самых популярных ANN-алгоритмов (наряду с IVF, PQ, ScaNN).
2. Проблема: Почему нельзя просто построить полный граф?
Наивная идея: соединить каждый вектор с K ближайшими соседями. Тогда поиск — это жадный обход по рёбрам. Проблемы:
- Размер графа для N=1M и K=32 нужно хранить 32M рёбер — много памяти
- Качество поиска жадный обход легко застревает в локальном оптимуме (далеко от глобального ближайшего соседа)
- Скорость в худшем случае нужно обойти O(N) вершин
HNSW решает обе проблемы через иерархию и вероятностное распределение слоёв.
3. Идея: Navigable Small World (NSW)
Navigable Small World (NSW) — предшественник HNSW. Идея: построить граф, где есть как короткие связи (между близкими точками), так и длинные (между удалёнными). Длинные связи позволяют быстро «перепрыгивать» в разные области пространства — как в социальных сетях через «знакомых знакомых».
Проблема NSW при вставке новых точек длинные связи могут теряться, и граф деградирует. HNSW решает это через иерархию.
4. Иерархическая структура HNSW
HNSW строит несколько слоёв графа (обычно 5–16). Каждый слой — это подграф, содержащий подмножество точек.
Правила построения слоёв
- Самый верхний слой (Layer L): содержит ~1% всех точек, связи очень редкие (длинные прыжки)
- Средние слои ~10–30% точек, связи средней плотности
- Нижний слой (Layer 0): содержит 100% точек, связи плотные (точный поиск)
Как точка попадает на слой при вставке случайным образом выбирается уровень l по экспоненциальному распределению:
P(level = l) = exp(-l / mL)
где mL — параметр (обычно 0.3–0.5). Точка появляется на всех слоях от 0 до l.
Визуализация
Layer 2 (верхний): ●──●──●
Layer 1 (средний): ●──●──●──●──●
Layer 0 (нижний): ●──●──●──●──●──●──●──●
Чем выше слой, тем меньше точек и длиннее связи.
5. Построение графа (вставка точки)
При вставке новой точки q:
- Выбор уровня генерируем случайный
l(экспоненциальное распределение) - Поиск точки входа начинаем с верхнего слоя, выполняем жадный обход до слоя
l+1 - Вставка на слоях от
lдо 0: на каждом слое:- Находим efConstruction ближайших соседей (параметр, обычно 100–200)
- Соединяем
qсMближайшими из них (M— параметр, обычно 16–32) - Применяем эвристику pruning (обрезка): если у соседа уже много связей, удаляем самые длинные, чтобы сохранить баланс
Параметр M максимальное количество рёбер у вершины на каждом слое. Чем больше M, тем точнее поиск, но больше памяти.
Эвристика обрезки (pruning): при добавлении новой связи проверяем, не превышает ли степень вершины Mmax. Если превышает — удаляем самое длинное ребро (по евклидову расстоянию). Это сохраняет свойство small-world.
6. Поиск (жадный обход сверху вниз)
Поиск ближайших соседей для запроса q:
- Начинаем с верхнего слоя берём точку входа (обычно первая вставленная точка)
- На каждом слое выполняем жадный обход:
- Идём по рёбрам, выбирая соседа, ближайшего к
q - Повторяем, пока не достигнем локального минимума (все соседи дальше текущей точки)
- Идём по рёбрам, выбирая соседа, ближайшего к
- Спуск на слой ниже берём лучшую найденную точку как стартовую для следующего слоя
- На нижнем слое (Layer 0): выполняем более широкий поиск с параметром efSearch (обычно 100–500):
- Используем приоритетную очередь (кучу)
- Продолжаем, пока не найдём efSearch кандидатов
- Возвращаем top-k из них
Псевдокод поиска
def search_hnsw(q, layers, entry_point, efSearch, k):
# Фаза 1: спуск по верхним слоям
curr = entry_point
for layer in reversed(range(len(layers)-1, 0, -1)):
curr = greedy_search(q, layer, curr)
# Фаза 2: поиск на нижнем слое
candidates = priority_queue()
visited = set()
candidates.push((dist(q, curr), curr))
while len(candidates) < efSearch:
d, node = candidates.pop()
for neighbor in layers[0].neighbors[node]:
if neighbor not in visited:
visited.add(neighbor)
candidates.push((dist(q, neighbor), neighbor))
return top_k(candidates, k)
7. Параметры HNSW и их влияние
| Параметр | Типичное значение | Влияние на точность | Влияние на скорость | Влияние на память |
|---|---|---|---|---|
M | 16–32 | ↑ (больше связей) | ↓ (больше обход) | ↑ (больше рёбер) |
efConstruction | 100–200 | ↑ (лучше граф) | ↓ (медленнее вставка) | — |
efSearch | 100–500 | ↑ (шире поиск) | ↓ (больше кандидатов) | — |
mL (level multiplier) | 0.3–0.5 | ↑ (больше слоёв) | ↓ (больше спусков) | — |
Практические рекомендации
- Для высокой точности (recall@10 > 0.99):
M=32, efConstruction=200, efSearch=500 - Для низкой латентности (< 10ms):
M=16, efConstruction=100, efSearch=100 - Для больших датасетов (> 10M): увеличивайте
mLдо 0.5–0.7
8. Сравнение HNSW с другими ANN-алгоритмами
| Алгоритм | Сложность поиска | Точность (recall@10) | Память | Скорость индексации | Примечание |
|---|---|---|---|---|---|
| HNSW | O(log N) | 0.95–0.99 | Высокая | Средняя | Стандарт для RAG |
| IVF (Inverted File) | O(sqrt(N)) | 0.85–0.95 | Средняя | Быстрая | Хорош для кластеризации |
| PQ (Product Quantization) | O(N) сжатый | 0.80–0.90 | Низкая | Быстрая | Экономит память |
| ScaNN (Google) | O(log N) | 0.95–0.98 | Средняя | Средняя | Оптимизирован для MIPS |
Когда выбирать HNSW
- Нужна максимальная точность (RAG с критичными ответами)
- Память не является ограничением (до 10M векторов)
- Индекс строится один раз, поиск — часто
9. Преимущества и недостатки HNSW
Преимущества
- Логарифмическая сложность поиска O(log N) в среднем
- Высокая точность recall@10 часто > 0.98
- Поддержка динамических вставок можно добавлять векторы без перестроения индекса
- Параллелизм поиск легко распараллеливается (batch processing)
Недостатки
- Высокое потребление памяти каждый вектор хранит M рёбер (32–64 int32 на точку)
- Медленная вставка efConstruction требует поиска соседей на каждом слое
- Чувствительность к размерности при D > 1000 точность падает (проклятие размерности)
- Не подходит для разреженных векторов граф строится на плотных эмбеддингах
10. HNSW в контексте Agentic RAG
В архитектуре Agentic RAG HNSW используется как основной индекс для векторной памяти агента. Агент может:
- Хранить историю диалогов в HNSW-индексе для поиска релевантных контекстов
- Использовать несколько HNSW-индексов для разных типов знаний (факты, процедуры, примеры)
- Динамически добавлять новые знания через вставку в HNSW без перестроения
Пример: агент поддержки клиентов хранит 500K предыдущих обращений. При новом запросе HNSW находит top-10 похожих кейсов за 5ms, агент использует их для генерации ответа.
Пет-проект для закрепления
Задача Реализовать упрощённый HNSW на Python с нуля (без FAISS) для поиска по 10K случайных 128-мерных векторов.
Инструменты Python, NumPy, Matplotlib (для визуализации графа).
Шаги:
- Сгенерируйте 10K случайных векторов (np.random.randn)
- Реализуйте класс
HNSWIndexс методамиinsert()и search() - Используйте евклидово расстояние
- Реализуйте жадный поиск на одном слое (greedy_search)
- Добавьте иерархию: при вставке выбирайте уровень по экспоненциальному распределению
- Реализуйте поиск с efSearch (приоритетная очередь)
- Сравните точность с точным kNN (sklearn.neighbors.NearestNeighbors)
Ожидаемый результат
- Индекс строится за < 1 минуты
- Поиск top-10 выполняется за < 10ms
- Recall@10 > 0.90 при efSearch=100
Код-скелет
import numpy as np
from heapq import heappush, heappop
class HNSWIndex:
def __init__(self, M=16, efConstruction=100, efSearch=100, mL=0.3):
self.M = M
self.efConstruction = efConstruction
self.efSearch = efSearch
self.mL = mL
self.layers = [] # список слоёв, каждый слой - dict {id: neighbors}
self.vectors = []
self.entry_point = None
def _random_level(self):
return int(-np.log(np.random.random()) * self.mL)
def insert(self, vector):
# TODO: реализовать вставку
pass
def search(self, query, k=10):
# TODO: реализовать поиск
pass
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 220 | Как работают эмбеддинги и векторные представления? |
| 222 | Что такое гибридный поиск (hybrid search) и как его реализовать? |
| 226 | Как работает кэширование в RAG? |
| 230 | Как устроена архитектура FAISS? |
| 235 | Как выбрать ANN-алгоритм для векторной БД? |
Навигация
- Предыдущий: 220
- Следующий: 222
- Индекс: 00. Индекс разборов