Як вибрати embedding-модель для української, коли жоден бенчмарк вам не підходить
Вибір embedding-моделі для RAG зазвичай виглядає просто: відкриваєш MTEB leaderboard, дивишся на топ, береш першу модель, яка влазить у бюджет. Для англійської це працює. Для української юридичної — ні, і нижче я покажу, чому.
Про проєкт
Юстай — RAG-чатбот для консультацій з українського законодавства: користувач ставить питання українською, система знаходить релевантні фрагменти офіційних документів і передає їх LLM разом із питанням (покликати можна тут — yust.ai). Якість retrieval тут — головне вузьке місце: якщо потрібна стаття закону не потрапила в top-K результатів пошуку, жодна LLM її не «довигадає» (точніше, довигадає — і це буде галюцинація).
Вектори ми зберігаємо в PostgreSQL з pgvector, а не в окремому векторному сервісі (Qdrant, Weaviate, Milvus). Наш корпус — ~308 тисяч чанків з ~1040 документів; для такого масштабу pgvector з HNSW-індексом більш ніж достатній: одиниці мілісекунд на 11 тисячах рядків, десятки — на 308 тисячах. А операційний виграш від однієї бази суттєвий: немає синхронізації між метаданими чанків і векторами, один бекап, одна модель доступу, і гібридний пошук (vector + FTS + trigram) живе в одному engine.
Перший pipeline працював на OpenAI text-embedding-3-small. Питання стояло так: чи залишаємо якість на столі, і скільки коштує це перевірити? Виявилося — менше $0.50 в API-витратах, а результат — +15 процентних пунктів Recall@5. Нижче повна історія, включно з несподіванкою з Matryoshka-розмірностями.
Про дати: бенчмарк ми проводили у квітні
Що таке embedding-модель
Embedding-модель перетворює текст на вектор — масив із сотень або тисяч чисел, у якому семантично близькі тексти отримують схожі вектори. «Розірвання шлюбу» і «як розлучитися» опиняться поруч у векторному просторі, хоча не мають жодного спільного слова — саме тому векторний пошук знаходить те, що пропустив би пошук за ключовими словами.
У RAG це працює так: кожен фрагмент корпусу один раз перетворюється на вектор і зберігається в базі; запит користувача теж перетворюється тією ж моделлю, і система шукає найближчі до нього вектори (у нас — за косинусною відстанню). Якість embedding-моделі напряму визначає, чи потраплять правильні фрагменти в top-K — тобто це перша ланка, від якої залежить все інше.
Проблема: MTEB не знає про українську юридичну
MTEB (Massive Text Embedding Benchmark) — стандартний спосіб порівнювати моделі embedding. Там сотні моделей, десятки задач, зручні фільтри. Мені потрібні були два фільтри одночасно: мова = українська, домен = legal.
Що я знайшов:
- Український пресет: 203 моделі, ранжовані за українськими завданнями. Але всі задачі — у загальному домені: Вікіпедія, новини, відгуки. Юридичних текстів немає.
- Legal-пресет (MTEB Law, RTEB Legal): юридичний retrieval є — але тільки англійською та німецькою (AILA, GerDaLIR, LegalQuAD...). Української немає взагалі.
- Комбінований фільтр «українська + legal»: 0 задач. Перетин порожній.
Тобто, прочитати готове число з leaderboard неможливо. Але часткові фільтри все одно дали корисний сигнал.
Що показав український general-domain пресет
Retrieval-оцінки для української (MTEB multilingual, відфільтрований по укр. задачах):
| Модель | UK Retrieval | Параметри | Примітка |
|---|---|---|---|
| Qwen3-Embedding-8B | 98.45 | 7.6B | Matryoshka; топ leaderboard |
| Gemini Embedding 001 | 96.26 | — | тільки API |
| Qwen3-Embedding-4B | 94.46 | 4.0B | Matryoshka |
| multilingual-e5-large | 93.73 | 0.56B | маленька, перевірена |
| text-embedding-3-large | 92.45 | — | OpenAI |
| bge-m3 | 92.40 | 0.57B | популярна multilingual |
| text-embedding-3-small | 80.60 | — | наш production baseline |
Ключовий рядок тут — останній. text-embedding-3-small — сильна універсальна модель для англійської, але для української це середина рейтингу: ~12 пунктів відставання від топових моделей. Тобто запас для покращення, найімовірніше, реальний.
Але оцінка на загальних текстах — ще не оцінка на юридичних. Чи переноситься перевага на вузьку юридичну лексику ( «набрання чинності», «диспозиція статті», «речові права») — leaderboard не скаже. Треба міряти на своєму корпусі.
Shortlist на власний бенчмарк:
text-embedding-3-small— діючий чемпіон, якого треба перемогтиbge-m3— перевірена мультимовна середня вагаqwen3-embedding-4b— баланс якість/вартістьqwen3-embedding-8b— перевірка стелі
Архітектурні рішення перед бенчмарком
Окрема таблиця векторів на кожну модель
Розмірності у моделей різні: 1024 у BGE-M3, 1536 у OpenAI, 2560 у Qwen3-4B, 4096 у Qwen3-8B. Колонки pgvector типізовані за розмірністю, тому «одна таблиця з nullable-колонками під кожну модель» швидко перетворюється на болото.
Натомість: таблиця legal_chunk_vec_<slug> для кожної моделі, з CASCADE FK на спільну таблицю чанків. Можна дропнути або перебудувати вектори однієї моделі, не чіпаючи інші. Бонус: таблиця baseline-моделі сама визначає бенчмарк-вибірку — наступні моделі векторизують рівно ті самі чанки через INNER JOIN з нею.
Чанкування окремо від embedding
Чанки (текст, breadcrumb, метадані) живуть у власній таблиці, незалежній від моделей. Зміна стратегії чанкування → перебудова чанків → усі моделі перевекторизуються. Зміна моделі → перебудовується тільки її таблиця векторів. А production тим часом працює на старому pipeline і нічого не помічає.
Gold set: свій, бо чужого немає
Retrieval-бенчмарк — це пари (запит, релевантні chunk_id). Публічного набору для українського legal немає, тому генеруємо власний.
Процес:
- Семплюємо випадкові чанки з контентом довжиною ≥ 120 символів.
- Для кожного відправляємо breadcrumb + контент у Gemini 2.5 Flash.
- Просимо: сформулюй українською питання, яке поставила б звичайна людина і на яке цей чанк відповідає. Плюс категорія (
direct,definition,procedural,temporal,paraphrase) і self-confidence флаг.
Стартовий набір: 203 запити. Зберігаємо їх як чернетки, з окремим ручним затвердженням для чисел, які підуть у публікацію. Але для порівняння моделей використовуємо чернетки як є: ranking моделей стійкий до шуму — відбраковка кількох слабких
Чесне обмеження: кожен запит згенеровано з одного чанка, тому релевантним позначено лише його. Для питань, відповідь на які розмазана по сусідніх чанках («які кроки процедури?»), модель може повернути валідного «сусіда» і отримати незаслужений промах. Приймаємо це як шум вимірювання — він однаковий для всіх моделей.
Бенчмарк-вибірка: 11.5K чанків замість 308K
Повний корпус на кожну модель — це можливо (~$0.40 за прохід), але повільно для ітерацій. Натомість:
- усі 203 чанки, на які посилаються запити (gold);
- плюс ~10K випадкових чанків як дистрактори;
- разом 11 516 чанків — ~250 дистракторів на кожен gold-чанк.
Цього достатньо, щоб тиснути на якість retrieval, а прохід однієї моделі займає ~6 хвилин. Абсолютні числа трохи оптимістичніші, ніж були б на повному корпусі, але ranking моделей переноситься надійно.
Що саме ми міряємо: словничок метрик
Перш ніж дивитись на таблицю, що означають її рядки? Всі метрики рахуються так: беремо запит («чи звільняє відсутність роботи від сплати аліментів?»), шукаємо по векторах, дивимося, на якій позиції у видачі опинився gold-чанк — той самий, з якого питання було згенероване. І так 203 рази.
- Recall@K — частка запитів, у яких gold-чанк потрапив у перші K результатів. Recall@5 = 93% означає: у 93% випадків правильний фрагмент закону є серед топ-5. Це головна метрика для RAG: у промпт LLM потрапляє саме top-K, тому Recall@K — це буквально «ймовірність, що модель взагалі побачить правильну статтю».
- MRR (Mean Reciprocal Rank) — середнє значення 1/позиція першого правильного результату. Знайшли на
1-му місці — запит дає 1.0, на2-му — 0.5, на5-му — 0.2, не знайшли — 0. MRR = 0.92 означає «правильна відповідь майже завжди перша». На відміну від Recall, MRR розрізняє «знайшли першим» і «знайшли п’ятим». - nDCG@10 (normalized Discounted Cumulative Gain) — схожа ідея, але з логарифмічним дисконтом за позицію: результат на
2-му місці «коштує» трохи менше, ніж на1-му, на10-му — суттєво менше. Стандартна метрика в search-літературі; у нас із одним релевантним чанком на запит вона поводиться майже як MRR, наводимо для порівнюваності з іншими бенчмарками. - Rank-1 hits — найпростіша для інтуїції: як часто правильний чанк стоїть на першому місці видачі.
- Hard misses — протилежний кінець: частка запитів, де gold-чанк не знайшовся навіть у top-50. Це найболючіша категорія: якщо retrieval промахнувся на 50 позицій, ніякий reranker чи збільшення K не врятує — відповідь буде або «не знаю», або галюцинація.
Якщо запам’ятати одне: Recall@5 — скільки запитів взагалі мають шанс на правильну відповідь; hard misses — скільки приречені.
Серце бенчмарку — це два фрагменти коду
Звучить як «фреймворк», але насправді весь харнес — це один SQL-запит і один цикл. Retrieval — звичайний top-K по таблиці векторів моделі (pgvector, HNSW cosine):
SELECT chunk_id, embedding <=> :query_vector AS dist FROM legal_chunk_vec_qwen3_8b -- таблиця векторів конкретної моделі ORDER BY dist LIMIT 50;
А метрики — цикл по gold-запитах (псевдокод; повна версія ще рахує nDCG, гістограму рангів і розбивку по категоріях):
для кожного запиту q з gold set: vector = embed(q.text) # той самий шлях, що й у чаті hits = top_k(таблиця_моделі, vector, 50) rank = позиція q.expected_chunk_id у hits (або "не знайдено") для k у [5, 10, 20, 50]: recall[k] += 1, якщо rank знайдено і rank <= k mrr += 1 / rank, якщо rank знайдено, інакше 0 поділити акумулятори на кількість запитів — таблиця результатів готова
Плюс невелика консольна команда, яка запускає це для вибраної моделі та зберігає результати прогону в таблицю — щоб порівнювати запуски між собою. Онбординг нової моделі — це дві функції (embed батчами + embed одного запиту) і рядок у списку дозволених таблиць.
Результати: очне порівняння чотирьох моделей
Всі чотири моделі векторизували ідентичні 11 516 чанків, усі 203 запити, вектори в pgvector з HNSW cosine:
| Метрика | OpenAI-3s | BGE-M3 | Qwen3-4B | Qwen3-8B |
|---|---|---|---|---|
| Recall@5 | 78.3% | 81.3% | 89.7% | 93.1% |
| Recall@10 | 79.8% | 81.8% | 89.7% | 93.1% |
| Recall@50 | 80.8% | 81.8% | 91.1% | 93.6% |
| MRR | 0.765 | 0.802 | 0.891 | 0.921 |
| nDCG@10 | 0.772 | 0.806 | 0.892 | 0.924 |
| Rank-1 hits | 74.4% | 79.3% | 88.7% | 91.6% |
| Hard misses (немає в top-50) | 19.2% | 18.2% | 8.9% | 6.4% |
По категоріях (Recall@5; вибірки для не-direct категорій маленькі, числа шумні):
| Категорія | OpenAI | BGE-M3 | Qwen3-4B | Qwen3-8B |
|---|---|---|---|---|
| direct | 77.5% | 79.8% | 90.8% | 93.1% |
| definition | 75.0% | 93.8% | 81.2% | 100.0% |
| procedural | 92.9% | 85.7% | 85.7% | 85.7% |
Що тут цікавого:
- Qwen3-8B ріже hard misses втричі порівняно з OpenAI (19.2% → 6.4%) і бере 100% на визначеннях. Для юридичного продукту, де «що таке X?» — базовий тип запиту, це саме по собі виправдовує міграцію.
- BGE-M3 не виправдала очікувань: +3 пункти до OpenAI замість +12, які обіцяв general-domain пресет MTEB. Гіпотеза: юридична українська — вузька, специфічна лексика; перевага мультимовного генералізму згладжується, коли домінують доменні терміни. Це головний аргумент, чому міряти треба на своєму корпусі: MTEB-розрив не переноситься на домен один-в-один.
- BGE-M3 при цьому виграла у Qwen3-4B на визначеннях (93.8% vs 81.2%), програвши в усьому іншому. Можливо, спадок sparse+dense тренування, яке краще чіпляється за термінологічні збіги. Маленька вибірка — ставимось скептично.
- 12 запитів не знайшла жодна з 4 моделей. Це не проблема моделей — це або невдало згенеровані питання, або чанки, зміст яких не відповідає патерну питання. Кандидати на кураторську чистку gold set.
Несподіванка: більше Matryoshka-розмірностей ≠ краще
Нативна розмірність Qwen3-8B — 4096. Але HNSW-індекс для halfvec у pgvector обмежений 4000 розмірностями, тому truncation обов’язковий. На щастя, Qwen3 тренована з Matryoshka Representation Learning: перші N компонент вектора — валідний embedding для будь-якого N.
Публічні числа (model card) обіцяють: 3072 розмірності → втрата ~0.3% від повних 4096; 2048 → ~1%. Ми вибрали 2048 для зберігання, але вирішили перевірити: чи матеріалізується той обіцяний виграш від 3072 на нашому домені? Перевектеризували ті ж 11 516 чанків на 3072 і прогнали бенчмарк ще раз:
| Метрика | Qwen3-8B @ 2048 | Qwen3-8B @ 3072 | Δ |
|---|---|---|---|
| Recall@5 | 93.1% | 92.1% | —1.0 пп |
| Recall@50 | 93.6% | 92.6% | —1.0 пп |
| MRR | 0.921 | 0.908 | —0.013 |
| Hard misses | 6.4% | 7.4% | +1.0 пп |
3072 виявилася гіршою за 2048. Не драматично, але стабільно гірша за всіма показниками.
Наша інтерпретація: Matryoshka-моделі тренуються з явними loss-термами на стандартних breakpoints (1024, 2048, 4096). 3072 — між ними: додаткові 1024 компоненти не оптимізувалися як самостійний префікс, модель «витратила» їхню виразність на те, за що її не винагороджували. Додайте ~1 пп шуму вимірювання на 203 запитах — і 3072 програє.
Урок: перевіряйте Matryoshka-розмірності на своєму домені. Це один вечір і копійки грошей, а економить від неприємного сюрпризу в production.
fp16: чи болить halfvec
halfvec(2048) зберігає
Вартість і латентність
Весь бенчмарк — 5 векторизацій + 5 прогонів — коштував менше $0.50 через OpenRouter + OpenAI напряму. Повна перевекторизація корпусу з 308K чанків на Qwen3-8B: ~$0.25 одноразово.
Єдина реальна ціна — латентність запиту:
| Модель | ms/запит | Маршрут |
|---|---|---|
| OpenAI-3s | ~180 | direct API |
| BGE-M3 | ~660 | OpenRouter |
| Qwen3-4B | ~750 | OpenRouter |
| Qwen3-8B | ~855 | OpenRouter |
Qwen3-8B через OpenRouter у ~5 разів повільніша за OpenAI. Для чат-UX це непомітно —
Прихований дивіденд: хороший embedding прибирає reranker
Є наслідок сильного embedding, який легко недооцінити: зникає потреба в reranker-і.
У часи text-embedding-3-small ми компенсували слабкий retrieval окремою стадією reranking: витягнути ~50 кандидатів вектором, прогнати через reranker (пробували і Cohere Rerank, і
- ще одна API-залежність зі своїми лімітами, ключами і downtime;
- додаткова мережева петля в найгарячішому шляху — між пошуком і генерацією;
- вартість, що не амортизується: embedding корпусу платиться один раз, reranker — за кожен запит користувача.
Подивіться на числа порівняння ще раз: у Qwen3-8B 91.6% запитів мають правильний чанк на першій позиції, Recall@5 — 93.1%. Reranker-у просто нічого виправляти: він переставляє top-50, у якому правильна відповідь і так майже завжди в top-5. У OpenAI-3s з rank-1 у 74.4% reranker мав що робити — саме тому ми його колись і додали.
Після переходу на Qwen3-8B reranker у нас вимкнений у production (код стадії залишився за feature-флагом — стратегія з Null-реалізацією, нуль коштує). Пайплайн став простішим: запит → embedding → HNSW → LLM.
Загальне правило, яке я звідси виніс: перш ніж додавати reranker — перевірте, чи не дешевше підняти якість embedding. Reranker виправляє ranking, коли перша стадія слабка. Якщо перша стадія сильна, reranker — це латентність і залежність, які ви оплачуєте окремо.
Що б я зробив інакше
- Генерував би запити з урахуванням категорій. Наш
LLM-генератор видав ~170direct, ~16definition, ~14proceduralі нуль решти категорій. Якщо семплювати чанки під категорію (чанки з визначеннями — для definition-питань тощо) — сигнал по категоріях був би збалансованішим. - Multi-relevant розмітку — раніше. Дозволити запиту посилатись на
2-3 сусідні чанки, щоб procedural-питання мірялись чесно.
Висновки
Конкретний вердикт для нас: міграція з text-embedding-3-small на Qwen3-Embedding-8B @ 2048 dims, halfvec + HNSW cosine у pgvector. Це +14.8 пп Recall@5 (кожен сьомий запит, який раніше промахувався, тепер влучає), втричі менше hard misses, 100% на визначеннях — за $0.25 одноразової перевектеризації.
Загальні висновки, якщо ви вибираєте embedding для непопулярної мови чи вузького домену:
- Не довіряйте leaderboard сліпо, але використовуйте його як prior. MTEB правильно вказав напрямок (Qwen3 топ, OpenAI-small слабка для української), але помилився в масштабі для BGE-M3 (+12 обіцяних пп перетворились на +3 реальні).
- Власний бенчмарк — це дешево.
LLM-генерація gold set + вибірка з ~10K дистракторів + метрики Recall/MRR — кілька днів роботи і менше долара витрат. Це найкраща інвестиція в RAG-якість, яку можна зробити. - Ranking переноситься, абсолютні числа — ні. Маленька вибірка з розумною щільністю дистракторів надійно показує, яка модель краща, навіть якщо абсолютний Recall трохи завищений.
- Перевіряйте нестандартні Matryoshka-розмірності. Між тренувальними breakpoints якість може просідати, а не рости.
P.S. Чотири місяці по тому
Бенчмарк був у квітні; цей текст я пишу в серпні, і за цей час рішення встигло пройти перевірку production:
- Qwen3-8B @ 2048 halfvec — досі наш єдиний retrieval-вектор. Наприкінці квітня ми повністю перейшли на нього і вимкнули старий pipeline. Жодного разу не виникло бажання відкотитися.
- Корпус виріс з ~308 до ~316 тисяч чанків (законодавство живе: редакції, нові акти), а кількість документів після нормалізації ієрархії — ~940. Довектеризація нових чанків іде фоново і коштує копійки — та сама економіка, що й у бенчмарку.
- Латентність через OpenRouter на практиці не болить. Єдиний нюанс: довелося запінити конкретного провайдера через
provider.order, бо дефолтний роутинг інколи віддавав запит провайдеру з іншою поведінкою — дрібниця, але для embedding-консистентності критична: вектори з різних inference-стеків можуть ледь помітно відрізнятись.
Якби я переганяв бенчмарк сьогодні, до shortlist додав би свіжіші моделі з верхівки українського пресету MTEB — leaderboard за ці місяці помітно оновився. Але це і є головна перевага власного бенчмарку: він переживає будь-яку конкретну модель. Нова кандидатка — це один запуск векторизації на 11,5K чанків і шість хвилин очікування, а не новий дослідницький проєкт.
27 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівЩо на рахунок локальних моделей? Робили якісь бенчмарки чи все погано?
Не дуже зрозумів — локальні це українські? Чи на своєму залізі?
На своєму залізі
На своєму залізі не робили. Головні мотиви — економія, меньший time-to-market та легше в підтримці.
Моделі Perplexity начебто зараз SOTA, але дійсно — для англійської мови. В описі вказано, що є підтримка української мови.
А взагалі, мені якось дивно бачити цю проблему. За такого корпусу документів можна згенерувати triplet-пари та донавчити (LoRA) будь-яку модель, у тому числі Perplexity або Qwen. І буде ще краще.
Щодо 90+% на Recall@5. На мою думку, у вас дуже слабкі питання. Таких цифр я ніколи не бачив на реальних питаннях користувачів або згенерованих складних питаннях (зокрема multi-hop). Або це специфіка юридичного контексту, де кожен окремий документ дублює ті самі закони, які описані в інших документах.
До речі, на моєму корпусі документів (ІТ-сфера) класичний RAG завжди програє summary-підходам. І це при тому, що класичний RAG дуже прокачаний: було донавчання reranker та embedding-моделі на тому самому корпусі. Окремо пробував підходи із Self-Corrector (Adaptive RAG), заміною косинусної відстані та багато чого іншого.
Про 90+%: на сирих питаннях користувачів таких цифр не буде, але пост цього і не стверджує — gold set згенерований LLM з одного чанка (single-hop за побудовою), вибірка 11.5K замість 316K, тому абсолюти оптимістичні. Це інструмент порівняння моделей, не заява «93% на проді»: ranking переноситься, абсолюти — ні.
Нюанс, який зближує бенчмарк з продом: сирий запит користувача у нас взагалі не вектеризується. Пошуковий запит формулює LLM в агентному циклі — переписує побутове «як розлучитися, якщо чоловік не згоден» юридичною мовою перед embedding. Тобто у векторний індекс і на проді приходитьLLM-сформульований текст, тож gold set реалістичніший, ніж здається. Розрив лишається у складності та single-hop природі питань. Multi-hop закриває не один retrieval-виклик, а агентний цикл: кілька пошуків плюс детерміновані тули (дістати статтю за адресою, навігація по структурі). Дублювання документів тут ні до чого — корпус це самі тексти законодавства, кожен чанк — унікальне положення конкретної редакції з breadcrumb-префіксом.
Про LoRA: згоден, файнтюн на корпусі майже напевно додав би кілька пунктів. Не йдемо туди з економіки: зараз ембединги — це API-виклик ($0.25 за весь корпус), а свій файнтюн — GPU-хостинг назавжди плюс перенавчання, бо корпус живий (редакції змінюються постійно). І після Qwen3-8B вузьке місце вже не retrieval, а якість синтезу на складних питаннях.
Про summary-підходи — частково підтверджую: для судової практики (окремий корпус) ми ретрівимо саме поLLM-екстрагованих правових позиціях, бо сирий текст рішень для ембедингів майже безнадійний. Для законів навпаки лишаємо verbatim — точне формулювання і є продукт, цитата має відтворюватись до редакції.
Perplexity-ембединги не бенчив.
В цілому я думаю, що в більш вузікий сфері (наприклад підтримка одного-декілька програмних продуктів) можна векторизувати сирий запит користувача, але в більш широкій сфері (медицина, право) — це не вийде, бо спочатку потрібно перекласти побутову мову на домену. Це теж може буте задача донавченної моделі теоретично, але у нас окрема слабша «search planner» не спрацювала. Тому пошук виконує головна велика модель, інакше велике просідання по якості.
це називається corpus-adapted query expansion.
Я таке теж пробував. Основна ідея така: отримуємо обмежену кількість чанків на сирому запиті. Додаємо (або замінюємо) до сирого запиту додаткові слова, терміни або формулювання з чанків, щоб покращити пошук.
Увесь сенс цієї роботи в тому, щоб додаткові терміни підбиралися з урахуванням конкретного корпусу документів, за яким ведеться пошук.
Особисто на моєму корпусі ситуація за підсумком була дивною. Це незначно покращило результати на реальних запитах користувачів (у межах1–2%), але synthetic_questions помітно погіршився.
Ідея цікава — в літературі це CSQE: робимо перший пошук, беремо з його результатів влучні фрагменти тексту, дописуємо їх до запиту і шукаємо ще раз — уже мовою самого корпусу. Огляди обіцяють найбільший виграш саме там, де юридична мова розходиться з побутовою — тобто в нашому випадку. Я попросив Claude перевірити на власних даних: взяли з тестового прогону всі 13 статей, які пошук реально не знайшов, і подивилися, чи здатне таке розширення запиту підняти їх на перші позиції.
Пробували три варіанти — від простого усереднення векторів до чесного CSQE, де окрема модель відбирає найкорисніші речення зі знайденого набору. Результат однаковий: майже нічого не врятувалося. Найприкріше, що модель відбирала речення дуже влучно — для кейсу зі спадщиною вона взяла текст, який прямо згадує статті 1272 і 1280, рівно ті, що потрібні. А потрібна стаття все одно не піднялася.
Здається, що такий підхід може лише підсилити те, що перший пошук уже якось зачепив: додаєш до запиту речення зі знайденого — і другий пошук повертає майже те саме, що й перший. А наші «загублені» статті лежать так далеко в видачі, що потрібних слів-містків немає навіть у перших двадцяти результатах — підсилювати нічого. І друге: якщо у знайденому тексті написано «див. статтю 1272», для пошуку за схожістю це просто слова — щоб цим скористатися, треба не переформульовувати запит, а витягнути номер і відкрити саму статтю. Це вже зовсім інший механізм, і він у нас реалізований окремо.
Але напрямок цікавий — подивлюсь ще в цей бік.
Цікава стаття, дякую автору, дуже корисний практичний розбір.
Усі моделі ви тестували через HNSW (Hierarchical Navigable Small World). Чи робили для контролю хоча б на вибірці з 11,5 тисячі чанків точний пошук без HNSW? Було б цікаво дізнатися, де промахується сама embedding-модель, а де щось губить приблизний індекс.
І ще: якщо заново випадково вибрати ті 10 тисяч чанків-дистракторів, порядок моделей лишається таким самим? Або, можливо, ви перевіряли переможця вже на всіх 308 тисячах чанків?
Для розуміння цифр зі швидкістю також цікаво, на якій машині працював PostgreSQL, версія, та хоча б CPU, RAM і тип диска (його I/O performance) та які були основні налаштування HNSW. Тут постійно треба шаманіти з налаштуваннями, я теж PostgreSQL+pgvector юзаю. Чи вас перформанс тут зовсім не турбував?
На останок запитання, чи зберігся виграш Qwen3 у реальному гібридному пошуку vector + FTS + trigram після вимкнення reranker, чи +14.8 пункту це результат саме чистого vector-пошуку?
Дуже гарні питання, є гарні ідеї, де ще можна покращитись.
1. Точний пошук як контроль — ні, не робили, і це слушна ідея. Всі моделі міряли через ідентичний HNSW (m=16, ef_construction=64, тобто дефолти pgvector), тож порівняння між моделями apples-to-apples, але «промах моделі vs промах індексу» ми не розділяли. Потрібно буде ще спробувати.
2. Пересемплювання дистракторів — не робили. Вибірка зафіксована конструктивно: бенч-вибірка = вміст вектор-таблиці baseline-моделі, інші моделі вектеризували рівно ті самі чанки. На повних 308K Recall@K-бенч переможця не ганяли — але прод уже рік працює на повному корпусі (~315K чанків), і якість там гейтиться окремим end-to-end evalом (40 фікстур,LLM-judge + citation-recall + key-facts інструменти), який ловив би деградацію відносно бенч-очікувань.
3. Залізо і налаштування. Це важливе уточнення до цифр швидкості: ms/query у таблиці — це латентність embedding API (OpenAI direct vs OpenRouter), а не Postgres. Сам pgvector-пошук на 11.5K halfvec(2048) — одиниці мілісекунд і в бенчі взагалі не був вузьким місцем. Залізо: дев-машина (i7-12700K, 20 потоків / 64 GB RAM / NVMe SSD), PostgreSQL 18 в Docker (образ pgvector/pgvector:pg18), конфіг дефолтний. На продакшені ми використуваємо managed Postgres від хостера, де можливості конфігурації обмежені.
4. Гібрид і reranker. +14.8pp — це чистий vector-vs-vector на ізольованому бенчі, без FTS/trigram/reranker. Qwen3 всередині старого гібрида ми не тестували — і ось чому: рішення про міграцію гейтилось не бенчем, а end-to-end evalом усього RAG-флоу(LLM-judge + фактові інструменти на фікстурах реальних питань). Там V1-гібрид (OpenAI + FTS + trigram + Cohere reranker) проти V2 (чистий vector Qwen3-8B, без reranker) дав: overall 7.5 → 7.6, correctness 7.00 → 7.22 при помітно менших токенах. Reranker вимкнули з окремої виміряної причини: на дрібних чанках V2 Cohere ставив >0.9 всім 25 кандидатам і замість top-5 повертав усі 25 → токени ×1.4 і гірша correctness (LLM тонув у звалищі чанків). А лексичний сигнал ми пізніше повернули — але не як trigram-гібрид, а як BM25-augmentation (OpenSearch з українським аналізатором) поверх vector-результатів Qwen3: +BM25-append, +пошук по заголовках, +act-scoped append. Це дало найбільший приріст фактової повноти за всю історію проєкту (key-facts 57.5% → 64%).
Обіцяний контроль зробили. Взяли 94 реальні пошукові запити з еталонного прогону нашого evalу і прогнали кожен двічі по повному корпусі (~315 тис. чанків): продакшн-конфігурація HNSW (ef_search=200) проти точного перебору (індекс вимкнено, повний скан).
Результат: розбіжності практично немає. Збіг топ-5 — 99.6%, топ-50 — 97.8%; у 93 запитів з 94 топ-5 ідентичний аж до порядку, перший результат збігся у всіх 94. Окремо перевірили всі «загублені» цільові статті з нашого evalу — під точним пошуком їхні позиції не змінились узагалі: що було глибше50-го місця під HNSW, там же і під exact.
Тобто індекс не винен: промахи живуть у самому векторному просторі (запит і потрібна норма справді далекі одне від одного), а не в апроксимації пошуку.
Ви не разглядели можливість використання Graph RAG або комбінаціі Graph RAG та RAG на ембедінгах? Бо RAG на ембедінгах не зі всіма кейсами може впоратись.5–10 шматків тексту, де найчастіше зустрічається його прізвище. GraphRAG знайде як поєднуються транзакції, установчі документи та довіреності. Він покаже, що громадянин X -> видав довіреність -> громадянин Y -> переказ коштів -> [Фірма-одноденка Z]
Кейс А.
Потрібно перевірити, чи немає прихованого конфлікту інтересів між новим клієнтом (Компанія А) та чинними клієнтами. Компанія А напряму не згадується у справах інших клієнтів. Але вона володіє дочірньою Фірмою Б, яка через офшор В контролює частку в Компанії Г, що є опонентом вашої фірми в суді. Векторний пошук знайде шматки документів про Компанію А, але не зможе «здогадатися» пройти ланцюжком із 4 документів углиб: [Компанія А] -> володіє -> [Фірма Б] -> контролює -> [Офшор В] -> акціонер -> [Компанія Г].
Кейс Б.
Потрібно зрозуміти роль громадянина X у виведенні активів збанкрутілого банку. Громадянин X може згадуватися в тисячах договорів як номінальний директор, підписант або свідок. Векторний пошук поверне
І таких кейсів може бути багато.
Розглядали — робили огляд літератури по legal RAG + knowledge graphs:
1. Legal Query System: Ontology-Knowledge Graph Integration — в’єтнамське дорожнє право, ручна онтологія + RDF-трійки, чисто символьний пошук;
2. Schema.org Mapping for Brazilian Legal Knowledge Graph — LRMoo-модель Work/Expression/Manifestation для правових норм;
3. Multi-Layered Embedding-Based Retrieval for Legal Knowledge — ієрархічні ембедінги по 6 рівнях, 2.3× виграш проти flat-чанкінгу;
4. SAT-Graph RAG (Structure-Aware Temporal Graph RAG) — найближча до Graph RAG робота в легальному домені: структура + темпоральні версії + provenance.
Цікавий факт: жодна з них не використовує графову БД — навіть SAT-Graph явно обирає relational+vector, бо векторний пошук і обхід графа мають жити в одному сховищі, інакше з’являються sync-проблеми і joins через межу систем. «Knowledge graph» у всіх чотирьох — це модель даних, а не вибір сховища.
Тому ми взяли Graph RAG-модель, а не сховище: наш корпус законодавства — це граф у Postgres (ієрархія норм зі стабільною ідентичністю компонентів крізь редакції, темпоральні версії, події-поправки, перехресні посилання як edge-таблиці). Multi-hop обхід у рантаймі робить агент інструментами навігації по цьому графу — це фактично traversal-крок Graph RAG, тільки ним керує LLM у tool-loop, і на нашому evalі це виграло у планувальника запитів.
Щодо кейсів А і Б — вони валідні, але це граф сутностей (корпоративні структури, особи в договорах), тобто домен conflict-check / due diligence, а наш продукт — консультації щодо законодавства і судової практики. Важливо: у цих кейсах вузьке місце не векторний пошук і не графова БД, а походження ребер. Зв’язок «дочірня фірма → офшор → акціонер опонента» не «здогадається» ні ембедінг, ні GraphRAG — його треба мати як дані (ЄДР, реєстри бенефіціарів) або витягатиLLM-ом з документів з окремою entity resolution (це найдорожча частина, і вона однакова за складністю незалежно від сховища).
Тобто в графі найцікавіше — це те, як побудувати онтологію. Одна справа — ПДР, інша — Податковий кодекс, а зовсім інше — право взагалі. Можливо, будемо пробувати ще експерименти в цьому напрямку.
Взагалі я б сказав — найгеморніше. Чим rлибше в доменну модель закопуєшся, тим більше з’являється сутностей, у кожної купа пропертів і зв’язків.
Я потроху пиляю щось що починалось як спроба подивитись наскільки на Java зараз можна написати graph knowledge base (бо Java в плані матюрності AI-ліб відстає від Python/JavaScrypt) + паралельно заюзати SDD (OpenSpec) для розробки і переросло в тюнінг парсінга, чанкінга і інструмент для побудови/зміни graph схеми в Neo4J на базі документів з доменної області.
І от виходить що побудувати схему на базі десятка вхідних документів, які ніби відносяться до доменної області, але скоріше кожен до якоїсь її частини — це відрезольвити кучу конфліктів, вирішити що робити з синонімами, які не завжди синоніми, які проперті потрібні, а які не дуже, чи красиво це коли в тебе у ноди типу Person дофіга зв’язків на саму себеі т.д. і т.п.
Я цей резолвінг поки роблю вручну через UI, бо ллмка якось не дуже справляється з цією частиною (точніше щось типу gpt-5-mini не справляється, а пробувати ганяти цю частину на gpt-5.6 Sol — є сумнів, що навіть вона розгребе, але головне — шкода грошей. ))
p.s.
Вийшов нехилий такий монстрик (особливо якщо піднімати його з Langfuse стеком) із сотнею налаштуваннь і окремим UI щоб це юзати/обслуговати.
Згоден. Я по Neo4J дивився купу відео, презентацій та в усіх граф будується якимось магічним способом — вводиш купу тексту і граф будується сам :)
угу.5-10 нодами і стількома ж зв’язками між ними. )

