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

MessengerContactSearchDocument

Rebuildable-проекция поиска MessengerContactSearchDocument в Elasticsearch — для поиска контактов/пользователей по имени и телефону в мессенджере (опциональный search страницы «Контакты», ../api/contacts/list-contacts.md). Обслуживает fuzzy-поиск по ФИО с typo-tolerance и транслитерацией; глобальный точечный поиск по телефону остаётся прямым lookup-ом в PostgreSQL (см. «Границы»).

Назначение

Source of truth — реплика MessengerUserEntity (messenger.users, ФИО + нормализованный телефон из identity); проекция хранит только индексируемые/фильтруемые поля для fuzzy- поиска (typo-tolerance, транслитерация кириллица↔латиница, морфология ФИО — ADR-0059 п.1). Наполняется тем же консюмером, что и реплика (../consumers/identity-user-upserted.md), плюс rebuild-джоб.

Границы. Проекция обслуживает FTS по имени/телефону (поиск внутри подтверждённых контактов — list-contacts). Глобальный точечный поиск по телефону (../api/contacts/search-users-by-phone.md) — не search- поверхность: телефон уникален (ux_users_phone), это прямой btree-lookup в PostgreSQL, Elasticsearch не задействован (ADR-0056).

Index

ПолеЗначение
Index aliasmessenger-contacts
Versioned indexmessenger-contacts-v1 (переключение alias, ADR-0029)
Owner servicemessenger
Source of truthMessengerUserEntity (../domain/messenger-user.md) в PostgreSQL (messenger.users)
Rebuild jobMessengerContactSearchRebuildJob

Document Id И Routing

  • Document id: UserId (Guid = messenger.users.user_id = identity.users.id; детерминирован — upsert идемпотентен).
  • Routing key: не требуется — справочник person-level (глобальный, вне tenant, ADR-0052); кластер малый, шардирование не нужно (ADR-0056).
  • Delete/tombstone: строка живёт, пока жив identity-пользователь (реплика не hard-удаляется). Блокировка/ архивация в identityIsActive = false (документ остаётся, но security-trimming-ом вырезается из discovery-выдачи, INV-MU); физическое удаление реплики → delete документа по UserId.

Поля

ПолеТипIndexedStoredAnalyzerSourceSecurity
UserIdkeywordдадаMessengerUserEntity.UserIdinclude
FirstNametextдадаru_name (russian morphology + lowercase + edge-ngram)FirstNamePII (маска не на имя)
MiddleNametextдадаru_nameMiddleNamePII
LastNametextдадаru_nameLastNamePII (в выдаче маска до Accepted, INV-MU-3)
NameTranslittextданетtranslit (ICU кириллица↔латиница)FirstName ⊕ MiddleName ⊕ LastNamePII
FullNameSearchtextданетru_nameFirstName ⊕ MiddleName ⊕ LastName (copy_to)основной матч searchTerm
PhoneNumberkeywordдадаPhoneNumber (E.164)PII (маска +7***NNNN до Accepted)
PhoneDigitstextданетdigits_edge_ngramPhoneNumber (только цифры)префикс/подстрока телефона
IsActivebooleanдадаIsActivesecurity-trimming discovery
SchemaVersionintegerдадаверсия схемы документаinclude
UpdatedAtdateдадаMessengerUserEntity.UpdatedAtfreshness watermark
IndexedAtdateдадамомент индексацииfreshness

Внешний ввод (searchTerm) идёт только параметром поискового запроса (multi-match по FullNameSearch/ NameTranslit/PhoneDigits с fuzziness) — не в raw-DSL. IdentityUserVersion в проекцию не кладётся (guard применяется в источнике до индексации).

Source Events

EventVersionTransform
pin.identity.users.v11Тот же консюмер, что наполняет реплику (в том же согласованном изменении): при payload.Version > IdentityUserVersion — upsert документа (map ФИО/телефон/IsActive); stale/дубль → skip (version-guard INV-MU-1).

Проекция наполняется инкрементально консюмером реплики (identity-user-upserted.md) + периодический/ручной MessengerContactSearchRebuildJob из messenger.users для bootstrap и восстановления консистентности. Собственного доменного события у реплики нет — индексация выполняется в той же обработке user-upserted (после успешной фиксации реплики).

Freshness

  • SLA: search (секунды–минуты; eventual относительно PostgreSQL).
  • Watermark: IndexedAt + offset топика pin.identity.users.v1.
  • Stale behavior: показать результаты с freshness-маркером; адресуемость/статус контакта критичные проверки (IsActive, снятие маски) делаются по source-of-truth (messenger.users / ContactEntity), не по проекции.

Согласованность и деградация

  • Порядок обновлений — per-user. Индексация идёт тем же консюмером и в том же порядке, что реплика: топик pin.identity.users.v1, ключ UserId, монотонный version-guard (payload.Version > IdentityUserVersion, INV-MU-1). Устаревшее событие пропускается — документ не «омолаживается» назад; между разными UserId порядок не важен (id детерминирован, upsert идемпотентен).
  • ES недоступен/отстаёт. Поиск-параметр search в list-contacts деградирует: листинг контактов без search работает по PostgreSQL немедленно (../api/contacts/list-contacts.md); точечный поиск по телефону (../api/contacts/search-users-by-phone.md) не затронут — это прямой btree-lookup, вне Elasticsearch. Восстановление консистентности — консюмер (догон offset) + MessengerContactSearchRebuildJob.
  • Гонка индексации и чтения. Матч по проекции возвращает только UserId + score; актуальные IsActive/маскирование/статус контакта потребитель перечитывает по source-of-truth (messenger.users / ContactEntity) — устаревший документ не приводит к утечке (заблокированный/архивный вырезается security-trimming-ом, INV-MU) и не снимает маску раньше Accepted.
  • Удаление/tombstone. IsActive = false (блокировка/архивация в identity) оставляет документ, но вырезает из discovery-выдачи; физическое удаление реплики → delete документа по UserId. Повторная доставка удаления — идемпотентный no-op.

Security

  • Scope-фильтр (до выполнения запроса, ADR-0059 п.3) для list-contactsrecord-level: сузить к множеству UserId подтверждённых контактов актора (ContactEntity.Status = Accepted, обе стороны пары); IsActive = false вырезается из discovery-поверхностей.
  • PII fields: FirstName/MiddleName/LastName/PhoneNumber — индексируются для матча, но выдаются по политике маскирования: до Accepted-контакта фамилия маскируется первой буквой, телефон +7***NNNN (INV-MU-3); в list-contacts все строки уже Accepted → профиль полный (INV-CT-5). Маскирование выполняет потребитель проекции, индекс отдаёт только UserId + match-score.
  • Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные, тела сообщений, IP/UA.

Rebuild

  1. Создать версионный индекс messenger-contacts-vN с analyzers (ru_name, translit, digits_edge_ngram).
  2. Backfill MessengerContactSearchRebuildJob из PostgreSQL (messenger.users): map строк MessengerUserEntity в MessengerContactSearchDocument.
  3. Валидация counts/checksums против источника (messenger.users).
  4. Атомарно переключить alias messenger-contacts на новую версию.
  5. Держать предыдущий индекс до конца rollback-окна.

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

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

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