1. Introducción

Elasticsearch es un motor de búsqueda basado en Lucene escrito en Java por la gente de elastic. Al igual que MongoDB es un almacen de datos NoSQL basado en documentos. Provee de un motor de búsqueda de texto completo y tiene una api RESTful con la que vamos a interactuar en este post.

Entre sus carácterísticas principales destacamos:

  • Opensource
  • Api basada en REST
  • Soporta búsquedas de texto completo rápido y potente
  • Altamente escalable
  • Pensado para entornos cloud y big data

2. Intalación en Docker

La instalación de Elasticsearch la haremos sobre Docker haciendo uso de docker-compose. Si no tienes Docker instalado, podrás ver cómo harcerlo en el post SpringBoot – Docker.

Utilizamos la versión 2.4.0 de Elasticsearch por ser la última estable que es compatible con Spring Data para la versión 1.5.2 de Spring Boot. Aquí podrán encontrar las compatibilidades entre las versiones de Spring Boot, Spring Cloud y Elasticsearch.

elasticsearch:
  image: elasticsearch:2.4.0
  command: elasticsearch
  ports:
    - "9200:9200"    
    - "9300:9300"

Arrancamos nuestro contenedor docker a través de docker-compose

docker-compose up

3. Creación índices, tipos e inserción de documentos

En elasticsearch no insertamos los documentos sobre bases de datos. Lo hacemos sobre índices. A su vez la inserción sobre éstos se puede hacer a través de tipos aunque no es obligatorio. En nuestro caso insertaremos los datos en

  • Índice: myindex
  • Tipo: user

Verificación de la instalación

El parámetro pretty es opcional y nos permite ver el resultado devuelto formateado. Es aplicable a todas las consultas que hagamos sobre el api Restful.

curl -X GET http://localhost:9200?pretty
{
  "name" : "Alfie O'Meggan",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.4.0",
    "build_hash" : "ce9f0c7394dee074091dd1bc4e9469251181fc55",
    "build_timestamp" : "2016-08-29T09:14:17Z",
    "build_snapshot" : false,
    "lucene_version" : "5.5.2"
  },
  "tagline" : "You Know, for Search"
}

Creación del índice myindex

curl -X PUT http://localhost:9200/myindex?pretty
{
  "acknowledged" : true
}

Borrar un índice

curl -X DELETE http://localhost:9200/myindex?pretty
{
  "acknowledged" : true
}

Inserción de documentos sobre un tipo
Insertaremos los mismos datos que utilizamos en el post sobre MongoDB. Las inserciones las haremos sobre el tipo user. La especificación de éste es opcional. Además se especifica el id del documento que se esta insertando.

curl -X PUT 'localhost:9200/myindex/user/1?pretty' -H 'Content-Type: application/json' -d'
{
  "name": "admin",
  "surname": "admin",
  "gender": "male",
  "money": 0,
  "roles": [
    "ROLE_ADMIN"
  ]
}'

curl -X PUT 'localhost:9200/myindex/user/2?pretty' -H 'Content-Type: application/json' -d'
{
  "name": "Jorge",
  "surname": "Hernández Ramírez",
  "gender": "male",
  "money": 1000,
  "roles": [
    "ROLE_ADMIN"
  ],
  "teams": [
    {
      "name": "UD.Las Palmas",
      "sport": "Football"
    },
    {
      "name": "Real Madrid",
      "sport": "Football"
    },
    {
      "name": "McLaren",
      "sport": "F1"
    }
  ]
}'

curl -X PUT 'localhost:9200/myindex/user/3?pretty' -H 'Content-Type: application/json' -d'
{
  "name": "Jose",
  "gender": "male",
  "surname": "Hernández Ramírez",
  "money": 2000,
  "roles": [
    "ROLE_USER"
  ],
  "teams": [
    {
      "name": "UD. Las Palmas",
      "sport": "Football"
    },
    {
      "name": "Magnus Carlsen",
      "sport": "Chess"
    }
  ]
}'

curl -X PUT 'localhost:9200/myindex/user/4?pretty' -H 'Content-Type: application/json' -d'
{
  "name": "Raul",
  "surname": "González Blanco",
  "gender": "male",
  "money": 200000,
  "roles": [
    "ROLE_USER"
  ],
  "teams": [
    {
      "name": "Real Madrid",
      "sport": "Football"
    },
    {
      "name": "Real Madrid",
      "sport": "Basketball"
    }
  ]
}'

