Producto

Entender "Query Then Fetch" frente a "DFS Query Then Fetch"

En nuestro último artículo sobre la coincidencia de frases que comienzan con, ejecutamos una situación en la que las puntuaciones devueltas eran sospechosas. Como recordatorio, aquí está la búsqueda en cuestión:

$ curl -XGET localhost:9200/startswith/test/_búsqueda?pretty -d '{
        "query": {
        "match_phrase_prefix": {
           "title": {
             "búsqueda": "d",
             "max_expansions": 5
           }
         }
       }
     }' | grep title

      "_score" : 1.0, "_source" : {"title":"drunk"}
      "_score" : 0.30685282, "_source" : {"title":"dzone"}
      "_score" : 0.30685282, "_source" : {"title":"data"}
      "_score" : 0.30685282, "_source" : {"title":"drive"}

¿Ves cómo el documento “drunk” recibe una puntuación de 1.0, mientras que el resto tiene una puntuación de 0.3? ¿No deberían tener todos estos documentos la misma puntuación, ya que coinciden con la búsqueda de “d” de la misma manera? La respuesta es sí, pero hay una muy buena razón para esta discrepancia en la puntuación.

Puntuación de relevancia

Parte del algoritmo de puntuación utilizado por Elasticsearch (y Lucene por debajo) incluye estadísticas de “Frecuencia de término – Frecuencia inversa de documento” (TF-IDF) para ayudar a calcular la relevancia de los documentos en el índice.

Se ha escrito mucho sobre el tema de TF-IDF, pero básicamente dice: “cuanto más aparece un término en un documento, más relevante es este documento. Pero la relevancia se ve atenuada por la frecuencia con la que el término aparece en todo el índice”.

Los términos raros solo están presentes en unos pocos documentos, lo que significa que cualquier búsqueda que coincida con un término raro se vuelve altamente relevante. Por el contrario, los términos comunes se encuentran en todas partes, por lo que su relevancia para la búsqueda es baja.

Elasticsearch se enfrenta a un dilema interesante cuando ejecutas una búsqueda. Tu búsqueda necesita encontrar todos los documentos relevantes… pero estos documentos están dispersos en cualquier número de shards en tu cluster.

Cada shard es básicamente un índice de Lucene, que mantiene sus propias estadísticas de TF y DF. Un shard solo sabe cuántas veces aparece “pineapple” dentro del shard, no en todo el cluster.

Pero el algoritmo de relevancia utiliza TF-IDF… ¿no necesita saber cómo son el TF y el DF para el índice completo, no para cada shard?

Tipo de búsqueda predeterminado: Query Then Fetch

La respuesta es sí y no. De forma predeterminada, Elasticsearch utilizará un tipo de búsqueda llamado “Query Then Fetch“. La forma en que funciona es la siguiente:

  1. Envía la búsqueda a cada shard
  2. Encuentra todos los documentos coincidentes y calcula las puntuaciones usando frecuencias de término/documento locales
  3. Crear una cola de prioridad de resultados (clasificación, paginación con desde/hasta, etc.)
  4. Devolver metadatos sobre los resultados al Node solicitante. Ten en cuenta que el documento real aún no se envía, solo las puntuaciones
  5. Las puntuaciones de todos los shards se fusionan y ordenan en el Node solicitante, los documentos se seleccionan según los criterios de búsqueda
  6. Finalmente, los documentos reales se recuperan de los shards individuales donde residen.
  7. Los resultados se devuelven al cliente

Este sistema normalmente funciona bien. En la mayoría de los casos, tu índice tiene "suficientes" documentos para suavizar las estadísticas de frecuencia de término/documento. Así que, aunque cada shard puede no tener un conocimiento completo de las frecuencias en todo el cluster, los resultados son "lo suficientemente buenos" porque las frecuencias son bastante similares en todas partes.

Pero en el caso de nuestra búsqueda mencionada al principio de este artículo, el tipo de búsqueda predeterminado a veces falla.

DFS Query Then Fetch

En el último artículo, creamos un índice sin especificar el recuento de shards: ElasticSearch usó el valor predeterminado de 5 shards. Luego insertamos unos insignificantes cinco documentos en el índice y exigimos que ES devolviera resultados relevantes y puntuaciones precisas. No es muy justo, ¿verdad?

Las discrepancias en la puntuación fueron causadas por el tipo de búsqueda Query Then Fetch. Cada shard solo contenía 1 o 2 documentos (el algoritmo de hashing usado por ES asegura una distribución relativamente aleatoria). Cuando pedimos a Elastic que calculara las puntuaciones, cada shard solo tenía una visión minúscula del índice de cinco documentos... por lo que las puntuaciones fueron inexactas.

Afortunadamente, Elasticsearch no te deja en la estacada. Si tienes una situación en la que esta discrepancia de puntuación es problemática, ES proporciona un tipo de búsqueda llamado "DFS Query Then Fetch". El procedimiento es casi idéntico a Query then Fetch, excepto que realiza una búsqueda previa para calcular las frecuencias globales de los documentos.

  1. Realizar una búsqueda previa en cada shard preguntando por las frecuencias de términos y documentos
  2. Envía la búsqueda a cada shard
  3. Encuentra todos los documentos coincidentes y calcula las puntuaciones usando frecuencias de término/documento globales calculadas a partir de la búsqueda previa.
  4. Crear una cola de prioridad de resultados (clasificación, paginación con desde/hasta, etc.)
  5. Devolver metadatos sobre los resultados al Node solicitante. Ten en cuenta que el documento real aún no se envía, solo las puntuaciones
  6. Las puntuaciones de todos los shards se fusionan y ordenan en el Node solicitante, los documentos se seleccionan según los criterios de búsqueda
  7. Finalmente, los documentos reales se recuperan de los shards individuales donde residen.
  8. Los resultados se devuelven al cliente

Si aplicamos este nuevo tipo de búsqueda a nuestra consulta anterior, obtenemos resultados de puntuación que tienen sentido (p. ej., todos son idénticos):

$ curl -XGET 'localhost:9200/startswith/test/_search?pretty=true&search_type=dfs_query_then_fetch' -d '{
        "query": {
        "match_phrase_prefix": {
           "title": {
             "búsqueda": "d",
             "max_expansions": 5
           }
         }
       }
     }' | grep title

      "_score" : 1.9162908, "_source" : {"title":"dzone"}
      "_score" : 1.9162908, "_source" : {"title":"data"}
      "_score" : 1.9162908, "_source" : {"title":"drunk"}
      "_score" : 1.9162908, "_source" : {"title":"drive"}

Conclusión

Por supuesto, una mejor precisión no es gratuita. La prebúsqueda provoca un viaje de ida y vuelta adicional entre los shards, lo que podría causar una caída en el rendimiento dependiendo del tamaño del índice, la cantidad de shards, la tasa de búsqueda, etcétera. Y en la mayoría de los casos, es totalmente innecesario... tener "suficientes" datos resuelve el problema por ti.

Pero a veces te encontrarás con situaciones de puntuación extrañas y, en esos casos, es útil saber cómo ajustar el plan de ejecución de búsqueda con DFS Query then Fetch.