Agregaciones y búsquedas en elasticsearch

Elasticsearch

Sabemos que todas las agregaciones se ejecutan en el contexto de una búsqueda y además si queremos podemos utilizar la misma query de agregaciones para devolver una muestra de documentos utilizados para calcular dicha agregación.

Este comportamiento lo podemos modificar. Vamos a ver como.

Devolver los documentos de una query y sobre ellos aplicar una serie de agregaciones.

Esto devuelve una respuesta con:

  • documentos que cumplen la query
  • las agregaciones aplicadas a los documentos resultado de la query

Por ejemplo

GET .../_search
{
    "size" : 10,
    "query" : {
        ...
    },
    "aggs" : {
        "nombre_agg": {
            ...
        }
    }
}

Devolver los documentos de una query y aplicar un filtrado a los documentos antes de aplicar las agregaciones

Esto devuelve una respuesta con:

  • documentos que cumplen la query
  • las agregaciones aplicadas a los documentos filtrados sobre el resultado de la query

Por ejemplo

GET .../_search
{
   "size" : 10,
   "query":{
      ...
   },
   "aggs":{
      "filtro": {
         "filter": { 
            ...
         },
         "aggs": {
            "nombre_agg":{
               ...
            }
         }
      }
   }
}

Devolver los documentos de una query y aplicar a esos documentos un filtro, pero generar las agregaciones sobre los documentos devueltos por la query

Esto devuelve una respuesta con:

  • documentos filtrados que han cumplido la query
  • las agregaciones aplicadas sobre los documentos devueltos por la query

Por ejemplo

GET .../_search
{
    "size" : 10,
    "query": {
        ...
    },
    "post_filter": {    
        "term" : {
        ...
        }
    },
    "aggs" : {
        "nombre_agg": {
            ...
        }
    }
}

elasticsearch Cookbook, similitud con SQL para las búsquedas IS NULL e IS NOT NULL

Elasticsearch

En elasticsearch también podemos realizar búsquedas preguntando por la existencia de un campo, es decir, podemos preguntarle si un campo tiene algún valor en alguno de sus documentos o si no por el contrario no tiene ningún valor.

Si hablamos en términos de SQL estaríamos hablando de sentencias como

  • Select * from table_name where column_1 IS NOT NULL;
  • Select * from table_name where column_1 IS NULL;

Ejemplo de ambos tipos de búsqueda utilizando el API Rest

La funcionalidad que buscamos la conseguimos aplicando filtros. En el ejemplo siguiente vamos a buscar todos los artículos del índice loquemeinteresadelared donde su título contenga ciertas palabras, y donde el campo autor tenga al menos un valor (IS NOT NULL) y donde el campo colaborador no tenga ningún valor (IS NULL).

curl -XPOST 'http://localhost:9200/loquemeinteresadelared/articulo/_search?pretty' -d '{
    "query" : {
      "filtered" : {
          "query": {
          "match": {
              "titulo": "elasticsearch cookbook"
            }
          }, 
        "filter" : {
          "bool" : {
                    "must" : [
                        {"exists" : { "field" : "autor" }},
                        {"missing" : { "field" : "colaborador" }}
                    ]
                }
          }
    }
  }
}'

El API Java de scan y scroll de elasticsearch

Elasticsearch

En el anterior artículo hemos visto las principales características del API Rest de scan y scroll de elasticsearch así como su utilización.

En este artículo vamos a ver cómo se utiliza el cliente Java para realizar lo mismo, sin entrar en detalles, ya que la descripción y característica son las mismas que el API Rest.

Fragmentos de código de utilización del API Java de Scan/scroll

public void iniciarScanScroll(String query) throws Exception {
    String scrollId = elasticSearchManagerImpl.prepararScanScroll(query, "nombre_campo");
    SearchResponse resp = elasticSearchManagerImpl.buscarScanScroll(scrollId);
    while (resp != null) {
        for (SearchHit hit : resp.getHits().getHits()) {
            /* Procesar los documentos */
        }
        resp = elasticSearchManagerImpl.buscarScanScroll(resp.getScrollId());
    }           
}


public String prepararScanScroll(String query, String field) {
    SearchResponse resp = client.prepareSearch(NOMBRE_INDICE)
        .setTypes(TIPO_DOCUMENTO)
        .setSearchType(SearchType.SCAN)
        .setScroll(new TimeValue(1000))         
        .setSize(500)
        .setQuery(QueryBuilders.queryStringQuery(query).field(field))
        .execute()
        .actionGet();
    return resp.getScrollId();
}

