Skip to content

Instantly share code, notes, and snippets.

@marcossegovia
Last active December 22, 2016 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcossegovia/edcc2b766eede8ddc2409e59f10e43c3 to your computer and use it in GitHub Desktop.
Save marcossegovia/edcc2b766eede8ddc2409e59f10e43c3 to your computer and use it in GitHub Desktop.

De Sphinx a Elastic

¿Qué ganamos con Elastic?

  • Despreocuparnos de la sincronización de las replicas En el caso de tener que reindexar un índice, nos despreocuparemos de sus replicas, ya que ES lo gestiona internamente. Esto hace que no podamos tener innumerables réplicas y teniendo que realizar una única reindexación.

  • Olvidarnos de balancear recursos para las replicas y los shards ES se encarga de autobalancear las replicas entre los nodos disponibles. Así como repartir los shards de los diferentes indices dentro de un nodo o en diferentes nodos, según los recursos disponibles. Las replicas SIEMPRE estarán en nodos distintos. (Si no, tendría poco sentido) https://www.elastic.co/videos/distributed-diagram

  • Mejoramos la performance mediante la distribución de los datos en shards Cuando creamos un indice, podemos decirle explícitamente a ES en cuantos shardings queremos que el indice sea distribuido, por defecto son 5, además cuantas replicas de ese indice queremos, para en caso de downtime de un nodo que contenga ese indice, una replica pueda servir la información.

  • Olvidarnos de MySQL Esta vez, de verdad. Cómo? Con ES tenemos tipos para cualquier dato lo que hace que podamos volcar toda esa información de MySQL hacia los índices de ES, con lo que la misma query a ES sería la que serviriamos con toda la información necesaria.

Shard: Contiene parte de la información.

Réplica: Contiene toda la información. Si un indice tiene esta dividido en 2 shards, su réplica también tendrá 2 shards.

