ІнфраструктураSenior

Векторні бази даних

Векторна база відповідає на питання «що найбільш схоже на це» серед мільйонів обʼєктів за мілісекунди — і робить це, не дивлячись на більшість із них. У цьому й уся угода: кожна відповідь наближена, а наскільки саме — це число, яке ви задаєте, а не властивість продукту. На цій сторінці — що насправді зберігається, родини індексів і чого кожна коштує в памʼяті, чому фільтрація за метаданими складніша, ніж здається, що роблять з індексом видалення й переобчислення ембедингів, і коли колонка в базі, яку ви вже тримаєте, є кращою відповіддю.

Кожна відповідь
Наближена
Повнота — це
Налаштування
Обмежує зазвичай
Памʼять, не CPU

Угода, яку ви насправді укладаєте

Модель ембедингів перетворює шматок тексту, зображення чи товар на список чисел — зазвичай від кількох сотень до кількох тисяч — розміщених так, що речі, які людина назвала б схожими, опиняються поруч. Щойно зміст стає координатами, «знайти повʼязані документи» перетворюється на «знайти найближчі точки» — задачу геометрії з довгою історією й однією незручною властивістю: розвʼязати її точно означає виміряти відстань до кожної наявної точки. На десяти тисячах обʼєктів це миттєво. На пʼятдесяти мільйонах це вже не запит, а пакетна задача.

Тому кожна векторна база на масштабі відмовляється від точності. Вона будує індекс, що дозволяє пошуку відвідати кількасот перспективних кандидатів замість пʼятдесяти мільйонів, і мириться з тим, що справді ближчий обʼєкт іноді лишається невідвіданим. Частку справжніх найближчих сусідів, яку пошук насправді повертає, називають повнотою (recall), і найважливіше, що варто зрозуміти про ці системи: повнота — це регулятор. Підкрутіть його вгору — і ви відвідуєте більше кандидатів за більшу затримку й більше CPU; вниз — і дістаєте швидку відповідь, яка тихо щось пропускає.

Векторний індекс наближений за самою побудовою. Графовий індекс тримає розріджений верхній рівень для довгих стрибків і дедалі щільніші рівні під ним; запит входить згори, жадібно йде до ближчих сусідів і спиняється, коли жоден сусід не покращує результат. Цей обхід швидкий, бо відвідує кілька десятків точок замість мільйонів, — і наближений рівно з тієї ж причини: справді ближчу точку, на яку не веде жодне ребро від відвіданих вузлів, ніхто не побачить. Скільки кандидатів обхід тримає в роботі — це параметр часу пошуку: efSearch у графовому індексі, nprobe в індексі з інвертованими списками. Піднімати його означає купувати повноту за затримку: приблизно девʼяносто один відсоток за вісім мілісекунд проти девʼяноста девʼяти за двадцять шість. Тому повнота — це число, яке ви обираєте, а не властивість бази, і єдиний спосіб дізнатися своє — порівняти індекс із повним перебором на вибірці справжніх запитів.

Одна деталь підводить людей раніше, ніж усе решта починає важити: метрика відстані має бути тією, під яку тренували модель ембедингів. Косинусна подібність порівнює напрямок і ігнорує довжину; скалярний добуток винагороджує і довші вектори, і співнапрямлені; евклідова відстань міряє пряму лінію між точками. Візьміть скалярний добуток на векторах, які модель очікує нормалізованими, — і популярні обʼєкти домінуватимуть у кожному результаті з причин, що не мають стосунку до релевантності. Дивіться картку моделі, беріть названу нею метрику й нормалізуйте, якщо вона так каже.

Родини індексів — і чого коштує кожна