public SearchResponse buscarScanScroll(String scrollId) {
    SearchResponse resp = client.prepareSearchScroll(scrollId).setScroll(new TimeValue(1000)).execute().actionGet();        
    if (resp.getHits().getHits().length == 0) {
        return null;
    }
    return resp;
}

Donde,

  • client, es el cliente Java de elasticsearch
  • iniciarScanScroll, bucle principal donde iniciar y procesar el scan/scroll
  • prepararScanScroll, preparación del scan/scroll
  • buscarScanScroll, recuperación de cada una de las páginas

El API Rest de scan y scroll de elasticsearch

Elasticsearch

El tipo de búsqueda scan junto con el API de scroll nos permite :

  • Recuperar gran cantidad de documentos de forma eficiente
  • Realizar paginación o scroll sobre los resultados

Además,

  • este API toma un snapshot en el momento iniciar la primera búsqueda, de forma que aunque se modifique el índice, no afectará a los resultados de la búsqueda (ni a la primera página, ni a las siguientes)
  • en cada página o scroll se devuelve un nuevo scroll_id, que debe de ser pasado en las siguientes llamadas de este API
  • en cada página o scroll le indicamos un intervalo de tiempo, que será el tiempo máximo que tardaremos en procesar cada página
  • inicialmente indicaremos un tamaño de página, el cual como sabemos, será el resultado de multiplicar (ese tamaño) * (número de shards del íncide)

La forma de proceder es la siguiente

  • Se hace la busqueda de tipo scan
  • Se recupera el scroll_id
  • Se pide al API de scroll el primer bloque de documentos pasándole el scroll_id y nos guardamos el nuevo scroll_id generado
  • Se procesan los documentos
  • Se pide al API de scroll el siguiente bloque de documentos pasándole el nuevo scroll_id.
  • etc

Su similitud en una base de datos relacional sería un cursor.

Iniciamos el scan scroll

Petición

curl -XPOST 'http://localhost:9200/usuarios/_search?search_type=scan&scroll=5m&size=250' -d '{
  "query": {
    "query_string": {
      "default_field": "nombre",
      "query": "garcia"
    }
  }
}'

Respuesta

{
   "_scroll_id": "c2Nhbjs1OzkyOmEyalU1TDRIUTFxeDNwODhVUzlXX3c7OTA6YTJqVTVMNEhRMXF4M3A4OFVTOVdfdzs5MTphMmpVNUw0SFExcXgzcDg4VVM5V193Ozk0OmEyalU1TDRIUTFxeDNwODhVUzlXX3c7OTM6YTJqVTVMNEhRMXF4M3A4OFVTOVdfdzsxO3RvdGFsX2hpdHM6NjQ3Mjs=",
   "took": 30,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 1567,
      "max_score": 0,
      "hits": []
   }
}

Como vemos en la respuesta no se devuelve ningún documento. Solo se informa del total de documentos que cumpleten la query y el scroll_id que debemos utilizar para pedir el primer batch o página de documentos.

Si tenemos un índice con 5 shards, el número total de documentos devueltos en cada página es de 5 * 250 = 1250 documentos

Primera página y resto de páginas

Petición

GET _search/scroll?scroll=1m&scroll_id=c2Nhbjs1OzE2NTphMmpVNUw0SFExcXgzcDg4VVM5V193OzE2MzphMmpVNUw0SFExcXgzcDg4VVM5V193OzE2NzphMmpVNUw0SFExcXgzcDg4VVM5V193OzE2NDphMmpVNUw0SFExcXgzcDg4VVM5V193OzE2NjphMmpVNUw0SFExcXgzcDg4VVM5V193OzE7dG90YWxfaGl0czo2NDcyOw==

Respuesta

{
   "_scroll_id": "c2NhbjswOzE7dG90YWxfaGl0czo2NDcyOw==",
   "took": 11,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 1567,
      "max_score": 0,
      "hits": [
         {
            ...
         }
      ]
   }
}

Última página

Petición

GET _search/scroll?scroll=1m&scroll_id=c2NhbjswOzE7dG90YWxfaGl0czo2NDcyOw==

Respuesta

{
   "_scroll_id": "c2NhbjswOzE7dG90YWxfaGl0czo2NDcyOw==",
   "took": 0,
   "timed_out": false,
   "_shards": {
      "total": 0,
      "successful": 0,
      "failed": 0
   },
   "hits": {
      "total": 1567,
      "max_score": 0,
      "hits": []
   }
}

