Перейти к основному содержимому

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).

Metadata

ПолеЗначение
Сервис/контекстsupport
API areapublic
Feature groupknowledge-base
Статусdraft
OwnerPIN Platform (Care squad)
Документdocs/06-services/support/api/public/search-kb-articles.md

Актор и доступ

ПроверкаЗначение
Actor typeAnonymous (гость) / User (любой авторизованный пользователь)
Auth policy / Permissionsnone — public read; вместо отказа применяется Audience-гейт (полнота выдачи определяется наличием валидного JWT)
Scope (RBAC)не применяется (платформенное чтение published-среза; support — single-tenant platform-tool, ADR-0052)
Record-levelAudience-гейт: AnonymousPublic; UserPublic + AuthenticatedUsers; SupportInternal исключены всегда (INV-KB-3)
Tenant isolationTenantId = PIN CompanyId (единый tenant платформы); чтение с ScopeMode.System (у анонима нет tenant-claim), published-срез задаётся явным предикатом (ADR-0052)
Auditnone (read; ViewsCount инкрементится только при детальном чтении статьи — get-kb-article.md, INV-KB-4)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/public/support/kb/articles
Success status200
Idempotency headerне применяется (safe GET)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
IsAuthenticatedidentity claimsboolДа (вычисляется)из IdentityContext middleware; аноним = false; определяет Audience-гейтопределяет срез выдачи
Qquery (q)string?Нетtrim, 1..256; пустой после trim → полнотекстовый матч не применяется; передаётся параметром поиска (не raw-DSL/конкатенация)untrusted; значение поиска в логи не пишется
Categoryquery (category)KbCategory? (string)Нетenum Investor/Agent/Developer/General/SupportInternal (knowledge-base-article.md); null = все; SupportInternal на public даёт пустой срез (Audience-гейт)
Tagquery (tag)string?Нетtrim, 1..64; точный тег из KbTags.Items (jsonb-containment)
PagequeryintНет (default 1)>= 1 (PagingQuery)
PageSizequeryintНет (default 20)1..50 (PagingQuery; поисковая выдача bounded)

Сортировка не задаётся клиентом: при заданном Q — по релевантности (score Elasticsearch), иначе — по дате публикации (см. алгоритм, шаг 3).

Модель ответа: постраничный список KbListItemModel

Постраничная выдача: Items, Page, PageSize, TotalCount.

ПолеТипОбязательностьИсточникОписание
ItemsIReadOnlyList<KbListItemModel>Да (может быть пустым)страница выборкиЭлементы страницы результатов.
PageintДавходТекущая страница.
PageSizeintДавходРазмер страницы.
TotalCountlongДаCount по предикатуВсего статей под фильтром/аудиторией.

Вложенная модель KbListItemModel (проекция knowledge-base-article.md)

ПолеТипОбязательностьИсточникОписание
IdGuidДаKnowledgeBaseArticleEntity.IdИдентификатор статьи.
SlugstringДаSlugURL-суффикс для перехода /help/{slug} (get-kb-article.md).
TitlestringДаTitleЗаголовок статьи.
Summarystring?НетSummaryКраткий анонс/превью результата.
CategoryKbCategory (string)ДаCategoryКатегория (knowledge-base-article.md).
AudienceKbAudience (string)ДаAudienceАудитория (Public / AuthenticatedUsers); подсказка «войдите, чтобы читать больше».
TagsIReadOnlyList<string>Да (может быть пустым)Tags.Items (jsonb)Теги статьи (проекция KbTags).
PublishedAtDateTimeOffsetДаPublishedAtМомент публикации (non-null: отдаются только Published).
ViewsCountlongДаViewsCountПопулярность FAQ (справочно; ранжирование по популярности отложено).

Тело статьи (Body) в список не проецируется — только в детальной операции get-kb-article.md.

Алгоритм

  1. Контекст и доступ. Читателем может быть гость (Anonymous) или авторизованный пользователь (User; валидный JWT определяет полноту выдачи). support — единый платформенный tenant; поиск идёт по published-срезу, отказа по доступу нет — применяется audience-gate (шаг 3).
  2. Проверка входа. Q — trim и 1..256; Category — допустимое KbCategory; Tag — trim и 1..64; Page >= 1; PageSize ∈ 1..50; нарушение → VALIDATION_FAILED (400). Ссылочных id во входе нет.
  3. Поиск по 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; при пустом QPublishedAt desc, тай-брейк ArticleId asc (стабильная пагинация);
    • пагинации Page/PageSize; TotalCount — из общего числа совпадений.
  4. Маппинг. Поле-в-поле из документа проекции в KbListItemModel: ArticleIdId, Slug, Title, Summary, Category, Audience, Tags, PublishedAt, ViewsCount; Body в список не выдаётся. INV-KB-3 обеспечен scope-фильтром шага 3. Релевантность используется только для сортировки — наружу не отдаётся. ViewsCount в поиске не инкрементится (INV-KB-4 — только при детальном чтении статьи, get-kb-article.md).
  5. Результат. Постраничный список KbListItemModel200; пустая выдача → Items = [], TotalCount = 0 (валидное «ничего не найдено»). Свежесть: проекция eventually-consistent относительно PostgreSQL (< 1 мин); тело/точный ViewsCount отдаёт get-kb-article из source-of-truth.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsnone (база знаний Kafka-событий не публикует)
External callsElasticsearch (поисковая проекция support-kb-articles)
Cache / projections / searchчтение Elasticsearch-проекции support-kb-articles (search/kb.md, ADR-0059); индексируется Published-срез, наполнение — при изменениях статьи + rebuild; кэша нет (ADR-0056)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Нарушение правил таблицы входа (Q/Tag длина, неизвестный Category, Page/PageSize)«Некорректные параметры запроса»retry после исправления
INTERNAL_ERROR500Инфраструктурный сбой (Elasticsearch недоступен)genericretry (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 200Public + 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/ (будущий путь по конвенции).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы