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

Search Document: ProjectSearchDocument

Elasticsearch document ProjectSearchDocument — rebuildable search projection для поиска проектов (ЖК/апарт-отелей) в ленте каталога: полнотекстовый поиск по названию проекта и адресу с typo-tolerance (fuzzy), транслитерацией (кириллица ↔ латиница) и морфологией русского языка.

Назначение

Фасетные фильтры ленты — тип/класс/регион/город/статус строительства/цена; гео-фасеты — по ссылкам справочника RegionId/CityId, display — RegionName/CityName. Source of truth остаётся ProjectEntity в PostgreSQL (../domain/project.md); поисковый индекс — вторичная проекция (ADR-0059), scope-фильтр применяется до выполнения запроса.

Index

ПолеЗначение
Index aliaspin-chessboard-projects
Versioned indexpin-chessboard-projects-v1 (переключение alias, ADR-0029)
Owner servicechessboard
Source of truthProjectEntity (../domain/project.md) в PostgreSQL
Rebuild jobProjectSearchRebuildJob

Наполнение — инкрементально по событию pin.chessboard.project-upserted.v1 + периодический/ручной rebuild (ADR-0059 п.2).

Document Id И Routing

  • Document id: <TenantId>:<ProjectId> (tenant-scoped, детерминирован — upsert идемпотентен).
  • Routing key: TenantId (= DeveloperId) — документы застройщика в одном шарде (кластер малый, ADR-0056).
  • Delete/tombstone: документ индексируется только при Status = Published; переход в Draft/Hidden/Archived (или soft-delete проекта) удаляет документ из индекса (fail-closed — скрытый проект не находится поиском). Developer.Status = Hidden каскадно удаляет документы всех его проектов.

Поля

ПолеТипIndexedStoredAnalyzerSourceSecurity
DocumentIdkeywordдадаTenantId:ProjectIdinclude
ProjectIdkeywordдадаProjectEntity.Idinclude
TenantIdkeywordданетProjectEntity.TenantIdscope-only (не выдаётся)
DeveloperIdkeywordдадаProjectEntity.DeveloperIdinclude
Nametextдадаru_name (russian morphology + lowercase + edge-ngram)ProjectEntity.Nameinclude
NameTranslittextданетtranslit (ICU кириллица↔латиница)ProjectEntity.Nameinclude
Addresstextдадаru_textProjectEntity.CityName + Location.District + Location.AddressLine (jsonb)include
RegionIdkeywordдадаProjectEntity.RegionId (ссылка справочника — ключ фасета «регион»)include
RegionNametextдадаru_textProjectEntity.RegionName (display + полнотекстовый матч)include
CityIdkeywordдадаProjectEntity.CityId (ссылка справочника — ключ фасета «город»)include
CityNametextдадаru_textProjectEntity.CityName (display + полнотекстовый матч)include
TypekeywordдадаProjectEntity.Type (ProjectType)include
ClasskeywordдадаProjectEntity.Class (ProjectClass)include
ConstructionStatuskeywordдадаProjectEntity.ConstructionStatusinclude
StarsshortдадаProjectEntity.Starsinclude
Price.Minscaled_floatдадаProjectEntity.Price.Min (NullableRange<decimal>)include
Price.Maxscaled_floatдадаProjectEntity.Price.Maxinclude
CurrencyIdshortнетдаProjectEntity.CurrencyId (ссылка справочника; RUB-only, ADR-0048)include
PaybackYearsscaled_floatдадаProjectEntity.PaybackYearsinclude
CoverImageFileIdkeywordнетдаProjectEntity.CoverImageFileIdinclude
CreatedAtdateдадаProjectEntity.CreatedAt (сортировка «по дате»)include
SchemaVersionintдадаверсия схемы документа индексаinclude
UpdatedAtdateдадаProjectEntity.UpdatedAtinclude
IndexedAtdateдадамомент индексации (watermark)include
Внешний ввод — не raw-DSL

Внешний ввод (searchTerm) идёт только типизированным параметром поиска (multi-match по Name/NameTranslit/ Address/RegionName с fuzziness), не в raw-DSL. Фасеты (Type/Class/RegionId/CityId/ ConstructionStatus/диапазон Price — гео-фасеты фильтруются по ссылкам справочника, display-имена — из RegionName/CityName) и сортировки (relevance / Price.Min / CreatedAt) — типизированными параметрами запроса. Точные цены/статусы для неавторизованных урезаются потребителем витрины (catalog) — сам индекс держит только Published-срез без PII.

Source Events

