docs: add search morphology + hybrid-search-exposure plans
Two feature plans grounded in the current develop: - search-language-morphology-plan.md: make the FTS text-search config selectable (env SEARCH_TS_CONFIG, whitelist english|russian|simple|ru_en; recommend ru_en for the RU/EN wiki). Documents every hardcoded 'english' touchpoint (pages.tsv trigger, page_embeddings.fts generated column, attachments.tsv, search.service.ts, hybrid lexical CTE), the DDL-baked config constraint, reindex strategy, and the regconfig SQL-injection guard. - hybrid-search-general-plan.md: expose the existing pgvector/RRF hybrid search (today agent-only via searchPages) on the user-facing /search, the UI, and the MCP search tool, reusing page-embedding.repo.hybridSearch with identical CASL/permission post-filtering and lexical fallback.
This commit is contained in:
144
docs/hybrid-search-general-plan.md
Normal file
144
docs/hybrid-search-general-plan.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Векторный / гибридный поиск в основном поиске (вынос из агента) — план
|
||||
|
||||
> Статус: план (не реализовано). **Важно про текущее состояние:** векторный
|
||||
> (pgvector) и гибридный (RRF) поиск в форке **уже есть** — но только внутри
|
||||
> агента. Пользовательский поиск `/search` (а с ним и UI-поиск, и MCP-инструмент
|
||||
> `search`) всё ещё **чисто лексический**. Эта фича — вынести существующий
|
||||
> семантический/гибридный движок на общий поисковый поверхностный слой.
|
||||
|
||||
## Как сверялось с реальным кодом (что есть, чего нет)
|
||||
|
||||
**Семантика уже реализована — но только для агента:**
|
||||
- `page_embeddings` — pgvector, **dimension-agnostic** колонка `embedding`,
|
||||
`model_name`/`model_dimensions` по строке; per-workspace; индексация через
|
||||
BullMQ (`reindexPage`/`reindexWorkspace`). Активная модель деплоя — OpenAI
|
||||
`text-embedding-3-large` (3072d). (См. [rag-improvements-plan.md](./rag-improvements-plan.md).)
|
||||
- [page-embedding.repo.ts](../apps/server/src/database/repos/ai-chat/page-embedding.repo.ts):
|
||||
- `searchByEmbedding()` — косинус `<=>` по чанкам (~стр. 143).
|
||||
- `hybridSearch()` — **RRF-слияние** косинуса и полнотекста (`fts`-CTE на
|
||||
`websearch_to_tsquery`), `k = 60`, равные веса, scope по workspace +
|
||||
доступным спейсам, фильтр по совпадающей размерности эмбеддинга (~стр. 211).
|
||||
- Поиск идёт **seq-scan** по `<=>` (ANN-индекса нет; в комментарии репо прямо
|
||||
сказано «re-add an HNSW index if [scale grows]»).
|
||||
- Потребитель — **только** агент: инструмент `searchPages` в
|
||||
[ai-chat-tools.service.ts](../apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts).
|
||||
|
||||
**Основной поиск — лексический:**
|
||||
- [search.service.ts](../apps/server/src/core/search/search.service.ts) (`/search`):
|
||||
только `pages.tsv` + `to_tsquery('english', …)`. Никаких эмбеддингов.
|
||||
- **MCP-инструмент `search`** дергает именно этот REST:
|
||||
[packages/mcp/src/client.ts:1818](../packages/mcp/src/client.ts#L1818) → `POST /search`.
|
||||
Значит, вынеся семантику в `/search`, мы автоматически прокачаем и MCP-поиск.
|
||||
- `AiService.getEmbeddingModel(workspaceId)` ([ai.service.ts](../apps/server/src/integrations/ai/ai.service.ts))
|
||||
умеет строить embedding-модель из per-workspace конфига — то есть всё нужное для
|
||||
получения вектора запроса уже есть.
|
||||
- В окружении есть `SEARCH_DRIVER` (`database` | `typesense`). Семантику делаем
|
||||
как улучшение драйвера `database`, **не** переопределяя `SEARCH_DRIVER`.
|
||||
|
||||
**Вывод:** «добавить векторный поиск» = не писать с нуля, а **переиспользовать
|
||||
`hybridSearch` в `SearchService`** с тем же контролем доступа, что у лексического
|
||||
`/search`, + graceful-фолбэк. Это главная мысль плана.
|
||||
|
||||
## Цель
|
||||
|
||||
Дать семантический/гибридный результат на общем поисковом слое (UI-поиск, REST
|
||||
`/search`, MCP `search`), а не только агенту — чтобы «уволить» находило
|
||||
«расторжение трудового договора», и чтобы это было доступно вне чата.
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Контракт API
|
||||
Добавить в `SearchDTO` параметр `mode: 'lexical' | 'semantic' | 'hybrid'`.
|
||||
- Дефолт — `lexical` (обратная совместимость; тайп-ахед остаётся дешёвым).
|
||||
- `hybrid`/`semantic` — включается явно (страница полнотекстового поиска, тумблер
|
||||
в UI, или MCP с `mode:'hybrid'`).
|
||||
|
||||
### Поток в `SearchService.searchPage` для hybrid/semantic
|
||||
1. Если эмбеддинги настроены (`getEmbeddingModel` не кидает) → эмбеддить **запрос**
|
||||
(один вызов на поиск).
|
||||
2. Вызвать `hybridSearch(workspaceId, queryVector, queryText, candidates, accessibleSpaceIds)`
|
||||
— over-fetch чанков.
|
||||
3. Чанки → страницы: дедуп по `pageId` (лучший score), маппинг в `SearchResponseDto`.
|
||||
4. **Контроль доступа 1-в-1 с лексическим путём**: пост-фильтр через
|
||||
`pagePermissionRepo.filterAccessiblePageIds(...)` (как в текущем `/search`,
|
||||
стр. 129–139). Scope по доступным спейсам уже внутри `hybridSearch`, но
|
||||
post-filter по правам страниц обязателен.
|
||||
5. Highlight — `ts_headline` по `content` чанка (релевантнее, чем по странице).
|
||||
6. Любой сбой/некочиг (эмбеддинги не настроены, embedding упал, нет доступных
|
||||
спейсов, гибрид пуст) → **graceful fallback на лексический путь** (тот же
|
||||
паттерн, что уже использует агентский инструмент).
|
||||
|
||||
### MCP и UI
|
||||
- MCP: после появления `mode` в `/search` — прокинуть его в
|
||||
[packages/mcp/src/client.ts](../packages/mcp/src/client.ts) и в схему MCP-тула.
|
||||
Помнить: `packages/mcp` держит **свою копию** схемы (по `AGENTS.md`).
|
||||
- UI: тайп-ахед (`searchSuggestions`) остаётся лексическим; семантику включать на
|
||||
полной странице поиска / тумблером, не на каждое нажатие клавиши.
|
||||
|
||||
## Итерации
|
||||
|
||||
### Итерация 1 (MVP, backend)
|
||||
`mode` в DTO; ветка hybrid в `SearchService` (эмбеддинг запроса → `hybridSearch`
|
||||
→ чанк→страница дедуп → пост-фильтр прав → highlight; иначе лексический фолбэк).
|
||||
Спеки на: паритет прав (закрытая страница не утекает), фолбэк без эмбеддингов,
|
||||
дедуп страниц.
|
||||
|
||||
### Итерация 2 (MCP + UI)
|
||||
Прокинуть `mode` в MCP-тул `search` (+ синхронизировать схему-зеркало) и добавить
|
||||
переключатель/режим на странице поиска клиента.
|
||||
|
||||
### Итерация 3 (производительность и качество)
|
||||
- Кеш/дебаунс эмбеддингов запроса (не эмбеддить одинаковые запросы повторно).
|
||||
- ANN-индекс при росте корпуса (см. оговорку про dimension-agnostic ниже).
|
||||
- Общий оценочный харнес с [rag-improvements-plan.md §C](./rag-improvements-plan.md)
|
||||
(один золотой датасет на агентский и пользовательский поиск).
|
||||
|
||||
## Точки изменения
|
||||
|
||||
- [search.service.ts](../apps/server/src/core/search/search.service.ts) — ветвление по `mode`,
|
||||
переиспользование `hybridSearch`, маппинг чанк→страница, общий пост-фильтр прав.
|
||||
- `search.dto.ts` — поле `mode`.
|
||||
- [page-embedding.repo.ts](../apps/server/src/database/repos/ai-chat/page-embedding.repo.ts) —
|
||||
`hybridSearch`/`searchByEmbedding` уже есть; при необходимости — перегрузка,
|
||||
возвращающая поля под `SearchResponseDto` (без дублирования логики).
|
||||
- `search.module.ts` — подключить доступ к embedding-модели и репозиторию
|
||||
эмбеддингов (DI).
|
||||
- [packages/mcp/src/client.ts](../packages/mcp/src/client.ts) + схема MCP-тула — `mode`.
|
||||
- Клиент: страница/тумблер поиска (итерация 2).
|
||||
|
||||
## Безопасность и граничные случаи
|
||||
|
||||
- **Паритет прав — риск №1.** Агентский `searchPages` скоупит по доступным
|
||||
спейсам и пост-фильтрует права; общий поиск **обязан** делать то же
|
||||
(`filterAccessiblePageIds`), иначе семантика утечёт чанки закрытых страниц.
|
||||
Покрыть спеком утечки.
|
||||
- **Путь шар (`shareId`)** в `/search` — анонимный, без per-user скоупа
|
||||
эмбеддингов. Для шар оставить лексический поиск (или строго ограничить
|
||||
поддеревом шары); семантику для анонимов в MVP не включать.
|
||||
- **Стоимость/латентность.** Каждый семантический запрос = 1 вызов embedding-API
|
||||
(~сотни мс + токены). Поэтому дефолт `lexical`, семантика — по явному режиму,
|
||||
не на тайп-ахед.
|
||||
- **Чанк→страница.** Страница может прийти из нескольких чанков — дедуп с лучшим
|
||||
score; иначе дубликаты в выдаче.
|
||||
- **Свежие страницы.** Только что созданная/изменённая страница попадёт в
|
||||
семантику после отработки BullMQ-`reindexPage`. До этого её ловит лексическая
|
||||
сторона (если есть `fts`-чанк) либо общий лексический фолбэк. Документировать
|
||||
как осознанный лаг.
|
||||
- **Фильтр размерности.** `hybridSearch` сравнивает только чанки с
|
||||
`model_dimensions == dim(query)`. После смены embedding-модели старые чанки
|
||||
невидимы до переиндексации (свойство уже существующего движка).
|
||||
- **ANN-индекс vs dimension-agnostic колонка.** Сейчас seq-scan по `<=>` — норм на
|
||||
масштабе вики. HNSW/IVFFlat требуют фиксированной размерности, а колонка
|
||||
намеренно dimension-agnostic → ANN потребует либо фиксации размерности, либо
|
||||
частичных индексов на размерность. Решать при реальном росте, не в MVP.
|
||||
- **Связь с морфологией.** Лексический CTE гибрида использует `'english'`
|
||||
(`page_embeddings.fts`). План [search-morphology-language-plan.md](./search-morphology-language-plan.md)
|
||||
меняет этот конфиг — координировать, чтобы язык был единым в обоих поисках.
|
||||
|
||||
## Оговорки
|
||||
|
||||
- Это **не дубль** [rag-improvements-plan.md](./rag-improvements-plan.md): тот про
|
||||
качество retrieval агента (реранкер, чанкинг, вложения, харнес). Здесь — про
|
||||
**поверхность** (вынос уже готового движка в пользовательский/MCP поиск).
|
||||
- Реранкер из rag-плана (бэклог §A), когда появится, можно переиспользовать и
|
||||
здесь — точка вставки та же (между over-fetch гибрида и финальным срезом).
|
||||
178
docs/search-morphology-language-plan.md
Normal file
178
docs/search-morphology-language-plan.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Выбор языка морфологии для полнотекстового поиска — план
|
||||
|
||||
> Статус: план (не реализовано). Контекст: gitmost — форк Docmost. Весь
|
||||
> лексический поиск сейчас жёстко прибит к конфигу `'english'`, из-за чего на
|
||||
> русской вики не работает стемминг (по запросу «сервер» не находятся
|
||||
> «серверы / серверов / сервером»). Цель — сделать язык текстового поиска
|
||||
> конфигурируемым, с разумным дефолтом для русско-английского контента.
|
||||
|
||||
## Как сверялось с реальным кодом
|
||||
|
||||
Все факты ниже проверены по дереву `develop` на момент написания.
|
||||
|
||||
### Где сейчас зашит `'english'`
|
||||
|
||||
**Индексная сторона (DDL — конфиг «запечён» в схему):**
|
||||
|
||||
1. `pages.tsv` — наполняется **триггером** `pages_tsvector_trigger()`
|
||||
(`BEFORE INSERT OR UPDATE ON pages`). Тело функции:
|
||||
`setweight(to_tsvector('english', f_unaccent(coalesce(new.title,''))),'A') || setweight(to_tsvector('english', f_unaccent(...text_content...)),'B')`.
|
||||
Заведено в [20240324T086800-pages-tsvector-trigger.ts](../apps/server/src/database/migrations/20240324T086800-pages-tsvector-trigger.ts),
|
||||
обновлено под `f_unaccent` в [20250729T213756-add-unaccent-pg_trm-update-tsvector..ts](../apps/server/src/database/migrations/20250729T213756-add-unaccent-pg_trm-update-tsvector..ts).
|
||||
GIN-индекс — `pages_tsv_idx`.
|
||||
2. `page_embeddings.fts` — **GENERATED ALWAYS … STORED** колонка:
|
||||
`to_tsvector('english', f_unaccent(content))`, GIN-индекс `idx_page_embeddings_fts`.
|
||||
Заведена в [20260618T150000-page-embeddings-fts.ts](../apps/server/src/database/migrations/20260618T150000-page-embeddings-fts.ts).
|
||||
Это лексическая сторона гибридного (RRF) поиска агента.
|
||||
3. `attachments.tsv` — колонка `tsvector` + GIN `attachments_tsv_idx`
|
||||
([20250901T184612-attachments-search.ts](../apps/server/src/database/migrations/20250901T184612-attachments-search.ts)).
|
||||
**Путь наполнения этой колонки в коде не локализован** (в миграции триггера
|
||||
нет) — перед реализацией нужно найти, кто и каким конфигом её пишет, и
|
||||
привести к тому же языку (или признать колонку неиспользуемой).
|
||||
|
||||
**Сторона запроса (рантайм SQL — должна совпадать с индексом):**
|
||||
|
||||
4. [search.service.ts](../apps/server/src/core/search/search.service.ts) — пользовательский
|
||||
REST-поиск `/search`. Три вхождения `'english'`: `ts_rank(tsv, to_tsquery('english', …))`
|
||||
(стр. 50), `ts_headline('english', …)` (стр. 53), `WHERE tsv @@ to_tsquery('english', …)`
|
||||
(стр. 60). **Через этот же эндпоинт ходит MCP-инструмент `search`**
|
||||
([packages/mcp/src/client.ts:1818](../packages/mcp/src/client.ts#L1818) → `POST /search`),
|
||||
поэтому фикс автоматически чинит и MCP-поиск.
|
||||
5. [page-embedding.repo.ts](../apps/server/src/database/repos/ai-chat/page-embedding.repo.ts) —
|
||||
лексический CTE гибридного поиска: `websearch_to_tsquery('english', f_unaccent(queryText))`
|
||||
(~стр. 252) + `ts_rank(pe.fts, q.query)`.
|
||||
|
||||
### Что уже есть и помогает
|
||||
|
||||
- Расширения `unaccent` и `pg_trgm` **уже установлены** (миграция 20250729T213756),
|
||||
`f_unaccent(text)` объявлена `IMMUTABLE` (важно: только IMMUTABLE-функцию можно
|
||||
использовать в выражении GENERATED-колонки и в индексе).
|
||||
- `searchSuggestions` (тайп-ахед по `users`/`groups`/`pages.title`) работает не
|
||||
через `tsvector`, а через `ILIKE`/`f_unaccent` (подстрока) — он **уже
|
||||
языконезависим**, морфология его не касается. Трогать не нужно.
|
||||
- В окружении уже есть абстракция `SEARCH_DRIVER` (`database` | `typesense`,
|
||||
дефолт `database`) — см. `environment.service.ts` / `environment.validation.ts`.
|
||||
Наша задача относится к драйверу `database`.
|
||||
|
||||
## Ключевое ограничение (почему это не «рантайм-переключатель»)
|
||||
|
||||
Конфиг текстового поиска у GIN-индекса и у `tsvector`-колонки **запечён в DDL**
|
||||
(в теле триггера и в выражении GENERATED-колонки). При запросе конфиг в
|
||||
`to_tsquery(<config>, …)` **обязан совпадать** с тем, которым построен индекс,
|
||||
иначе токены не сматчатся. Поэтому язык — это **выбор уровня деплоя**, а не
|
||||
параметр запроса. Сделать конфиг по-настоящему «на строку» (per-workspace) можно,
|
||||
но дорого: `to_tsvector(regconfig_column, text)` неиммутабельна, значит её
|
||||
**нельзя** положить в GENERATED-колонку `page_embeddings.fts` (только в
|
||||
триггер-наполняемую), а запрос по корпусу со смешанными конфигами требует знать
|
||||
конфиг каждой строки. Это вариант D ниже — откладываем.
|
||||
|
||||
Ещё нюанс выбора конфига:
|
||||
- `russian` — снежковый стемминг + русские стоп-слова. Минус: режет английские
|
||||
технические термины и выкидывает русские стоп-слова (по «и»/«в»/«не» не найти).
|
||||
- `english` (как сейчас) — стеммит по-английски, для русского почти бесполезен.
|
||||
- `simple` — без стемминга и стоп-слов: только токенизация + lowercase (+ наш
|
||||
`f_unaccent`). Языконезависим, но нет морфологии («серверы» ≠ «сервер»).
|
||||
- Технические RU+EN-вики (как WirenBoard) — это **смесь**: лучший охват даёт
|
||||
объединённый вектор `to_tsvector('russian', x) || to_tsvector('english', x)`.
|
||||
|
||||
## Варианты решения (по возрастанию сложности)
|
||||
|
||||
### Вариант A — глобальный конфиг через env (рекомендуемый механизм)
|
||||
Ввести `SEARCH_TS_CONFIG` (значения из белого списка: `english` | `russian` |
|
||||
`simple` | `ru_en`), дефолт `english` (обратная совместимость для текущих
|
||||
инсталляций). Значение применяется в трёх местах: тело триггера `pages`,
|
||||
выражение GENERATED-колонки `page_embeddings.fts`, и интерполяция в запросах
|
||||
(`search.service.ts`, `page-embedding.repo.ts`).
|
||||
- **Плюсы:** одна понятная ручка; покрывает 95 % кейсов (один язык на инсталляцию).
|
||||
- **Минусы:** смена значения требует пересборки индексов и переиндексации (см. ниже).
|
||||
|
||||
### Вариант B — объединённый RU+EN вектор (значение `ru_en` варианта A)
|
||||
В тех же местах генерировать `to_tsvector('russian', f_unaccent(x)) || to_tsvector('english', f_unaccent(x))`,
|
||||
а на стороне запроса OR-ить два `to_tsquery`. **Рекомендуемый дефолт для этой
|
||||
русско-английской вики.**
|
||||
- **Плюсы:** морфология и для русского, и для английского без per-row конфига.
|
||||
- **Минусы:** ~2× размер `tsvector`, чуть «шумнее» ранжирование (приемлемо на
|
||||
масштабе вики в сотни–тысячи страниц).
|
||||
|
||||
### Вариант C — `simple` + pg_trgm
|
||||
`SEARCH_TS_CONFIG=simple` + триграммный фолбэк (`pg_trgm` уже стоит) для нечёткого
|
||||
совпадения по `title`/`text_content`.
|
||||
- **Плюсы:** работает на любом языке без выбора; дёшево.
|
||||
- **Минусы:** нет морфологии; trgm даёт только похожесть подстрок, не словоформы.
|
||||
Запасной вариант, если не хотим фиксировать язык.
|
||||
|
||||
### Вариант D — per-workspace/per-space `regconfig`
|
||||
Колонка `search_config regconfig` + триггерное наполнение `pages.tsv`; для
|
||||
`page_embeddings.fts` пришлось бы заменить GENERATED-колонку на
|
||||
триггер-наполняемую. Максимум гибкости для мультиязычных инсталляций, максимум
|
||||
сложности и риска. **Откладываем**, пока не появится реальная мультиязычность.
|
||||
|
||||
## Рекомендация
|
||||
|
||||
Механизм — **вариант A** (env `SEARCH_TS_CONFIG`, белый список, дефолт `english`),
|
||||
с поддержкой значения **`ru_en`** (вариант B) и рекомендацией ставить именно его
|
||||
на этой вики. `simple`/`russian` остаются доступными значениями.
|
||||
|
||||
## Точки изменения
|
||||
|
||||
**Backend / DB:**
|
||||
- Новая миграция (timestamp — позже последней применённой, см. правило
|
||||
упорядочивания миграций в `AGENTS.md`):
|
||||
- `CREATE OR REPLACE` функции `pages_tsvector_trigger()` с выбранным конфигом.
|
||||
- Пересборка существующих строк: одноразовый `UPDATE pages SET title = title`
|
||||
(перефайр триггера) либо явный `UPDATE pages SET tsv = <новое выражение>`.
|
||||
- `page_embeddings.fts`: `DROP COLUMN fts` + повторный `ADD COLUMN fts … GENERATED …`
|
||||
с новым конфигом (GENERATED-колонка пересчитается для всех строк
|
||||
автоматически; переэмбеддинг **не нужен** — это только текст), пересоздать
|
||||
`idx_page_embeddings_fts`.
|
||||
- `attachments.tsv` — привести к тому же конфигу после локализации её писателя.
|
||||
- `environment.validation.ts` / `environment.service.ts`: добавить `SEARCH_TS_CONFIG`
|
||||
+ геттер `getSearchTsConfig()` с **валидацией по белому списку** (см. безопасность).
|
||||
- Запросы: [search.service.ts](../apps/server/src/core/search/search.service.ts) (3 места) и
|
||||
[page-embedding.repo.ts](../apps/server/src/database/repos/ai-chat/page-embedding.repo.ts)
|
||||
(лексический CTE) — взять конфиг из `EnvironmentService`. Для `ru_en` — OR двух
|
||||
`to_tsquery`/`websearch_to_tsquery` и `||` двух `to_tsvector`.
|
||||
- `.env.example` — задокументировать переменную.
|
||||
|
||||
**Frontend:** изменений не требуется (поиск получает результаты как раньше).
|
||||
|
||||
## Безопасность
|
||||
|
||||
Имя `regconfig` **нельзя** интерполировать в SQL как сырую строку из env — это
|
||||
SQL-инъекция/невалидный конфиг → 500. Разрешать только из **белого списка**
|
||||
(`english`/`russian`/`simple`/`ru_en`) на уровне геттера; в SQL подставлять уже
|
||||
сматченное константное имя, а не пользовательский ввод.
|
||||
|
||||
## Граничные случаи и оговорки
|
||||
|
||||
- **Highlight (`ts_headline`) должен использовать тот же конфиг**, что и матч,
|
||||
иначе подсветка «съедет». Для `ru_en` подсветку проще делать одним конфигом
|
||||
(`russian`) либо вызывать `ts_headline` по тому конфигу, который дал матч.
|
||||
- **Стоп-слова `russian`** удаляются из индекса — по ним искать нельзя (компромисс
|
||||
морфологии). `simple`/`ru_en` это смягчают.
|
||||
- **Свежесозданные/изменённые страницы**: `pages.tsv` пересчитывается триггером
|
||||
на каждый write — без проблем. `page_embeddings.fts` пересчитывается при
|
||||
следующей переиндексации чанков (BullMQ `reindexPage`), но миграция уже
|
||||
пересоберёт колонку для всех текущих строк.
|
||||
- **Переиндексация после смены конфига обязательна** (иначе старые `tsv` останутся
|
||||
в прежнем языке). Для `pages`/`attachments` — в самой миграции; для крошек/
|
||||
контента эмбеддингов — кнопка «Reindex now» (см.
|
||||
[rag-improvements-plan.md](./rag-improvements-plan.md)).
|
||||
- **Связь с гибридным поиском агента**: меняя конфиг в `page_embeddings.fts` и в
|
||||
лексическом CTE `page-embedding.repo.ts`, мы меняем и качество RRF-поиска агента
|
||||
— это согласованное улучшение, но проверить регрессии тестами `ai-chat`.
|
||||
- **Зависимость с планом «гибридный поиск в основном поиске»**
|
||||
([hybrid-search-general-plan.md](./hybrid-search-general-plan.md)): оба плана
|
||||
трогают `'english'` в лексических запросах. Координировать порядок, чтобы конфиг
|
||||
везде был единым.
|
||||
|
||||
## Тестирование
|
||||
|
||||
- Интеграционный спек: проиндексировать страницу со словом «серверы», искать
|
||||
«сервер» → при `russian`/`ru_en` находит, при `english` — нет.
|
||||
- Смешанный RU+EN документ под `ru_en`: матч и по русской словоформе, и по
|
||||
английскому термину.
|
||||
- Проверка whitelist: некорректное значение env → конфигурация падает на старте
|
||||
(validation), а не уходит в SQL.
|
||||
- Регрессия MCP `search` и REST `/search` на латинице (поведение `english`
|
||||
сохраняется при дефолте).
|
||||
Reference in New Issue
Block a user