Skip to main content

search-aggregation-service — Event Schemas

Companion: DOMAIN_MODEL · APPLICATION_LOGIC · 04 Event-Driven Architecture · Naming

All events follow melmastoon.<service>.<aggregate>.<verb_past>.v<n> and ship over GCP Pub/Sub with the transactional outbox pattern. For events produced by this service, ordering key is propertyId (cross-tenant by design — tenantId is included in the envelope for cascade-purge audit, never used for routing). Schemas live in services/search-aggregation-service/contracts/events/*.json (JSON Schema draft 2020-12) and are CI-validated against the example payloads in this document.

1. Envelope (shared)

{
"eventId": "evt_01H...",
"eventType": "melmastoon.search.projection.updated.v1",
"schemaVersion": 1,
"occurredAt": "2026-04-22T10:00:00.123Z",
"tenantId": "tnt_01H...",
"aggregate": { "type": "hotel_index_entry", "id": "srh_01H..." },
"version": 7,
"producer": { "service": "search-aggregation-service", "version": "1.0.0" },
"trace": {
"traceId": "00f067aa0ba902b7",
"spanId": "00f067aa0ba902b8",
"causationId": "evt_01H...",
"correlationId":"req_01H..."
},
"data": { /* payload, schema-version-specific */ }
}

Pub/Sub attributes:

AttributeValue
eventTypesame as JSON eventType
tenantIdsame as JSON tenantId (audit only)
propertyIdaggregate's propertyId when applicable (used as ordering key)
schemaVersionstring '1'
traceparentW3C-formatted trace context
idempotencyKeysame as eventId
retentionClassoperational | regulated | audit

2. Versioning rules

  • Additive changes (new optional field) → bump schemaVersion minor in JSON Schema, same topic version (.v1).
  • Breaking changes → publish a new version (.v2) on a new topic for ≥ 90 days alongside .v1. Mark .v1 deprecated in contracts/events/DEPRECATIONS.md.
  • All consumers must tolerate unknown additional fields.

3. Topics & subscriptions

TopicProducerSubscribers (initial)Retention class
melmastoon.search.projection.v1search-aggregationanalytics, bff-consumer (cache-warm), bff-tenant-booking (deep-link cache invalidation)operational
melmastoon.search.click.v1search-aggregationanalytics, ai-orchestrator (Phase 2+ ranking signal), search-aggregation (popularity recompute self-loop)operational
melmastoon.search.query.v1search-aggregationanalyticsregulated (sampled; PII-redacted by domain)
melmastoon.search.boost_rule.v1search-aggregationbff-consumer (cache invalidation), analyticsaudit
melmastoon.search.index.v1search-aggregationplatform SRE (alerting), analyticsaudit

DLQ per subscription with 5 delivery attempts, exponential backoff (1s..60s).

4. Events published

4.1 melmastoon.search.projection.updated.v1

Emitted on every successful upsert/suppress/delete of a HotelIndexEntry, RateSnapshot, AvailabilityHint. Payload identifies what changed but does not duplicate the full document (consumers re-read via the API or the /internal/projection/changes stream if they need full state).

{
"data": {
"kind": "hotel_index_entry",
"propertyId": "ppt_01H...",
"tenantId": "tnt_01H...",
"operation": "upserted",
"changedSlices": ["amenities", "hero_photo", "popularity"],
"vectorClock": { "propertyService": 12, "pricingService": 47, "inventoryService": 91 },
"occurredAt": "2026-04-22T10:00:00.123Z"
}
}

kindhotel_index_entry | rate_snapshot | availability_hint. operationupserted | suppressed | reinstated | deleted.

4.2 melmastoon.search.projection.failed.v1

Emitted when a projection write failed permanently (after retries; row went to DLQ).

