DealSearchDocument
Поисковая проекция списков сделок DealSearchDocument: обслуживает поиск по Title и ФИО клиента в
списках агента, застройщика и Менеджера ПИН (../api/agent/list-deals.md,
../api/developer/list-deals.md,
../api/pin-manager/list-deals.md). Проекция перестраиваемая — её всегда
можно восстановить из источника истины.
Назначение
Источник истины — сущность DealEntity в PostgreSQL (crm.deals); проекция хранит
только индексируемые/фильтруемые/сортируемые/возвращаемые в список поля + ФИО клиента (snapshot из
ClientEntity на момент создания/смены). Прямого обращения к поисковому индексу из бизнес-логики нет — она
работает с проекцией через типизированный поисковый запрос
(ADR-0059).
Index
| Поле | Значение |
|---|---|
| Index alias | crm-deals |
| Versioned index | crm-deals-v1 (переключение alias — ADR-0029) |
| Owner service | crm |
| Source of truth | DealEntity (../domain/deal.md) в PostgreSQL (crm.deals) |
| Rebuild job | периодический/ручной ребилд DealSearchRebuildJob |
Наполнение — инкрементально по доменным событиям DealEntity плюс периодический/ручной rebuild для
bootstrap и восстановления консистентности (ADR-0059).
Document Id И Routing
- Document id:
<TenantId>:<DealId>(TenantId = DeveloperCompanyId, ADR-0052) — детерминирован, upsert идемпотентен. - Routing key:
TenantId(DeveloperCompanyId) — сделки одного застройщика колокализуются (кластер малый, ADR-0056). - Delete/tombstone: soft-delete (
DeletedAt != null, административное сокрытие) → удаление документа. Финалы (Completed/Cancelled) документ не удаляют (завершённые сделки ищутся в списках/статистике), лишь меняют фильтруемыеStatus/StepGroup.
Поля
| Поле | Тип | Indexed | Stored | Analyzer | Source | Security |
|---|---|---|---|---|---|---|
DocumentId | keyword | да | да | — | TenantId:DealId | include |
DealId | keyword | да | да | — | DealEntity.Id | include |
TenantId | keyword | да | нет | — | DealEntity.TenantId (DeveloperCompanyId) | scope-only (не выдаётся) |
OriginAgencyCompanyId | keyword | да | да | — | DealEntity.OriginAgencyCompanyId | record-level scope (агентство-атрибуция) |
ExecutorAgentUserId | keyword | да | да | — | DealEntity.ExecutorAgentUserId | record-level scope (текущий исполнитель) + фильтр |
PrimaryInvestorUserId | keyword | да | да | — | DealEntity.PrimaryInvestorUserId | record-level scope («Мои сделки») |
Title | text | да | да | ru_translit_fuzzy | DealEntity.Title | include/матч |
ClientNameSearch | text | да | нет | ru_translit_fuzzy | ФИО клиента (snapshot из ClientEntity) | PII (только матч, не выдаётся; маска в логах) |
SearchText | text | да | нет | ru_translit_fuzzy | Title ⊕ ClientNameSearch (copy_to) | основной матч поиска |
Status | keyword | да | да | — | DealEntity.Status (DealStatus) | фильтр |
StepGroup | keyword | да | да | — | DealEntity.StepGroup (PipelineStepGroup) | фильтр (канбан-группы) |
StepId | keyword | да | да | — | DealEntity.StepId | фильтр |
ProjectId | keyword | да | да | — | DealEntity.ProjectId (реплика chessboard) | фильтр (US-DEV-01) |
BuildingId | keyword | да | да | — | DealEntity.BuildingId | фильтр |
PropertyObjectId | keyword | да | да | — | DealEntity.PropertyObjectId | include |
CreatedAt | date | да | да | — | DealEntity.CreatedAt | фильтр-диапазон + сортировка |
UpdatedAt | date | да | да | — | DealEntity.UpdatedAt | фильтр-диапазон + сортировка (default desc) + freshness |
SchemaVersion | integer | да | да | — | версия схемы проекции | версия схемы проекции |
IndexedAt | date | да | да | — | индексатор | freshness |
Внешний ввод — только поисковая строка (multi-match по SearchText с fuzziness), не raw-DSL. Фильтры
(Status/StepGroup/StepId/ProjectId/BuildingId/ExecutorAgentUserId/диапазоны CreatedAt/UpdatedAt) и
сортировки (CreatedAt/UpdatedAt/relevance) задаются типизированными параметрами запроса. ФИО клиента
индексируется для матча, но не возвращается в выдаче (Stored = нет).
Source Events
| Event | Version | Transform |
|---|---|---|
DealCreated | 1 | upsert документа из состояния DealEntity (Title + ФИО клиента snapshot) |
DealStageChanged / DealPropertyObjectChanged / DealUpdated | 1 | partial-upsert изменённых полей (Status/StepGroup/StepId/ProjectId/BuildingId/PropertyObjectId/Title/UpdatedAt) |
DealCompleted / DealCancelled | 1 | partial-upsert Status (+StepGroup) |
pin.identity.users.v1 | 1 | обновление ClientNameSearch при смене ФИО зарегистрированного клиента (по ClientId сделки) |
soft-delete (DeletedAt) | 1 | delete документа по DocumentId |
Проекция наполняется по доменным событиям агрегата DealEntity плюс периодический/ручной rebuild для
bootstrap и восстановления консистентности из PostgreSQL.
События одной сделки упорядочены по DealId; partial-upsert применяется монотонно по UpdatedAt —
более старое/переставленное событие документ не откатывает (отбрасывается как устаревшее); upsert по
детерминированному DocumentId делает повтор идемпотентным. pin.identity.users.v1 для сделки, чей клиент
ещё не отражён в проекции, — no-op. Неизвестный тип события пропускается (лог, без остановки
индексатора); рассинхрон добирается ближайшим rebuild.
Freshness
- SLA:
search(near-real-time, < 1 мин; eventual относительно PostgreSQL,FreshnessView— ADR-0019). - Watermark:
IndexedAt+UpdatedAtсделки в документе. - Stale behavior: показать результаты с freshness-маркером; актуальную карточку/канбан пользователь открывает через ../api/agent/get-deal.md, читающий source-of-truth PostgreSQL.
Security
- Scope-фильтр применяется до выполнения запроса (ADR-0059, ADR-0014) — record-level: агент видит
сделки, где он текущий исполнитель (
ExecutorAgentUserId = actor); руководитель/админ агентства — сделки своегоOriginAgencyCompanyIdc фильтром по агенту; застройщик — сделки своегоTenantId(DeveloperCompanyId); инвестор — только свои (PrimaryInvestorUserId = actor, «Мои сделки», без фиксаций); Менеджер ПИН — platform-scope (сквозные списки). Trimming — до query execution. - Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные;
TenantId— только scope-фильтр.
ClientNameSearch (ФИО клиента) — индексируется только для матча, наружу не выдаётся (Stored = нет); в
логах — маска. EscrowAccountNumber/документы/суммы в проекцию не кладутся.
Rebuild
- Создать версионный индекс
crm-deals-vN(маппинг +ru_translit_fuzzy). - Backfill из PostgreSQL: живые сделки (
DeletedAt == null) стримом поDealEntity, map вDealSearchDocument(ФИО клиента — joincrm.clientsпоClientId). - Валидация counts/checksums против источника (
crm.deals). - Атомарно переключить alias
crm-dealsна новую версию. - Держать предыдущий индекс до конца rollback-окна.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: DealEntity
- API (3): GET /api/agent/crm/deals — Сделки агента (список/поиск), GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/pin-manager/crm/deals — Сквозной список всех сделок платформы
- Поиск (2): ClientSearchDocument, FixationRequestSearchDocument
Связанные документы
- Сущность-источник: ../domain/deal.md; потребители поиска: ../api/agent/list-deals.md, ../api/developer/list-deals.md, ../api/pin-manager/list-deals.md.
- Соседние проекции: clients.md, fixations.md.
- ADR: 0059 Elasticsearch-проекции, 0008 Elasticsearch, 0014 scope-filters, 0052 tenancy, 0056 simplicity-first.