Skip to content

ИИ и машинное обучение·6 мин чтения

Почему гибридный поиск обошел чистые эмбеддинги в нашем пайплайне Invoice AI

Чистые плотные эмбеддинги не справились с буквенно-цифровыми серийными номерами в нашем пайплайне Invoice AI. Объединение полнотекстового поиска PostgreSQL tsvector с косинусным расстоянием pgvector подняло точность совпадений top-1 с 61,4% до 94,8%.

Анатолий НовогродскийОснователь и CEO, GlanitОпубликовано 28 августа 2026

Почему плотные эмбеддинги не справились с точными серийными номерами в Invoice AI?

Векторные эмбеддинги проецируют текст в многомерные геометрические пространства на основе семантического сходства, отображая SN-8849-X и SN-8849-Y практически в одну и ту же точку с косинусным сходством выше 0,962. В нашем движке извлечения данных Invoice AI, который обрабатывает более 45 000 счетов-фактур поставщиков в месяц, чистый поиск (retrieval) по плотным векторам с использованием text-embedding-3-small ошибочно классифицировал 38,6% буквенно-цифровых артикулов. Плотные модели отлично понимают, что «гидравлический клапан» соответствует «узлу управления жидкостью», но сглаживают символьные различия на уровне токенов, разделяющие две разные позиции в каталоге. Сочетание лексического полнотекстового поиска через PostgreSQL tsvector с поиском сходства pgvector подняло полноту (recall) совпадений позиций top-1 с 61,4% до 94,8%.

Когда 14 марта 2024 года мы запустили первый прототип Invoice AI, наша архитектура полностью опиралась на векторы OpenAI text-embedding-3-small, хранящиеся в базе данных PostgreSQL 16 с расширением pgvector. Для описательных позиций вроде «Heavy Duty 12V Alternator» плотный векторный поиск возвращал правильный внутренний SKU в 98,1% тестовых случаев. Сбой происходил, когда счета поставщиков содержали сырые коды деталей вроде MOD-9942-B наряду с одинаковыми строками описания. Модель эмбеддингов игнорировала различие в односимвольном суффиксе, возвращая MOD-9942-A как лучшего кандидата, поскольку их положение в векторном пространстве отличалось менее чем на 0,004 евклидова расстояния.

Лексические поисковые движки обрабатывают такой случай тривиально, поскольку инвертированные индексы работают с точными токенами строк, а не с семантическими концептами. Стандартный индекс GIN, сопоставляющий MOD-9942-B, оценивает точное равенство символьных токенов, выставляя точным совпадениям строк более высокий балл, чем частичным. Однако чистый поиск по ключевым словам пасовал всякий раз, когда поставщики сокращали описания товаров или использовали региональные синонимы, не имевшие общих токенов с нашей главной таблицей номенклатуры.

Как мы структурировали запрос гибридного поиска в PostgreSQL 16?

Мы отказались от запуска отдельных векторных БД вроде Qdrant параллельно с Elasticsearch, чтобы избежать задержек синхронизации между базами данных при высокой активности загрузки счетов. Поддержка двойных пайплайнов индексации приводила к временному расхождению состояний всякий раз, когда обновление каталога сбоило в одном из кластеров. Вместо этого мы разместили колонки pgvector 0.7.0 и нативные tsvector PostgreSQL в одной таблице, объединяя ранги разреженных ключевых слов и расстояния плотных векторов внутри одного SQL-запроса с помощью Reciprocal Rank Fusion (RRF).

Наша таблица каталога хранит как предварительно рассчитанные 1536-мерные эмбеддинги, так и комбинированный вектор полнотекстового поиска, сгенерированный из SKU продукта, артикула поставщика и описания. SQL-запрос ниже выполняет оба сканирования индексов параллельно и объединяет их ранги с использованием константы RRF k = 60:

WITH vector_matches AS (
    SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <-> $1) AS rank
    FROM master_catalog
    ORDER BY embedding <-> $1
    LIMIT 40
),
text_matches AS (
    SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(fts_vector, plainto_tsquery('simple', $2)) DESC) AS rank
    FROM master_catalog
    WHERE fts_vector @@ plainto_tsquery('simple', $2)
    ORDER BY ts_rank_cd(fts_vector, plainto_tsquery('simple', $2)) DESC
    LIMIT 40
)
SELECT 
    COALESCE(v.id, t.id) AS catalog_id,
    COALESCE(1.0 / (60 + v.rank), 0.0) + COALESCE(1.0 / (60 + t.rank), 0.0) AS rrf_score
FROM vector_matches v
FULL OUTER JOIN text_matches t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT 10;

Использование plainto_tsquery('simple', $2) было осознанным решением. Стандартный словарь english стеммирует слова, превращая артикулы вроде CAT-442-9 в изолированные цифры. Конфигурация simple сохраняет исходные строки токенов, позволяя выполнять точное сопоставление буквенно-цифровых префиксов без специфичных для языка трансформаций.

Плотный, разреженный или гибридный: что показал бенчмарк на нашем датасете?

Мы проверили четыре стратегии поиска на тестовом датасете из 12 000 исторических позиций счетов-фактур, извлеченных из PDF-документов. Каждая позиция оценивалась по ее способности извлечь точно совпадающий SKU из основного каталога, содержащего 420 000 строк товаров. Тестирование проводилось на инстансе AWS Aurora PostgreSQL db.r6g.xlarge с 32 ГБ RAM.

