Главная / Статьи / За пределами наивного RAG: память HNSW, гибридный поиск и длинный контекст

За пределами наивного RAG: память HNSW, гибридный поиск и длинный контекст

Почему векторные индексы в памяти упираются в стену на больших объёмах, где ломается поиск по точному совпадению и как BM25-гибриды, RRF и PostgreSQL вписываются в мир длинного контекста.

1175 слов

Контекстные окна уже дошли до миллиона токенов и больше, и это незаметно меняет саму роль векторного поиска. Тот же сдвиг показывает, почему многомерные индексы плохо масштабируются, и подсказывает, куда движется корпоративный поиск.

Лёгкие годы RAG

Когда retrieval-augmented generation стал стандартным паттерном для корпоративных LLM-приложений, рецепт выглядел просто:

  1. Нарезать корпус на фрагменты меньше 1000 токенов.
  2. Прогнать каждый фрагмент через embedding-модель вроде text-embedding-3-large.
  3. Хранить плотные векторы в отдельной векторной базе на графах Hierarchical Navigable Small World (HNSW).
  4. При запросе выполнять приближённый поиск ближайших соседей (ANN) и вставлять лучшие совпадения в промпт.

При 10 000 документов и окне в 4k это прекрасно работало. Потом окна выросли за 1M токенов, и экономика поменялась. Если инженер может положить в промпт всю кодовую базу, юридический архив или годовую отчётность, доводы в пользу наивного векторного поиска слабеют.

Масштаб добавляет ещё одну проблему. Когда данные растут от миллионов до миллиардов векторов, вылезают три скрытые издержки чистого векторного поиска: закончившаяся RAM, качество индекса, падающее с ростом размерности, и непредсказуемая семантическая полнота. Дальше разберём, почему векторные базы упираются в эти пределы и что сейчас делают боевые системы.

Проблема памяти HNSW

Почти все крупные векторные движки, включая Pinecone, Qdrant, Milvus, Weaviate и pgvector, опираются на графы HNSW. HNSW даёт быстрый логарифмический поиск, раскладывая точки по слоям. Верхние слои хранят дальние связи для быстрых прыжков, нижние — плотные локальные связи с соседями.

Подвох в том, что для быстрого обхода граф должен жить в оперативной памяти.

Арифметика

Возьмём 1536-мерные эмбеддинги, которые выдают text-embedding-3-small или ada-002 от OpenAI, в виде 32-битных float. Каждый вектор занимает 1536 × 4 байта, то есть 6144 байта, около 6 КБ.

На 100 миллионах векторов:

  • Сами векторы: 100 миллионов × 6 КБ — примерно 600 ГБ.
  • Накладные расходы графа: связи каждого узла с 16–64 соседями добавляют ещё 20–50 процентов.
  • Итого по памяти: где-то от 750 ГБ до 1 ТБ быстрой RAM.

По облачным ценам аренда терабайта памяти только ради индексации текстовых фрагментов быстро начинает стоить больше, чем сам инференс LLM.

Product Quantization (PQ) и скалярное квантование (SQ8) сжимают векторы из float32 в int8 или бинарный вид, но приносят вторую проблему: падает полнота.

Проклятие размерности и семантический дрейф

Примерно после 1000 измерений в векторных пространствах проявляются геометрические странности, известные как проклятие размерности. Одна из них — концентрация расстояний. С ростом размерности отношение расстояния до ближайшей точки к расстоянию до самой дальней стремится к 1.

Для косинусной близости почти каждый вектор начинает казаться одинаково далёким от всех остальных. Добавьте квантование, и границы между разными понятиями размываются ещё сильнее.

Где ломается точное совпадение

Векторный поиск вероятностный по своей природе. Он измеряет смысл, а не буквальные токены. В корпоративных системах, где важны буквальные совпадения, это приводит к серьёзным промахам:

  • Артикулы и идентификаторы. Запрос "Part #AB-9941-X" может вернуть "Part #AB-9942-X", потому что оба эмбеддинга попадают в один кластер.
  • Отрицание и логика. Запрос вроде "Contracts without liability caps" часто возвращает как раз договоры с ограничением ответственности, потому что embedding-модель цепляется за фразу «liability caps».

