KbArticleSearchDocument
Elasticsearch document KbArticleSearchDocument — rebuildable search projection публичного
полнотекстового поиска базы знаний/FAQ (../api/public/search-kb-articles.md,
поиск по заголовку/анонсу/тексту/тегам с фильтром по категории/тегу и аудитории актора). Переводит FTS с
PostgreSQL-tsvector/GIN на Elasticsearch-проекцию (ADR-0059).
Назначение
Source of truth — KnowledgeBaseArticleEntity в PostgreSQL; проекция
хранит только индексируемые/фильтруемые/сортируемые/возвращаемые в карточку поля опубликованных статей. Поиск
идёт через Talent.Common.Domain.Search (ElasticsearchDocument,
IElasticsearchIndexRepository<KbArticleSearchDocument>, SearchQuery) — прямого клиента Elasticsearch в
handlers/domain нет (ADR-0059). Драйвер
перехода — typo-tolerance (fuzzy), транслитерация кириллица↔латиница и морфология русского языка для
заголовков/тела. Индекс держит только Status = Published-срез: back-office-список статей
(Draft/Archived, любые аудитории — ../api/support/list-kb-articles.md)
читает PostgreSQL напрямую и в этот индекс не ходит. Cross-cutting — по
Общим Требованиям К Документам.
Index
| Поле | Значение |
|---|---|
| Index alias | support-kb-articles |
| Versioned index | support-kb-articles-v1 (переключение alias — IElasticsearchAliasManager, ADR-0029) |
| Owner service | support (Pin.Support) |
| Source of truth | KnowledgeBaseArticleEntity (../domain/knowledge-base-article.md) в PostgreSQL |
| Rebuild job | KbArticleSearchRebuildJob : ISearchRebuildJob |
Документы — в src/services/support/Infrastructure/Search/Documents/; наполнение — инкрементально in-service
проектором по доменным событиям статьи + периодический/ручной rebuild (ADR-0059 п.2).
Document Id И Routing
- Document id:
<TenantId>:<ArticleId>(TenantId = PIN CompanyId, единый tenant платформы, ADR-0052) — детерминирован, upsert идемпотентен. - Routing key:
TenantId(единый tenant платформы; кластер малый, ADR-0056). - Delete/tombstone: документ индексируется только при
Status = Published; переход вDraft/Archivedили soft-delete (DeletedAt) удаляет документ из индекса (fail-closed — неопубликованная статья поиском не находится).
Поля
| Поле | Тип | Indexed | Stored | Analyzer | Source | Security |
|---|---|---|---|---|---|---|
DocumentId | keyword | да | да | — | TenantId:ArticleId | include |
ArticleId | keyword | да | да | — | KnowledgeBaseArticleEntity.Id | include |
TenantId | keyword | да | нет | — | KnowledgeBaseArticleEntity.TenantId | scope-only (не выдаётся) |
Slug | keyword | да | да | — | Slug | include (переход /help/{slug}) |
Title | text | да | да | ru_translit_fuzzy | Title | include |
Summary | text | да | да | ru_translit_fuzzy | Summary | include |
Body | text | да | нет | ru_translit_fuzzy | Body | полнотекст (в список не выдаётся — только матч) |
Tags | keyword + text | да | да | ru_translit_fuzzy (text-подполе) | Tags.Items (jsonb) | фильтр «тег» + матч |
SearchText | text | да | нет | ru_translit_fuzzy | Title ⊕ Summary ⊕ Body ⊕ Tags (copy_to) | основной матч Q |
Category | keyword | да | да | — | Category (KbCategory) | фильтр |
Audience | keyword | да | да | — | Audience (KbAudience) | security-trimming (аудитория) |
Status | keyword | да | нет | — | Status (только Published индексируется) | security-trimming |
PublishedAt | date | да | да | — | PublishedAt | сортировка (default при пустом Q) |
ViewsCount | long | да | да | — | ViewsCount | ранжирование «по популярности» (задел, отложено) |
SchemaVersion | integer | да | да | — | ElasticsearchDocument | версия схемы проекции |
UpdatedAt | date | да | да | — | UpdatedAt | freshness watermark |
IndexedAt | date | да | да | — | индексатор | freshness |
Внешний ввод (Q) идёт только параметром SearchQuery (multi-match по SearchText с fuzziness),
не в raw-DSL. Фильтры (Category/Tags) и релевантность/PublishedAt-сортировка — типизированными
полями SearchQuery. Анализатор ru_translit_fuzzy — русская морфология + ICU-транслитерация
(кириллица↔латиница) + edge-ngram для typo-tolerance (ADR-0059 п.1).
Source Events
| Event | Version | Transform |
|---|---|---|
ArticlePublished (IDomainEvent) | 1 | upsert документа из состояния статьи (Status = Published, маппинг полей выше) |
контент-Update при Status = Published | 1 | re-upsert изменённых полей (Title/Summary/Body/Tags/Category/Audience) |
ArticleArchived / soft-delete (DeletedAt) | 1 | delete документа по DocumentId |
База знаний интеграционных Kafka-событий не публикует (внутренний справочник); проекция наполняется
in-service проектором KbArticleSearchProjector по доменным событиям агрегата + периодический/ручной
KbArticleSearchRebuildJob для bootstrap и восстановления из PostgreSQL.
Отказоустойчивость индексации
- Транзиентный сбой записи/удаления документа — ретраи проектора; исчерпание попыток → карантин события с
алертом, без блокировки проекции. Пропущенный документ дорисовывается ближайшим
KbArticleSearchRebuildJob. - Публикация → находимость и смена
Audience(сужение) применяются как upsert/переиндексация документа; до синхронизации поиск может краткий промежуток отдавать прежнее состояние, но фактический доступ на детальном чтении всегда проверяется по актуальной записи (INV-KB-3), поэтому неопубликованный или суженный по аудитории контент через поиск не утекает (fail-closed). - Out-of-order. Watermark
UpdatedAtдокумента не даёт запоздавшему обновлению перетереть более свежее состояние; расхождение с PostgreSQL всегда сходится rebuild-ом (индекс — ускоритель, не источник истины).
Freshness
- SLA:
search(near-real-time, < 1 мин от публикации до появления в поиске; eventual относительно PostgreSQL,FreshnessView— ADR-0019). - Watermark:
IndexedAt+UpdatedAtстатьи в документе. - Stale behavior: показать результаты с freshness-маркером; тело статьи и точный
ViewsCountотдаёт ../api/public/get-kb-article.md, читающая source-of-truth PostgreSQL.
Security
- Audience/scope trimming выполняется через
SearchScopeFilterдо query execution (ADR-0059 п.3):Anonymous→Audience = Public;User→Audience ∈ {Public, AuthenticatedUsers};SupportInternalна публичном поиске никогда не отдаётся (INV-KB-3). В индексе — толькоStatus = Published. - PII fields: none (контент справочный; ФИО/телефон/реквизиты в базу знаний не попадают).
- Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные;
TenantId— только scope-фильтр, наружу не выдаётся.
Rebuild
- Создать версионный индекс
support-kb-articles-vN(маппинг +ru_translit_fuzzy). - Backfill
KbArticleSearchRebuildJobиз PostgreSQL:Filter(Status == Published && DeletedAt == null)поKnowledgeBaseArticleEntityстримом (SearchAsStream), map вKbArticleSearchDocument. - Валидация counts/checksums против источника.
- Атомарно переключить alias
support-kb-articlesна новую версию (IElasticsearchAliasManager). - Держать предыдущий индекс до конца rollback-окна.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: KnowledgeBaseArticleEntity
- API (4): GET /api/public/support/kb/articles — Поиск базы знаний/FAQ, POST /api/support/kb/articles — Создать статью базы знаний, POST /api/support/kb/articles/{id}/publish — Опубликовать/снять статью базы знаний, PUT /api/support/kb/articles/{id} — Обновить статью базы знаний
- События (1): События сервиса support: карта топиков и решения об интеграциях
- index.md (1): Support Service
Связанные документы
- Сущность-источник: ../domain/knowledge-base-article.md; потребитель поиска: ../api/public/search-kb-articles.md; детальное чтение — ../api/public/get-kb-article.md.
- ADR: 0059 Elasticsearch-проекции, 0008 Elasticsearch, 0014 scope-filters, 0052 tenancy, 0056 simplicity-first.
- SDK:
Talent.Common.Domain.Search(ElasticsearchDocument,IElasticsearchIndexRepository,SearchQuery,ISearchRebuildJob,SearchScopeFilter); шаблон: search-document-template.md.