curl -X PUT 'localhost:9200/myindex/user/5?pretty' -H 'Content-Type: application/json' -d'
{
  "name": "Constanza",
  "surname": "Ramírez Rodríguez",
  "gender": "female",
  "money": 500,
  "roles": [
    "ROLE_USER"
  ],
  "teams": [
    {
      "name": "UD. Las Palmas",
      "sport": "Football"
    }
  ]
}'

Para verificar que se han insertado correctamente podemos hacer un findAll

curl -X GET 'localhost:9200/myindex/user/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "match_all": {}
    }
}
'

4. Realizando búsquedas

En Elasticsearch hay diferentes formas de realizar búsquedas, podemos básicamente buscar por términos, por coincidencia o realizando querys.

4.1 Term

Buscamos en un atributo un término que debe ser una palabra sin espacios.

Buscar aquellos documentos que sean mujeres

curl -X GET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "term" : {
            "gender" : "female"
        }
    }
}
'
{
  "took" : 11,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.30685282,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "5",
      "_score" : 0.30685282,
      "_source" : {
        "name" : "Constanza",
        "surname" : "Ramírez Rodríguez",
        "gender" : "female",
        "money" : 500,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        } ]
      }
    } ]
  }
}

4.2 Match

Las búsquedas se realizan sobre un campo. Obtiene los documentos que contengan alguna de las palabras indicadas. Permite realizar búsquedas de palabras sobre textos.

Obtener aquellos documentos cuyo surname sea Hernández o Ramírez

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "match" : {
            "surname" : "Hernández Ramírez"
        }
    }
}
'
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.8838835,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "2",
      "_score" : 0.8838835,
      "_source" : {
        "name" : "Jorge",
        "surname" : "Hernández Ramírez",
        "gender" : "male",
        "money" : 1000,
        "roles" : [ "ROLE_ADMIN" ],
        "teams" : [ {
          "name" : "UD.Las Palmas",
          "sport" : "Football"
        }, {
          "name" : "Real Madrid",
          "sport" : "Football"
        }, {
          "name" : "McLaren",
          "sport" : "F1"
        } ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "3",
      "_score" : 0.2712221,
      "_source" : {
        "name" : "Jose",
        "gender" : "male",
        "surname" : "Hernández Ramírez",
        "money" : 2000,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        }, {
          "name" : "Magnus Carlsen",
          "sport" : "Chess"
        } ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "5",
      "_score" : 0.028130025,
      "_source" : {
        "name" : "Constanza",
        "surname" : "Ramírez Rodríguez",
        "gender" : "female",
        "money" : 500,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        } ]
      }
    } ]
  }
}

Obtener aquellos documentos cuyo surname sea Hernández y Ramírez

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "match" : {
            "surname" : {
            	"query": "Hernández Ramírez",
            	"operator" : "and"
            }
        }
    }
}
'
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.8838835,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "2",
      "_score" : 0.8838835,
      "_source" : {
        "name" : "Jorge",
        "surname" : "Hernández Ramírez",
        "gender" : "male",
        "money" : 1000,
        "roles" : [ "ROLE_ADMIN" ],
        "teams" : [ {
          "name" : "UD.Las Palmas",
          "sport" : "Football"
        }, {
          "name" : "Real Madrid",
          "sport" : "Football"
        }, {
          "name" : "McLaren",
          "sport" : "F1"
        } ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "3",
      "_score" : 0.2712221,
      "_source" : {
        "name" : "Jose",
        "gender" : "male",
        "surname" : "Hernández Ramírez",
        "money" : 2000,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        }, {
          "name" : "Magnus Carlsen",
          "sport" : "Chess"
        } ]
      }
    } ]
  }
}

4.3 Query

Permite hacer búsquedas o querys sobre diferentes campos. Podemos indicar el tipo de operador que queremos especificar en cada caso. Debemos indicar el atributo y el texto de buscar en cada nodo de la query.

Buscar aquellos elementos cuyo surname contenga Hernández o Ramírez y sea mujer

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "query_string" : {
            "query" : "(surname:Herández OR surname:Ramírez) AND gender:female"
        }
    }
}
'
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.11336874,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "5",
      "_score" : 0.11336874,
      "_source" : {
        "name" : "Constanza",
        "surname" : "Ramírez Rodríguez",
        "gender" : "female",
        "money" : 500,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        } ]
      }
    } ]
  }
}

Indicando un atributo por defecto
Si no se indica el atributo por el que se va a buscar se utiliza el que indicamos por defecto. Si no existe un atributo por defecto se busca en todos los campos del documento.

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "query": {
        "query_string" : {
            "default_field" : "surname",
            "query" : "Herández OR Ramírez AND gender:female"
        }
    }
}
'
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.093574196,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "5",
      "_score" : 0.093574196,
      "_source" : {
        "name" : "Constanza",
        "surname" : "Ramírez Rodríguez",
        "gender" : "female",
        "money" : 500,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        } ]
      }
    } ]
  }
}

