RETRIEVAL BENCHMARK · RUSSIAN BUSINESS EMAIL CORPUS
OpenSearchvspgvector
Кто находит нужное письмо и тред точнее и быстрее — BM25+kNN на OpenSearch или
косинус+tsvector на Postgres. Одни и те же эмбеддинги (GigaEmbeddings, dim=2048),
один и тот же чанкинг, честная матрица 2 бэкенда × 3 режима × 2 варианта.
Порог топ-K
действует на все блоки ниже, кроме latency
ИТОГ
Кто выигрывает
Авто-вывод по всей матрице бенчмарка, 220 golden-запросов × 3 повтора на конфигурацию.
КАЧЕСТВО ПОИСКА
EmailHit@K и ThreadHit@K по режимам
Доля запросов, где нужное письмо / нужный тред попал в топ-10.
EmailHit@10
попадание нужного письма в топ-10
ThreadHit@10
попадание нужного треда в топ-10
OpenSearchpgvector
ЖИВЫЕ ПРИМЕРЫ
Четыре кейса с настоящими цифрами
Конкретные golden-запросы из корпуса и то, что на них случилось в бенчмарке — не усреднённые проценты,
а реальный запрос → реальный результат конкретной конфигурации.
ПОЛНАЯ МАТРИЦА
Все 12 конфигураций
2 бэкенда × 3 режима (dense / lexical / hybrid) × 2 варианта (без / с summary-векторами треда). ★ — лучший результат в столбце.
ПО ТИПАМ ЗАПРОСОВ
ThreadHit@10: где именно каждый режим силён
220 запросов поровну по 4 типам: смысловой перефраз (semantic), точная сущность вроде номера
договора (lexical), вопрос про исход треда без явных сущностей (thread-level),
запрос с SQL/DSL-фильтром по отправителю или дате (filtered).
СКОРОСТЬ
Latency без учёта эмбеддинга запроса
Медиана и p95 по 220 запросам × 3 повтора, логарифмическая шкала — диапазон от 0.4 мс до 50 мс.
p50 → p95 (диапазон)OpenSearchpgvector
СКЕЙЛ-ТЕСТ · РАЗОВЫЙ
Что будет на 247 225 чанках (×145)
Существующий корпус размножен (с шумом на векторах, чтобы не было буквальных дублей) в отдельный
variant=scale, не пересекающийся с plain/sum.
После теста удалён — здесь остаются только цифры. Один прогон, честный A/B (sum и scale измерены подряд, в одной сессии).
pgvector dense на 1705 строках вообще не использовал HNSW
-- chunks_sum (1705 строк), 58.9 ms
Limit -> Sort -> Seq Scan on chunks_sum
Buffers: shared hit=13978
(HNSW-индекс в плане не участвует)
-- chunks_scale (247225 строк), 3.1 ms
Limit -> Index Scan using chunks_scale_hnsw
Buffers: shared hit=1136
Планировщик на маленькой таблице честно выбрал точный перебор — он и правда дешевле. Значит
весь прежний quality-бенчмарк pgvector/dense считался на exact-поиске, не на приближённом
ANN — recall настоящего HNSW мы там не проверяли.
НАХОДКА №2
pgvector lexical деградирует в ×80, OpenSearch — линейно в ×2
ts_rank_cd пересчитывает ранг по всем найденным строкам перед сортировкой —
на масштабе это дорого. Инвертированный индекс OpenSearch спроектирован именно под этот сценарий.
Если лексика важна как самостоятельный канал (не только внутри hybrid) на растущем объёме —
здесь OpenSearch безопаснее.
НАХОДКА №3
Filtered-запросы: pre-filter у pgvector подтверждён и на масштабе
Limit -> Sort (точный cosine)
-> Bitmap Heap Scan Rows: 1885 из 247225
-> BitmapAnd
-> Bitmap Index Scan chunks_scale_sender
-> Bitmap Index Scan chunks_scale_date
55/55 filtered golden-запросов — на любом масштабе pgvector сначала
пересекает btree-индексы (BitmapAnd), затем точно пересчитывает
косинус по узкому набору. HNSW не используется ни разу — переключение зависит не от размера
таблицы, а от селективности фильтра.
ГРАНУЛЯРНОСТЬ ЧАНКИНГА · ГИПОТЕЗА НЕ ПОДТВЕРДИЛАСЬ
Вектор на переписку, а не на письмо?
Проверили: что если резать чанки не внутри границ письма (plain), а
окном поверх всей переписки треда (convo) — окно может покрывать
несколько писем сразу. Тот же токен-бюджет (800, overlap 120), меняется только граница чанка.
email_id чанка convo — anchor (первое письмо
в окне), поэтому EmailHit@k для convo — консервативная оценка; ThreadHit@k этому смещению не
подвержен (дедуп по треду) и служит главным честным срезом.
ThreadHit@10: plain vs convo, по конфигурациям
Полый маркер = plain (по письму), закрашенный = convo (по переписке).
OpenSearchpgvector○ plain → ● convo
НАХОДКА №1 · ГЛАВНАЯ
convo хуже plain по ThreadHit@10 на 5 из 6 конфигураций
Проверено на всех K: 1/3/5/10 — деградация системная, не артефакт отсечки. Единственное
исключение — pgvector lexical, где ts_rank_cd и так слаб (0.33–0.36)
и разница на грани точности измерения.
НАХОДКА №2
Механизм: склейка писем размывает эмбеддинг
запрос: "Д-2026/4626"
plain топ-1 (score 0.761): thread=t0016
верный тред найден сразу
convo топ-1 (score 0.767): thread=t0165
НЕ тот тред — склеенный чанк размыл вектор
convo топ-2 (score 0.756): thread=t0016
верный тред — но только 2-м местом
Чанк convo в среднем содержит 2–5 склеенных писем (636 токенов
из 800 — почти всегда «под завязку»). Эмбеддинг усредняет смысл сразу нескольких сообщений и
теряет фокус на конкретной детали, которую ищет точечный запрос.
НАХОДКА №3
Компромисс: convo компактнее и быстрее, но платит точностью
×3.6 меньше векторов: 1525 → 469pgvector p95: 43.7 → 15.1 msценой ThreadHit@10 и EmailHit@10
106 из 180 тредов (короткие/шумовые) целиком уместились в 1 чанк, 74 длинных треда дали
2–8 чанков. Меньше векторов и ниже latency — но recall для точечных запросов ощутимо просел.
Рекомендация: оставить plain (чанк на письмо), а доступ к контексту всей переписки
давать через summary-векторы (variant=sum) — они добавляются поверх, не заменяя точность.
SUMMARY-ВЕКТОРЫ ТРЕДА
Помогает ли отдельный вектор саммари треда?
В среднем — почти нейтрально. В одном режиме — решающе.
На dense и hybrid у обоих бэкендов ThreadHit@K и так близок к 1.0 — добавить нечего.
Но у pgvector lexical summary-вектор чинит главную слабость: короткие треды
без общей лексики между письмами и запросом.
—
Среднее по всем режимам и бэкендам: — — summary-векторы чуть помогают и нигде не вредят.
ПРИМЕРЫ
Как выглядят golden-запросы
По 3 случайных примера на тип из 220 сгенерированных запросов, каждый привязан к конкретному письму или треду в корпусе.