You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(PRD): status table, context-mode advisory, retrieval §5.8, NR backlog
Sync public knowledge PRD with shipped search (corpus, locale, MCP rate limit).
Clarify context-mode is client-side; server may adopt BM25 ideas separately.
Made-with: Cursor
Copy file name to clipboardExpand all lines: docs/PRD-public-knowledge-search-service.md
+94-16Lines changed: 94 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,6 +14,8 @@
14
14
15
15
**Связь с MCP:** текущий поисковый sidecar — **основной upstream для инструментов знаний в `mcp-server`** (поиск по корпусу для MCP-клиентов). Любые изменения сервиса **`search`** (имя контейнера, URL, порты, формат вызова, переменные окружения, контракт ответа, health, аутентификация внутри сети при необходимости) **обязаны** быть отражены **в реализации MCP в том же репозитории `repo/api`**: код сервера MCP, конфигурация Docker Compose, деплой, тесты. Публичный REST под **`/knowledge/api`** и браузерный UI — **дополнительная** поверхность того же сервиса; **нельзя** менять только контейнер поиска без обновления путей вызова и контракта, которыми пользуется MCP. Подробно §5.3.
16
16
17
+
**Инструменты агента (например [context-mode](https://github.com/mksglu/context-mode)):** см. **§13** — это **не** замена сервису `search` на сервере, а дополнительный слой на машине разработчика/агента.
18
+
17
19
---
18
20
19
21
## 2. Goals
@@ -34,14 +36,19 @@
34
36
-**Внутренняя реализация ответа** (как формируется текст ответа, откуда берётся контекст, какие компоненты задействованы) — **вне этого PRD** и **не отражается** во внешнем REST-контракте; клиентам это недоступно и не требуется.
35
37
- Оплата/биллинг — вне scope; tier привязан к **валидности токена** и **конфигу**, не к Stripe.
36
38
- Глобальный CDN — не требуется для v1.
39
+
-**Встраивание клиентского MCP (в т.ч. context-mode) внутрь контейнера `search` как обязательной зависимости публичного API** — вне scope: см. **§13**.
37
40
38
41
---
39
42
40
43
## 4. Current state (baseline)
41
44
42
-
- Прод-стек: **Caddy** проксирует большую часть трафика на **`mcp-server:3000`**; sidecar поиска (сейчас **`qwen-search`**) слушает внутри сети (например `:8790`).
43
-
-**MCP (`mcp-server`) напрямую зависит от этого сервиса:** инструменты knowledge вызывают поиск по HTTP (или контейнерному режиму) через конфигурацию вроде **`QWEN_MODE`**, **`QWEN_API_URL=http://qwen-search:8790`**, клиент в **`src/`** (qwen/MCP-логика), **`depends_on` / health** в **`docker-compose.prod.yml`**. Без работоспособного поискового sidecar цепочка MCP → знания **не выполняется**.
44
-
- Токен **`API_TOKEN`** уже выдаётся оператором/ботом и кладётся в `.env` (для публичного API и других компонентов; для внутреннего вызова MCP из docker-сети правила могут отличаться — см. реализацию).
45
+
*(по состоянию репозитория `repo/api`, дорожная карта — **§12**.)*
46
+
47
+
- Прод-стек: **Caddy** отдаёт **`/knowledge*`** на контейнер **`search`** (внутренний HTTP, порт **`8790`**); остальной трафик — на **`mcp-server:3000`** по существующим правилам.
48
+
-**Сервис Compose:** имя **`search`**, образ **`spawndock/search:prod`**; каталог корпуса **`./knowledge:/corpus:ro`**; переменная **`KNOWLEDGE_ROOT=/corpus`** (см. `docker-compose.prod.yml`).
49
+
-**MCP (`mcp-server`):** при **`SEARCH_REST_API=true`** вызывает **`SEARCH_API_URL`** (например `http://search:8790`) на **`POST /knowledge/api/v1/search`**; внутрисетевые запросы **без**`X-Forwarded-For`**не** попадают под публичный rate limit (**§5.4.6** реализовано).
50
+
-**Пайплайн ответа:** локальное ранжирование фрагментов из корпуса (Markdown под `KNOWLEDGE_ROOT`), подстановка выдержек и директивы **`locale`** в промпт **Qwen Code CLI**; поля **`sources`** заполняются из ответа модели либо fallback по ранжированным файлам. Аутентификация Qwen — OAuth (`PROD_QWEN_OAUTH_CREDS` / `QWEN_OAUTH_CREDS_B64` → `~/.qwen/oauth_creds.json` в entrypoint).
51
+
- Токен **`API_TOKEN`** выдаётся оператором/ботом и кладётся в `.env` на **`search`** и связанные сервисы; невалидный Bearer → **401**.
45
52
46
53
## 5. Target architecture
47
54
@@ -256,27 +263,49 @@ rate_limit_tiers:
256
263
-**Шифрование в транзите до пользователя** обеспечивается **Caddy** (и инфраструктурой перед ним), не отдельной настройкой `search`.
257
264
- Рассмотреть **WAF-уровень** позже (на Caddy или выше — вне scope `search` v1).
Раздел **не меняет** внешний REST-контракт, но фиксирует текущую реализацию и направления улучшений.
269
+
270
+
| Компонент | Статус | Заметки |
271
+
|-----------|--------|---------|
272
+
| Монтирование корпуса **`/corpus`** (прод) и **`KNOWLEDGE_ROOT`**|**Done**| Образ также содержит снимок **`knowledge/`** в **`/opt/search/knowledge`** для запуска без volume. |
273
+
| Ранжирование фрагментов перед вызовом LLM |**Done**| Эвристика по токенам запроса и `.md` (Unicode-токены для кириллицы); см. `docker/search/knowledge-rank.mjs`, зеркало логики в `src/local-search.ts`. |
274
+
| Учёт **`locale`** в промпте |**Done**| Явные инструкции `ru` / `en` / авто по языку запроса. |
275
+
| Поле **`sources`**|**Done**| Из JSON ответа модели; если пусто — fallback из ранжированных источников. |
276
+
| Диагностика сбоев Qwen (**stdout/stderr** в **502**) |**Done**| Усечённые потоки в `message` для оператора. |
277
+
278
+
**Будущие улучшения (см. §12, NR-RET):** по желанию заменить или дополнить эвристику поиском уровня **BM25 / FTS** (как в локальных MCP-инструментах), **RRF**, нормализация запросов — без изменения путей **`/knowledge/api/v1/*`**.
279
+
259
280
---
260
281
261
282
## 6. Configuration reference (для операторов)
262
283
263
-
Пример переменных (имена итоговые — утвердить в implementer pass):
284
+
Актуальный набор (расширять по мере появления переменных в `docker-compose` / entrypoint):
264
285
265
-
-`SEARCH_HTTP_PORT`
266
-
-`SEARCH_RATE_LIMIT_TIERS` (JSON) **или** путь к файлу `SEARCH_RATE_LIMIT_CONFIG`
267
-
-`API_TOKEN` (shared secret для basic-tier в v1)
268
-
-`TRUSTED_PROXY_CIDRS` / `REAL_IP_HEADER` для корректного IP клиента **за Caddy** (ingress уже терминирует TLS и проксирует HTTP на `search`)
286
+
| Переменная | Назначение |
287
+
|------------|------------|
288
+
|`SEARCH_HTTP_PORT` / `QWEN_HTTP_PORT`| Порт HTTP listener (по умолчанию **8790**). |
289
+
|`SEARCH_HTTP_BIND` / `QWEN_HTTP_BIND`| Bind address (по умолчанию **0.0.0.0**). |
290
+
|`KNOWLEDGE_ROOT`| Корень Markdown-корпуса (**рекомендуется `/corpus`** в проде). |
291
+
|`SEARCH_RATE_LIMIT_TIERS`| JSON override лимитов **free** / **basic** (см. §5.6). |
292
+
|`API_TOKEN`| Общий секрет для **Bearer** и tier **basic** на **`search`**. |
293
+
|`PROD_QWEN_OAUTH_CREDS` / `QWEN_OAUTH_CREDS_B64`| Base64 **oauth_creds** для Qwen CLI в контейнере; после смены секрета — **пересобрать/перезапустить****`search`**. |
|`X-Forwarded-For`| От Caddy — определяет публичный клиентский IP для rate limit; внутренние вызовы без этого заголовка лимит не потребляют. |
296
+
297
+
*Исторически:* часть имён сохраняет префикс **`QWEN_*`** у sidecar; новые параметры по возможности именовать **`SEARCH_*`**.
269
298
270
299
---
271
300
272
301
## 7. Migration plan
273
302
274
-
1. Ввести сервис **`search`** параллельно с `qwen-search` (опциональный short-lived alias в compose).
275
-
2.**Обновить реализацию MCP** под новый hostname/URL/контракт: код (`src/mcp.ts`, `src/qwen/*` и смежное), переменные окружения **`mcp-server`**, интеграционные тесты; убедиться, что инструменты knowledge снова получают ответы от **`search`**.
4.~~Обновить ENV **`mcp-server`**; зачистить устаревшие **`QWEN_*`** где заменены~~ — **частично** (некоторые **`QWEN_*`**ещё используются у образа/search — см. compose; при рефакторинге свести к одной схеме имён).
6.Документация оператора / бот-тексты — **по необходимости** (ссылка на этот PRD и OPERATOR.md).
280
309
281
310
---
282
311
@@ -285,6 +314,8 @@ rate_limit_tiers:
285
314
- Метрики: счётчики по tier (accepted, 429), latency search.
286
315
- Алерты: рост доли 429 на free (возможная атака) vs. organic traffic.
287
316
317
+
**Статус:** в коде **`search`** пока нет встроенного Prometheus/OpenTelemetry; **NR-OBS** — вынести счётчики и гистограммы latency в следующую итерацию.
318
+
288
319
---
289
320
290
321
## 9. Testing requirements
@@ -294,12 +325,15 @@ rate_limit_tiers:
294
325
-**MCP ↔ `search`:** тесты, подтверждающие, что **`mcp-server`** (или его модуль knowledge tool) получает корректный ответ от **`search`** при конфигурации, совпадающей с compose-деплоем.
295
326
- Contract: snapshot OpenAPI + тесты **`/knowledge/api/v1/health`**, **`/knowledge/api/v1/search`**, плюс отдача UI и спецификации под **`/knowledge/`**.
296
327
328
+
**Сделано в репозитории:** оффлайн-проверка OpenAPI (`src/__tests__/knowledge-openapi-contract.test.ts`); **smoke** против прода/стенда: **`npm run smoke:knowledge`** (`scripts/smoke-knowledge-search.mjs`). E2E с поднятым Caddy в CI — **желательно** (NR-TEST).
329
+
297
330
---
298
331
299
332
## 10. Open questions
300
333
301
-
1. Дневной лимит: граница **UTC 00:00** vs. часовой пояс оператора (для v1 обычно UTC).
334
+
1. Дневной лимит: граница **UTC 00:00** vs. часовой пояс оператора (для v1 обычно UTC) — **зафиксировать в OPERATOR.md**.
302
335
2. Нужны ли дополнительные read-only эндпоинты под **`/knowledge/api/v1/...`** (например метаданные корпуса) с правилом «без квоты» или «с квотой».
336
+
3. Нужна ли явная **версия корпуса** / ETag в **`meta`** ответа **search** для клиентов SDK.
303
337
304
338
---
305
339
@@ -310,4 +344,48 @@ rate_limit_tiers:
310
344
311
345
---
312
346
313
-
*Document version: 1.4 — 2026-03-24 — добавлена обязательная связь с реализацией **MCP** (§1, §4, §5.3, G7, миграция, тесты); уточнены внутренний vs публичный вход и rate limit для MCP (§5.4.6).*
|**NR-RET-1**| Оценить **FTS5 / BM25** или **сведение рангов (RRF)** для релевантности фрагментов корпуса вместо или вместе с текущей эвристикой; сохранить контракт API. | P2 |
366
+
|**NR-RET-2**| Опциональный **кэш** ответов по `(query нормализованный, locale, версия корпуса)` при неизменном корпусе — снижение стоимости Qwen и latency. | P3 |
367
+
|**NR-OBS-1**| Метрики (**accepted/429/latency/502**) и точки интеграции с мониторингом хоста. | P2 |
368
+
|**NR-HA-1**| При **>1 реплики**`search` — вынести дневные/минутные счётчики rate limit из in-memory (**Redis** и аналоги); см. §5.6.3. | P2 |
369
+
|**NR-TEST-1**| CI: e2e контейнер **`search`** + health + search с моком Qwen или dry-run режимом. | P3 |
## 13. Context-mode ([`mksglu/context-mode`](https://github.com/mksglu/context-mode)) и оптимизация
375
+
376
+
**Что это:** MCP-сервер и связанные хуки для **локальной** среды агента (Cursor, Claude Code и др.). Даёт **песочницу** для команд, **индексацию** (FTS5, BM25, RRF) и **поиск по индексу** так, чтобы **сырые объёмы** не попадали в контекстное окно модели.
377
+
378
+
**Связь с сервисом `search`:**
379
+
380
+
| Вопрос | Ответ |
381
+
|--------|--------|
382
+
| Можно ли «просто подключить» context-mode внутрь Docker **`search`** как замену Qwen? |**Нет.** context-mode не является HTTP-сервисом ответов на вопросы пользователей; это протокол **MCP** и инструменты для **клиента** агента. |
383
+
| Дает ли context-mode выигрыш **на сервере** SpawnDock? |**Не напрямую.** Запуск MCP stdio в sidecar усложнит архитектуру и не даст публичному REST стабильного контракта без отдельного адаптера. Имеет смысл **заимствовать идеи** (BM25, RRF,/snippets) в **`knowledge-rank`** или отдельном модуле — см. **NR-RET-1** / §5.8. |
384
+
| Где context-mode полезен продукту SpawnDock? | Для **разработчиков и операторов**: при работе с логами деплоя, длинной документацией и ответами **`/knowledge/api`** использовать **`ctx_execute` / `ctx_batch_execute` / `ctx_index`** так, чтобы агент не засорял контекст; при интеграции с публичным API — снимать крупные JSON через sandbox. |
385
+
| Лицензия | У проекта context-mode лицензия **ELv2** — перед копипастой кода в **`repo/api`** нужна **правовая** проверка; безопаснее внедрять схожие алгоритмы самостоятельно или через зависимости с совместимой лицензией. |
386
+
387
+
**Итог:** context-mode **не** ставится обязательной зависимостью сервиса **`search`**; оптимизация **серверного** поиска идёт через **NR-*** и §5.8; context-mode рекомендуется как **дополнение на стороне клиента** при разработке и эксплуатации.
388
+
389
+
---
390
+
391
+
*Document version: 1.5 — 2026-03-25 — §4/§6/§7 актуализированы; §5.8 пайплайн; §12 статус и backlog; §13 context-mode; §9 smoke-тесты; NR для retrieval/observability/HA.*
0 commit comments