FixationRequestSearchDocument
Поисковая проекция заявок на фиксацию FixationRequestSearchDocument: обслуживает поиск в списках
агента/агентства/застройщика (../api/agent/list-fixations.md,
../api/developer/list-fixations.md) и дубль-детект по ФИО/телефону
при подаче заявки и открытии карточки застройщиком. Проекция перестраиваемая — её всегда можно восстановить
из источника истины.
Назначение
Источник истины — сущность FixationRequestEntity в PostgreSQL
(crm.fixation_requests); проекция хранит индексируемые/фильтруемые/сортируемые поля + иммутабельный snapshot
ФИО/телефона клиента (застройщик видит то, что подал агент). Прямого обращения к поисковому индексу из
бизнес-логики нет — она работает с проекцией через типизированный поисковый запрос
(ADR-0059).
Index
| Поле | Значение |
|---|---|
| Index alias | crm-fixations |
| Versioned index | crm-fixations-v1 (переключение alias — ADR-0029) |
| Owner service | crm |
| Source of truth | FixationRequestEntity (../domain/fixation-request.md) в PostgreSQL |
| Rebuild job | периодический/ручной ребилд FixationRequestSearchRebuildJob |
Наполнение — инкрементально по доменным событиям агрегата плюс периодический/ручной rebuild для bootstrap и восстановления консистентности (ADR-0059).
Document Id И Routing
- Document id:
<TenantId>:<FixationRequestId>(TenantId = DeveloperCompanyId, ADR-0052) — детерминирован, upsert идемпотентен. - Routing key:
TenantId(DeveloperCompanyId) — заявки одного застройщика колокализуются; дубль-детект ограничен одним застройщиком, routing совпадает со scope. - Delete/tombstone: soft-delete (
DeletedAt != null, административное сокрытие) → удаление документа. Финалы (Rejected/Cancelled) документ не удаляют (история ищется), лишь меняют фильтруемоеStatusи выводят заявку из активного множества дубль-детекта (IsActive).
Поля
| Поле | Тип | Indexed | Stored | Analyzer | Source | Security |
|---|---|---|---|---|---|---|
DocumentId | keyword | да | да | — | TenantId:FixationRequestId | include |
FixationRequestId | keyword | да | да | — | FixationRequestEntity.Id | include |
TenantId | keyword | да | нет | — | FixationRequestEntity.TenantId (DeveloperCompanyId) | scope-only (не выдаётся) |
OriginAgencyCompanyId | keyword | да | да | — | FixationRequestEntity.OriginAgencyCompanyId | record-level scope (агентство-атрибуция) |
ExecutorAgentUserId | keyword | да | да | — | FixationRequestEntity.ExecutorAgentUserId | record-level scope (текущий исполнитель) + фильтр |
InvestorUserId | keyword | да | да | — | FixationRequestEntity.InvestorUserId | include |
LeadId | keyword | да | да | — | FixationRequestEntity.LeadId | include |
ClientId | keyword | да | да | — | FixationRequestEntity.ClientId | include |
Status | keyword | да | да | — | FixationRequestEntity.Status (FixationStatus) | фильтр |
IsActive | boolean | да | да | — | Status ∈ {Pending, FixedUnique, FixedNonUnique} | фильтр дубль-детекта |
Uniqueness | keyword | да | да | — | FixationRequestEntity.Uniqueness | include |
ClientName | text | да | да | ru_translit_fuzzy | FixationRequestEntity.ClientName (snapshot FullName) | PII (маска в логах) |
ClientPhoneNumber | keyword + text | да | да | digits_edge_ngram (text-подполе) | FixationRequestEntity.ClientPhoneNumber (E.164, snapshot) | PII (ключ дубль-детекта; маска в логах) |
SearchText | text | да | нет | ru_translit_fuzzy | ClientName ⊕ ClientPhoneNumber (copy_to) | основной матч поиска |
CreatedAt | date | да | да | — | FixationRequestEntity.CreatedAt | фильтр-диапазон + сортировка (default desc) |
DecidedAt | date | да | да | — | FixationRequestEntity.DecidedAt | сортировка |
SchemaVersion | integer | да | да | — | версия схемы проекции | версия схемы проекции |
UpdatedAt | date | да | да | — | FixationRequestEntity.UpdatedAt | freshness watermark |
IndexedAt | date | да | да | — | индексатор | freshness |
Внешний ввод — только поисковая строка (multi-match по SearchText с fuzziness), не raw-DSL. Фильтры
(Status/ExecutorAgentUserId/OriginAgencyCompanyId/диапазон CreatedAt) и сортировки (CreatedAt/DecidedAt/Status,
relevance при поиске) задаются типизированными параметрами запроса.
Дубль-детект
При создании заявки и открытии карточки застройщиком — проверка по активным фиксациям того же TenantId
(scope: TenantId = DeveloperCompanyId, IsActive = true; исключая текущую заявку):
- Точное совпадение телефона — exact term по
ClientPhoneNumber(keyword) →Phone. - Fuzzy-совпадение ФИО — multi-match по
SearchText/ClientNameс fuzziness; порог score — конфигурация →Name. - Лучшее совпадение пишется в
DuplicateOfFixationId+DuplicateMatchKind(Phone/Name/PhoneAndName),Uniqueness = HasDuplicates; иначеNoDuplicates.
Решение «уникальный/не уникальный» принимает менеджер застройщика — авто-проверка только подсвечивает совпадение, но не блокирует и не решает за него.
Source Events
| Event | Version | Transform |
|---|---|---|
FixationRequested | 1 | upsert документа из состояния FixationRequestEntity (ФИО/телефон snapshot, IsActive = true) |
FixationDecided | 1 | partial-upsert Status/Uniqueness/DecidedAt; пересчёт IsActive |
soft-delete (DeletedAt) | 1 | delete документа по DocumentId |
ФИО/телефон в проекции — иммутабельный snapshot (не обновляются событием identity: застройщик видит то, что
подал агент). Наполняется по доменным событиям агрегата + периодический/ручной rebuild для bootstrap и
восстановления консистентности из PostgreSQL.
FixationDecided может прийти повторно/с перестановкой — partial-upsert применяется монотонно по
UpdatedAt, upsert по детерминированному DocumentId идемпотентен; пересчёт IsActive детерминирован от
Status, поэтому повтор не меняет активное множество дубль-детекта. Неизвестный тип события пропускается (лог,
без остановки индексатора).
Freshness
- SLA:
search(near-real-time, < 1 мин; eventual относительно PostgreSQL,FreshnessView— ADR-0019). - Watermark:
IndexedAt+UpdatedAtзаявки в документе. - Stale behavior: дубль-детект подсвечивает, не блокирует; финальное решение менеджер принимает по
карточке (../api/developer/approve-fixation.md), читающей
source-of-truth PostgreSQL; уникальность триплета гарантирует уникальный индекс активной тройки
ux_fixation_active_triple(INV-FIX-001).
Окно свежести: проекция near-real-time (< 1 мин), поэтому две почти одновременные заявки на один триплет
могут обе не увидеть друг друга в авто-проверке (обе ещё не проиндексированы). Это допустимо — жёсткую
единственность активной тройки (агент, инвестор, объект) держит ux_fixation_active_triple (INV-FIX-001) на
источнике истины: проигравшая заявка отклоняется независимо от свежести проекции.
Security
- Scope-фильтр применяется до выполнения запроса (ADR-0059, ADR-0014) — record-level: агент видит
заявки, где он текущий исполнитель (
ExecutorAgentUserId = actor); руководитель/админ агентства — все заявки своегоOriginAgencyCompanyIdc фильтром по агенту; застройщик — только заявки своегоTenantId(DeveloperCompanyId); инвестор доступа не имеет; Менеджер ПИН — platform-scope. Дубль-детект — всегда в scope одногоTenantId. - Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные;
TenantId— только scope-фильтр.
ClientName, ClientPhoneNumber индексируются для поиска/дубль-детекта; маскирование в списках застройщика до
решения не требуется — данные переданы для фиксации осознанно; в логах — маска.
Rebuild
- Создать версионный индекс
crm-fixations-vN(маппинг +ru_translit_fuzzy/digits_edge_ngram). - Backfill из PostgreSQL: живые заявки (
DeletedAt == null) стримом поFixationRequestEntity, map вFixationRequestSearchDocument(IsActiveизStatus). - Валидация counts/checksums против источника (
crm.fixation_requests). - Атомарно переключить alias
crm-fixationsна новую версию. - Держать предыдущий индекс до конца rollback-окна.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: FixationRequestEntity
- API (2): GET /api/agent/crm/clients — Клиентская база агента (список + поиск), GET /api/agent/crm/fixations — Список фиксаций агентства (+ поиск)
- Поиск (2): ClientSearchDocument, DealSearchDocument
Связанные документы
- Сущность-источник: ../domain/fixation-request.md; потребители поиска: ../api/agent/list-fixations.md, ../api/developer/list-fixations.md; дубль-детект при создании — ../api/agent/create-fixation.md.
- Соседние проекции: clients.md, deals.md.
- ADR: 0059 Elasticsearch-проекции, 0008 Elasticsearch, 0014 scope-filters, 0052 tenancy, 0056 simplicity-first.