ClientSearchDocument
Поисковая проекция клиентской базы агента ClientSearchDocument: обслуживает поиск на странице
/crm/clients по ФИО, нормализованному телефону и email
(../api/agent/list-clients.md). Проекция перестраиваемая — её всегда можно
восстановить из источника истины.
Назначение
Источник истины — сущность ClientEntity в PostgreSQL (crm.clients); проекция
хранит только те поля, которые индексируются, фильтруются, сортируются или возвращаются в список. Поиск
обеспечивает typo-tolerance (fuzzy), транслитерацию кириллица↔латиница и морфологию ФИО, а также префиксный
матч по номеру телефона. Прямого обращения к поисковому индексу из бизнес-логики нет — она работает с
проекцией через типизированный поисковый запрос (ADR-0059).
Index
| Поле | Значение |
|---|---|
| Index alias | crm-clients |
| Versioned index | crm-clients-v1 (переключение alias — ADR-0029) |
| Owner service | crm |
| Source of truth | ClientEntity (../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.
Поля
| Поле | Тип | Indexed | Stored | Analyzer | Source | Security |
|---|---|---|---|---|---|---|
DocumentId | keyword | да | да | — | TenantId:ClientId | include |
ClientId | keyword | да | да | — | ClientEntity.Id | include |
TenantId | keyword | да | нет | — | ClientEntity.TenantId (AgencyCompanyId) | scope-only (не выдаётся) |
OriginAgentUserId | keyword | да | да | — | ClientEntity.OriginAgentUserId | record-level scope (агент-создатель) + фильтр по агенту |
Name | text | да | да | ru_translit_fuzzy | ClientEntity.Name (FullName: First⊕Middle⊕Last) | PII (маска в логах; agency-only) |
PhoneNumber | keyword + text | да | да | digits_edge_ngram (text-подполе) | ClientEntity.PhoneNumber (E.164) | PII (маска +7***NNNN в логах) |
Email | keyword + text | да | да | ru_translit_fuzzy (text-подполе) | ClientEntity.Email | PII (маска в логах) |
SearchText | text | да | нет | ru_translit_fuzzy | Name ⊕ PhoneNumber ⊕ Email (copy_to) | основной матч поиска |
Status | keyword | да | да | — | ClientEntity.Status (ClientStatus) | фильтр (default Active) |
Source | keyword | да | да | — | ClientEntity.Source (ClientSource) | фильтр |
InvestorUserId | keyword | да | да | — | ClientEntity.InvestorUserId | include (exists = зарегистрирован) |
ActiveLeadId | keyword | нет | да | — | ClientEntity.ActiveLeadId | include (быстрый переход) |
LastActivityAt | date | да | да | — | ClientEntity.LastActivityAt | сортировка (default desc) |
CreatedAt | date | да | да | — | ClientEntity.CreatedAt | фильтр-диапазон + сортировка |
SchemaVersion | integer | да | да | — | версия схемы проекции | версия схемы проекции |
UpdatedAt | date | да | да | — | ClientEntity.UpdatedAt | freshness watermark |
IndexedAt | date | да | да | — | индексатор | freshness |
Внешний ввод — только поисковая строка (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
| Event | Version | Transform |
|---|---|---|
ClientCreated | 1 | upsert документа из состояния ClientEntity (маппинг полей выше) |
ClientArchived | 1 | partial-upsert Status = Archived |
смена контактов/активности (Name/PhoneNumber/Email/LastActivityAt/ActiveLeadId) | 1 | partial-upsert изменённых полей |
pin.identity.users.v1 | 1 | обновление Name/PhoneNumber для зарегистрированного клиента (display-реплика) |
soft-delete (DeletedAt) | 1 | delete документа по 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-фильтр, наружу не выдаётся.
Name, PhoneNumber, Email (индексируются для поиска по клиенту); индекс agency-only, публичной выдачи
нет, в логах — маска (+7***NNNN, И***); в списках чужих агентов не показываются.
Rebuild
- Создать версионный индекс
crm-clients-vN(маппинг +ru_translit_fuzzy/digits_edge_ngram). - Backfill из PostgreSQL: живые карточки (
DeletedAt == null) стримом поClientEntity, map вClientSearchDocument. - Валидация counts/checksums против источника (
crm.clients). - Атомарно переключить alias
crm-clientsна новую версию. - Держать предыдущий индекс до конца rollback-окна.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ClientEntity
- API (1): GET /api/agent/crm/clients — Клиентская база агента (список + поиск)
- Поиск (2): DealSearchDocument, FixationRequestSearchDocument
Связанные документы
- Сущность-источник: ../domain/client.md; потребитель поиска: ../api/agent/list-clients.md; реплика ФИО/телефона — ../consumers/identity-user-upserted.md.
- Соседние проекции: fixations.md (дубль-детект), deals.md.
- ADR: 0059 Elasticsearch-проекции, 0008 Elasticsearch, 0014 scope-filters, 0052 tenancy, 0056 simplicity-first.