Точность поиска и бенчмарки производительности на 12 000 позиций счетов-фактур
СтратегияTop-1 Полнота (Recall)Top-5 Полнота (Recall)p95 Задержка (мс)Размер индекса на 100k строк
Чистый плотный (HNSW, text-embedding-3-small)61,4%82,1%14,2 мс610 МБ
Чистый разреженный (tsvector, GIN-индекс)74,8%86,3%6,1 мс125 МБ
Гибридный (RRF, k=60, HNSW + GIN)94,8%98,7%22,6 мс735 МБ
Гибридный (Нормализованная линейная сумма баллов)89,2%95,1%28,4 мс735 МБ
Точность поиска и бенчмарки производительности на 12 000 позиций счетов-фактур

Что сломалось при настройке параметров Reciprocal Rank Fusion?

Настройка параметров RRF ломала граничные случаи до того, как мы ее стабилизировали. 4 ноября 2024 года автоматический тестовый деплой снизил точность поиска позиций на 11,3% после того, как мы уменьшили константу RRF k с 60 до 10. Бо́льший вес получили совпадения по ключевым словам на первых позициях, из-за чего короткие числовые строки вроде дат счетов (2024-03-01) или почтовых индексов в шапке поставщика стали перевешивать сильные семантические описания.

Мы также попытались заменить RRF линейной взвешенной суммой нормализованного косинусного расстояния и оценок ts_rank_cd. Функция линейного скоринга оказалась неуправляемой, поскольку ts_rank_cd возвращает неограниченные значения с плавающей точкой в зависимости от длины документа и частоты токенов, тогда как косинусное расстояние находится строго в диапазоне [0, 1]. Нормализация оценок требовала динамического получения статистики min-max по всему набору кандидатов при каждом запросе, что добавляло 5,8 мс к задержке выполнения.

RRF полностью устраняет необходимость в нормализации оценок, так как опирается исключительно на относительные порядковые позиции, а не на сырые значения расстояний. Элемент с рангом #1 в поиске по ключевым словам получает компоненту RRF-балла 1.0 / (60 + 1) = 0.01639 независимо от того, равнялся ли его сырой балл ts_rank_cd 0,12 или 4,85.

Сколько задержки и памяти добавляет гибридный поиск в продакшене?

Гибридный поиск добавил 8,4 мс задержки на запрос по сравнению с чистым векторным поиском HNSW, сдвинув время выполнения p95 с 14,2 мс до 22,6 мс. Плата за производительность вполне приемлема для нашего пайплайна Invoice AI, поскольку предварительная обработка OCR и парсинг документов занимают 1,8 секунды на страницу PDF. Дополнительные 8,4 мс внутри PostgreSQL незаметны для конечных пользователей, ожидающих результатов асинхронной пакетной обработки.

Потребление памяти увеличилось на 14% на нашем инстансе базы данных. Хранение векторного индекса HNSW для 1536-мерного вектора чисел с плавающей точкой требует около 610 МБ на 100 000 строк при сборке с m = 16 и ef_construction = 64. Соответствующий GIN-индекс для полнотекстового поиска добавил 125 МБ на 100 000 строк. PostgreSQL удерживал оба индекса в памяти одновременно в пуле shared_buffers без своппинга.

Мы столкнулись с одной эксплуатационной проблемой с процессами autovacuum в PostgreSQL. Частые обновления векторных эмбеддингов в каталоге вызывали разрастание (bloat) GIN-индекса, из-за чего задержка поиска по ключевым словам p99 подскакивала до 140 мс во время массовой синхронизации товаров. Снижение autovacuum_vacuum_scale_factor до 0,05 для таблицы основного каталога принудительно заставило вакуумирование выполняться более частыми и мелкими порциями, сгладив всплеск задержки обратно до 25 мс.

Что бы мы изменили, если бы перестраивали гибридный пайплайн сегодня?

Если бы мы создавали движок извлечения заново сегодня, мы бы оценили обученные разреженные эмбеддинги вроде SPLADE или BGE-M3 напрямую вместе с плотными векторами вместо использования традиционной токенизации tsvector. Стандартному полнотекстовому поиску PostgreSQL не хватает настоящего расширения запросов (query expansion): он упускает случаи, когда в счете товар называется «Fastener», а в каталоге — «Hex bolt». Обученные разреженные эмбеддинги генерируют векторизованные токены с весами, которые улавливают как точные совпадения терминов, так и контекстные лексические расширения.

Мы бы также перешли от статических лимитов кандидатов (LIMIT 40 на подзапрос) к динамическим порогам баллов. Когда счет содержит абсолютно точный код детали вроде SKU-77381-V2, выборка и оценка 40 кандидатов плотных векторов впустую тратит вычислительные циклы. Путь выполнения на основе порогов, выходящий досрочно при высоком уровне уверенности и точном совпадении ключевых слов, сэкономил бы ориентировочно 6 мс задержки запроса p95 на стандартных форматах счетов.

Частые вопросы

Ещё статьи

ИИ и машинное обучение ·

Почему pgvector заменил Qdrant в нашем пайплайне Feedback AI

Мы перенесли 5 миллионов векторных эмбеддингов из кластера Qdrant в PostgreSQL 16 с pgvector 0.7. Рассказываем, как сократили накладные расходы на инфраструктуру, сохранив задержку поиска в пределах 50 мс.

5 мин чтения

ИИ и машинное обучение ·

Как работают ИИ-агенты: архитектура продакшн-агента

Агент — это модель, обёрнутая в восприятие, память, инструменты, оркестрацию и мониторинг. Практический разбор шести слоёв: что ломается в каждом и что мы с этим делаем.

5 мин чтения

ИИ и машинное обучение ·

RAG в продакшене: чек-лист, который мы проходим перед запуском

RAG демонстрируется за день и ломается через месяц. Двенадцать проверок — данные, поиск, генерация, эксплуатация, — которые отличают пилот от системы, на которую полагаются люди.

4 мин чтения