elasticsearch Cookbook, ver los términos generados por un analizador utilizando el API Java

Elasticsearch

Ya hemos visto en otro artículo cómo obtener los términos de un analizador utilizando el API Rest. Ahora vamos a ver lo mismo pero utilizando el API Java.

Como sabemos podemos obtener los términos generados por un analizador de dos formas distintas

  • invocando al API indicándole el nombre del analizador
  • invocando al API indicándole un nombre del campo para que utilice el analizador asociado a ese campo

En este artículo vamos a ver las dos opciones.

Invocando al API indicándole el nombre del analizador

public List<String> analyzer(String texto, String analyzer) {
    AnalyzeResponse aresp = client.admin().indices().analyze(new AnalyzeRequest(INDEX_NAME, texto).analyzer(analyzer)).actionGet();
    List<String> terms = new ArrayList<String>();
    if (aresp != null && aresp.getTokens() != null) {
        aresp.getTokens().forEach(token -> {
            terms.add(token.getTerm());
        });
    }
    return terms;
}

Invocando al API indicándole un nombre del campo

public List<String> analyzerByField(String texto, String field) {
    AnalyzeResponse aresp = client.admin().indices().analyze(new AnalyzeRequest(INDEX_NAME, texto).field(field)).actionGet();
    List<String> terms = new ArrayList<String>();
    if (aresp != null && aresp.getTokens() != null) {
        aresp.getTokens().forEach(token -> {
            terms.add(token.getTerm());
        });
    }
    return terms;
}

En ambos casos

  • client, es un objeto de la clase org.elasticsearch.client.Client obtenido utilizando el API Java de elasticsearch. Tanto con la clase NodeBuilder como TransportClient podemos obtener el cliente de acceso a elasticsearch.

    No vamos a ver como obtener este objeto client. Esperamos hacerlo en otro artículo que publicaremos más adelante.

elasticsearch Cookbook, obtener todos los valores diferentes de un campo

Elasticsearch

Otro ejemplo de utilización de agregaciones en elasticsearch. La similitud con una base de datos relacional sería la query

SELECT 
    DISTINCT(column_name)
FROM 
    table_name;

Y en elasticsearch podemos conseguir lo mismo utilizando la agregación terms. Esta agregación devuelve por defecto los 10 primeros términos más repetidos, ordenados por el número de veces que aparecen.

Si el campo donde vamos a aplicar la agregación es un campo de tipo string not_analyzed, obtendremos como resultado todos los valores distintos posibles para ese campo. Lo mismo que conseguiríamos con la sentencia SELECT DISTINCT(column_name) FROM table_name; en una base de datos relacional.

Si el campo es analizado, obtendremos todos los términos diferentes que existen en ese campo.

Este tipo de agregación podemos parametrizarlo.

  • Podemos cambiar el tipo de ordenación de los datos devueltos
  • Podemos cambiar el número de terminos devueltos en el resultado
  • Podemos cambiar el comportamiento del operador cuando tenemos varios shards, para minimizar el problema de que no siempre se devuelven los más repetidos. En ocasiones al tener multiples shards el operador no siempre va a devolver los más repetidos
  • Podemos cambiar la ordenación utilizando una segunda agregación de tipo estadísica y ordenar por uno de sus valores
  • etc

Vamos a verlo con un ejemplo.

Creación del índice y alta de nuevos documentos

POST usuarios
POST usuarios/_mapping/perfil
{
"properties": {
  "nombre": {
    "type": "string"
  },
  "name": {
    "type": "string",
    "index": "not_analyzed"
  }
}
}
POST usuarios/perfil
{
  "nombre":"michael jackson",
  "name":"michael jackson"
}
POST usuarios/perfil
{
  "nombre":"michael jordan",
  "name":"michael jordan"
}
POST usuarios/perfil
{
  "nombre":"michael johnson",
  "name":"michael johnson"
}

API Rest en un campo de tipo string analyzed

GET usuarios/perfil/_search?search_type=count
{
  "aggs": {
    "nombres_diferentes": {
      "terms": {
        "field": "nombre"
      }
    }
  }
}

# Respuesta
{
   "took": 3,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 3,
      "max_score": 0,
      "hits": []
   },
   "aggregations": {
      "nombres_diferentes": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "michael",
               "doc_count": 3
            },
            {
               "key": "jackson",
               "doc_count": 1
            },
            {
               "key": "johnson",
               "doc_count": 1
            },
            {
               "key": "jordan",
               "doc_count": 1
            }
         ]
      }
   }
}