Длинный контекст против чанков

С моделями, которые изначально держат большие окна, баланс между предварительной индексацией и прямым вниманием в контексте сместился. Разрезая документ на куски по 512 токенов, вы рвёте перекрёстные ссылки, зависимости таблиц и логические условия, растянутые по странице. Когда большое окно вмещает документы целиком, наивный чанкинг теряет смысл, а векторный поиск из поиска фрагментов превращается в маршрутизацию крупных блоков документов.

Гибридный поиск и возвращение BM25

Чтобы обойти эти ограничения, дата-инженерия уходит от чисто векторных хранилищ к гибридным архитектурам из трёх частей:

  1. Плотный поиск по векторам — ловит общее намерение и смысловые вопросы.
  2. Разреженный поиск через BM25 или SPLADE — ловит точные ключевые слова, серийные номера и конкретные сущности.
  3. Reciprocal Rank Fusion (RRF) — сливает оба списка результатов до того, как top-K кандидатов попадут в модель-реранкер.

Бэкенд-пайплайн может объединить плотные и разреженные оценки через RRF на Python так:

from typing import List, Dict

def reciprocal_rank_fusion(
    dense_results: List[str],
    sparse_results: List[str],
    k: int = 60
) -> List[Dict[str, float]]:
    """
    Combines dense vector search results and sparse BM25 search results
    using Reciprocal Rank Fusion (RRF).
    """
    rrf_scores: Dict[str, float] = {}

    # Score Dense Results
    for rank, doc_id in enumerate(dense_results):
        if doc_id not in rrf_scores:
            rrf_scores[doc_id] = 0.0
        rrf_scores[doc_id] += 1.0 / (k + (rank + 1))

    # Score Sparse Results (BM25)
    for rank, doc_id in enumerate(sparse_results):
        if doc_id not in rrf_scores:
            rrf_scores[doc_id] = 0.0
        rrf_scores[doc_id] += 1.0 / (k + (rank + 1))

    # Sort documents by descending fusion score
    sorted_docs = sorted(
        rrf_scores.items(), key=lambda item: item[1], reverse=True
    )

    return [{"doc_id": doc, "score": score} for doc, score in sorted_docs]

Почему реляционные базы возвращают себе нагрузку

Потребность в гибриде тянет нагрузку обратно к традиционным базам. PostgreSQL (с pgvector и pg_trgm), Elasticsearch и SingleStore позволяют выполнять векторные запросы рядом с операционными метаданными, реляционными джойнами и ACID-транзакциями, без отдельной векторной базы, которую нужно обслуживать.

- PostgreSQL Hybrid Query: Vector Distance + Full Text Match + Relational Filter
SELECT
 id,
 title,
 (ts_rank(text_search_vector, websearch_to_tsquery('english', 'liability clause')) * 0.5) +
 ((1 - (embedding <=> '[0.012, -0.043, …]')) * 0.5) AS hybrid_score
FROM enterprise_documents
WHERE tenant_id = 'org_99412'
 AND status = 'ACTIVE'
ORDER BY hybrid_score DESC
LIMIT 10;

Выводы

Векторные базы никуда не денутся. Закончилась привычка бездумно запихивать каждый чанк документа в HNSW-индекс в памяти. Четыре правила на 2026 год:

  1. Не решайте задачи точного совпадения одними векторами. Если пользователи ищут по SKU, имени или синтаксису кода, с самого начала добавляйте к векторам BM25 или инвертированный индекс.
  2. Следите за счётом за память HNSW. После примерно 10M векторов присмотритесь к дисковым индексам вроде DiskANN от Microsoft или к бинарному квантованию, пока расходы не вышли из-под контроля.
  3. Используйте векторы для маршрутизации, а не для чтения. Вместо крошечных чанков по 200 токенов выбирайте три–пять целых документов по 50k токенов и больше и отдавайте их LLM с длинным контекстом.
  4. Консолидируйте. Если у вас не масштаб в миллиарды векторов с SLA меньше 10 мс, вашей нынешней реляционной базы, например PostgreSQL с pgvector, скорее всего, хватит, и вы избавитесь от багов синхронизации распределённой системы.