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

ADR-0059: Поиск во всех поверхностях PIN — Elasticsearch-проекции через Talent.Common.Domain.Search

Статус

Accepted

Дата

2026-07-14

Контекст

Поиск в PIN нужен в разных контекстах: каталог недвижимости, шахматка (поиск проектов), пользователи Identity, контакты мессенджера, клиенты/фиксации/сделки CRM, тикеты/база знаний/арбитраж поддержки, посты ленты. Первые детальные ТЗ реализовывали его на PostgreSQL-FTS (tsvector + generated-колонка

  • GIN-индекс, websearch_to_tsquery), трактуя это как следствие simplicity-first (ADR-0056): «не добавляем отдельное поисковое хранилище».

PostgreSQL-FTS оказался недостаточным для реальных требований поиска платформы:

  • нет typo-tolerance (fuzzy) и транслитерации (кириллица ↔ латиница) — критично для имён, адресов, названий проектов и застройщиков;
  • слабая работа с алиасами/синонимами и морфологией нескольких языков одновременно;
  • relevance-ранжирование (ts_rank) плохо совмещается с keyset-курсорами и не масштабируется на подсветку;
  • разъезжающиеся реализации FTS в каждом сервисе (свои SearchVector, свои GIN-индексы, свои правила scope-фильтрации) — дублирование без единого канона.

Elasticsearch как search projection уже принят платформой (ADR-0008) и поддержан реальным SDK: провайдер-нейтральные контракты Talent.Common.Domain.Search (ISearchIndex/SearchIndexResult/ SearchPage) над живой инфраструктурой Talent.Common.Infrastructure.ElasticSearch (клиент Elastic.Clients.Elasticsearch v9). Целевая нагрузка (ADR-0056: 1–10k одновременно активных) не требует шардирования — это единый небольшой кластер как read/search projection, а не операционное хранилище.

Историческая заметка: ранние черновики называли движок «OpenSearch» и пакет Talent.Common.Search. Фактический SDK работает на Elasticsearch (Talent.Common.Domain.Search + инфра Talent.Common.Infrastructure.ElasticSearch); OpenSearch-пакет в SDK — заглушка. Терминология приведена в соответствие с кодом.

Решение

Поиск во всех поисковых поверхностях PIN переводится с PostgreSQL-FTS на Elasticsearch-проекции через единый SDK-слой Talent.Common.Domain.Search (инфра — Talent.Common.Infrastructure.ElasticSearch). Собственные поисковые движки, прямой клиент Elasticsearch в бизнес-коде и PostgreSQL-FTS (tsvector/GIN) как основной механизм поиска — не используются.

1. Единый строительный слой — Talent.Common.Domain.Search (+ инфра ElasticSearch)

  • ElasticsearchDocument — базовый typed-документ проекции (DocumentId, TenantId, BrandId?, SchemaVersion, UpdatedAt, IndexedAt); конкретные документы каждого сервиса — в src/services/<service>/Infrastructure/Search/Documents/.
  • ISearchIndex / IElasticsearchIndexRepository<TDocument> — индексация/upsert/delete и чтение (SearchIndexResult/SearchPage); никакого прямого клиента Elasticsearch в handlers/domain.
  • SearchQuery — типизированный запрос (фильтры, пагинация, сортировка); внешний ввод (searchTerm) идёт только параметром запроса, не в raw-DSL.
  • ISearchRebuildJob — полная переиндексация из source-of-truth PostgreSQL (bootstrap нового индекса, восстановление после расхождения/потери проекции).
  • IElasticsearchIndexManager / IElasticsearchAliasManager — версионирование индексов и zero-downtime reindex через переключение alias между версиями (ADR-0029: alias strategy).
  • SearchScopeFilter — tenant/brand/actor-фильтр, применяемый до выполнения запроса (единообразно с repository-pipeline scope-rules, ADR-0014); security-trimming выполняется до query execution, не после получения результата.

2. Модель данных: source of truth остаётся в PostgreSQL

  • Source of truth каждой поверхности — PostgreSQL owning-сервиса (ADR-0004). Elasticsearch — rebuildable read/search projection и не является owner-ом данных (ADR-0035, data-ownership-and-lineage).
  • Проекция наполняется инкрементально по доменным/интеграционным событиям сервиса (upsert/delete документа при изменении агрегата) плюс периодический/ручной rebuild-джоб (ISearchRebuildJob) для bootstrap и восстановления консистентности.
  • Каждый search-документ имеет documented owner, source events, transform, documentId, routing key, tenant/brand trimming, PII-поля, freshness SLA и rebuild-джоб (data-ownership-and-lineage → Elasticsearch Projections).