API Rest en un campo de tipo string not_analyzed

GET usuarios/perfil/_search?search_type=count
{
  "aggs": {
    "nombres_diferentes": {
      "terms": {
        "field": "name"
      }
    }
  }
}

# Respuesta
{
   "took": 2,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 3,
      "max_score": 0,
      "hits": []
   },
   "aggregations": {
      "nombres_diferentes": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "michael jackson",
               "doc_count": 1
            },
            {
               "key": "michael johnson",
               "doc_count": 1
            },
            {
               "key": "michael jordan",
               "doc_count": 1
            }
         ]
      }
   }
}

Ejemplo de API Java

public void searchDistinct(String field, Integer size) {
public List<String> searchDistinct(String field, Integer size) {
    SearchResponse resp = client.prepareSearch()
        .setSearchType(SearchType.COUNT)
        .addAggregation(AggregationBuilders.terms("nombres_diferentes").field(field).size(size))
        .execute()
        .actionGet();
    return ((MultiBucketsAggregation)resp.getAggregations().get("nombres_diferentes")).getBuckets()
        .stream()
        .map((bucket) -> {
            return bucket.getKey();
        }
    ).collect(Collectors.toList());
}

NOTA. Todos los comandos de este artículo del tipo API Rest (comandos POST, GET, etc), son comandos que podemos lanzar directamente utilizando la interface Marvel de elasticsearch. Si no utilizamos Marvel, podemos sustituirlo por el comando de unix curl, tal y como lo hemos visto en otros artículos de la serie elasticsearch Cookbook de este blog.

elasticsearch Cookbook, obtener la media, desviación estándar y la varianza de los valores de un campo

Elasticsearch

Tenemos una forma directa y fácil de conseguirlo. Utilizaremos agregaciones, concretamente la agregación extended_stats. Solo tenemos que indicar el nombre de la agregaciób y el campo sobre el que necesitamos obtener estos valores.

En el ejemplo, hemos dado de alta 10 documentos, con los valores del campo valor a 1, 2, 3, 4, 5, 6, 7, 8, 9 y 10. Los valores que nos proporciona esta agregación son los siguientes:

  • count: 10, es el número total de documentos o valores analizados.
  • min: 1, el valor mínimo encontrado
  • max: 10, el valor máximo encontrado
  • avg: 5.5,, la media
  • sum: 55, la suma total de todos los valores
  • sum_of_squares: 385, la suma total de todos los valores al cuadrado
  • variance: 8.25, la varianza
  • std_deviation: 2.8722813232690143, la desviación estandar
  • std_deviation_bounds: son límites por encima y debajo de los valores devueltos para poder representarlos correctamente, en, por ejemplo, una gráfica

Relación entre varianza y desviación estándar

La desviación estándar sirve para identificar de una muestra de datos, cuales son los valores normales y los que sobresalen por encima o por debajo. Es decir, dada la media de la muestra, los valores comprendidos entre (la media + desviación estándar) y (la media – desviación estándar) son los valores normales, y los que se encuentren fuera de ese rango son los valores menos normales de la muestra. Así que nos informa de como se separan los valores respecto a toda la muestra.

La desviación estándar es la raíz cuadrada de la varianza. Y la varianza es la media de las diferencias con la media elevadas al cuadrado.

Por ejemplo, para los valores de la muestra 3, 7 y 8 vamos a calcular cada uno de estos valores

Media
media = (3+7+8)/3 = 6

Varianza
varianza = ((3 – 6)^2 + (7 – 6)^2 + (8 – 6)^2)/3 = (-3^2 + 1^2 + 2^2)/3 = (9 + 1 + 4)/3 = 4.66

Desviación estádar
desviación estándar= Raiz Cuadrada(4.66)= 2.15

Utilización de la agregación extended_stats

Ejemplo API Rest

POST pruebas/numeros/_search?search_type=count
{
  "aggs": {
    "estadisticas": {
      "extended_stats": {
        "field": "valor"
      }
    }
  }
}

Respuesta

{
   "took": 1,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 10,
      "max_score": 0,
      "hits": []
   },
   "aggregations": {
      "estadisticas": {
         "count": 10,
         "min": 1,
         "max": 10,
         "avg": 5.5,
         "sum": 55,
         "sum_of_squares": 385,
         "variance": 8.25,
         "std_deviation": 2.8722813232690143,
         "std_deviation_bounds": {
            "upper": 11.244562646538029,
            "lower": -0.24456264653802862
         }
      }
   }
}