4.4 Bool

Hemos visto hasta ahora la forma de realizar búsquedas a través de term, match y query. Si queremos combinar varias de estas alternativas deberemos utilizar la etiqueta bool seguido del operador a aplicar para cada una de ellas el cual se especificará a continuación como:

  • must. Indica que las búsquedas que se especifiquen les aplicará el operador AND
  • should. Indica que las búsquedas que se especifiquen les aplicará el operador OR
  • not must. Indica que las búsquedas que se especifiquen les aplicará el operador NOT

Obtener los documentos que tengan como apellido Ramírez y sean mujeres

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool" : {
      "must" : [
      	{"term" : { "gender" : "female" }},
      	{"match" : { "surname" : "Ramírez" }}
      ]
    }
  }
}
'
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.35258877,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "5",
      "_score" : 0.35258877,
      "_source" : {
        "name" : "Constanza",
        "surname" : "Ramírez Rodríguez",
        "gender" : "female",
        "money" : 500,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        } ]
      }
    } ]
  }
}

Obtener los documentos que tengan como nombre a Jorge o Jose
Podríamos utilizar la equiqueta match, sin embargo en este caso hacemos uso de query_string

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool" : {
      "should" : [
      	{"query_string" : { "query" : "name:Jose" }},
      	{"query_string" : { "query" : "name:Jorge" }}
      ]
    }
  }
}
'
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.25427115,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "2",
      "_score" : 0.25427115,
      "_source" : {
        "name" : "Jorge",
        "surname" : "Hernández Ramírez",
        "gender" : "male",
        "money" : 1000,
        "roles" : [ "ROLE_ADMIN" ],
        "teams" : [ {
          "name" : "UD.Las Palmas",
          "sport" : "Football"
        }, {
          "name" : "Real Madrid",
          "sport" : "Football"
        }, {
          "name" : "McLaren",
          "sport" : "F1"
        } ]
      }
    }, {
      "_index" : "myindex",
      "_type" : "user",
      "_id" : "3",
      "_score" : 0.04500804,
      "_source" : {
        "name" : "Jose",
        "gender" : "male",
        "surname" : "Hernández Ramírez",
        "money" : 2000,
        "roles" : [ "ROLE_USER" ],
        "teams" : [ {
          "name" : "UD. Las Palmas",
          "sport" : "Football"
        }, {
          "name" : "Magnus Carlsen",
          "sport" : "Chess"
        } ]
      }
    } ]
  }
}

4.5 Agregaciones

Obtener por género el número de documentos existentes

curl -XGET 'localhost:9200/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "size" : 0,
    "aggs" : {
        "group_by_gender" : {
            "terms" : { "field" : "gender" }
        }
    }
}
'
{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 10,
    "successful" : 10,
    "failed" : 0
  },
  "hits" : {
    "total" : 5,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "group_by_gender" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "male",
        "doc_count" : 4
      }, {
        "key" : "female",
        "doc_count" : 1
      } ]
    }
  }
}

Podemos utilizar una búsqueda previa

curl -XGET 'localhost:9200/_search?pretty' -H 'Content-Type: application/json' -d'
{
    "size" : 0,
    "query": {
       "match": {"surname": "Ramírez"}
    },
    "aggs" : {
        "group_by_gender" : {
            "terms" : { "field" : "gender" }
        }
    }
}
'
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 10,
    "successful" : 10,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "group_by_gender" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "male",
        "doc_count" : 2
      }, {
        "key" : "female",
        "doc_count" : 1
      } ]
    }
  }
}

Obtener por género el número de documentos que hay así como la suma y la media de su dinero

curl -XGET 'localhost:9200/myindex/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "group_by_product": {
      "terms": {
        "field": "gender"
      },
      "aggs": {
        "average_number": {
          "avg": {
            "field": "money"
          }
        },
        "sum_number": {
          "sum": {
            "field": "money"
          }
        }
      }
    }
  }
}
'
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 5,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "group_by_product" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "male",
        "doc_count" : 4,
        "sum_number" : {
          "value" : 203000.0
        },
        "average_number" : {
          "value" : 50750.0
        }
      }, {
        "key" : "female",
        "doc_count" : 1,
        "sum_number" : {
          "value" : 500.0
        },
        "average_number" : {
          "value" : 500.0
        }
      } ]
    }
  }
}

5. Referencias