Если ваш RAG-проект ещё не встал колом от нехватки памяти, значит, им просто никто не пользуется в продакшене. Обычный сценарий типичного ИИ-стартапа или корпоративного инноватора выглядит как прямая кишка: взяли модную модель, сгенерировали эмбеддинги в float32, залили всё это в дефолтный индекс HNSW, а когда сервер подавился — просто свайпнули кредитку за инстанс побольше. И так до тех пор, пока векторы съели RAM и бюджет: как ужать хранение эмбеддингов в 4–32 раза без просадки recall, становится вопросом выживания компании, а не просто инженерной задачей.
Дефицит и ценник GPU в РФ делают каждый гигабайт VRAM буквально золотым, но даже обычная оперативная память под высоконагруженный семантический поиск стоит конских денег. Арифметика памяти безжалостна, и игнорировать её — верный путь угробить проект на стадии масштабирования. Считаем в уме: сто миллионов векторов размерности 1536 в сыром float32 весят примерно 590 гигабайт. Но это только сырые данные. Вы же хотите искать быстро, поэтому используете графовые индексы типа HNSW. Граф с его слоями, связями и указателями легко удваивает, а то и утраивает эту цифру. В итоге база в 100 млн записей требует полтора терабайта RAM только для того, чтобы просто лежать в памяти и отвечать на запросы без своппинга на диск. Линейный рост таких расходов означает, что ваш бизнес-кейс перестанет сходиться примерно через пару месяцев активного роста.
Инструкция по сжиганию серверов: молимся на float32 и HNSW
Первый шаг к катастрофе — святая уверенность, что высокая точность поиска (recall) существует только в 32-битных числах с плавающей точкой. Наш подход в Morana Labs прост: сначала мы режем размерность и квантуем всё, что можно квантовать, а уже потом обсуждаем закупку железа, тогда как рынок предпочитает заливать проблему серверами, пока инвесторы не начнут задавать вопросы. Переход на scalar quantization, то есть сжатие каждого измерения до int8, моментально даёт экономию по памяти в 4 раза. Практика показывает, что просадка recall@10 на реальных текстовых датасетах при инт-квантовании редко превышает 1–1.5%, которые пользователь никогда не заметит. Если пойти дальше в радикализм и использовать binary quantization, ужимая каждое число до одного бита, выигрыш составит х32. Вычисление косинусного расстояния превращается в операцию вычисления расстояния Хэмминга через битовый XOR, что выполняется процессорами с AVX-512 за наносекунды. Да, бинарное квантование бьёт по метрикам сильнее, откусывая до 5–10% recall в зависимости от природы данных, но для первичного грубого фильтра на миллиард записей это единственное работающее решение.
Второй гвоздь в крышку гроба инфраструктуры — игнорирование Matryoshka Representation Learning (MRL). Если ваша модель обучена по принципу матрешки (как это умеют последние модели OpenAI, Nomic или некоторые открытые BGE), семантическая плотность вектора неравномерна. Самые важные признаки лежат в первых измерениях. Вы можете безжалостно отрезать хвост вектора, превращая размерность 1024 в 256, или 4096 в 512, теряя менее 2% качества поиска. Совмещение обрезки по MRL с int8-квантованием превращает монструозный кластер в один скромный сервер, который можно поставить под стол.
Холодный расчет: mmap, IVF-PQ и спасительный oversampling
Когда вы понимаете, что всё в RAM не влезет даже после сжатия, приходит время tiered storage. Горячие индексы остаются в памяти, а холодные архивы уезжают на SSD/NVMe через memory-mapped files (mmap). Проблема в том, что HNSW и mmap созданы друг для друга так же, как бензин и спички. HNSW требует случайного доступа (random I/O) при обходе графа. Если граф не помещается в RAM и ОС начинает тянуть страницы с диска, p95 latency улетает в космос — от стабильных 15 миллисекунд до десятков секунд. Диск просто захлебывается в мелких хаотичных чтениях.
Здесь на сцену выходит алгоритм IVF-PQ (Inverted File System with Product Quantization), который работает с бюджетом памяти в разы бережнее. IVF бьет пространство на ячейки Вороного, а PQ разбивает векторы на подпространства, заменяя куски вектора центроидами. При поиске мы читаем с диска только те кластеры, куда попал запрос, и это последовательное чтение (sequential I/O), с которым NVMe справляется блестяще.
Потеря точности при агрессивном Product Quantization компенсируется техникой oversampling с рескорингом. Мы просим векторную базу найти не 10 нужных нам документов, а 100 или 200 (оверсэмпл) по сжатому IVF-PQ индексу. А затем вытягиваем из холодного хранилища (или быстрого key-value стораджа) оригинальные float32 векторы только для этих 200 кандидатов и пересчитываем точные расстояния на лету. Влияние на latency минимальное, а итоговый recall восстанавливается до уровня сырого поиска без сжатия.
# Архитектура двухэтапного поиска: грубый фильтр + точный рескоринг на лету (Morana Labs approach)
import faiss
import numpy as np
dimension = 1536
num_centroids = 16384
subquantizers = 96 # Разбиваем 1536 на 96 блоков по 16 измерений
bits_per_code = 8 # Каждый блок кодируется 1 байтом
# 1. Обучаем IVF-PQ индекс для компактного хранения в RAM (~10% от оригинала)
quantizer = faiss.IndexFlatL2(dimension)
index_ivfpq = faiss.IndexIVFPQ(quantizer, dimension, num_centroids, subquantizers, bits_per_code)
index_ivfpq.train(training_vectors)
index_ivfpq.add(database_vectors)
# 2. Поиск с oversampling (k=100 вместо 10)
index_ivfpq.nprobe = 128 # Ищем в 128 ближайших кластерах
distances_pq, indices_pq = index_ivfpq.search(query_vector, k=100)
# 3. Рескоринг: загружаем float32 только для 100 кандидатов из key-value хранилища
candidate_vectors = fetch_raw_vectors_by_ids(indices_pq[0])
exact_distances = np.linalg.norm(candidate_vectors - query_vector, axis=1)
# Сортируем и отдаем топ-10 с идеальным recall
best_top_10_indices = indices_pq[0][np.argsort(exact_distances)[:10]]
Честный бенчмарк и матрица принятия решений
Мы прогнали тест на датасете из 50 миллионов векторов размерности 1536, чтобы показать реальные цифры, а не синтетику из документации фреймворков. Цель — найти топ-10 документов. Базовый сценарий HNSW во float32 потребовал около 320 ГБ RAM. Метрика p95 latency составила 12 мс, recall@10 принят за единицу (идеал), а инфраструктура обходилась примерно в 140 000 рублей в месяц только за инстансы с нужным объемом памяти.
| Метод хранения и поиска | Потребление RAM | p95 latency | Recall@10 | Инфраструктура (₽/мес) |
|---|---|---|---|---|
| HNSW + float32 (Baseline) | ~320 GB | 12 ms | 1.000 | ~140,000 ₽ |
| HNSW + int8 (Scalar) | ~85 GB | 14 ms | 0.985 | ~45,000 ₽ |
| HNSW + MRL (512 dim) + int8 | ~28 GB | 9 ms | 0.970 | ~15,000 ₽ |
| IVF-PQ + mmap NVMe + rescoring | ~6 GB (остальное на диске) | 55 ms | 0.992 | ~8,000 ₽ |
Построить правильную стратегию хранения можно через простую матрицу зависимости объёма от метода сжатия и допустимой деградации. Если база меньше миллиона векторов, любые оптимизации — это преждевременная инженерия, просто держите всё во float32 и HNSW, ресурсы стоят дешевле времени разработчиков. В диапазоне от 1 до 10 миллионов векторов идеальным компромиссом становится скалярное квантование (int8): вы срезаете потребление памяти в четыре раза, сохраняя p95 latency на уровне единиц миллисекунд и теряя жалкие доли процента в точности. Когда счет идет на десятки и сотни миллионов, ваш путь — это жесткая обрезка по Matryoshka (если позволяет энкодер), переход на IVF-PQ индексы и вытеснение холодных данных на NVMe-накопители с обязательным оверсэмплингом и точным рескорингом кандидатов.
Перед выкаткой любых оптимизаций в продакшен необходим строгий процесс замера деградации. Сначала вы собираете золотой датасет из реальных запросов пользователей и фиксируете ответы базовой неоптимизированной системы. Затем поднимаете теневой контур с новым сжатым индексом и зеркалируете туда трафик. Вы обязаны сравнить не абстрактные метрики, а процент пересечения топ-10 результатов (Intersection over Union) между старой и новой базами. Если пересечение стабильно превышает 90%, а бизнес-метрики кликабельности (CTR) на выдаче не падают, квантование можно катить на всех. Оптимизация векторного поиска и инфраструктуры под нагрузку и бюджет — это то, с чем мы в Morana Labs сталкиваемся ежедневно, и практика доказывает, что правильная математика всегда побеждает тупую закупку железа.