Antes de seguir, repasemos la [terminología básica] (https://gist.github.com/MarcosSegovia/c4f9585d0450791470485c68514acc05#terminology)

##Política de reindexación

Reindexación de deltas durante el día (incremental indexing) y reindexación completa durante la noche.

###Sphinx

Debido a la reindexación continua que tiene que ir sufriendo el sistema debido a la actualización de productos y la imposibilidad de una reindexación incremental, se idearon unos indices que pudieran ser reindexados en poco tiempo para ir actualizando datos durante el dia.

Una vez terminado el día, se lleva a cabo un proceso de reindexación entera de la base de datos hacia los indices de Sphinx, en ambas réplicas. Por tanto tenemos una reindexación x2. Indices duplicados, totalmente independientes y que no colaboran en caso de downtime. El apuntado de un índice a otro ser haría de forma manual.

###ElasticSearch

Podemos mantener la política de incremental indexing tirando de la Update API mientras no hagamos uso de concurrencias (que no es el caso). Por tanto, podemos cojer toda la nueva info del producto que ha cambiado y actualizar su documento en el indice.

Mientras que para los actuales rebuilds de 0 de cada noche podriamos hacer resync de las últimas 24 horas + algo más (último resync) mediante la Bulk API, además podemos llegar a hacerlo mediante el uso de aliases para realizar el proceso sin downtime.

El updateo de las últimas 24h+ garantizará que todos aquellos comandos que hayan fallado, entradas manuales o errores en las queries de mysql y que, por lo tanto, no hayan generado la petición de update de los documentos a ES, sean actualizados en los índices. El proceso será mucha más rápido, teniendo en cuenta que:

  1. No hará falta hacer resync de todos los indices again.
  2. Con hacer un solo resync de los índices que necesiten actualizar documentos bastará, ya que ES autogestiona sus réplicas. 🙂

Recordemos que el Update Api realmente:

EXTRA: Podriamos llegar a utilizar los bulks de ElasticSearch en caso de que fuese necesario realizar el proceso de reindexación o de updateo más eficiente. https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html

#Multi-queries

##Sphinx Actualmente la necesidad de la multiqueries viene dada por al imposibilidad de poder agregar sobre la misma query múltiples filtros. Con lo que al final acaba siendo múltiples queries donde se agrupan los resultados por un filtro concreto y van siendo filtrados por la query siguiente.

Ejemplo de uso de una multi-query:

// Most relevant regions.
        $sql = <<<SQL
SELECT
	groupby() AS group_field ,
	count(*) AS count_field ,
	id
FROM
	$index
WHERE
	MATCH( '( @path_names: _1_ ) && (@countries_sales:( sales$country))')
GROUP BY
	id_category1
ORDER BY
	count_field desc
LIMIT
	0, 50;
SQL;
        $this->sphinxql->addQuery($sql, 'Categories first level relevance.');

// Most relevant appellations.
        $sql = <<<SQL
SELECT
	groupby() AS group_field ,
	count(*) AS count_field ,
	id
FROM
	$index
WHERE
	MATCH( '( @path_names: _1_ ) && (@countries_sales:( sales$country))')
GROUP BY
	id_category3
ORDER BY
	count_field desc
LIMIT
	0, 2000;
SQL;

        $this->sphinxql->addQuery($sql, 'Categories appellations level relevance.');

        return $this->sphinxql->multiQuery('GET categories relevance');

##ElasticSearch

Por un lado podemos solventar este problema si realmente volcamos todos los datos necesarios sobre cada producto y utilizamos la combinación de filtros.

Para solventar el problema de poder comparar entre diferentes agrupaciones de Documentos, en este caso con ES existe el concepto de los Aggregations Donde entran en juego distintas categorias, una de las cuales pueden ser la más interesante y la que creo que encaja con lo que intentamos simular mediante Sphinx:

  • Bucketing: Donde se crean 'buckets' en función de un criterio concreto. Se resuelven una serie de documentos en base a un criterio para cada bucket y se devuelven esos documentos de cada uno de los buckets.

Atacar el modelo actual de Sphinx

A través del modelo principal verticalroot\SharedSphinxqlModel

Existen métodos como:

  • Query para obtener identificadores de productos en función de la categoria y los filtros aplicados a través del $params
  • Obtener filtrado por geolocalización, para saber que sobre que locale realizamos la búsqueda
  • Obtener filtrado por descuento aplicado: free-shipping, store-delivery, express-shipping
  • Obtener el nombre de indice en función de una key (definido a través del sphinx.config)
  • Setear los filtros que se aplican en función de los diferentes parámetros que puedan llegar: countries, regions, appellations, categories, attributes, shipping_filter, onsale, price, robert-parker, id_maker, stores, search_term. En función del campo, o bien se setea como un campo más en el $filters_query (id_category2 = 100) o se añade una condición en el $matches_query (@discounts:('discountES'))
  • Setear ordenación: relevancia, ventas, baratos...
  • Existen métodos para añadir distintas queries(addQuery) para poder después correrlas como multiqueries (runQueries)

Verticalroot\SharedSphinxqlModel tiene 15 casos que extienden:

  • HomeIndexModel
  • MenuGeneratedHeaderTabsModel
  • ListsDidYouMeanModel
  • ListsFiltersModel
  • ListsAutocompleteModel
  • MakersMakersListModel
  • SharedFooterModel
  • StoresStoresSphinxListsModel
  • CellarsDataModel
  • ProductsFeaturedModel
  • CategoriesAppellationsModel
  • CategoriesCountriesModel
  • AttributesListModel
  • CategoriesRegionsModel

##Mapping actual de un Producto en las fichas/listas

Campos de un Producto en los listados /vinos:1

  "products" => [
    1086815 => [
      "id_product" => "1086815"
      "id_category" => "1645"
      "timestamp" => "2015-10-26 17:37:32"
      "updated_on" => "2016-12-21 05:14:19"
      "trusted" => "1"
      "id_user" => "33729"
      "price_aprox" => "8.25"
      "id_entity" => "0"
      "id_product_main" => null
      "name" => "José Pariente Verdejo"
      "urlize" => "jose-pariente-verdejo-2015"
      "rank" => "4.21"
      "num_opinions" => "44"
      "image_url" => "/wines/1086815.jpg"
      "image_url_default" => "1"
      "id_maker" => "735"
      "cellar" => "Bodegas José Pariente"
      "urlized_cellar" => "bodegas-jose-pariente"
      "producer_description" => """
        NOTA DE CATA: \r\n
        - Vista: amarillo pajizo brillante, reflejos verdosos.\r\n
        - Nariz: intenso, elegante, fresco, complejo. Aromas de frutas blancas, cítricos y tropicales, hinojo, monte bajo y fondo anisado, balsámico.\r\n
        - Boca: fruta fresca, untuoso, goloso, elegante. Buena estructura.\r\n
        \r\n
        DENOMINACIÓN DE ORIGEN: Rueda\r\n
        VIÑEDO: Bodegas José Pariente\r\n
        UVAS: Verdejo 100%\r\n
        \r\n
        MARIDAJE DEL VINO: quesos, pasta, arroces, pescados, mariscos, jamón ibérico, foie.\r\n
        TEMPERATURA DE CONSUMO: 6-8ºC\r\n
        GRADUACIÓN ALCOHÓLICA: 13.5%
        """
      "category" => "Rueda"
      "urlized_category" => "vino-rueda"
      "category_leaf" => "1"
      "non_vintage_urlize" => "jose-pariente-verdejo"
      "badge" => "top_sales"
      "price" => "7.87"
      "discounts" => []
      "free_shipping" => false
      "id_opinion" => "5598201"
      "opinion_title" => "José Pariente Verdejo 2015"
      "opinion_rank" => "5.0"
      "opinion_content" => "Muy buen vino"
      "opinion_timestamp" => "2016-12-03 09:04:00"
      "opinion_city" => "Vilassar de Dalt"
      "opinion_user" => "jaumeparadis"
      "opinion_id_user" => "294263"
      "opinion_name" => "Jaume"
      "opinion_fullname" => "Jaume Paradis Balaux"
      "key" => 0
      "offer_id_product" => "1086815"
      "id_store" => "199"
      "store" => "Campoluz Enoteca"
      "urlized_store" => "campoluz-enoteca"
      "discount" => "7.87"
      "original_price" => "9.45"
      "currency" => "EUR"
      "stock" => "120"
      "weight" => "1.8"
      "store_image_url" => "/wines/1086815-s199.jpg"
      "offer_main_url" => "jose-pariente-verdejo-2015"
      "num_offers" => "12"
      "has_to_show_stock_alert" => false
      "saving" => 1.58
      "saving_percent" => 17.0
      "other_shipping" => []
      "flat_rate_shipping_price" => 4.95
      "flat_rate_saving_percentage" => -32
      "flat_rate_max_units" => 6.0
      "url" => "https://www.uvinum.marcos.vm/vino-rueda/jose-pariente-verdejo-2015"
      "attributes" => []
      "path" => []
      "category_url" => "https://www.uvinum.marcos.vm/vino-rueda"
      "volume_data" => []
      "long_name" => "José Pariente Verdejo 2015"
      "image_mini" => "https://media-verticommnetwork1.netdna-ssl.com/wines/jose-pariente-verdejo-1086815_m.jpg"
      "image_avatar" => "https://media-verticommnetwork1.netdna-ssl.com/wines/jose-pariente-verdejo-1086815_a.jpg"
      "image_thumbnail" => "https://media-verticommnetwork1.netdna-ssl.com/wines/jose-pariente-verdejo-1086815_t.jpg"
      "image_medium" => "https://media-verticommnetwork1.netdna-ssl.com/wines/jose-pariente-verdejo-1086815_d.jpg"
      "image_profile" => "https://media-verticommnetwork1.netdna-ssl.com/wines/jose-pariente-verdejo-1086815_p.jpg"
      "image_full" => "https://media-verticommnetwork1.netdna-ssl.com/wines/jose-pariente-verdejo-1086815.jpg"
      "image_maker_logo" => "https://static.marcos.vm/img/makers/default-uvinum-logo.png"
      "image_maker_full" => "https://static.marcos.vm/img/makers/default-uvinum-full.png"
      "ratings_to_show" => []
      "have_badge" => true
      "buy_options" => []
    ],
	...
	...
]

Campo discounts dentro de cada Producto

"discounts" => [
	"id_store" => "199"
	"id_discount_coupon" => "25235"
	"code" => null
	"usage_limit" => "0"
	"valid_from" => "2016-05-27"
	"valid_to" => "2018-07-01"
	"cart_subtotal" => "150.00"
	"discount_amount" => "100.00"
	"type" => "percentage"
	"apply_to_shipping" => "yes"
	"apply_to_shipping_type" => "1"
	"active" => "yes"
	"verticomm_percentage" => "50"
	"created_at" => "2015-12-23 13:19:46"
	"last_mod_user_id" => "39313"
	"last_mod_at" => "2016-10-11 16:02:24"
	"id_vertical" => "1"
	"id_user" => null
	"payment_method_code" => null
	"key" => 7
]

Campo attributes dentro de cada Producto

"attributes" => [
	"vintage" => []
	"wine_type" => []
	"bottle_volume" => []
	"grapes" => []
	"pairing" => []
	"penin" => []
	"robert_parker" => []
	"uvinum_score" => []
	"alcohol_volume" => []
	"temperature_consumption" => []
	"product_weight" => []
	"trusted_ean" => []
]

Donde cada uno de estos attributes contiene un array de uno o varios campos

[
    "id_product" => "1086815"
    "id_attribute" => "1"
    "attribute_name" => "vintage"
    "multi_value" => "no"
    "tree_value" => false
    "id_value" => "2015"
    "value" => "2015"
    "urlize" => null
    "additional" => null
    "common_urlize" => null
    "attribute_name_description" => "Añada"
    "depth" => null
    "masculine_singular" => null
    "masculine_plural" => null
    "feminine_singular" => null
    "feminine_plural" => null
    "neuter_singular" => null
    "neuter_plural" => null
    "connector1" => null
    "connector2" => null
    "attribute_urlize" => "anada"
    "unit" => ""
]

Campo path dentro de cada Producto, donde se repite siempre la misma estructura dependiendo de la profundidad de su categoría.

"path" => [
	0 => [
	      "id_category" => "1"
	      "name" => "Vino"
	      "masculine_singular" => "Vino"
	      "masculine_plural" => "Vinos"
	      "feminine_singular" => null
	      "feminine_plural" => null
	      "neuter_singular" => null
	      "neuter_plural" => null
	      "connector1" => ""
	      "connector2" => ""
	      "urlize" => "vinos"
	      "depth" => "0"
	    ],
	    ...
	    ...
]

Campo volume_data dentro de Producto

"volume_data" => [
	"name" => "José Pariente Verdejo"
	"clean_name" => "José Pariente Verdejo"
	"string_volume" => "75cl"
	"urlize_volume" => ""
]

Campo ratings_to_show dentro de Producto

"ratings_to_show" => [
    0 => "robert_parker"
    1 => "uvinum_score"
    3 => "penin"
]

Campo buy_options dentro de Producto

"buy_options": 
  [
    {
      "id_store": "312",
      "id_product_affiliate": "RO-0063",
      "ean": "8501110088619",
      "price": 9.5,
      "discount": 0,
      "currency": "EUR",
      "stock": "39",
      "min_stock_quantity": "1",
      "min_sale_quantity": "1",
      "max_sale_quantity": "0",
      "min_sale_quantity_pack": "no",
      "weight": "1.5",
      "preparation_time": "0",
      "expedition_time": "0",
      "available_time": "",
      "timestamp": "2016-12-22 05:14:08",
      "tax": "21.00",
      "unit_price": "9.50",
      "shipping_price": 4.9,
      "delivery_time": "72-120",
      "country": "España",
      "store": "Gourmet en casa TCM",
      "min_order_qty": "1",
      "max_order_qty": "39",
      "multiple_of": "1",
      "custom_profile_page": "no",
      "has_tier_price_applied": false,
      "other_shipping": [],
      "final_price_currency": 9.5,
      "user_credits_rewards": 0.13,
      "user_credits_rewards_multiplier": null,
      "original_price": 9.5,
      "selling_countries": [
        {
          "id_country": "18",
          "code": "DE",
          "name": "Alemania"
        },
        {
          "id_country": "2",
          "code": "AT",
          "name": "Austria"
        },
        {
          "id_country": "9",
          "code": "BE",
          "name": "Bélgica"
        },
        {
          "id_country": "71",
          "code": "HR",
          "name": "Croacia"
        },
        {
          "id_country": "22",
          "code": "DK",
          "name": "Dinamarca"
        },
        {
          "id_country": "28",
          "code": "ES",
          "name": "España"
        },
        {
          "id_country": "64",
          "code": "FR",
          "name": "Francia"
        },
        {
          "id_country": "20",
          "code": "GR",
          "name": "Grecia"
        },
        {
          "id_country": "14",
          "code": "HU",
          "name": "Hungría"
        },
        {
          "id_country": "29",
          "code": "IT",
          "name": "Italia"
        },
        {
          "id_country": "41",
          "code": "LU",
          "name": "Luxembourg"
        },
        {
          "id_country": "44",
          "code": "MC",
          "name": "Mónaco"
        },
        {
          "id_country": "19",
          "code": "NL",
          "name": "Países Bajos"
        },
        {
          "id_country": "47",
          "code": "PL",
          "name": "Polonia"
        },
        {
          "id_country": "48",
          "code": "PT",
          "name": "Portugal"
        },
        {
          "id_country": "13",
          "code": "GB",
          "name": "Reino Unido"
        },
        {
          "id_country": "65",
          "code": "CZ",
          "name": "República Checa"
        },
        {
          "id_country": "66",
          "code": "SE",
          "name": "Suecia"
        }
      ],
      "estimated_country": "ES",
      "estimated_availability": true
    },
    ...
    ...
  ]
}