3. PII-trimming и scope-фильтр

  • В документ проекции кладутся только поля, разрешённые для соответствующей поисковой поверхности; чувствительные данные (ФИО/телефон/паспорт/реквизиты) в индекс не попадают либо помечаются PII-полями и вырезаются security-trimming-ом до выдачи.
  • SearchScopeFilter навешивает tenant/brand/record-level ограничения (ADR-0052 tenancy, ADR-0014 scope-filters) — пользователь видит только то, что ему разрешено; фильтрация применяется до query execution.

4. Поисковые поверхности (scope перевода)

ПоверхностьСервис (owner)ИщетсяПримечание
Проекты и объекты недвижимостиcatalog (публичный каталог); поиск проектов — chessboard (search-projects)проекты, дома/здания, объекты недвижимости/планировки, застройщики, адресаfuzzy/транслитерация/алиасы — основной драйвер
Пользователиidentityпользователи (агенты/менеджеры/сотрудники) по имени/телефону/e-mailPII-trimming обязателен; scope по правам
Контактыmessengerконтакты/пользователи для старта чата (поиск по телефону/имени)заменяет прямой lookup/FTS
Клиенты / фиксации / сделкиcrmклиенты, фиксации, сделкиrecord-level (владелец + группа/руководитель) в SearchScopeFilter
Тикеты / база знаний / арбитражsupportтикеты, KB-статьи, арбитражные кейсыпубличный KB-поиск + внутренний по тикетам/арбитражу с разными scope
Постыnewsопубликованные посты ленты (заголовок/подзаголовок/текст блоков)заменяет SearchVector/GIN partial (UC-NWS-005)

Каждая поверхность фиксирует свой search-документ и rebuild-джоб в детальном ТЗ сервиса (docs/06-services/<context>/search/<index>.md). PostgreSQL-FTS-артефакты (SearchVector, GIN-индексы, websearch_to_tsquery) из уже написанных ТЗ заменяются Elasticsearch-проекцией при доработке контекстов.

5. Отношение к ранее принятым решениям

  • Подтверждает ADR-0008 (Elasticsearch как search projection): настоящий ADR распространяет уже принятый канон на все поисковые поверхности PIN и фиксирует единый SDK-слой реализации.
  • Отменяет (supersedes) FTS-пункт ADR-0056: трактовка simplicity-first как «поиск на PostgreSQL-FTS, отдельное поисковое хранилище не добавляем» больше не действует — поиск идёт через Elasticsearch-проекции. Остальные положения ADR-0056 остаются в силе: PostgreSQL — основная СУБД и source of truth; Redis не используется в sessions/favorites/selections; закрытие сессий/просмотров — polling-delta.

Последствия

  • Единый поисковый слой (Talent.Common.Domain.Search) во всех сервисах; никаких разъезжающихся FTS-реализаций, прямых клиентов Elasticsearch и сторонних движков.
  • Появляется operational-компонент Elasticsearch как search projection: нужны index/alias-версионирование (ADR-0029), rebuild/backfill-джобы (ADR-0035), freshness-мониторинг (ADR-0019, data-freshness-standard) и наблюдаемость каждого search-запроса (ADR-0047).
  • Проекция eventually-consistent относительно PostgreSQL: результаты поиска сопровождаются freshness-семантикой; для admin/decision/support-запросов возвращается FreshnessView (data-ownership-and-lineage).
  • Детальные ТЗ поверхностей (catalog, chessboard project-search, identity, messenger, crm, support, news) дорабатываются: search-документ + консюмеры наполнения + ISearchRebuildJob + PII/scope-правила; ранее описанные PostgreSQL-FTS-механизмы (например, UC-NWS-005, search-projects, search-users-by-phone, search-kb-articles) переводятся на проекцию.
  • Security-trimming и tenant/record-level scope выполняются до query execution через SearchScopeFilter — единообразно с repository-pipeline (ADR-0014).

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

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

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