GET /api/public/support/kb/articles — Поиск базы знаний/FAQ
Назначение
Публичный полнотекстовый поиск справки/FAQ ПИН (матрица 4.7 «база знаний для пользователей», 7.1/7.2):
пользователь (гость или авторизованный) ищет опубликованные статьи по строке запроса, категории и тегу и
получает страницу карточек результатов. Owning-контекст — support; source of truth — support.kb_articles
в PostgreSQL, а поиск идёт по Elasticsearch-проекции support-kb-articles
(search/kb.md, ADR-0059).
Feature group — knowledge-base.
Доступ без гейта отказа: аноним и авторизованный видят разный срез по аудитории (INV-KB-3) — гостю
отдаются только Public-статьи, авторизованному — Public + AuthenticatedUsers; SupportInternal
(внутренние инструкции службы заботы) на public-API никогда не отдаются. Границы: детальное чтение
статьи и инкремент ViewsCount — соседняя операция get-kb-article.md; авторская
выдача (Draft/Archived, любые аудитории) — support-area (list-kb-article, README).
| Поле | Значение |
|---|
| Сервис/контекст | support |
| API area | public |
| Feature group | knowledge-base |
| Статус | draft |
| Owner | PIN Platform (Care squad) |
| Документ | docs/06-services/support/api/public/search-kb-articles.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Anonymous (гость) / User (любой авторизованный пользователь) |
| Auth policy / Permissions | none — public read; вместо отказа применяется Audience-гейт (полнота выдачи определяется наличием валидного JWT) |
| Scope (RBAC) | не применяется (платформенное чтение published-среза; support — single-tenant platform-tool, ADR-0052) |
| Record-level | Audience-гейт: Anonymous → Public; User → Public + AuthenticatedUsers; SupportInternal исключены всегда (INV-KB-3) |
| Tenant isolation | TenantId = PIN CompanyId (единый tenant платформы); чтение с ScopeMode.System (у анонима нет tenant-claim), published-срез задаётся явным предикатом (ADR-0052) |
| Audit | none (read; ViewsCount инкрементится только при детальном чтении статьи — get-kb-article.md, INV-KB-4) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/public/support/kb/articles |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
IsAuthenticated | identity claims | bool | Да (вычисляется) | из IdentityContext middleware; аноним = false; определяет Audience-гейт | определяет срез выдачи |
Q | query (q) | string? | Нет | trim, 1..256; пустой после trim → полнотекстовый матч не применяется; передаётся параметром поиска (не raw-DSL/конкатенация) | untrusted; значение поиска в логи не пишется |
Category | query (category) | KbCategory? (string) | Нет | enum Investor/Agent/Developer/General/SupportInternal (knowledge-base-article.md); null = все; SupportInternal на public даёт пустой срез (Audience-гейт) | — |
Tag | query (tag) | string? | Нет | trim, 1..64; точный тег из KbTags.Items (jsonb-containment) | — |
Page | query | int | Нет (default 1) | >= 1 (PagingQuery) | — |
PageSize | query | int | Нет (default 20) | 1..50 (PagingQuery; поисковая выдача bounded) | — |
Сортировка не задаётся клиентом: при заданном Q — по релевантности (score Elasticsearch), иначе — по дате
публикации (см. алгоритм, шаг 3).
Модель ответа: постраничный список KbListItemModel
Постраничная выдача: Items, Page, PageSize, TotalCount.
| Поле | Тип | Обязательность | Источник | Описание |
|---|
Items | IReadOnlyList<KbListItemModel> | Да (может быть пустым) | страница выборки | Элементы страницы результатов. |
Page | int | Да | вход | Текущая страница. |
PageSize | int | Да | вход | Размер страницы. |
TotalCount | long | Да | Count по предикату | Всего статей под фильтром/аудиторией. |
| Поле | Тип | Обязательность | Источник | Описание |
|---|
Id | Guid | Да | KnowledgeBaseArticleEntity.Id | Идентификатор статьи. |
Slug | string | Да | Slug | URL-суффикс для перехода /help/{slug} (get-kb-article.md). |
Title | string | Да | Title | Заголовок статьи. |
Summary | string? | Нет | Summary | Краткий анонс/превью результата. |
Category | KbCategory (string) | Да | Category | Категория (knowledge-base-article.md). |
Audience | KbAudience (string) | Да | Audience | Аудитория (Public / AuthenticatedUsers); подсказка «войдите, чтобы читать больше». |
Tags | IReadOnlyList<string> | Да (может быть пустым) | Tags.Items (jsonb) | Теги статьи (проекция KbTags). |
PublishedAt | DateTimeOffset | Да | PublishedAt | Момент публикации (non-null: отдаются только Published). |
ViewsCount | long | Да | ViewsCount | Популярность FAQ (справочно; ранжирование по популярности отложено). |
Тело статьи (Body) в список не проецируется — только в детальной операции get-kb-article.md.
Алгоритм
- Контекст и доступ. Читателем может быть гость (
Anonymous) или авторизованный пользователь (User;
валидный JWT определяет полноту выдачи). support — единый платформенный tenant; поиск идёт по
published-срезу, отказа по доступу нет — применяется audience-gate (шаг 3).
- Проверка входа.
Q — trim и 1..256; Category — допустимое KbCategory; Tag — trim и 1..64;
Page >= 1; PageSize ∈ 1..50; нарушение → VALIDATION_FAILED (400). Ссылочных id во входе нет.
- Поиск по Elasticsearch-проекции. Разрешённые аудитории по актору:
Anonymous → { Public };
User → { Public, AuthenticatedUsers }; SupportInternal не включается никогда (INV-KB-3). Единый
поисковый запрос к проекции support-kb-articles (search/kb.md) собирается из:
- жёсткого scope-фильтра (до выполнения запроса, ADR-0059 п.3):
Status = Published + Audience ∈ allowedAudiences (публичный поиск не видит SupportInternal/Draft/Archived);
- фильтра по
Category (при заданном);
- точного фильтра по тегу
Tag (объём справочника мал, ADR-0056);
- полнотекстового матча
Q по Title/Summary/Body/Tags с fuzziness/транслитерацией (анализатор
ru_translit_fuzzy, ADR-0059 п.1) — параметром запроса, не raw-DSL;
- сортировки: при заданном
Q — по релевантности, тай-брейк PublishedAt desc; при пустом Q —
PublishedAt desc, тай-брейк ArticleId asc (стабильная пагинация);
- пагинации
Page/PageSize; TotalCount — из общего числа совпадений.
- Маппинг. Поле-в-поле из документа проекции в
KbListItemModel: ArticleId→Id, Slug, Title,
Summary, Category, Audience, Tags, PublishedAt, ViewsCount; Body в список не выдаётся.
INV-KB-3 обеспечен scope-фильтром шага 3. Релевантность используется только для сортировки — наружу не
отдаётся. ViewsCount в поиске не инкрементится (INV-KB-4 — только при детальном чтении статьи,
get-kb-article.md).
- Результат. Постраничный список
KbListItemModel → 200; пустая выдача → Items = [],
TotalCount = 0 (валидное «ничего не найдено»). Свежесть: проекция eventually-consistent относительно
PostgreSQL (< 1 мин); тело/точный ViewsCount отдаёт get-kb-article из
source-of-truth.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (база знаний Kafka-событий не публикует) |
| External calls | Elasticsearch (поисковая проекция support-kb-articles) |
| Cache / projections / search | чтение Elasticsearch-проекции support-kb-articles (search/kb.md, ADR-0059); индексируется Published-срез, наполнение — при изменениях статьи + rebuild; кэша нет (ADR-0056) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | Нарушение правил таблицы входа (Q/Tag длина, неизвестный Category, Page/PageSize) | «Некорректные параметры запроса» | retry после исправления |
INTERNAL_ERROR | 500 | Инфраструктурный сбой (Elasticsearch недоступен) | generic | retry (client backoff) |
Пустая выдача — 200 c Items = [], TotalCount = 0 (не ошибка).
Acceptance criteria
- AC-1 (гость видит только Public). Given опубликованные статьи разных аудиторий; When
GET /api/public/support/kb/articles анонимом; Then 200 — в выдаче только Audience = Public
(Status = Published), AuthenticatedUsers/SupportInternal/Draft/Archived исключены.
- AC-2 (авторизованный видит больше). Given те же статьи; When запрос авторизованным
User;
Then 200 — Public + AuthenticatedUsers (INV-KB-3).
- AC-3 (внутренние статьи скрыты всегда). Given
Category = SupportInternal или статьи
Audience = SupportInternal; When любой public-запрос; Then они никогда не попадают в выдачу
(жёсткий scope-фильтр до выполнения запроса).
- AC-4 (полнотекст и сортировка). Given задан
Q; When поиск; Then сортировка по
релевантности с тай-брейком PublishedAt desc; Given Q пуст; Then сортировка PublishedAt desc,
тай-брейк ArticleId asc (стабильная пагинация).
- AC-5 (фильтры). Given заданы
Category/Tag; When поиск; Then выдача сужена по категории и
точному тегу.
- AC-6 (пустая выдача). Given под фильтром ничего не найдено; When поиск; Then
200 с
Items = [], TotalCount = 0 (не ошибка).
- AC-7 (поиск недоступен). Given Elasticsearch недоступен; When поиск; Then
INTERNAL_ERROR
(500), клиент повторяет с backoff; source of truth (PostgreSQL) и детальное чтение статьи не затронуты.
- AC-8 (валидация). Given
Q > 256/Tag > 64/неизвестный Category/Page < 1/PageSize ∉ 1..50;
When запрос; Then VALIDATION_FAILED (400).
Совместимость и наблюдаемость
- Новые optional фильтры/поля карточки — additive (без breaking); удаление/переименование/смена типа поля
или расширение выдачи
SupportInternal-аудитории на public — breaking (новая версия route + rollout).
- Enum-поля (
KbCategory/KbAudience) сериализуются строками; клиент обязан иметь fallback на
unknown-значения (knowledge-base-article.md).
- Logs: actor type (
anonymous/user), hasQuery, Category, hasTag, Page/PageSize, TotalCount
(без значения Q); metrics: support_kb_search_total{authenticated}, latency histogram (SLO поиска
p95 < 1 с — README «Runtime И Эксплуатация»); traces: OTel span
support.search-kb-articles. Dashboard pin-support; runbook —
docs/06-services/support/runbooks/ (будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы