Проєктування RAG для 100 мільйонів документів: шардування, гібридний пошук, витрати
Які зміни відбуваються, коли система RAG досягає мільярдів фрагментів: асинхронне завантаження, маршрутизація, шардування, гібридний пошук, переранжування, авторизація, кешування та контроль витрат.
Щоб запустити демо-версію 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
Три мільярди векторів. Об’єм необробленого зберігання їх у форматі float становить приблизно:
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
ID частинки може бути хешем елементів, які її визначають:
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 мільйонів документів це перетворюється на потребу у пошуковій інфраструктурі, розподілених системах, інженерії даних, забезпеченні безпеки та контролі витрат, що вимагає уваги одночасно. Векторні бази даних мають велике значення, як і моделі ембеддингу та LLM, проте архітектура, яка їх оточує, має ще більше значення, ніж визнають більшість посібників.
Ментальна модель, яка масштабується: зберігайте джерело інформації дешево, обробляйте дані асинхронно, ведіть версіювання всього, навмисно діліть інформацію на частини, заздалегідь впроваджуйте механізми авторизації, отримуйте дані за допомогою кількох сигналів, суворо ранжуйте результати, надавайте моделі лише високоякісні дані та оцінюйте кожен етап окремо. Коли обсяг корпусу даних досягає мільярдів частин, найкращою оптимізацією вже не є прискорення пошуку — це організація системи так, щоб основна частина системи ніколи не мала справи з великою кількістю даних корпусу.