# Улучшение качества RAG-поиска агента — план по итерациям > Статус: живой документ. Итерация 1 **реализована** (см. ниже). Остальное — > бэклог на следующие итерации, отсортированный по «качество / усилие». > Контекст: gitmost — форк Docmost. Семантический поиск агента: per-workspace > эмбеддинги в `page_embeddings` (pgvector, dimension-agnostic колонка, seq-scan > с `<=>`), индексация через BullMQ (`reindexPage` / `reindexWorkspace`). > Активная embedding-модель деплоя: OpenAI `text-embedding-3-large` (3072d). ## Как сверялось с реальным кодом Внешнее предложение по улучшению RAG было сверено с кодовой базой. Точные факты на момент итерации 1: - Хранилище: [page_embeddings](../apps/server/src/database/migrations/20260617T120000-page-embeddings.ts), колонка `embedding` сделана dimension-agnostic в [20260617T140000](../apps/server/src/database/migrations/20260617T140000-page-embeddings-dimension-agnostic.ts); `model_name` / `model_dimensions` хранятся по строке. - Полнотекстовые индексы **уже существуют** (предложение ошибочно утверждало обратное): `pages_tsv_idx` на `pages.tsv` и `attachments_tsv_idx`. Конфигурация — `to_tsvector('english', f_unaccent(...))` + `setweight` ([тут](../apps/server/src/database/migrations/20250729T213756-add-unaccent-pg_trm-update-tsvector..ts)). - Чанкинг: `RecursiveCharacterTextSplitter` 1000/200, без префиксов. - Префиксы `query:` / `passage:` **не нужны**: они требуются для e5/bge/gte/Qwen3, а деплой на OpenAI `text-embedding-3-large` (этот пункт предложения неприменим). - Вложения (`attachment_id` в схеме есть) **не индексируются** — индексатор всегда пишет `attachmentId: null`. --- ## Итерация 1 — РЕАЛИЗОВАНО Три «низковисящих фрукта»: ### 1. Хлебные крошки заголовков в чанках Файл: [embedding-indexer.service.ts](../apps/server/src/core/ai-chat/embedding/embedding-indexer.service.ts). Каждый чанк префиксуется путём заголовков `«Заголовок страницы > H1 > H2»` перед эмбеддингом. Крошки строятся обходом **ProseMirror JSON** (`heading`-ноды с `attrs.level`), а не markdown-текста — поэтому `#` внутри fenced-код-блока (типичный bash-сниппет в WirenBoard-вики) **никогда** не принимается за заголовок. Деградация к старому plain-text чанкингу при отсутствии/сбое `content`. Префикс попадает и в эмбеддинг, и в `content` (а значит — в лексический индекс `fts` и в сниппет агента). ### 2. Гибридный поиск (RRF), слияние двух инструментов в один - Миграция [20260618T150000-page-embeddings-fts.ts](../apps/server/src/database/migrations/20260618T150000-page-embeddings-fts.ts): генерируемая колонка `fts tsvector GENERATED ALWAYS AS (to_tsvector('english', f_unaccent(content))) STORED` + GIN-индекс. Конфиг совпадает с `pages.tsv` (та же обработка unaccent/Cyrillic); `f_unaccent` IMMUTABLE → триггер не нужен. - Репозиторий: метод `hybridSearch` в [page-embedding.repo.ts](../apps/server/src/database/repos/ai-chat/page-embedding.repo.ts) — один SQL-запрос, два CTE (cosine + `websearch_to_tsquery`), слияние Reciprocal Rank Fusion через FULL OUTER JOIN на уровне чанков. `k=60` (дефолт Cormack 2009 / ES / OpenSearch / Weaviate), равные веса 1.0/1.0. RRF сливает **ранги**, поэтому несовместимость шкал BM25 и косинуса не требует нормализации. Dimension-фильтр — только на семантической стороне. - Инструменты: `semanticSearch` удалён, `searchPages` стал единым гибридным инструментом ([ai-chat-tools.service.ts](../apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts)). Контроль доступа сохранён 1-в-1 (scope по доступным спейсам + пост-фильтр прав страниц). Если эмбеддинги не настроены / эмбеддинг упал / нет доступных спейсов / гибрид пуст → graceful fallback на прежний REST-полнотекст (CASL-enforced). ### 3. Переписывание запроса + описания инструментов - Описание `searchPages` теперь явно просит агента переформулировать вопрос в сфокусированный поисковый запрос и переискивать при слабой выдаче (это переживает кастомный admin-промпт, т.к. лежит в описании инструмента). - Одна строка-подсказка добавлена в `DEFAULT_PROMPT` ([ai-chat.prompt.ts](../apps/server/src/core/ai-chat/ai-chat.prompt.ts)). > ВАЖНО после деплоя: чтобы крошки и `fts` появились у существующих страниц, нужна > **переиндексация корпуса** (кнопка «Reindex now» / `WORKSPACE_CREATE_EMBEDDINGS`). > Миграция заполнит `fts` у текущих строк автоматически, но крошки добавляются только > при переиндексации (она же перезапишет `content`). ### Известные нюансы текущей реализации (осознанные компромиссы) - Гибрид покрывает только проиндексированные чанки. Свежесозданная страница становится искомой после отработки её BullMQ-`reindexPage`. Пока эмбеддинги не настроены — работает только REST-fallback (полнотекст уровня страницы по `pages.tsv`). - Если **весь** пул кандидатов гибрида (до 200 чанков) оказался из закрытых для пользователя страниц, инструмент вернёт пусто, а не уйдёт в keyword-fallback. Узкий кейс; возможное улучшение — fallback и при пустом результате пост-фильтра. - `fts` использует конфиг `english` (как и `pages.tsv`) — без русской стеммизации. Для русской вики это консистентно с текущим поиском; переход на `simple`/`russian` конфиг — отдельная задача с переиндексацией. - `candidates` (=clamp(limit×5, 50, 200)) служит и per-CTE лимитом, и финальным лимитом слияния; веса RRF равные. Тюнится после появления оценочного харнесса. --- ## Бэклог следующих итераций (по приоритету «качество / усилие») ### A. Реранкер (cross-encoder) — наибольший ROI после гибрида Вставить между over-fetch гибрида и дедупом: брать топ-50–100 кандидатов от `hybridSearch`, реранкать, оставлять топ-5–10. Ожидаемый прирост precision/MRR +10–25 %. Точка вставки уже готова — это шаг между `hybridSearch(... candidates)` и циклом дедупа в `searchPages`. - Хостовый старт (раз уже на OpenAI-инфраструктуре): **Cohere Rerank** или **Voyage `rerank-2.5`** — провайдер по аналогии с текущим pluggable embedding-конфигом. - Self-hosted (под Ollama-этос): **BGE-reranker-v2-m3** через HF Text Embeddings Inference (`/rerank`), либо FlashRank (ONNX/CPU, ~15–30 мс). - Диагностика: если реранк не двигает метрики — узкое место в recall (чанкинг/гибрид), а не в ранжировании. ### B. Индексация вложений — закрыть пробел покрытия Схема уже готова (`attachment_id`). Добавить в BullMQ-flow шаг извлечения текста из PDF/документов (PyMuPDF для цифровых PDF; OCR для сканов; для таблиц — markdown через LLM-парсер) и вливать его в тот же путь чанк→эмбеддинг→`fts`, помечая `attachment_id`. Структура извлечённых данных важнее голой точности OCR. ### C. Тюнинг гибрида и оценочный харнесс - Золотой датасет 30–100 примеров (вопрос → нужная страница/чанк) + Ragas/DeepEval (Recall@k, MRR/nDCG, context precision/recall, faithfulness). Прогон до/после каждого изменения. **Прерогатива пропущена в итерации 1 осознанно** — без неё все нижеследующие тюнинги делаются «на глаз». - После харнесса: тюнить веса RRF (старт 1.0/1.0), `k` (старт 60), число `candidates`. - Эксперимент: чанки ~512 симв. против 1000 (предложение указывает на рост precision). ### D. Contextual Retrieval (Anthropic), если крошек мало Один LLM-вызов на чанк добавляет предложение-контекст. Снижение провалов выдачи на 35–49 %. Ложится в BullMQ-`reindexPage`; на сотнях страниц с prompt caching — копейки. Применять, только если хлебных крошек окажется недостаточно против потери контекста. ### E. ParadeDB `pg_search` (настоящий BM25), если лексика станет узким местом Нативный `ts_rank` использует только TF и длину документа, без IDF. `pg_search` (Rust/Tantivy) даёт честный BM25-индекс. Не drop-in (свои операторы вместо `@@`) — это изменение кода, а не флаг. На сотнях страниц нативного `tsvector` хватает; брать только если качество лексического ранжирования упрётся в потолок. ### F. Прочее - **Префиксы query/passage** — НЕ нужны на OpenAI. Внедрять только при переходе на e5/bge/gte/Qwen3 (тогда индексатор ставит `passage:`, запрос — `query:`; BGE-v1.5, наоборот, префиксов НЕ должна получать). Зафиксировано как ловушка на будущее. - **Апгрейд embedding-модели** — уже на `text-embedding-3-large` (топ среди закрытых). Matryoshka (обрезка размерности) — запас на будущее; dimension-agnostic колонка делает миграцию тривиальной (цена — переэмбеддинг корпуса). - **HyDE и широкий multi-query/RAG-Fusion** — НЕ рекомендуются как дефолт: в свежих бенчмарках уступали и добавляют задержку/галлюцинации. ## Оговорки - Все внешние числа (62→84 % precision, +17 % Recall@5, −35…49 % провалов, +10–25 % от реранка) получены на ДРУГИХ корпусах (SEC-отчёты, финтекст, право, медицина). На этой вики величины будут иными — поэтому пункт C (свой датасет) обязателен перед тонким тюнингом. Внешние числа — направление, не гарантия величины. - Часть источников предложения — вендорский маркетинг (Cohere, Voyage, ParadeDB); направление подтверждается независимыми (T2-RAGBench, оценка Anthropic), но величины у вендоров могут быть завышены.