Проектирование RAG для 100 миллионов документов: шардинг, гибридный поиск, затраты
Какие изменения происходят, когда система RAG достигает миллиардов фрагментов: асинхронная обработка данных, маршрутизация, шардинг, гибкий способ поиска, переранжирование результатов, авторизация, кэширование и контроль затрат.
Источники: Designing RAG for 100 Million Documents
Чтобы запустить демо-версию RAG, требуется один день. Необходимо разделить PDF на фрагменты, встроить их, сохранить векторы в базе данных, выбрать пять ближайших аналогов, вставить их в запрос и вызвать модель.
Такой подход вполне подходит для нескольких тысяч документов. Однако когда объем корпуса достигает 100 миллионов документов, система перестает функционировать эффективно, и вопрос меняется. Он уже не звучит как «какую базу векторных данных выбрать?», а становится таким: как сократить миллиарды потенциальных источников информации до 10–20 ключевых фрагментов, не ухудшая при этом скорость обработки, качество поиска, доступность или бюджет?
Это совершенно другая инженерная проблема. При таком масштабе RAG представляет собой распределенную систему хранения и поиска данных с привязанным к ней языковым модулем.
Что должна делать система
Чтобы сделать обсуждение более конкретным, предположим следующие требования:
- Более 100 миллионов документов в форматах PDF, HTML и DOCX
- Миллионы пользователей, задающих вопросы на естественном языке
- Цитаты, прилагаемые к каждому ответу
- Документы, которые со временем обновляются и удаляются
- Быстрое поисковое восстановление — примерно 1–3 секунды до начала генерации
- Высокая доступность
- Прием документов, происходящий асинхронно
Всё сводится к одному строгому требованию: найти нужные 10–50 единиц информации среди потенциально миллиардов фрагментов.
Начнем с арифметики
Первая ошибка в проектировании крупных систем RAG заключается в том, что документ и вектор считаются одним и тем же. Предположим:
Documents = 100,000,000
Average chunks/document = 30
Embedding dimensions = 768
Storage/dimension = 4 bytes (float32)
Количество векторов составляет:
100,000,000 × 30
= 3,000,000,000 vectors
Три миллиарда векторов. Объем необработанного хранения чисел с плавающей точкой для них составляет примерно:
3B × 768 × 4 bytes
≈ 9.2 TB
И это ещё только сами цифры. На самом деле при развертывании также требуется:
ANN index structures
chunk text
document metadata
ACL metadata
IDs
lexical indexes
replicas
snapshots
WALs
temporary indexes
operational headroom
При наличии двух копий векторного индекса объем сырых векторов уже превышает 18 ТБ, еще до учета структур поиска. Если документы в среднем состоят из 50 фрагментов, а не 30:
100M documents × 50 chunks
= 5 billion vectors
Вот почему фраза «мы поместим все фрагменты в векторную базу данных и будем выполнять поиск по сходству» не описывает архитектуру крупномасштабных систем. Она лишь указывает на точку старта.
Два независимых уровня
Самый чистый способ организации системы — разделить её на два независимых уровня: один для приема данных и другой для их поиска. Это разделение имеет большое значение. Клиент, загружающий 20 миллионов новых документов, не должен приводить к увеличению времени обработки запроса с 300 мс до 8 секунд. Прием данных оптимизируется с учетом пропускной способности, а поиск — с учетом времени отклика; попытка обрабатывать их через одну и ту же систему — это надежный способ создать себе проблемы в будущем.
Векторная база данных — не источник истины
Храните оригинальные документы в недорогих сервисах хранения объектов, таких как S3, GCS или Azure Blob. Векторная база данных представляет собой индекс, а индексы можно воссоздать. Если через шесть месяцев вы перейдете с модели эмбеддингов E1 на E2, вам потребуется сгенерировать векторы заново на основе сохраненных оригиналов, а не пытаться восстановить содержимое непосредственно из самой векторной базы данных.
Здесь также полезна версионирование объектов, поскольку обновление может быть зафиксировано в виде новой версии вместо уничтожения старой. Например, Amazon S3 позволяет хранить несколько версий объекта, а политики жизненного цикла могут перемещать более старые версии на более дешевые уровни хранения или истекать их срок действия.
Процесс ввода данных должен быть асинхронным
Предположим, клиент загружает 500 000 PDF-файлов. Вам не нужен запрос POST /documents, который сначала анализирует файлы, применяет OCR, очищает их, делиит на части, встраивает содержимое, индексирует данные, а только затем возвращает код 200. Такая обработка может занять часы. Вместо этого конечная точка должна выполнять минимум действий:
POST /documents
│
▼
Store document
│
▼
Create ingestion event
│
▼
Return 202 Accepted
Все остальное происходит параллельно, под воздействием событий. Kafka является одним из вариантов для формирования основной структуры, поскольку темы делятся на партиции, с которыми потребители могут работать одновременно, а внутри каждой партиции сохраняется порядок обработки, что удобно, когда события, связанные с одним документом или ключом абонента, требуют предсказуемой последовательности.
Конкретный инструмент имеет меньшее значение, чем свойство, которое он обеспечивает: для приема данных необходим буфер. Если ваш сервис генерации эмбеддингов обрабатывает 30 000 блоков в секунду, а вдруг ему приходится обрабатывать 100 000 блоков в секунду, система должна формировать очередь задач и не выходить из строя.
Сделайте каждый шаг приема данных идемпотентным
На таком масштабе задачи выполняются дважды. Сообщения доставляются заново. Рабочие процессы завершаются после записи данных, но до подтверждения выполнения задачи. Запросы тайм-аутят, даже если удаленная сторона уже завершила работу. Необходимо с самого начала учитывать все эти факторы, генерируя детерминистичные идентификаторы на основе стабильных входных данных:
document_id
document_version
chunk_index
embedding_model_version
Идентификатор блока может быть хешем элементов, которые его определяют:
hash(
tenant_id +
document_id +
document_version +
chunk_index
)
Получение одного и того же события индексации дважды больше не приводит к появлению дублирующегося контента, потому что
upsert(chunk_id, ...)
Можно безопасно попробовать снова. Обеспечить выполнение операции ровно один раз сложно. Обычно более реалистичной целью является идемпотентное выполнение.
Разбиение на части — это решение инфраструктуры
При небольшом объеме данных изменение размера частей кажется безвредным. Однако при миллиардах векторов это сопряжено с значительными финансовыми затратами. Возьмем 100 миллионов документов: стратегия, при которой с каждого документа получается 20 частей, приведет к общему количеству 2 миллиарда векторов, тогда как стратегия с 60 частями с каждого документа даст 6 миллиардов векторов. Изменение, казавшееся незначительным, просто увеличило их количество в три раза:
embedding compute
vector storage
indexing work
retrieval fan-out
reindexing time
replication traffic
Таким образом, разбиение на части — это не просто настройка в области NLP. Это параметр планирования емкости.
Каждая часть также должна содержать достаточно метаданных, чтобы можно было отследить её происхождение:
{
"chunk_id": "c_982...",
"document_id": "doc_51...",
"document_version": 8,
"tenant_id": "tenant_17",
"page": 43,
"section": "Risk Factors",
"language": "en",
"created_at": "...",
"acl_groups": ["finance", "executives"],
"embedding_version": "embed_v4"
}
Сам текст может храниться в другом месте в зависимости от вашей стратегии хранения, но информация о его происхождении должна сохраняться при извлечении. В противном случае цитирование рано или поздно станет проблематичным.
Не ищите во всем корпусе данных, если это не абсолютно необходимо
Это, возможно, самая важная идея всей концепции. Возьмем вопрос: «Что изменилось в нашей европейской политике возврата средств в этом квартале?» Примитивный подход заключается в поиске среди всех 3 000 000 000 векторов. Однако сам вопрос уже даёт нам много информации:
organization = current tenant
region = Europe
topic = refund policy
time = current quarter
permissions = documents user may access
Архитектура поиска звуков активно использует эти ограничения, сокращая миллиарды фрагментов до небольшого числа перед началом поиска по сходству. Точные цифры зависят от объема работы, поэтому рассматривайте их как примеры. Принцип остается прежним: сначала маршрутизация, затем поиск.
Шардинг делает векторный слой распределенным
В какой-то момент один узел не может хранить весь поисковый корпус, и индекс необходимо разделить на части. Распределенные векторные движки, такие как Qdrant, масштабируются горизонтально путем распределения точек по шардам, причем репликация позволяет размещать копии этих шардов на нескольких узлах для обеспечения доступности и дополнительной пропускной способности.
Однако есть нюанс. Если каждый запрос должен обрабатываться на каждом шарде, добавление новых шардов со временем увеличивает объем работы по координации вместо того, чтобы уменьшить его. Лучший дизайн позволяет направлять как можно больше запросов. Такие атрибуты могут помочь определить, какие шарды действительно должны участвовать в обработке:
tenant_id
region
document namespace
language
time partition
Это наиболее эффективно в системах с несколькими арендаторами. Вместо того чтобы
Query
↓
128 shards
Запрос может быть обработан
tenant_id = 429
↓
Shard group 18
↓
4 shards
Например, текущая распределённая архитектура Qdrant поддерживает пользовательское шардингование, что позволяет операциям направляться к выбранным ключам шардов и избегать воздействия на всю коллекцию. Этот принцип актуален независимо от поставщика: хорошее разделение данных снижает количество запросов.
Репликация решает другую проблему
Люди часто путают шардинг и репликацию, но они решают разные задачи. Шардинг помогает разделить набор данных, слишком большой для одной машины. Репликация отвечает на вопрос о том, что произойдёт, если одна из этих машин выйдет из строя. Представьте следующую схему:
Shard A → Node 1
Shard B → Node 2
Shard C → Node 3
Если умирает узел 2, исчезает и соответствующий фрагмент корпуса. При репликации:
Shard A → Node 1 + Node 4
Shard B → Node 2 + Node 5
Shard C → Node 3 + Node 6
Система продолжает обрабатывать запросы даже при сбоях отдельных узлов. Редундантность требует дополнительных ресурсов для хранения данных, пропускной способности при записи и усилий по эксплуатации, но поисковый кластер в производственных условиях без неё сталкивается с серьезными проблемами при сбоях.
Одного векторного поиска недостаточно
Представьте, кто-то спрашивает: «Что гласит политика HR-48291?» Семантический поиск может вернуть информацию о политиках в области кадров в целом, тогда как лексический поиск отлично справляется с поиском точного токена HR-48291. А теперь другой вопрос: «Могут ли сотрудники брать дополнительный отпуск после рождения ребенка?» Здесь семантический поиск оказывается более полезным. Оба подхода пересекаются, но охватывают разные аспекты, поэтому обычно выбирают гибридную схему поиска, сочетающую оба метода.
Текущие рекомендации Elastic по гибридному поиску следуют тому же подходу: они сочетают лексический и векторный поиск, предлагая метод Reciprocal Rank Fusion в качестве способа объединения рейтингов. Преимущество метода RRF заключается в том, что не требуется притворяться, будто значения BM25 и косинусного сходства являются сопоставимыми числами — вместо этого объединяются позиции в рейтинге. В упрощённой форме:
RRF(d) = Σ 1 / (k + rank_i(d))
где rank_i(d) — это позиция документа d в системе поиска i.
Будьте осторожны с последовательным предфильтрованием
Существует соблазнительный упрощённый способ:
3B documents
↓
BM25 returns 10,000
↓
vector search only those
Этот метод может сработать для некоторых нагрузок, но он также может разрушить способность к семантическому поиску. Представьте, что пользователь вводит фразу «Какие правила действуют в отношении сотрудников, которые усыновляют?», а документ гласит: «Сотрудники, принимающие ребенка путем усыновления, имеют право на шестнадцать недель семейного отпуска». Пересечение ключевых слов в этом документе с формулировкой пользователя минимально. Если алгоритм BM25 исключит его еще до того, как семантический поисковик сможет его обработать, никакая модель вкладок не сможет его спасти.
Поэтому, помимо строгих ограничений маршрутизации, лучшей практикой является одновременное генерирование кандидатов с помощью обоих поисковых механизмов, а затем переранжирование объединенного набора. Сами строгие ограничения относятся к отдельной категории:
tenant
permissions
document status
Обычно их следует применить до начала получения данных.
Авторизация происходит до того, как LLM увидит что-либо
Представьте корпоративный корпус данных, содержащий:
Engineering documents
HR documents
Board documents
Legal documents
Executive compensation
Customer contracts
Семантическая релевантность — это не разрешение. Документ финансового директора может математически наиболее точно соответствовать вопросу сотрудника, но это всё равно не даёт сотруднику права на его прочтение. Неправильный подход выглядит следующим образом:
Vector Search
↓
Retrieve confidential chunk
↓
Send chunk to LLM
↓
Check permission
К моменту выполнения проверки разрешений конфиденциальный текст уже был передан в следующий компонент. Правильный порядок действий предусматривает сначала определение того, кто является пользователем:
User
↓
Identity
↓
Groups / roles / ACL
↓
Authorized search space
↓
Retrieval
Это означает, что метаданные управления доступом должны эффективно участвовать в процессе поиска. При очень больших масштабах схема ACL, требующая сканирования миллионов записей после поиска, превращается как в проблему безопасности, так и в проблему производительности.
Поиск генерирует кандидатов, а не контекст
Допустим, гибридный метод поиска даёт 400 кандидатов. Не включайте все 400 элементов в запрос. Сначала перейдите их в рангирование:
Vector candidates: 200
Lexical candidates: 200
│
▼
Fusion
│
▼
~250 unique chunks
│
▼
Reranker
│
▼
top 20–40
Поиск первой стадии предназначен для быстрого нахождения вероятно релевантных элементов. Ранжировщик используется для другой задачи: при работе с гораздо меньшим набором данных он более тщательно оценивает релевантность. Такая двухстадийная схема позволяет дорогостоящей модели релевантности обрабатывать сотни элементов вместо миллиардов.
Затем удаляется избыточность
Даже хороший ранжировщик может дать результат примерно такого рода:
Chunk 1 → page 14
Chunk 2 → page 14
Chunk 3 → page 15
Chunk 4 → page 14
Chunk 5 → page 15
Все пять вариантов могут по сути выражать одно и то же, и использование всех из них приводит к трате токенов контекста. Прежде чем сформировать окончательный запрос, выполните некоторую комбинацию следующих шагов:
deduplication
document diversity
section diversity
near-duplicate detection
adjacent-chunk merging
token budgeting
Иногда для правильного понимания текста также необходимы соседние фрагменты. Если в блоке 47 содержится ответ, но он начинается посреди объяснения, инструмент построения контекста может загрузить соседние данные.
chunk 46
chunk 47
chunk 48
Вместо того чтобы надеяться, что векторный поиск самостоятельно извлечёт каждый непрерывный фрагмент.
Построение контекста требует отдельного слоя
Удивительно распространённая реализация рассматривает контекст лишь как следующее:
context = "\n\n".join(retrieved_chunks)
Системы производства работают лучше при наличии специализированного инструмента для формирования контекста. Его задача — использовать отсортированные доказательства для создания эффективного пакета запросов без потери информации о цитатах. Концептуально:
Retrieved Evidence
↓
Deduplicate
↓
Merge related chunks
↓
Enforce token budget
↓
Preserve source IDs
↓
Order evidence
↓
Generate context
Модель может затем получить что-то структурно похожее на это:
SOURCE S1
Document: Employee Handbook
Page: 42
Version: 18
Text: ...
SOURCE S2
Document: European Leave Addendum
Page: 7
Version: 4
Text: ...
QUESTION
...
И вам говорят указывать источники S1, S2 и так далее. Это гораздо безопаснее, чем просить модель воссоздать URL-адреса или идентификаторы документов из памяти.
Понимание запросов должно быть простым
Некоторые запросы выигрывают от нормализации перед поиском. Пользователь вводит: «На сколько вырос доход в Европе в прошлом году?» Учитывая текущую дату и собственную терминологию компании, поиск может дать лучшие результаты при использовании структурированных полей, извлеченных из этого предложения:
metric = revenue
region = Europe
year = 2025
intent = financial comparison
В разговоре вопрос вроде «А как насчёт Азии?» требует переформулировки с учётом предыдущего контекста:
How much did revenue grow in Asia in 2025?
Суть заключается в том, что для выполнения этих шагов редко требуется самая дорогая модель обработки информации. Используйте самый дешевый компонент, который надежно справляется с задачей. Готовая стековая архитектура ИИ обычно выглядит следующим образом:
small model
↓
classification
small model
↓
query rewriting
embedding model
↓
retrieval
reranker
↓
relevance
large model
↓
final reasoning
а не вот так:
largest model everywhere
Разбиение вопросов с несколькими этапами обработки
Некоторые вопросы невозможно надёжно ответить на основе одного только отрывка текста. Вопрос вроде «Какой европейский продукт показал наибольшее снижение доходов, и как руководство это объяснило?» разделяется на два поиска:
Question 1:
Which European product had the largest decline?
Question 2:
What explanation did management provide for that product?
Затем система сбора информации может отдельно собирать доказательства для каждого подвопроса, а результаты объединяются перед генерацией ответа.
Complex Query
│
▼
Query Decomposer
/ \
/ \
▼ ▼
Subquery A Subquery B
│ │
▼ ▼
Retrieval Retrieval
\ /
\ /
▼ ▼
Evidence Join
│
▼
LLM
На этом этапе масштабные системы RAG начинают больше походить на системы планирования запросов, чем на простые методы поиска по ближайшему соседу.
Свежесть данных влияет на способ индексации
Предположим, что в корпусе содержится 100 млн документов, из которых 2 млн меняются ежедневно. Пересоздание индекса каждую ночь, вероятно, не является желаемым решением, поэтому требуется инкрементальная индексация:
Document updated
│
▼
new document version
│
▼
parse changed document
│
▼
generate new chunks
│
▼
embed
│
▼
write new index entries
│
▼
mark previous version inactive
Логическое версионирование лучше, чем немедленная уничтожающая замена. Например, для одного документа:
document_id = D17
version 41 → status=inactive
version 42 → status=active
Фильтрация запросов по
status = active
Старые записи могут удаляться в фоновом режиме с помощью механизма сбора мусора. С точки зрения приложения обновление происходит атомарно, и нет необходимости, чтобы все системы хранения менялись одновременно.
Версионируйте также модели
Рано или поздно вам придётся сменить модели встраивания, и вы никогда не должны хранить векторы, не зная, что их сгенерировало. По крайней мере, сохраняйте информацию, позволяющую отследить их происхождение:
embedding_model
embedding_version
embedding_dimensions
chunking_version
parser_version
В противном случае через шесть месяцев вы можете обнаружить, что 15% индекса было создано с использованием устаревшей схемы обработки, и у вас не будет простого способа определить, какие именно 15%. Миграция может выполняться одновременно с несколькими индексами:
Documents
│
┌─────────┴─────────┐
▼ ▼
embedding model V3 embedding model V4
│ │
▼ ▼
Index V3 Index V4
│
▼
shadow traffic
│
▼
evaluate
│
▼
switch alias
Не стоит перемещать миллиарды векторов в надежде на то, что новый способ поиска будет лучше. Измерьте его эффективность перед сменой.
Кэширование на нескольких уровнях
Не каждый запрос требует применения всей полной процедуры обработки. Если тысячи сотрудников задают вопрос «Сколько у нас дней отпуска?», то не стоит
embedding
→ distributed retrieval
→ reranking
→ large LLM
Каждый отдельный случай является расточительством. Более логично использовать многоуровневую систему кэшей:
User Query
│
▼
Exact Response Cache
│ miss
▼
Semantic Cache
│ miss
▼
Retrieval Cache
│ miss
▼
Search
Очевидным дополнением является кэш вжиманий:
hash(normalized_query)
↓
query embedding
Ситуация усложняется, когда меняется контент. Ответ, сохранённый в кэше,
Policy version 17
может ошибиться хотя бы один раз
Policy version 18
Начинается работа. Поэтому ключ кэша или схема аннулирования должны учитывать границы корпуса и версий. Хранение результатов модели без правил свежести опасно.
Планируйте сценарии сбоев, а не только успешное выполнение
Большие системы выходят из строя постепенно. Ожидайте таких ситуаций:
embedding provider unavailable
one vector shard unavailable
search cluster degraded
reranker timeout
LLM rate limited
LLM provider outage
queue backlog growing
parser crashing on malformed PDF
OCR worker running out of memory
Каждая ситуация с сбоем требует четкой политики. Возможно:
Vector search fails
↓
Can lexical search answer?
↓
yes
↓
return degraded retrieval mode
или:
Primary LLM unavailable
↓
Fallback model
↓
Lower quality but service continues
или:
Document parser fails 5 times
↓
Dead-letter queue
↓
record failure reason
↓
operator / automated remediation
Бесконечные попытки повторения — это не устойчивость. Иногда это просто бесконечный цикл с дополнительными затратами на облачные сервисы.
Обратное давление
Представьте, что процесс обработки данных обычно получает
20M chunks/hour
а емкость встраивания может обрабатывать
25M chunks/hour
Всё в порядке. Затем клиент выполняет миграцию и получает
200M chunks/hour
При отсутствии обратного давления сбой происходит следующим образом:
embedding service overloaded
↓
timeouts
↓
retries
↓
more load
↓
more failures
↓
retry storm
С буфером перед рабочими:
incoming workload
↓
durable queue
↓
workers consume at safe rate
↓
backlog temporarily grows
Теперь пропускная способность может настраиваться независимо от скорости поступления данных. Это ещё одна причина, по которой у уровня обработки данных должна быть собственная архитектура.
Возможность наблюдения должна включать контроль качества извлечения данных
Обычные отчёты о мониторинге показывают такие вещи, как:
HTTP 200 rate
CPU
memory
disk
p95 latency
error rate
requests/second
Все это полезно, но сервис RAG может вернуть код HTTP 200 за 800 мс и при этом дать совершенно неверный ответ. Также необходимы телеметрические данные, специфичные для ИИ. Полезная трассировка для каждого запроса может выглядеть так:
request_id: rq_912
query rewrite:
"parental policy?" → "parental leave policy"
vector retrieval:
200 candidates
145 ms
lexical retrieval:
200 candidates
91 ms
fusion:
278 unique candidates
reranking:
278 → 20
212 ms
context:
13 chunks
8,420 tokens
LLM:
input 9,104 tokens
output 681 tokens
1.8 sec
citations:
3 documents
total:
2.4 sec
Для офлайн-оценки необходимы показатели, которые позволяют ответить на такие вопросы: были ли найдены правильные доказательства, занимают ли они достаточно высокое место в рейтинге, подтверждается ли ответ этими доказательствами, улучшилась ли задача ранжирования настолько, чтобы оправдать свою задержку, и какие категории запросов чаще всего дают сбои? Это более практично, чем отслеживание галлюцинаций в виде единого глобального процента.
Оценивайте поиск отдельно от генерации
Когда окончательный ответ неверен, существует как минимум две разные категории сбоев. При сбое в поиске необходимые доказательства так и не попадают в контекст:
Correct document exists
↓
retriever misses it
↓
LLM never sees it
↓
wrong answer
Никакая правка промпта этого не исправит. При сбое генерации подходящие доказательства всё же поступили:
correct evidence
↓
context
↓
LLM
↓
incorrect interpretation
Теперь могут стать важными дизайн промпта, выбор модели, организация контекста или стратегия рассуждений. Если оценивать только окончательные ответы, они кажутся идентичными, поэтому серьезная система оценки должна анализировать промежуточные этапы.
Установите лимит на задержку
Фраза «Система должна работать быстро» не является требованием. Вместо этого установите лимит для каждого этапа. Для примера предположим, что цель — получить первый ответ примерно за 2,5 секунды, при этом распределение времени может выглядеть следующим образом:
API + auth 50 ms
query understanding 100 ms
retrieval 300 ms
fusion 20 ms
reranking 250 ms
context construction 30 ms
LLM time-to-first-token 1,200 ms
network / safety margin 550 ms
-----------------------------------
target ~2,500 ms
Это не универсальные цифры. Важность упражнения заключается в том, что оно показывает, куда на самом деле уходит время. Если на извлечение данных уходит 1,8 секунды, смена поставщиков LLM не повлияет на основное затруднение. Если LLM отвечает за 90% задержки, то тратить месяц на сокращение времени векторного поиска на 40 мс вряд ли будет наилучшим использованием усилий.
С расходами тоже необходима строгость
Большая система RAG имеет множество отдельных центров затрат:
object storage
document parsing
OCR
embedding generation
vector storage
vector replicas
lexical search
metadata database
network transfer
reranking
LLM input tokens
LLM output tokens
observability
backups
Итак, рассчитайте стоимость на одну полезную единицу, например:
cost / 1,000 indexed documents
cost / million chunks
cost / query
cost / successful answer
cost / tenant
Последний из этих факторов имеет наибольшее значение в SaaS. Вы можете обнаружить:
Tenant A
5% of revenue
42% of LLM spend
Без индивидуального учета потребления каждым арендатором вы видите лишь неожиданно высокий ежемесячный счет за инфраструктуру.
Используйте дорогое хранилище только там, где это действительно необходимо
Полезной аналогией может служить градиент от дорогого и быстрого к дешевому и объемному:
expensive / fast
▲
│
vector indexes
search indexes
Redis caches
│
metadata DB
│
object storage
│
▼
cheap / large
Полная версия PDF не имеет причин храниться в векторном индексе. Откладывайте дорогостоящее хранилище для тех случаев, когда оно действительно нужно, пусть объектное хранилище содержит исходные документы, а поисковая инфраструктура — их поисковые представления. Когда объем корпуса данных достигает сотен терабайт, это различие становится крайне важным.
Горячие и холодные данные
Не все 100 миллионов документов запрашиваются с одинаковой частотой. Возможно:
Last 30 days → 70% of queries
Last 12 months → 25%
Older archive → 5%
Равномерное обращение со всем приводит к расточительству инфраструктуры. Одним из вариантов является следующее разделение:
Query
│
├────→ Hot index
│
└────→ Archive index when necessary
Маршрутизация на основе времени также предотвращает смешивание информации о событиях текущей недели с десятилетиями исторических данных. Большие системы становятся эффективными, избегая лишней работы, а не просто выполняя её быстрее.
Многоклиентская архитектура меняет всё
Предположим, ваша платформа обслуживает
10,000 organizations
Вы вряд ли захотите это.
10,000 completely independent vector clusters
Однако сосредоточение всех арендаторов в одном пространстве имён приводит к проблемам в виде «шумных соседей» и сложностей с авторизацией. Рабочий подход заключается в группировке арендаторов по их объёму:
Small tenants
↓
shared shard groups
Medium tenants
↓
partitioned shared infrastructure
Very large tenants
↓
dedicated shard groups
Это предотвращает ситуацию, когда один клиент импортирует 500 миллионов блоков, засоряя тысячи более мелких, и обеспечивает способ изоляции для корпоративных клиентов, которым это необходимо. Ни одна стратегия разделения тенантов не подходит всем.
LLM находится ближе к концу
Посмотрите, сколько аспектов инженерии мы рассмотрели до того, как добрались до самой модели. Это сделано намеренно. Запрос в рамках зрелой системы RAG проходит следующим образом:
User
↓
API Gateway
↓
Authentication
↓
Authorization
↓
Rate Limiter
↓
Query Understanding
↓
Shard Routing
↓
Hybrid Candidate Retrieval
↓
Rank Fusion
↓
Reranker
↓
Deduplication
↓
Context Builder
↓
LLM
↓
Citation Validation
↓
Response
LLM действительно важен, но при объеме в 100 миллионов документов это лишь один из многих компонентов.
Полная картина производства
Если объединить всё вместе, вот путь обработки запроса, затем путь загрузки данных, а также системы, которые поддерживают оба процесса:
CLIENTS
│
▼
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
┌──────▼───────┐
│ Auth / ACL │
│ Rate Limits │
└──────┬───────┘
│
▼
┌────────────────────┐
│ Query Orchestrator │
└─────────┬──────────┘
│
┌─────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Cache Query Rewrite Metadata
Filters
│ │ │
└─────────────┼──────────────┘
▼
Shard Router
│
┌────────────┴────────────┐
▼ ▼
Lexical Search Vector Search
│ │
└───────────┬─────────────┘
▼
Rank Fusion
│
▼
Reranker
│
▼
Deduplicate
│
▼
Context Builder
│
▼
LLM
│
▼
Citation Validation
│
▼
RESPONSE
================================================================
INGESTION SYSTEM
Documents
│
▼
Object Store
│
▼
Event Queue
│
┌────────────┼────────────┐
▼ ▼ ▼
Parser Parser Parser
│ │ │
└────────────┼────────────┘
▼
Normalization
│
▼
Chunker
│
┌──────────────┴──────────────┐
▼ ▼
Embedding Queue Text Index Queue
│ │
┌─────┼─────┐ ▼
▼ ▼ ▼ Lexical Index
GPU GPU GPU
│ │ │
└─────┼─────┘
▼
Vector Index
================================================================
SUPPORTING SYSTEMS
Metadata DB → document state, versions, ownership
Object Storage → original source documents
Vector Cluster → semantic retrieval
Search Cluster → lexical retrieval
Redis → caches and rate limiting
Message Queue → asynchronous ingestion
Observability → traces, metrics, evaluation
Secrets / IAM → service authorization
Evaluation System → retrieval + answer-quality tests
Антипаттерны, которых следует избегать
При рассмотрении подобной архитектуры следующее вызывает немедленную озабоченность:
One giant vector collection with no routing strategy
Synchronous PDF ingestion
Only vector retrieval, no lexical search
ACL filtering after retrieval
No document versioning
No embedding model version
No reranking
Sending top-100 chunks directly to the LLM
Using the LLM for every trivial classification task
No dead-letter queue
No retrieval-level evaluation
No tenant-level cost attribution
Treating the vector DB as permanent document storage
Re-embedding the entire corpus for every small content change
Benchmarking only average latency instead of tail latency
Любой из них может использоваться в качестве прототипа. Вместе, в масштабе миллиардов векторов, они становятся чрезвычайно проблематичными.
Основной принцип
Самый полезный способ понимания крупных систем RAG — это не такой:
question
→ vector search
→ LLM
Вот это:
billions of possible evidence units
↓
routing constraints
↓
relevant partitions
↓
cheap candidate search
↓
hundreds of items
↓
expensive reranking
↓
tens of passages
↓
context optimization
↓
LLM
↓
grounded answer
Система работает как воронка. На каждом этапе мы тратим больше вычислительных ресурсов на меньшее количество кандидатов:
Cheap operation → huge search space
Moderate operation → smaller candidate set
Expensive operation → tiny candidate set
Most expensive model → final context only
Именно эта идея делает архитектуру экономически жизнеспособной.
Что взять с собой
Система RAG на несколько тысяч документов в основном представляет собой задачу прикладной инженерии. При объеме **100 миллионов документов** она превращается в инфраструктуру поиска, распределенные системы, инженерию данных, меры безопасности и контроль затрат, все из которых требуют одновременного внимания. Векторные базы данных имеют большое значение, как и модели эмбеддингов и большие языковые модели, но архитектура, окружающая их, имеет еще большее значение, чем признают большинство учебных материалов.
Модель мышления, способная к масштабированию: хранить исходные данные дешево, обрабатывать их асинхронно, вести версионирование всего, целенаправленно делить данные на части, заранее обеспечивать авторизацию, получать информацию с использованием нескольких сигналов, строго ранжировать результаты, предоставлять модели только качественные данные и оценивать каждый этап отдельно. Когда объем корпуса данных достигает миллиардов элементов, лучшей оптимизацией уже не является ускорение поиска — это организация работы системы так, чтобы основная часть системы никогда не взаимодействовала с большей частью корпуса данных.