← Knowledge Store

Hybrid search

knowledge-store · workzone

Knowledge Store владеет retrieval-слоем — отбором кандидатов из хранилища; RAG поверх (rerank моделью, упаковка контекста, вызов LLM) делает Query Engine. Поиск стоит на четырёх примитивах над тремя проекциями одной базы: текст ищут двумя дополняющими парадигмами сразу — плотным вектором и разреженной лексикой, — а граф и реляционку по их природе. KS отдаёт примитивы двояко: по отдельности (как tools) и слитыми в один результат (RRF / взвешенно). Права применяются один раз — в хранилище, как pre-filter, до ранжирования. Где кончается KS и начинаются Query / Agent Engine — на границе.

Четыре примитива поиска

Не один способ искать, а четыре дополняющих друг друга. Проекций три, но текст заслуживает двух парадигм сразу: плотный вектор ловит смысл, разреженная лексика — точную форму слов, которую эмбеддинг сглаживает; плюс обход связей по графу и точные фильтры по телу. Каждый — это самостоятельный SQL над своей таблицей; их можно слить в один результат либо отдать наружу как инструменты по намерению (ниже).

vector AI
поиск по близости · ANN
chunks · pgvector / HNSW

Запрос пользователя сперва эмбеддится моделью (инференс), затем по вектору ищутся ближайшие фрагменты — близость по смыслу, а не совпадение слов. Возвращает фрагменты, ранжированные по расстоянию.

lexical
поиск по словам · full-text
chunks · tsvector / GIN

Те же фрагменты, но поиск по форме слова, не по смыслу: точные термины, аббревиатуры, коды, имена — то, что вектор сглаживает. Postgres full-text (tsvector + GIN), ранг ts_rank. Разреженный сигнал рядом с плотным: вместе добирают то, что каждый поодиночке упускает.

graph
обход связей · 1–3 шага
entity_edge · recursive SQL (CTE)

Рекурсивный обход рёбер от стартовых узлов на ближний контекст (1–3 шага): тред, иерархия, упоминания. Приносит связанное, что соседствует в графе, но не похоже по тексту. Стартовые узлы даёт вызывающий: как tool — переданные сущности, в слитом режиме — топ-хиты текстовых примитивов и sql.

sql
точные фильтры · агрегаты
entities · реляционные предикаты

Точечный отбор по телу записи: тип, источник, статус, даты. Детерминированный фильтр — то, что нужно знать заведомо, без догадки о смысле. Поверх отбора — агрегаты (счёт, группировка, тренд) закрытым набором: числа считает Postgres детерминированно, не модель в уме.

Слияние списков: четыре парадигмы в один результат

Каждый примитив отдаёт свой ранжированный список; fusion сводит четыре в один — через RRF (reciprocal rank fusion, слияние по обратному рангу: вес позиции, а не сырого балла, который у разных парадигм несравним) или взвешенно. Слияние идёт по рангу, не по баллу, — потому и не «score-level»: сравнивать сырые скоры вектора, лексики и обхода нечем. Слияние четырёх парадигм под едиными правами — отличительная черта KS: «Query Engine собирает результаты» читается на RAG-слое, а не как сырое кросс-сторовое слияние из нескольких СУБД.

Списки разной зернистости: vector и lexical ранжируют фрагменты, sql и graph — сущности. Общий ключ слияния — сущность: фрагментные хиты сворачиваются к родителю по entity_id, лучший фрагмент остаётся при ней доказательством для упаковки контекста; сущность, пришедшую несколькими примитивами, RRF дедуплицирует и складывает её ранги из разных списков.

vector
lexical
graph
sql
weighted / RRF
fused
Инструменты по намерению tools · агент

Агенту даются три инструмента по намерению: search (текстовый retrieval — vector+lexical, слитые RRF в коде, модель не сводит dense+sparse руками), graph и sql. Качественно разные операции разведены, текстовое слияние не свалено на LLM — агент комбинирует осознанно.

Собранный hybrid-результат RAG retrieval

Готовый слитый список — один вызов, fusion внутри KS. Опора RAG-retrieval: дальше его остаётся переранжировать и упаковать в контекст.

ACL применяется один раз — в хранилище

Права — это не отдельный шаг после поиска, а условие самого запроса. Вектора, граф и реляционка живут в одной базе, поэтому ACL pre-filter — тот же SQL: кандидаты сужаются по доступу до ранжирования, одним JOIN, без второй СУБД и кросс-базовой сверки прав. Один проход покрывает все четыре примитива и их слияние — отозванное не доходит до fusion, а не отсеивается постфактум.

Личность вызывающего — вход retrieval: потребитель передаёт identity, а KS разворачивает её в резолв-джойн и применяет фильтр одним SQL. Контракт узкий — передай личность, получи кандидатов под правами; сам джойн потребитель не строит и схему прав не видит.

Запрос — значение, не код. Примитивы — параметризованный SQL: запрос, вектор и фильтры идут связанными значениями, не склейкой в тело запроса; поля sql-фильтра (тип, источник, статус, даты) — закрытый список, не произвольный ввод; агрегаты — тоже закрытый набор (метрика × измерение из перечня), не свободный SQL. Любой вход — хоть из RAG, хоть из агента — в SQL-инъекцию не превратится.

Жёсткий ACL-фильтр поверх приближённого индекса HNSW даёт подводный камень — разобран у самой таблицы chunks.

Граница: retrieval здесь, ответ — дальше

KS заканчивается на отборе кандидатов. Что идёт поверх — за границей модуля, у двух потребителей: один берёт собранный hybrid-результат одним вызовом и доводит его до ответа (Query Engine — по решению chat-модели), другой берёт инструменты по намерению — search · graph · sql — в агентном цикле (Agent Engine).

sql отдаёт и второй формат: не кандидатов для fusion, а готовый агрегат (счёт, группировка, тренд) над ACL-prefilter'нутым срезом. Это вне слияния — отдельный режим инструмента; интерпретирует итог уже агент.

Knowledge Store retrieval

Четыре ACL-фильтрованных примитива и их fusion. Отдаёт кандидатов — фрагменты и сущности, ранжированные, под правами. На этом storage-слой кончается.

Query Engine RAG по запросу

Когда chat-модель зовёт поиск, берёт собранный hybrid-результат и доводит до grounding-ответа: rerank (RRF из KS в v1), упаковка контекста и генерация AI LLM. Один опциональный вызов, без агентной петли.

Потребители retrieval-слоя — оба ещё стабы: собранный результат уходит на RAG в → Query Engine, а инструменты по намерению (search · graph · sql) — в → Agent Engine.

Пустой индекс: производное свойство is_empty

База может быть пуста — компания развернула платформу, но не подключила источники, или приём ещё не дал ни одного фрагмента. Тогда в chunks нет ни строки: retrieval не по чему работать, и звать его незачем. KS выставляет это наружу производным свойством is_empty — есть ли вообще фрагменты для поиска. Это не флаг и не «режим», который кто-то включает, а следствие наличия данных — ровно как is_available() у SMTP: свойство модели, не тумблер. Реализация вправе держать дешёвую кэшируемую метрику вместо счёта на лету.

Свойство читает общий tool-calling харнесс: при is_empty он не вносит KS-инструменты в схему модели вовсе — на обеих поверхностях, и в чате (search_knowledge), и у агента (ядро search · graph · sql). Модель не видит поиска и отвечает как обычный ассистент; заземление не «выключено», его условие просто не наступило. Первый принятый фрагмент переключает свойство сам — инструменты появляются без единой настройки.