martes, 1 de abril de 2014

Búsquedas en Elasticsearch

En este post voy a hacer una recopilación de algunas querys más o menos complejas sobre un índice de Elasticsearch.
En primer lugar, el mapping usado (ficticio) será el siguiente, con un tipo película donde tenemos varios campos de distintos tipos: cadenas de caracteres, números, un array de enteros y otro de objetos. La generación del mapping, modificada, se realizó a partir del índice de un modelo en Rails, como explico en el post sobre integrar Elasticsearch en Ruby.

{
  "pelicula":{
    "index_analyzer":"default_index",
    "dynamic_templates":[
      {
        "string_template":{
          "mapping":{
            "type":"multi_field",
            "fields":{
              "analyzed":{
                "index":"analyzed",
                "type":"string"
              },
              "{name}":{
                "index":"not_analyzed",
                "type":"string"
              }
            }
          },
          "match":"*",
          "match_mapping_type":"string"
        }
      }
    ],
    "properties":{
      "presupuesto":{
        "type":"float"
      },
      "imdb":{
        "type":"string",
        "fields":{
          "imdb_sort":{
            "type":"string",
            "index":"not_analyzed"
          }
        }
      },
      "categoria":{
        "type":"string",
        "analyzer":"keyword"
      },
      "duracion":{
        "type":"long"
      },
      "bn":{
        "type":"boolean"
      },
      "actores":{
        "type":"long",
        "index_name":"actor"
      },
      "titulo":{
        "type":"string",
        "fields":{
          "titulo_sort":{
            "type":"string",
            "index":"not_analyzed"
          }
        }
      },
      "premios":{
        "type":"nested",
        "properties":{
          "fecha":{
            "type":"date",
            "format":"dateOptionalTime"
          },
          "actor":{
            "type":"long"
          },
          "nombre":{
            "type":"string",
            "analyzer":"keyword"
          }
        }
      }
    }
  }
}

Un par de comentarios sobre el mapping.
En los campos tipo String sobre los que vamos a tener que realizar operaciones tanto de filtrado como de ordenación, tendremos que convertir el campo en un multifield, de modo que se duplique. Así, la ordenación se realizará sobre el campo not analyzed, y el filtrado sobre el analyzed.
También otra característica de los campos String es que elasticsearch realizará un análisis stemming dependiendo del idioma. En algunos campos querremos evitarlo, como en identificadores tipo cadena. Para ello usaremos el analyzer tipo keyword.



Generalmente las pruebas las hago mediante el plugin Sense de Chrome, bastante útil para esto. Ahora pondré los distintos tipos de consultas que he hecho sobre un conjunto de datos con un mapeo similar al indicado arriba.

Filtro con rango y elemento en array


En esta query filtraremos por un rango de valores de uno de los campos, y además queremos que el array contenga un valor.

GET _search
{
  "query":{
    "filtered":{
      "filter":{
        "and":{
          "filters":[
            {
              "range":{
                "duracion":{
                  "gte":"120",
                  "lte":"180"
                }
              }
            },
            {
              "query":{
                "term":{
                  "actores":154455
                }
              }
            }
          ]
        }
      }
    }
  },
  "size":30,
  "from":0,
  "sort":{
    "_score":"desc"
  }
}

Como resultado, obtendremos todas las películas en las que esté incluido el actor 154455 (ej. Charlton Heston) y que tengan un rango de duración entre 2 y 3 horas.

Filtro anidado


En esta query incluyo un nested filter sobre el array de objetos, buscando un valor de un campo dentro de estos objetos anidados:

{
  "query":{
    "nested":{
      "path":"premios",
      "query":{
        "query_string":{
          "default_field":"actor",
          "query":154455
        }
      }
    }
  },
  "size":10,
  "from":0,
  "sort":{
    "_score":"desc"
  }
}

Mediante esta consulta, obtendremos las películas en las que Charlton Heston ha obtenido algún premio.

Script de ordenación

Debido a la estructura que tenemos, en ocasiones puede que la ordenación se complique. Para ello, podemos usar un script haciendo una ordenación personalizada.

GET _search
{
  "query":{
      "match_all": {}
      },
  "size":20,
  "from":0,
  "sort":{
    "_script":{
      "script":"score=(doc.containsKey('presupuesto') && !doc['presupuesto'].empty && !doc['presupuesto'].value == 0)?doc['presupuesto'].value:999999;for (p: _source.premios){if (p.actor == 154455) {return (p.nombre == 'Oscar')? 9999999:score} } return 9999999",
      "type":"number",
      "order":"asc",
      "missing" : "_last"
    }
  }
}

Este script, bastante enrevesado, ordenará por presupuesto de mayor a menor las películas en las que Charlton Heston ha ganado un Oscar. Como es posible que no exista el dato de presupuesto en alguno de los casos, lo primero es comprobar que existe. Si no, le daremos un valor alto para que aparezcan al final. Si existe, recorremos los premios de la película, comprobamos que haya sido recibido por el actor, y si es un Oscar ordenamos por presupuesto; si no, lo ponemos al final.

Elementos en un array

En esta query queremos filtrar por un campo cuyo valor está dentro de un array dado:

GET _search
{
  "query":{
    "filtered":{
      "filter":{
          "terms":{
            "categoria":
              [
                "accion",
                "suspense",
                "aventuras"
              ]
        }
      }
    }
  },
  "size":30,
  "from":0,
  "sort":{
    "_score":"desc"
  }
}

Esto nos dará las películas cuya categoría esté entre las tres que hemos filtrado.

Agrupaciones / Facets

Para finalizar, mostramos un ejemplo de uso de facets sobre nuestro índice.

GET _search
{
  "query":{
    "match_all":{

    }
  },
  "size":1,
  "from":0,
  "facets":{
    "presupuesto":{
      "range":{
        "presupuesto":[
          {
            "to":1
          },
          {
            "from":1,
            "to":500000
          },
          {
            "from":500001,
            "to":1000000
          },
          {
            "from":1000001,
            "to":50000000
          },
          {
            "from":50000001
          }
        ]
      }
      
    }
  }
}


Con esto vamos a agrupar las películas según un rango de presupuesto, desde las más modestas a las superproducciones. En el resultado, veremos las estadísticas de cada rango, de una forma similar a esta:

{
  "from": 50000001,
  "count": 7,
  "min": 56108384,
  "max": 11050023936,
  "total_count": 7,
  "total": 23336485464,
  "mean": 3333783637.714286
}

Podría ser que quisiéramos sacar estos datos sólo para las películas de un determinado actor. Para filtrar los facets, usaremos la instrucción facet_filter, de modo que se limiten las estadísticas a un conjuntio determinado:

"facet_filter":{
  "term":{
    "actor":154455
  }
}

Y así, veremos sólo las estadísticas de presupuesto de las películas de Charlton Heston.