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

ClientSearchDocument

Поисковая проекция клиентской базы агента ClientSearchDocument: обслуживает поиск на странице /crm/clients по ФИО, нормализованному телефону и email (../api/agent/list-clients.md). Проекция перестраиваемая — её всегда можно восстановить из источника истины.

Назначение

Источник истины — сущность ClientEntity в PostgreSQL (crm.clients); проекция хранит только те поля, которые индексируются, фильтруются, сортируются или возвращаются в список. Поиск обеспечивает typo-tolerance (fuzzy), транслитерацию кириллица↔латиница и морфологию ФИО, а также префиксный матч по номеру телефона. Прямого обращения к поисковому индексу из бизнес-логики нет — она работает с проекцией через типизированный поисковый запрос (ADR-0059).

Index

ПолеЗначение
Index aliascrm-clients
Versioned indexcrm-clients-v1 (переключение alias — ADR-0029)
Owner servicecrm
Source of truthClientEntity (../domain/client.md) в PostgreSQL (crm.clients)
Rebuild jobпериодический/ручной ребилд ClientSearchRebuildJob

Наполнение — инкрементально по доменным событиям ClientEntity и по событию pin.identity.users.v1 (реплика ФИО/телефона), плюс периодический/ручной rebuild для bootstrap и восстановления консистентности (ADR-0059).

Document Id И Routing

  • Document id: <TenantId>:<ClientId> (TenantId = AgencyCompanyId, ADR-0052) — детерминирован, upsert идемпотентен.
  • Routing key: TenantId (AgencyCompanyId) — карточки одного агентства колокализуются (кластер малый, ADR-0056).
  • Delete/tombstone: soft-delete карточки (DeletedAt != null) → удаление документа из индекса (fail-closed — архивная карточка не находится поиском). Статус Archived не удаляет документ (архив ищется фильтром), лишь меняет фильтруемое поле Status.

Поля

ПолеТипIndexedStoredAnalyzerSourceSecurity
DocumentIdkeywordдадаTenantId:ClientIdinclude
ClientIdkeywordдадаClientEntity.Idinclude
TenantIdkeywordданетClientEntity.TenantId (AgencyCompanyId)scope-only (не выдаётся)
OriginAgentUserIdkeywordдадаClientEntity.OriginAgentUserIdrecord-level scope (агент-создатель) + фильтр по агенту
Nametextдадаru_translit_fuzzyClientEntity.Name (FullName: First⊕Middle⊕Last)PII (маска в логах; agency-only)
PhoneNumberkeyword + textдадаdigits_edge_ngram (text-подполе)ClientEntity.PhoneNumber (E.164)PII (маска +7***NNNN в логах)
Emailkeyword + textдадаru_translit_fuzzy (text-подполе)ClientEntity.EmailPII (маска в логах)
SearchTexttextданетru_translit_fuzzyName ⊕ PhoneNumber ⊕ Email (copy_to)основной матч поиска
StatuskeywordдадаClientEntity.Status (ClientStatus)фильтр (default Active)
SourcekeywordдадаClientEntity.Source (ClientSource)фильтр
InvestorUserIdkeywordдадаClientEntity.InvestorUserIdinclude (exists = зарегистрирован)
ActiveLeadIdkeywordнетдаClientEntity.ActiveLeadIdinclude (быстрый переход)
LastActivityAtdateдадаClientEntity.LastActivityAtсортировка (default desc)
CreatedAtdateдадаClientEntity.CreatedAtфильтр-диапазон + сортировка
SchemaVersionintegerдадаверсия схемы проекцииверсия схемы проекции
UpdatedAtdateдадаClientEntity.UpdatedAtfreshness watermark
IndexedAtdateдадаиндексаторfreshness
Внешний ввод — не raw-DSL

Внешний ввод — только поисковая строка (multi-match по SearchText с fuzziness), не raw-DSL. Фильтры (Status/Source/OriginAgentUserId) и сортировки (LastActivityAt/CreatedAt) задаются типизированными параметрами запроса. Анализатор ru_translit_fuzzy — русская морфология + ICU-транслитерация (кириллица↔латиница) + edge-ngram для typo-tolerance; digits_edge_ngram — префиксный/подстрочный матч телефона (ADR-0059).

Source Events

EventVersionTransform
ClientCreated1upsert документа из состояния ClientEntity (маппинг полей выше)
ClientArchived1partial-upsert Status = Archived
смена контактов/активности (Name/PhoneNumber/Email/LastActivityAt/ActiveLeadId)1partial-upsert изменённых полей
pin.identity.users.v11обновление Name/PhoneNumber для зарегистрированного клиента (display-реплика)
soft-delete (DeletedAt)1delete документа по DocumentId

Проекция наполняется по доменным событиям агрегата ClientEntity и по событию identity-user-upserted (реплика display-полей в том же коммите), плюс периодический/ручной rebuild для bootstrap и восстановления консистентности из PostgreSQL.

Порядок и устойчивость применения

События одной карточки упорядочены по ClientId; partial-upsert применяется монотонно по UpdatedAt — более старое/переставленное событие документ не откатывает (отбрасывается как устаревшее). Повтор одного события идемпотентен (upsert по детерминированному DocumentId). pin.identity.users.v1 для пользователя, у которого ещё нет карточки в проекции, — no-op (display-реплика лишь обновляет существующую; создание идёт от ClientCreated). Неизвестный/непрофильный тип события пропускается (лог, без остановки индексатора) — консистентность добирается ближайшим rebuild.

Freshness

  • SLA: search (near-real-time, < 1 мин от мутации до появления в поиске; eventual относительно PostgreSQL, FreshnessView — ADR-0019).
  • Watermark: IndexedAt + UpdatedAt карточки в документе.
  • Stale behavior: показать результаты с freshness-маркером; актуальную карточку агент открывает через ../api/agent/get-client.md, читающий source-of-truth PostgreSQL.

Security

  • Scope-фильтр применяется до выполнения запроса (ADR-0059, ADR-0014) — record-level: агент видит клиентов, где он создатель или активный участник процессов клиента (OriginAgentUserId = actor либо членство в его лидах/сделках — источник истины видимости в PostgreSQL); руководитель/администратор агентства — всех клиентов своего TenantId (AgencyCompanyId) с опциональным фильтром по агенту; Менеджер ПИН — platform-scope. Застройщик и инвестор доступа к клиентской базе не имеют — trimming выполняется до query execution.
  • Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные; TenantId — только scope-фильтр, наружу не выдаётся.
PII

Name, PhoneNumber, Email (индексируются для поиска по клиенту); индекс agency-only, публичной выдачи нет, в логах — маска (+7***NNNN, И***); в списках чужих агентов не показываются.

Rebuild

  1. Создать версионный индекс crm-clients-vN (маппинг + ru_translit_fuzzy/digits_edge_ngram).
  2. Backfill из PostgreSQL: живые карточки (DeletedAt == null) стримом по ClientEntity, map в ClientSearchDocument.
  3. Валидация counts/checksums против источника (crm.clients).
  4. Атомарно переключить alias crm-clients на новую версию.
  5. Держать предыдущий индекс до конца rollback-окна.

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

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

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