або «давайте продемонструємо як це працює на отакій простенькій схемі» — і далі схема із
А тут береш з вікі біографії кількох наукових фантастів, кажеш ллм-ці що тема буде «science fiction authors» і вона тобі видає такі ноди|релейшни про які ти і не думав спочатку.
Всякі «Person —[SUFFERED_FROM]→ MedicalCondition», «Person —[SERVED_IN]→ MilitaryService» і т.д.
А як погано відфільтрувати «синоніми», виходить щось таке як на скріншоті.
Взагалі UI — це хороша ідея. Можна залучати доменного експерта. Це хороший напрямок!
Гарна стаття, особливо сподобався підхід вимірювання на власному дата сеті з OpenRouter.
Цікавий висновок якщо гарний retrival — не треба reranking.
Наскільки користувачи задоволені якістю?
Сам тестувальний датасет виглядає якось дуже синтетично — модель нагенерувала питання — по цьому і перевіряємо.
Мені здається в ідеалі, якщо є прод треба взяти топ 100 реальних питань + 100 рандомних реальних питань юзерів і самому з експертом по праву прочекать відповіді.
Але це звісно якщо користувачі не задоволені якістію, бо це реально великий об’єм проботи.
Пропозиції хороші і мають сенс. Але є декілька моментів, які пов’язані не лише з обсягом роботи але з UX, конфеденційністю та молодістю проєкта:
1. Є можливість в UI залишити відгук до чату, але не дуже багато відгуков отримано.
2. Щодо реальних питань, користувачі мають дати згоду на те, що їхні питання будуть використовуватись для покращення сервісу. Є користовачі, які дали згоду, але це в більшості студенти, які вирішують задачі. Тобто це теж синтетика.
3. Щодо експерту по праву — це 100% правильний напрямок і він в процессі.
міряли який у вас p95 latency для всього флоу?
Повний флоу «питання → готова відповідь»: p50 ~34 с, p95 ~44 с. Виклик пошуку, всередині якого Qwen3-embedding через OpenRouter: медіана ~4 с, типово2–6 с. LLM-кроків за хід 3–5, кожен по 3–6 с.
В наших тестах найбільший вплив на латенсі має основна модель.
Доброго дня, а чи розглядали Ви моделі voyage? Чи можете тут щось сказать про них? Дякую!
Не розглядав. Станом на зараз в huggingface.co/spaces/mteb/leaderboard є нові лідери:
Mira190/Euler-Legal-Embedding-V1
geevec-ai/geevec-embeddings-1.0
perplexity-ai/pplx-embed-v1-4b
Почав би з них напевно.
норм стаття, дякую — цікаво. А щодо семантичних запитів користувачів та сама модель? як нормалізацію робите і guardrails?
Дякую. Про семантичний пошук я трохи писав раніше — dou.ua/forums/topic/58637 (4. Стратегія пошуку). Тобто, пошукові запити пише сама модель. Ми експериментували з більш детерміністичними механізмами, як-от робити векторний пошук за оригінальним запитом, але це не працює, бо юридична лексика має багато особливостей.
Про модель це може бути окрема стаття. Є міжнародний www.vals.ai/benchmarks/legal_bench. Він збігається з нашими результатами — Gemini 3 Flash найкраща. Запускалися з 3 Flash Preview. Зараз тестуємо 3.7.
Про захист — робимо перший запит без інструментів, який аналізує питання. А вже потім — запускаємо пошук. Технічно модель має дуже обмежений доступ до БД — через інструменти, тобто нічого поганого не може статися. Це більш моральні питання — чи юзер хоче отримати системний промпт, або обійти закон (хоча це теж сіра зона, бо боротися за свої права — це нормально).
Дякую, одна з найкорисніших статей на доу + особливо коментарі по темі — власний досвід на топову тему.