{
"data": {
"kind": "hotel_index_entry",
"propertyId": "ppt_01H...",
"tenantId": "tnt_01H...",
"sourceEventId": "evt_01H...",
"sourceTopic": "melmastoon.property.published.v1",
"errorCode": "MELMASTOON.SEARCH.FORBIDDEN_FIELD_IN_PROJECTION",
"errorDetail": "field 'guest_email' is not cross_tenant_searchable",
"attempts": 5,
"movedToDlqAt": "2026-04-22T10:00:00.123Z"
}
}

4.3 melmastoon.search.click.recorded.v1

{
"data": {
"clickEventId": "clk_01H...",
"queryId": "q_01H...",
"propertyId": "ppt_01H...",
"tenantId": "tnt_01H...",
"rank": 3,
"userBucket": "ub_anon_3f9a...",
"occurredAt": "2026-04-22T10:00:00.123Z"
}
}

4.4 melmastoon.search.query.executed.v1 (sampled)

Sampled at 1 % for anonymous traffic and 100 % for authenticated traffic. PII-redacted by the domain SearchQuery aggregate.

{
"data": {
"queryId": "q_01H...",
"canonicalQueryHash": "sha256:abc123…",
"locale": "ps",
"currency": "AFN",
"region": "AF",
"text": "guesthouse near garden",
"filter": {
"destination": { "city": "kabul", "near": { "center": { "lat": 34.5328, "lng": 69.1727 }, "radiusKm": 10 } },
"dates": { "from": "2026-05-01", "to": "2026-05-04" },
"occupancy": { "adults": 2, "children": 1, "rooms": 1 },
"amenities": ["wifi", "halal_kitchen"],
"starRatingMin": 3
},
"sortKey": { "kind": "price", "direction": "asc" },
"resultCount": 47,
"tookMs": 87,
"userBucket": "ub_anon_3f9a...",
"degradationLevel": "none",
"occurredAt": "2026-04-22T10:00:00.123Z"
}
}

If text matched the PII regex (email/phone), it is replaced by '[REDACTED]' before persistence and emission.

4.5 melmastoon.search.boost_rule.created.v1

{
"data": {
"boostRuleId": "brt_01H...",
"tenantId": "tnt_01H...",
"propertyId": "ppt_01H...",
"scope": {
"region": "AF",
"amenities": ["conference_hall"],
"locales": ["en"],
"dateRange": { "from": "2026-06-01", "to": "2026-09-30" }
},
"multiplier": 1.5,
"createdBy": "usr_01H...",
"createdAt": "2026-04-22T10:00:00.123Z",
"expiresAt": "2026-10-01T00:00:00Z"
}
}

4.6 melmastoon.search.boost_rule.activated.v1

{
"data": {
"boostRuleId": "brt_01H...",
"tenantId": "tnt_01H...",
"propertyId": "ppt_01H...",
"activatedBy": "usr_01H...",
"activatedAt": "2026-04-22T10:00:00.123Z",
"appliedMultiplier": 1.5
}
}

4.7 melmastoon.search.index.rebuilt.v1

{
"data": {
"buildId": "ibd_01H...",
"regions": ["AF", "TJ", "IR"],
"templateVersion": "v3",
"newIndexNames": {
"AF": "melmastoon-search-af-2026-04-22T08-00",
"TJ": "melmastoon-search-tj-2026-04-22T08-00",
"IR": "melmastoon-search-ir-2026-04-22T08-00"
},
"previousIndexNames": {
"AF": "melmastoon-search-af-2026-04-15T08-00"
},
"docsIndexed": 38712,
"durationSeconds": 612,
"swappedAt": "2026-04-22T08:14:11Z"
}
}

4.8 melmastoon.search.index.health_alert.v1

{
"data": {
"severity": "P1",
"kind": "circuit_open" ,
"details": "OpenSearch returned 5xx on 7/10 last requests; Postgres fallback engaged.",
"since": "2026-04-22T10:01:33Z",
"indexAlias": "melmastoon-search-current"
}
}