EventVersionTransform
pin.chessboard.project-upserted.v1 (ProjectUpsertedKafkaEvent)1При Status = Published — upsert документа (map скалярных полей + Price.Min/Max; гео — RegionId/CityId события, display-имена RegionName/CityName — из RegionSnapshot/CitySnapshot payload-а); при Draft/Hidden/Archived — delete по DocumentId. Refresh display-имён при переименовании в справочнике приходит этим же событием (re-publish HR-6, ../domain/handbook-replica.md).
pin.chessboard.developer-upserted.v1 (DeveloperUpsertedKafkaEvent)1Developer.Status = Hidden → delete всех документов застройщика (TenantId = Id); Published → повторная индексация из source (через rebuild).

Полный адрес (Address) собирается из display-имени города (CityName) и адресных деталей jsonb Location (District, AddressLine); content jsonb-вкладок в индекс не попадает. Наполнение — консюмером проекции, плюс bootstrap/восстановление через rebuild из PostgreSQL.

Согласованность и отказы индексации
  • Порядок и защита от переупорядочивания. События проекта упорядочены ключом (ProjectId); индексатор применяет upsert/delete, только если версия события больше уже проиндексированной (watermark по Version/IndexedAt), иначе — skip. Переупорядоченное старое событие не перезатирает свежий документ.
  • Идемпотентность. DocumentId детерминирован (TenantId:ProjectId) — повторная доставка upsert/delete повторяема без побочных эффектов; дубликат сообщения не создаёт второй документ.
  • Гонка publish ↔ hide/archive. Разрешается порядком per-key: конечное состояние документа (проиндексирован / удалён) соответствует последнему по версии событию; промежуточно допустим лаг в пределах freshness-SLA.
  • Ретраи и DLQ. Временная ошибка индексации → повтор с backoff; стабильно неиндексируемое событие → DLQ индексатора с алертом (партиция не блокируется). Любое накопленное расхождение устраняется периодическим или ручным rebuild из PostgreSQL — источника истины.

Freshness

  • SLA: near-real-time (< 1 мин от project-upserted до появления в поиске); для admin/decision-запросов — FreshnessView с watermark (ADR-0019).
  • Watermark: IndexedAt + offset топика pin.chessboard.project-upserted.v1.
  • Stale behavior: при расхождении/лаге — показывать marker свежести; критичные решения принимаются по source-of-truth (chessboard API), не по проекции.

Security

  • Tenant/brand trimming выполняется до выполнения запроса через scope-фильтр (ADR-0014, ADR-0052): публичная лента видит только Status = Published проекты published-застройщиков; developer/admin-запросы — свой tenant/полный доступ (ADR-0053).
  • PII fields: нет (проект — не персональные данные; юрлицо/декларации LegalInfo по 214-ФЗ публичны и в индекс не кладутся).
  • Полнотекстовый индекс не хранит secrets, tokens, платёжные/KYC-данные; TenantId — только scope-фильтр, наружу не выдаётся.

Поведение запроса (edge-cases)

  • Пустой searchTerm → фасетная лента без relevance-ранжирования: результат сортируется заданной сортировкой (по умолчанию — CreatedAt desc) и фильтруется фасетами.
  • searchTerm без совпадений (даже с fuzzy/транслитерацией) → пустая страница (0 hits); фасетные счётчики пересчитываются по текущему scope.
  • Неизвестное значение фасета (Type/Class/ConstructionStatus) → пустой результат по этому фильтру (fail-closed), а не ошибка запроса; клиент валидирует значения по reference-data.md. Неизвестный/архивный RegionId/CityId — так же fail-closed: пустой результат, не ошибка (актуальные значения справочника — ../domain/handbook-replica.md).
  • Инвертированный диапазон цены (min > max) → пустой результат; границы диапазона — типизированные параметры, не raw-DSL.
  • Границы пагинации — стандартные лимиты size/offset; превышение усекается до максимума (глубокая пагинация ограничена, кластер малый — ADR-0056).
  • Scope-trim применяется до запроса. Гость видит только Status = Published проекты published-застройщиков; смена доступа/публичности между индексацией и запросом отражается scope-фильтром запроса и не требует переиндексации.

Rebuild

  1. Создать версионный индекс pin-chessboard-projects-vN с analyzers (ru_name, ru_text, translit).
  2. Backfill из PostgreSQL: выборка Status = Published по ProjectEntity стримом, map в ProjectSearchDocument.
  3. Валидация counts/checksums против источника.
  4. Атомарно переключить alias pin-chessboard-projects на новую версию.
  5. Держать предыдущий индекс до конца rollback-окна.

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

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

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