Кожен продукт на ринку реалізує якусь підмножину чотирьох ідей, зазвичай під власними назвами. Знати ідеї, а не бренди, — саме це дозволяє чесно читати бенчмарк, бо бенчмарк, що порівнює два продукти, майже завжди порівнює дві конфігурації індексу.

  • Flat — без індексу

    Порівняти з усім. 100% повноти, лінійна вартість, без побудови й без параметрів. Правильний вибір до кількох сотень тисяч векторів — і базовий рівень, проти якого міряють решту індексів.

  • IVF — інвертовані списки

    Один раз кластеризувати простір, потім шукати лише в кількох найближчих кластерах. Регулятор — nprobe. Дешевий у побудові й у памʼяті, псується, коли дані відходять від кластеризації, на якій його збудовано.

  • HNSW — навігований граф

    Шари звʼязків: розріджені згори для довгих стрибків і щільні знизу для точності. Найкраща повнота на мілісекунду в загальному вжитку й типовий вибір майже всюди. Регулятор — efSearch; сам граф коштує відчутної памʼяті понад вектори.

  • Квантизація — PQ та інші

    Не структура пошуку, а стиснення: зберігати наближення кожного вектора, часто у 4–32 рази менше. Кладеться під IVF чи HNSW, коли памʼять є звʼязувальним обмеженням, — ціною ще трохи повноти.

Арифметику памʼяті варто зробити на папері до того, як щось обирати, бо саме вона зазвичай вирішує. Вектор на 1536 вимірів у 32-бітних числах — це 6144 байти. Мільйон таких — близько 6 ГБ самих лише векторів, а граф HNSW над ними додає ще приблизно 20–40% на звʼязки, тобто 8 ГБ до корисного навантаження, реплік і запасу. Десять мільйонів — це машина, що коштує справжніх грошей щомісяця. Два рухи різко це зменшують: узяти модель ембедингів із меншою розмірністю, якщо на ваших даних вона працює порівнянно, і зберігати вектори як 8-бітні цілі там, де втрата повноти прийнятна, — це рівно вчетверо менше.

Фільтрація — там, де індекс воює з вами

Майже кожен реальний запит має причеплену умову: лише цей робочий простір, лише опубліковані документи, лише в наявності, лише цією мовою, не видалені. У реляційній базі це нічим не примітно. Тут це найскладніше на сторінці, бо індекс збудовано над геометрією всієї колекції й він нічого не знає про вашу умову. Є три способи їх помирити, і кожен ламається по-своєму.

ПідхідЯк працюєДе ламається
Пост-фільтрЗнайти в індексі k кандидатів, потім відкинути ті, що не проходять умову.Вибіркова умова спорожняє результат. Просите 10, підходить 2% — дістаєте 0: класичний баг «пошук нічого не повертає».
Пре-фільтрСпершу розвʼязати умову, потім шукати лише в множині, що підійшла.Якщо множина велика, це майже повний перебір, і індекс перестає окупатися.
Фільтрований пошукУмова обчислюється під час обходу графа, тож обхід рухається лише дозволеними вузлами.Найкраща відповідь і те, що роблять добрі рушії, — але зі зростанням вибірковості фільтра повнота тихо падає.

Практичне правило — робити межі з високою вибірковістю структурними, а не фільтром. Якщо кожен запит обмежений одним тенантом, робочим простором чи мовою, дайте кожному власну колекцію або партицію — і проблема зникає: ви шукаєте в малому індексі, а не фільтруєте великий. Лишайте фільтри для умов, що справді змінюються від запиту до запиту, а будь-який фільтр, що вибирає менше кількох відсотків колекції, вважайте сигналом, що дані від початку варто було розділити.

Записи, видалення й міграція, яку ніхто не планує

Векторні індекси збудовані для читання. Графовий індекс може приймати вставки під час обслуговування, але кожна вставка додає звʼязки до наявної структури, і граф, який увібрав мільйони вставок, вимірювано гірший за ті самі дані, проіндексовані з нуля, — повнота повзе вниз без жодної помилки. З видаленнями гірше: більшість рушіїв позначає запис як вилучений замість того, щоб полагодити граф, тож видалений вектор і далі займає памʼять і далі обходиться, а колекція з великим оборотом поволі наповнюється надгробками. І те, і те лікується періодичною перебудовою — експлуатаційною задачею, яку хтось має мати за свою й планувати.

  • Наповнюйте пакетно, а не по одному вставленню: побудувати індекс один раз над усім набором і швидше, і краще звʼязано.
  • Вважайте видалення запитом, який буде вшанований у результатах негайно, а в памʼяті — пізніше, і перевірте, що саме гарантує ваш рушій.
  • Плануйте перебудови за оборотом, а не за календарем: колекція, де замінено 20% записів, дозріла — байдуже, яке число.
  • Тримайте вихідний текст і метадані у своїй основній базі, а векторне сховище — як похідний індекс, який можна знести й відбудувати.
  • Зберігайте назву й версію моделі ембедингів на кожному записі, бо одного дня в колекції одночасно опиняться два покоління.