kindcircuit_open | circuit_close | freshness_breach | rebuild_failed | mapping_drift | query_budget_breach.

5. Events consumed

The service is a conformist consumer — it does not negotiate contracts; it accepts the upstream service's published schema. The list below documents every consumed event, its inbox handling, and the consumer's effect.

5.1 From property-service

EventEffectVector-clock field
melmastoon.property.created.v1ignored — only published triggers indexingn/a
melmastoon.property.updated.v1UpsertHotelIndexEntry (delta)event.version
melmastoon.property.published.v1UpsertHotelIndexEntry (full upsert)event.version
melmastoon.property.unpublished.v1SuppressHotelIndexEntryevent.version
melmastoon.property.deleted.v1DeleteHotelIndexEntryevent.version
melmastoon.property.room_type.updated.v1UpsertHotelIndexEntry (capacity slice)event.version
melmastoon.property.amenity_set.updated.v1UpsertHotelIndexEntry (amenities slice)event.version
melmastoon.property.photo.added.v1UpsertHotelIndexEntry (hero refresh; only fires when is_hero=true)event.version
melmastoon.property.photo.removed.v1UpsertHotelIndexEntry (hero refresh)event.version

5.2 From pricing-service

EventEffectVector-clock field
melmastoon.pricing.rate_plan.published.v1UpsertRateSnapshot per affected (property, date)event.version
melmastoon.pricing.rate_plan.updated.v1UpsertRateSnapshot per affected (property, date)event.version
melmastoon.pricing.fx_snapshot.updated.v1UpsertFxSnapshot (cache + invalidate srh:q:*)event.snapshotVersion

5.3 From inventory-service

EventEffectVector-clock field
melmastoon.inventory.allocation.confirmed.v1UpsertAvailabilityHint (decrement)event.version
melmastoon.inventory.allocation.released.v1UpsertAvailabilityHint (increment)event.version
melmastoon.inventory.block.created.v1UpsertAvailabilityHint (decrement)event.version
melmastoon.inventory.block.released.v1UpsertAvailabilityHint (increment)event.version

5.4 From tenant-service

EventEffect
melmastoon.tenant.deleted.v1PurgeTenantFromIndex cascade
melmastoon.tenant.region_changed.v1per-property re-evaluation of RegionPinningPolicy

6. Sample inbound: melmastoon.property.published.v1

{
"eventId": "evt_01H...",
"eventType": "melmastoon.property.published.v1",
"schemaVersion": 1,
"occurredAt": "2026-04-22T08:14:11Z",
"tenantId": "tnt_01H...",
"aggregate": { "type": "property", "id": "ppt_01H..." },
"version": 12,
"producer": { "service": "property-service", "version": "1.7.3" },
"trace": { /* … */ },
"data": {
"id": "ppt_01H...",
"slug": "ghasi-house-kabul",
"name": { "default": "ps", "values": { "ps": "...", "en": "Ghasi House Kabul" } },
"description": { "default": "ps", "values": { "ps": "...", "en": "..." } },
"address": { "city": "Kabul", "countryIso2": "AF" },
"geo": { "lat": 34.5310, "lng": 69.1750, "source": "manual" },
"starRating": 3,
"amenities": ["wifi", "halal_kitchen", "prayer_room"],
"enabledLocales": ["en", "ps", "fa"],
"heroPhotoMediaId": "med_01H...",
"publishedAt": "2026-04-22T08:14:11Z"
}
}

The consumer reads only the allow-listed fields — anything else (e.g., owner email, phone) is rejected by the projection allow-list policy at the projector, never reaching the index.

7. Schema registry

  • AsyncAPI document at services/search-aggregation-service/contracts/asyncapi.yaml lists topics + schemas + retention classes.
  • CI runs asyncapi:check and the platform schema-registry compatibility check on every PR.
  • Breaking changes require a new .v(n+1) topic; old topic stays for ≥ 90 days.