Knowledge Store владеет retrieval-слоем — отбором кандидатов из хранилища; RAG поверх (rerank моделью, упаковка контекста, вызов LLM) делает Query Engine. Поиск стоит на четырёх примитивах над тремя проекциями одной базы: текст ищут двумя дополняющими парадигмами сразу — плотным вектором и разреженной лексикой, — а граф и реляционку по их природе. KS отдаёт примитивы двояко: по отдельности (как tools) и слитыми в один результат (RRF / взвешенно). Права применяются один раз — в хранилище, как pre-filter, до ранжирования. Где кончается KS и начинаются Query / Agent Engine — на границе.
Не один способ искать, а четыре дополняющих друг друга. Проекций три, но текст заслуживает двух парадигм сразу: плотный вектор ловит смысл, разреженная лексика — точную форму слов, которую эмбеддинг сглаживает; плюс обход связей по графу и точные фильтры по телу. Каждый — это самостоятельный SQL над своей таблицей; их можно слить в один результат либо отдать наружу как инструменты по намерению (ниже).
Запрос пользователя сперва эмбеддится моделью (инференс), затем по вектору ищутся ближайшие фрагменты — близость по смыслу, а не совпадение слов. Возвращает фрагменты, ранжированные по расстоянию.
Те же фрагменты, но поиск по форме слова, не по смыслу: точные
термины, аббревиатуры, коды, имена — то, что вектор сглаживает.
Postgres full-text (tsvector + GIN), ранг
ts_rank. Разреженный сигнал рядом с плотным: вместе
добирают то, что каждый поодиночке упускает.
Рекурсивный обход рёбер от стартовых узлов на ближний контекст (1–3 шага): тред, иерархия, упоминания. Приносит связанное, что соседствует в графе, но не похоже по тексту. Стартовые узлы даёт вызывающий: как tool — переданные сущности, в слитом режиме — топ-хиты текстовых примитивов и sql.
Точечный отбор по телу записи: тип, источник, статус, даты. Детерминированный фильтр — то, что нужно знать заведомо, без догадки о смысле. Поверх отбора — агрегаты (счёт, группировка, тренд) закрытым набором: числа считает Postgres детерминированно, не модель в уме.
Каждый примитив отдаёт свой ранжированный список; fusion сводит четыре в один — через RRF (reciprocal rank fusion, слияние по обратному рангу: вес позиции, а не сырого балла, который у разных парадигм несравним) или взвешенно. Слияние идёт по рангу, не по баллу, — потому и не «score-level»: сравнивать сырые скоры вектора, лексики и обхода нечем. Слияние четырёх парадигм под едиными правами — отличительная черта KS: «Query Engine собирает результаты» читается на RAG-слое, а не как сырое кросс-сторовое слияние из нескольких СУБД.
Списки разной зернистости: vector и lexical ранжируют
фрагменты, sql и
graph — сущности. Общий ключ слияния — сущность: фрагментные хиты
сворачиваются к родителю по entity_id, лучший фрагмент
остаётся при ней доказательством для упаковки контекста; сущность,
пришедшую несколькими примитивами, RRF дедуплицирует и складывает её
ранги из разных списков.
Агенту даются три инструмента по намерению: search
(текстовый retrieval — vector+lexical,
слитые RRF в коде, модель не сводит dense+sparse руками),
graph и sql. Качественно разные операции
разведены, текстовое слияние не свалено на LLM — агент комбинирует
осознанно.
Готовый слитый список — один вызов, fusion внутри KS. Опора RAG-retrieval: дальше его остаётся переранжировать и упаковать в контекст.
Права — это не отдельный шаг после поиска, а условие самого запроса. Вектора, граф и реляционка живут в одной базе, поэтому ACL pre-filter — тот же SQL: кандидаты сужаются по доступу до ранжирования, одним JOIN, без второй СУБД и кросс-базовой сверки прав. Один проход покрывает все четыре примитива и их слияние — отозванное не доходит до fusion, а не отсеивается постфактум.
Личность вызывающего — вход retrieval: потребитель передаёт identity, а KS разворачивает её в резолв-джойн и применяет фильтр одним SQL. Контракт узкий — передай личность, получи кандидатов под правами; сам джойн потребитель не строит и схему прав не видит.
Запрос — значение, не код. Примитивы —
параметризованный SQL: запрос, вектор и фильтры идут связанными
значениями, не склейкой в тело запроса; поля
sql-фильтра (тип, источник, статус, даты) — закрытый
список, не произвольный ввод; агрегаты — тоже закрытый набор
(метрика × измерение из перечня), не свободный SQL. Любой вход — хоть
из RAG, хоть из агента — в SQL-инъекцию не превратится.
Жёсткий ACL-фильтр поверх приближённого индекса HNSW даёт подводный
камень — разобран
у самой таблицы chunks.
KS заканчивается на отборе кандидатов. Что идёт поверх — за границей
модуля, у двух потребителей: один берёт собранный hybrid-результат
одним вызовом и доводит его до ответа (Query Engine — по
решению chat-модели), другой берёт инструменты по намерению —
search · graph · sql — в
агентном цикле (Agent Engine).
sql отдаёт и второй формат: не кандидатов для fusion, а
готовый агрегат (счёт, группировка, тренд) над
ACL-prefilter'нутым срезом. Это вне слияния — отдельный режим
инструмента; интерпретирует итог уже агент.
Четыре ACL-фильтрованных примитива и их fusion. Отдаёт кандидатов — фрагменты и сущности, ранжированные, под правами. На этом storage-слой кончается.
Когда 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). Модель не
видит поиска и отвечает как обычный ассистент; заземление не «выключено»,
его условие просто не наступило. Первый принятый фрагмент переключает
свойство сам — инструменты появляются без единой настройки.