event

Product Updated

Se actualizó un producto (Activity o Box) elegible para indexarse. Lleva el documento completo. También es el evento del resync masivo.

EventsearchAlto volumen

Overview

search.product.updated.v1 se emite cuando cambia un producto que debe estar indexado. El payload es el documento entero del producto y es idéntico al de search.product.created.

Es el topic de mayor volumen del catálogo, porque además del flujo normal es el que usa el resync masivo.

Cuándo se dispara — son dos caminos

  1. 1

    post_save (flujo normal)

    search.signals.products_post_save: el producto ya existía y sigue cumpliendo los filtros de indexación.

  2. 2

    Resync masivo

    SearchIndexer.index_async() recorre todos los productos de un país (o de todos) y emite un evento por cada uno. Es el pico de tráfico esperable del topic.

El mismo documento viaja por dos transportes

Existe un camino paralelo por HTTP (SearchIndexer.index()sync_products_batch) que no pasa por el bus. Solo ese camino hace reconciliación de documentos obsoletos.

Schema

Formato Avro, validado con fastavro.

search.product.updated.v1 — Avro
{
"type": "record",
"name": "ProductUpdated",
"namespace": "search.product",
"doc": "Emitido cuando se actualiza un producto indexable (Activity o Box) en bigbox-backend, y tambien por el resync masivo SearchIndexer.index_async. Payload = documento de producto serializado, identico al de search.product.created.v1. Consumido por el servicio search para indexar en Elasticsearch.",
"fields": [
{
"name": "id",
"type": "int",
"doc": "Id del producto."
},
{
"name": "type",
"type": {
"type": "enum",
"name": "ProductType",
"symbols": ["Activity", "Box"]
},
"doc": "Discriminador. El consumer rutea a ActivityDocument o BoxDocument segun este campo; un valor desconocido lanza error y el mensaje se nackea."
},
{
"name": "country_id",
"type": ["null", "int"],
"default": null,
"doc": "Id del pais del producto."
},
{
"name": "name",
"type": "string",
"doc": "Nombre del producto."
},
{
"name": "description",
"type": ["null", "string"],
"default": null
},
{
"name": "short_description",
"type": ["null", "string"],
"default": null
},
{
"name": "price",
"type": ["null", "double"],
"default": null,
"doc": "Precio. En Box se toma del campo de precio del pais (Box.price_fields[country_id])."
},
{
"name": "participants",
"type": { "type": "array", "items": "string" },
"doc": "Buckets de participantes: '1', '2', '2+'."
},
{
"name": "landing_slugs",
"type": { "type": "array", "items": "string" },
"doc": "Slugs de landings donde aparece el producto (directas + por product tag)."
},
{
"name": "category",
"type": [
"null",
{
"type": "record",
"name": "Category",
"fields": [
{ "name": "id", "type": ["null", "int"], "default": null },
{ "name": "name", "type": "string" },
{ "name": "slug", "type": "string" }
]
}
],
"default": null
},
{
"name": "category_visible",
"type": "boolean"
},
{
"name": "slug",
"type": "string"
},
{
"name": "most_sold",
"type": "boolean"
},
{
"name": "most_sold_order",
"type": ["null", "int"],
"default": null
},
{
"name": "rating",
"type": ["null", "double"],
"default": null,
"doc": "score_satisfaction del producto."
},
{
"name": "individual_purchase_order",
"type": ["null", "int"],
"default": null
},
{
"name": "units_sold_last_30_days",
"type": "int"
},
{
"name": "units_validated_last_30_days",
"type": "int"
},
{
"name": "campaign_slugs",
"type": { "type": "array", "items": "string" },
"doc": "Slugs de todas las campanas activas que incluyen al producto. No implica descuento."
},
{
"name": "product_tag_slugs",
"type": { "type": "array", "items": "string" }
},
{
"name": "has_campaign_discount",
"type": "boolean",
"doc": "True si el producto esta en al menos una campana activa con descuento > 0."
},
{
"name": "has_deal_today",
"type": "boolean",
"doc": "True si hay un ActivityDiscount aprobado aplicable hoy. Siempre false para Box."
},
{
"name": "individual_purchase_available",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "visible",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "box_names",
"type": ["null", { "type": "array", "items": "string" }],
"default": null,
"doc": "Solo Activity: nombres de los boxes que la contienen. null cuando type = Box."
},
{
"name": "box_ids",
"type": ["null", { "type": "array", "items": "int" }],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "partner_fantasy_name",
"type": ["null", "string"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "has_online_reservation",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "reviews_qty",
"type": ["null", "int"],
"default": null,
"doc": "Solo Activity (survey_qty). null cuando type = Box — ver observaciones del .md."
},
{
"name": "visible_catalog",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "country_delivery",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "is_online_activity",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "can_purchase_a_reservation",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "activity_type",
"type": ["null", "string"],
"default": null,
"doc": "Solo Activity: Activity | PartnerActivity | ComboActivity | TravelActivity. null cuando type = Box."
},
{
"name": "province",
"type": ["null", {
"type": "array",
"items": {
"type": "record",
"name": "Province",
"fields": [
{ "name": "id", "type": "int" },
{ "name": "name", "type": "string" },
{ "name": "slug", "type": "string" }
]
}
}],
"default": null,
"doc": "Provincias del producto. Activity: derivadas de sus locations (o todas las del pais si es online / country_delivery). Box: derivadas de sus activities activas."
},
{
"name": "cities",
"type": ["null", {
"type": "array",
"items": {
"type": "record",
"name": "City",
"fields": [
{ "name": "id", "type": "int" },
{ "name": "name", "type": "string" },
{ "name": "province_id", "type": ["null", "int"], "default": null }
]
}
}],
"default": null
},
{
"name": "subfilters",
"type": ["null", {
"type": "array",
"items": {
"type": "record",
"name": "SubFilter",
"fields": [
{ "name": "id", "type": "int" },
{ "name": "name", "type": "string" },
{ "name": "parent_id", "type": ["null", "int"], "default": null },
{ "name": "parent_name", "type": ["null", "string"], "default": null },
{ "name": "parent_order", "type": ["null", "int"], "default": null }
]
}
}],
"default": null
},
{
"name": "availability",
"type": ["null", {
"type": "array",
"items": {
"type": "record",
"name": "AvailabilitySlot",
"doc": "Slots con disponibilidad desde hoy hasta el maximo rango de ActivityAvailabilityService.",
"fields": [
{ "name": "location_id", "type": "int" },
{ "name": "province_id", "type": ["null", "int"], "default": null },
{ "name": "city_id", "type": ["null", "int"], "default": null },
{ "name": "date", "type": ["null", "string"], "default": null },
{ "name": "time", "type": ["null", "string"], "default": null },
{ "name": "bookable_until", "type": ["null", "string"], "default": null }
]
}
}],
"default": null,
"doc": "Solo Activity. null cuando type = Box."
},
{
"name": "has_an_activity_with_visible_catalog",
"type": ["null", "boolean"],
"default": null,
"doc": "Solo Box. null cuando type = Activity."
},
{
"name": "activities_count",
"type": ["null", "int"],
"default": null,
"doc": "Solo Box. null cuando type = Activity."
},
{
"name": "activities_visible_catalog_count",
"type": ["null", "int"],
"default": null,
"doc": "Solo Box. null cuando type = Activity."
},
{
"name": "occurred_at",
"type": "string",
"doc": "ISO-8601 naive en hora local del proceso. Lo inyecta events.event.Event.__init__."
}
]
}

Un payload, dos formas

Igual que en created: el serializador declara los campos de los dos tipos de producto y rellena con null los que no aplican. Ver el detalle de campos en search.product.created.

Consumidores

publishes
event
routes to
routes to
consumes
Viewing
Event
Product Updated(v1)
ecommerzeta
Service
Bigbox Backend(v1.0.0)
omega-backend+2
Channel
Google Cloud Pub/Subv1.0.0
At least oncepubsub
ecommerzeta
Service
Search(v1.0.0)
ecommerzeta
  • events (1)
  • services (2)
  • channels (2)

search hace upsert del documento completo en el índice products.

Este evento puede pisar la disponibilidad

El upsert reescribe el documento entero, incluido el array availability. Como no hay ordenamiento garantizado entre topics, un product.updated puede sobreescribir la disponibilidad que acaba de dejar un reservations.availability.updated.

Deuda conocida

  • Mismos puntos que created: reviews_qty null en Box, created_at/updated_at que nunca viajan, y costo alto de serialización por producto.
  • El resync masivo emite un evento por producto y por país a través de Cloud Tasks. Vale medirlo antes de correrlo en horario pico.