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

PostSearchDocument

Elasticsearch document PostSearchDocument — rebuildable search projection полнотекстового поиска опубликованных постов ленты новостей (GET /api/v1/posts с непустым searchTerm — ТЗ.md:47). Переводит поиск постов с PostgreSQL-tsvector/GIN на Elasticsearch (ADR-0059): заменяет SearchVector + partial GIN ix_posts_search_vector where status='Published' (ранее UC-NWS-005).

Назначение

Source of truth — агрегат Post; проекция хранит только индексируемые/фильтруемые/ сортируемые поля опубликованного поста (заголовок, подзаголовок, «название», денормализованный plaintext блоков SearchText, attribution). Поиск идёт через Talent.Common.Domain.Search (ElasticsearchDocument, IElasticsearchIndexRepository<PostSearchDocument>, SearchQuery) — прямого клиента Elasticsearch в handlers/domain нет. Драйвер перехода — fuzzy (typo-tolerance), транслитерация кириллица↔латиница и русская морфология для заголовков/текста постов и имён авторов/компаний (ADR-0059 п.1). Только searchTerm-поиск использует проекцию; лента без searchTerm читается keyset-ом из PostgreSQL (list-global-feed.md).

Index

ПолеЗначение
Index aliasnews-posts
Versioned indexnews-posts-v1
Owner servicenews
Source of truthPost (news.posts, только Status = Published)
Rebuild jobPostSearchRebuildJob (ISearchRebuildJob)

Document Id И Routing

  • Document id: Post.Id (Guid v7). Опубликованный контент — платформенного уровня (ADR-0052 п.3), tenant-префикс к id не применяется.
  • Routing key: AuthorCompanyId (при наличии) — посты одной компании колокализуются; для постов инвестора/ сотрудника (AuthorCompanyId = null) — routing по Id.
  • Delete/tombstone: Status покидает Published (Unpublished/Archived) или soft-delete поста → удаление документа из индекса. Пост компании со CompanyReplica.Status = Suspended → документ удаляется консюмером identity-companies и восстанавливается при снятии suspension (INV-REP-003).

Поля

ПолеТипIndexedStoredAnalyzerSourceSecurity
IdkeywordдадаPost.Id
AuthorUserIdkeywordдадаPost.AuthorUserIdscope-filter (авторские выборки)
AuthorTypekeywordдадаPost.AuthorTypeфильтр «тип автора»
AuthorCompanyIdkeywordдадаPost.AuthorCompanyIdrouting/фильтр «компания-автор»
CategoryIdkeywordдадаPost.CategoryIdфильтр «категория»
ProjectIdskeyword[]дадаPost.ProjectRefs.ProjectIdsфильтр «проект»
Nametextданетru_translit_fuzzyPost.Name
Titletextданетru_translit_fuzzyPost.Title
Subtitletextданетru_translit_fuzzyPost.Subtitle
BlocksTexttextданетru_translit_fuzzyPost.SearchText (plaintext блоков TextBlock.PlainText)
SearchTexttextданетru_translit_fuzzyName ⊕ Title ⊕ Subtitle ⊕ BlocksText (copy_to)основной матч searchTerm
PublishedAtdateдадаPost.PublishedAtсортировка «по дате»
ViewsCountlongдадаPost.ViewsCountсортировка «по популярности»
StatuskeywordдадаPost.Status (Published)security-trimming
IsAuthorCompanySuspendedbooleanдадаCompanyReplica.Status = Suspended (денорм.)security-trimming
SchemaVersionintegerдададокументверсия схемы проекции
UpdatedAtdateдадаPost.UpdatedAtfreshness watermark
IndexedAtdateдадаиндексаторfreshness

Анализатор ru_translit_fuzzy: русская морфология + ICU-транслитерация (кириллица↔латиница) + edge-ngram для typo-tolerance (ADR-0059 п.1). Тело блоков-подборок (SelectionEmbed), display-данные объектов недвижимости, галереи и обложки в индекс не попадают — ищется только текст (Name/Title/Subtitle/ SearchText). Ранжирование/PromotionBoost в проекции не участвуют — recommended отложен (решение №10).

Source Events

EventVersionTransform
PostVersionCapturedEvent (IDomainEvent, publish/republish → Published)v1upsert документа из текущего состояния Post (маппинг полей выше)
PostContentChangedEvent (IDomainEvent, пост уже Published)v1upsert документа (пересчёт BlocksText/SearchText)
PostUnpublishedEvent / PostArchivedDomainEvent (Unpublished/Archived)v1delete документа
CompanySuspended / CompanyUpserted (консюмер identity-companies)v1пересчёт IsAuthorCompanySuspended / delete-restore документов компании

Проекция наполняется инкрементально доменными событиями агрегата Post (тот же коммит, что мутация — через диспетчер доменных событий SDK) плюс периодический/ручной PostSearchRebuildJob для bootstrap и восстановления консистентности. Черновики (Draft) в индекс не попадают — публикация обязательна.

Идемпотентность и порядок индексации. Upsert по Id идемпотентен (повтор события даёт тот же документ). Конкурентные/переставленные события не должны откатывать проекцию назад: более раннее по Post.UpdatedAt событие не перезаписывает уже проиндексированное более свежее состояние (watermark UpdatedAt), а delete (Unpublished/Archived/soft-delete/suspend) не воскрешается последующим устаревшим upsert. Неизвестное значение AuthorType индексируется как есть (keyword): фильтр «тип автора» по нему просто не сматчит, но документ остаётся находимым по тексту (деградация фильтра, не потеря документа). Rebuild-джоб перекрывает пропуски инкрементальных событий (потеря/сбой доставки) полным backfill из источника.

Freshness

  • SLA: search (секунды–минуты; eventual относительно PostgreSQL, FreshnessView).
  • Watermark: Post.UpdatedAt в документе.
  • Stale behavior: показать результаты с freshness-маркером; критичных для решения проверок поиск не несёт — чтение поста идёт из источника (get-post.md).

Security

  • Tenant/brand/scope trimming выполняется через SearchScopeFilter до query execution (ADR-0059 п.3): публичный поиск — только Status = PublishedNOT IsAuthorCompanySuspended (INV-REP-003). Автор- подпись AuthorReplica.Status = Blocked пост не скрывает (INV-REP-002) — помечается на маппинге ленты.
  • PII fields: none — ФИО/телефон/реквизиты в проекцию не кладутся; подпись автора (DisplayName) хранится в реплике identity и добирается лентой на маппинге, не индексируется.
  • Полнотекстовый поиск не индексирует secrets, tokens, payment/KYC data, тело блоков-подборок.

Rebuild

  1. Create versioned index news-posts-v{N+1} (маппинг + анализатор ru_translit_fuzzy).
  2. Backfill из news.posts (Status = Published, компания не Suspended) через ISearchRebuildJob.
  3. Validate counts/checksums против source.
  4. Atomically switch alias news-posts на новую версию (IElasticsearchAliasManager, ADR-0029).
  5. Keep previous index до окончания rollback-окна.

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

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

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