Чи потрібна вам окрема база взагалі

Чесний вибір за замовчуванням для більшості продуктів — розширення в базі, яку ви вже тримаєте. Postgres із pgvector підтримує ті самі родини індексів, дає транзакції, зʼєднання й одну історію бекапів і дозволяє фільтру бути звичайним WHERE, який обчислює планувальник, обізнаний з іншими вашими колонками. Цього перестає вистачати на масштабі, до якого більшість проєктів ніколи не доходить, — приблизно тоді, коли вектори перестають комфортно вміщатися в памʼять поряд із рештою роботи бази або коли потрібен шардований, незалежно масштабований пошук із репліками на колекцію.

Розширення у вашій наявній базі

  • Одна система для бекапу, моніторингу, захисту й оплати, а вектори — усередині ваших наявних транзакцій.
  • Фільтри — це звичайний SQL по звичайних колонках, із зʼєднаннями до решти ваших даних.
  • Немає розриву узгодженості: рядок і його вектор пишуться разом або ніяк.
  • Впирається в стелю, коли вектори конкурують за памʼять із рештою навантаження.

Спеціалізована векторна база

  • Масштабує шар пошуку окремо: шардування, репліки й тюнінг індексу — керунки першого класу.
  • Фільтрований пошук реалізовано всередині обходу, а не прикручено навколо нього.
  • Друге сховище, яке треба тримати синхронним, із власними режимами відмов, бекапами й контролем доступу.
  • Виправдана приблизно від десяти мільйонів векторів або коли навантаження пошуку має масштабуватися окремо від записів.

І остання річ, що рятує проєкти: векторний пошук не завжди найкращий. Він сильний на перефразуванні й слабкий на точних токенах — артикулах, кодах помилок, іменах, рідкісних абревіатурах, — саме на тих запитах, де користувачі чекають буквального збігу й найменш поблажливі. Поєднання лексичного індексу з векторним і злиття двох списків результатів надійно перемагає кожен окремо — і це часто найдешевше доступне покращення якості. Якщо ваша оцінка показує, що векторний індекс програє звичайному пошуку за ключовими словами на ваших власних запитах, — вірте оцінці. Як проводити цю оцінку, належить сторінці про RAG, що володіє якістю пошуку як вимірюваною властивістю.

Антипатерни, які варто назвати

  • Реліз без жодного виміру повноти: ніхто не відрізнить проблему налаштувань від проблеми моделі.
  • Пост-фільтр по вибірковій умові — і порожній результат, поданий як «пошук зламався».
  • Метрика відстані, що не відповідає моделі ембедингів, — дає правдоподібну на вигляд нісенітницю.
  • Векторне сховище як єдина копія тексту: перебудова означає відновлення даних, яких у вас уже немає.
  • Окремий кластер заради двохсот тисяч векторів, де колонка в наявній базі була б і швидшою в запитах, і безкоштовною в експлуатації.
  • Багатотенантність, забезпечена фільтром, а не межею: одна забута умова — це розкриття даних.

Коли це застосовувати

Застосовуйте, коли

  • Пошук за змістом, а не за словами: статті підтримки, документація, каталоги товарів, де користувачі перефразовують.
  • Пошук, що живить мовну модель, де кілька найкращих результатів стають контекстом, з якого будується відповідь.
  • Рекомендації й дедуплікація за схожістю, де запитом є «щось таке, як оце», і немає ключового слова для збігу.
  • Колекції, достатньо великі, щоб точний перебір перестав бути миттєвим, — приблизно понад кількасот тисяч векторів.

Уникайте, коли

  • Запити з точними токенами — номери замовлень, артикули, коди помилок, — де лексичний індекс і швидший, і правильний.
  • Колекції, достатньо малі для точного перебору: індекс додає параметри, графік перебудов і наближеність — ні за що.
  • Як систему обліку. Це похідний індекс; джерело істини належить базі з транзакціями й бекапами.
  • До того, як зʼявився спосіб міряти якість пошуку: без нього кожне рішення про налаштування — справа смаку.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.