Mapeo que empleamos en ElasticSearch

{
	"mappings": {
		"product": {
			"properties": {
				"id_product": {"type": "integer"},
				"name": {"type": "text"},
				"long_name": {"type": "text"},
				"urlize": {
					"properties": {
						"default": {"type": "text"},
						"non_vintage": {"type": "text"}
					}
				},
				"timestamp": {"type": "date"},
				"updated_on": {"type": "date"},
				"trusted": {"type": "boolean"},
				"rank": {"type": "double"},
				"num_opinions": {"type": "integer"},
				"url": {"type": "text"},
				"image": {
					"properties": {
						"mini": {"type": "text"},
						"avatar": {"type": "text"},
						"thumbnail": {"type": "text"},
						"medium": {"type": "text"},
						"profile": {"type": "text"},
						"full": {"type": "text"},
						"maker_logo": {"type": "text"},
						"maker_full": {"type": "text"}
					}
				},
				"image_url": {"type": "text"},
				"image_url_default": {"type": "text"},
				"description": {
					"properties": {
						"emotional": {"type": "text"},
						"producer": {"type": "text"},
						"maker": {"type":"text"}
					}
				},
				"maker": {
					"properties": {
						"id_maker": {"type": "integer"},
						"name": {"type": "text"}
					}
				},
				"category": {
					"properties": {
						"id_category": {"type": "integer"},
						"name": {"type": "text"},
						"leaf": {"type": "integer"},
						"url": {"type": "text"}
					}
				},
				"num_favorites": {"type": "integer"},
				"badges": {"type": "keyword"},
				"price": {
					"type": "nested",
					"properties": {
						"currency": {"type": "keyword"},
						"amount": {
							"type": "scaled_float",
          					"scaling_factor": 100
						}
					}
				},
				"attributes": {
					"type": "nested",
					"properties": {
						"id": {"type": "integer"},
						"name": {"type": "text"},
						"key": {"type": "keyword"},
						"is_multi_value": {"type": "boolean"},
						"is_tree_value": {"type": "boolean"},
						"urlize": {"type": "text"},
						"common_urlize": {"type": "text"},
						"additional": {"type": "text"},
						"attribute_name_description": {"type": "text"},
						"value": {
							"properties": {
								"id": {"type": "integer"},
								"name": {"type": "text"},
								"urlize": {"type": "text"},
								"depth": {"type": "integer"},
								"connector1": {"type": "text"},
								"connector2": {"type": "text"},
								"masculine_singular": {"type": "text"},
								"masculine_plural": {"type": "text"},
								"femenine_singular": {"type": "text"},
								"femenine_plural": {"type": "text"},
								"neutral_singular": {"type": "text"},
								"neutral_plural": {"type": "text"},
								"unit": {"type": "text"}
							}
						}						
					}
				},
				"path": {
					"type": "nested",
					"properties": {
						"id_category": {"type": "integer"},
						"name": {"type": "text"},
						"masculine_singular": {"type": "text"},
						"masculine_plural": {"type": "text"},
						"femenine_singular": {"type": "text"},
						"femenine_plural": {"type": "text"},
						"neutral_singular": {"type": "text"},
						"neutral_plural": {"type": "text"},
						"connector1": {"type": "text"},
						"connector2": {"type": "text"},
						"urlize": {"type": "text"},
						"depth": {"type": "integer"}
					}
				},
				"volume_data": {
					"properties": {
						"name": {"type": "text"},
						"clean_name": {"type": "text"},
						"string_volume": {"type": "keyword"},
						"urlize_volume": {"type": "text"}
					}
				},
				"ratings_to_show": {
					"properties": {
						"rating_name": {"type": "text"}
					}
					
				},
				"opinion": {
					"properties": {
						"id": {"type": "integer"},
						"title": {"type": "text"},
						"comment": {"type": "text"},
						"rank": {"type": "half_float"},
						"timestamp": {"type": "date"},
						"user": {
							"properties": {
								"id": {"type": "integer"},
								"user": {"type": "text"},
								"name": {"type": "text"},
								"full_name": {"type": "text"},
								"city": {"type": "text"}
							}
						}
					}
				},
				"has_to_show_stock_alert": {"type": "boolean"},
				"discounts": {
					"type": "nested",
					"properties": {
						"id_store": {"type": "integer"},
						"id_discount_coupon": {"type": "integer"},
						"code": {"type": "text"},
						"usage_limit": {"type": "integer"},
						"valid_from": {"type": "date"},
						"valid_to": {"type": "date"},
						"cart_subtotal": {"type": "float"},
						"discount_amount": {"type": "float"},
						"type": {"type": "keyword"},
						"apply_to_shipping": {"type": "boolean"},
						"apply_to_shipping_type": {"type": "text"},
						"active": {"type": "boolean"},
						"verticomm_percentage": {"type": "half_float"},
						"created_at": {"type": "date"},
						"last_mod_user_id": {"type": "integer"},
						"last_mod_at": {"type": "date"},
						"id_vertical": {"type": "integer"},
						"id_user": {"type": "integer"},
						"payment_method_code": {"type": "text"},
						"key": {"type": "integer"}
					}
				},
				"buy_options": {
					"type": "nested",
					"properties": {
						"id_store": {"type": "integer"},
						"id_product_affiliate": {"type": "keyword"},
						"ean": {"type": "integer"},
						"price": {
							"type": "nested",
							"properties": {
								"currency": {"type": "keyword"},
								"amount": {
									"type": "scaled_float",
		          					"scaling_factor": 100
								}
							}
						},
						"original_price": {
							"type": "nested",
							"properties": {
								"currency": {"type": "keyword"},
								"amount": {
									"type": "scaled_float",
		          					"scaling_factor": 100
								}
							}
						},
						"discount": {
							"type": "nested",
							"properties": {
								"currency": {"type": "keyword"},
								"amount": {
									"type": "scaled_float",
		          					"scaling_factor": 100
								}
							}
						},
						"unit_price": {
							"type": "nested",
							"properties": {
								"currency": {"type": "keyword"},
								"amount": {
									"type": "scaled_float",
		          					"scaling_factor": 100
								}
							}
						},
						"shipping_price": {
							"type": "nested",
							"properties": {
								"currency": {"type": "keyword"},
								"amount": {
									"type": "scaled_float",
		          					"scaling_factor": 100
								}
							}
						},
						"tax": {"type": "keyword"},
						"stock": {
							"type": "nested",
							"properties": {
								"current": {"type": "integer"},
								"minimum_quantity": {"type": "integer"}
							}
						},
						"sale_quantity": {
							"type": "nested",
							"properties": {
								"minimum": {"type": "integer"},
								"maximum": {"type": "integer"},
								"minimum_pack": {"type": "boolean"}
							}
						},
						"weight": {"type": "keyword"},
						"time": {
							"type": "nested",
							"properties": {
								"preparation": {"type": "integer"},
								"expedition": {"type": "integer"},
								"available": {"type": "integer"},
								"delivery": {"type": "keyword"}
							}
						},
						"country_name": {"type": "text"},
						"min_order_quantity": {"type": "integer"},
						"max_order_quantity": {"type": "integer"},
						"user_credits_rewards": {"type": "float"},
						"user_credits_rewards_multiplier": {"type": "integer"},
						"has_tier_price_applied": {"type": "boolean"},
						"custom_profile_page": {"type": "boolean"},
						"selling_countries": {
							"type": "nested",
							"properties": {
								"id_country": {"type": "integer"},
								"code": {"type": "keyword"},
								"name": {"type": "text"}
							}
						},
						"estimated_country": {"type": "keyword"},
						"estimated_availability": {"type": "boolean"}
					}
				}
			}
		}
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment