Fundamentos de Elasticsearch

Elasticsearch — un motor de búsqueda con API REST en JSON, que utiliza Lucene y está escrito en Java. La descripción de todas las ventajas de este motor está disponible en el sitio web oficial. A partir de ahora, nos referiremos a Elasticsearch como ES.

Motores similares se utilizan para búsquedas complejas en bases de documentos. Por ejemplo, búsqueda teniendo en cuenta la morfología del idioma o búsqueda por coordenadas geográficas.

En este artículo, hablaré sobre los fundamentos de ES usando como ejemplo la indexación de publicaciones en un blog. Mostraré cómo filtrar, clasificar y buscar documentos.

Para no depender del sistema operativo, haré todas las solicitudes a ES utilizando CURL. También hay un plugin para Google Chrome llamado sense.

A lo largo del texto, se incluyen enlaces a la documentación y otras fuentes. Al final, hay enlaces para acceso rápido a la documentación. Las definiciones de términos desconocidos se pueden leer en el glosario.

Instalación de ES

Para esto, primero necesitaremos Java. Los desarrolladores recomiendan deben instalar versiones de Java más nuevas que Java 8 update 20 o Java 7 update 55.

El paquete de ES está disponible en el sitio del desarrollador. Después de descomprimir el archivo, debemos iniciar bin/elasticsearch. También están disponibles paquetes para apt y yum. Hay una imagen oficial para docker. Más detalles sobre la instalación.

Después de instalar y ejecutar, verifiquemos su funcionamiento:

# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

Recibiremos una respuesta aproximada como esta:

{
  "name" : "Heimdall",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.2.1",
    "build_hash" : "d045fc29d1932bce18b2e65ab8b297fbf6cd41a1",
    "build_timestamp" : "2016-03-09T09:38:54Z",
    "build_snapshot" : false,
    "lucene_version" : "5.4.1"
  },
  "tagline" : "You Know, for Search"
}

Indexación

Agreguemos una publicación a ES:

# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Una historia divertida sobre gatitos<p>",
  "tags": [
    "gatitos",
    "historia divertida"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

respuesta del servidor:

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : false
}

ES creó automáticamente el índice blog y tipo post. Se puede hacer una analogía condicional: un índice es una base de datos, y un tipo es una tabla en esa base de datos. Cada tipo tiene su propio esquema — mapeo, al igual que una tabla relacional. El mapeo se genera automáticamente al indexar un documento:

# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"

En la respuesta del servidor, añadí en los comentarios los valores de los campos del documento indexado:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Una historia divertida sobre gatitos<p>", */ 
          "content" : {
            "type" : "string"
          },
          /* "published_at": "2014-09-12T20:44:42+00:00" */
          "published_at" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          /* "tags": ["котята", "смешная история"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Веселые котята" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Cabe mencionar que ES no hace distinción entre un valor único y un arreglo de valores. Por ejemplo, el campo title contiene simplemente un encabezado, mientras que el campo tags es un arreglo de cadenas, aunque se presentan en el mapeo de la misma manera.
Más adelante hablaremos sobre el mapeo de manera más detallada.

Consultas

Extracción de un documento por su id:

# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"
{
    "_index": "blog",
    "_type": "post",
    "_id": "1",
    "_version": 1,
    "found": true,
    "_source": {
        "title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>Una historia divertida sobre gatitos<p>",
        "tags": [
            "\u043a\u043e\u0442\u044f\u0442\u0430",
            "\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
        ],
        "published_at": "2014-09-12T20:44:42+00:00"
    }
}

En la respuesta aparecieron nuevas claves: _version y _source. En general, todas las claves que comienzan con _ son clave de sistema.

Clave _version muestra la versión del documento. Es necesario para el funcionamiento del mecanismo de bloqueos optimistas. Por ejemplo, queremos modificar un documento que tiene la versión 1. Enviamos el documento modificado e indicamos que es una edición del documento de versión 1. Si alguien más también ha editado el documento de versión 1 y ha enviado sus cambios antes que nosotros, ES no aceptará nuestros cambios, ya que almacena el documento de versión 2.

Clave _source contiene el documento que hemos indexado. ES no utiliza este valor para operaciones de búsqueda, ya que se utilizan índices para buscar. Para ahorrar espacio, ES almacena el documento original comprimido. Si solo necesitamos el id, y no el documento original completo, se puede desactivar el almacenamiento del original.

Si no necesitamos información adicional, podemos obtener solo el contenido de _source:

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
    "title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
    "content": "<p>Una historia divertida sobre gatitos<p>",
    "tags": [
        "\u043a\u043e\u0442\u044f\u0442\u0430",
        "\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
    ],
    "published_at": "2014-09-12T20:44:42+00:00"
}

También podemos seleccionar solo ciertos campos:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Gatitos felices"
  }
}

Vamos a indexar algunos posts más y realizar consultas más complejas.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Perros divertidos",
  "content": "<p>Una historia divertida sobre perros<p>"
  "tags": [
    "perros",
    "historia divertida"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Cómo llegó mi gatito",
  "content": "<p>Una historia desgarradora sobre un pobre gatito de la calle<p>"
  "tags": [
    "gatitos"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Ordenación

# найдем последний пост по дате публикации и извлечем поля title и published_at
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "size": 1,
  "_source": ["title", "published_at"],
  "sort": [{"published_at": "desc"}]
}'
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : null,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : null,
      "_source" : {
        "title" : "Gatitos felices",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Hemos seleccionado el último post. tamaño limita la cantidad de documentos en el resultado. total muestra el número total de documentos que coinciden con la consulta. sort en el resultado contiene un arreglo de números enteros, por los cuales se realiza la ordenación. Es decir, la fecha se ha convertido en un número entero. Puedes leer más sobre ordenación en la documentación.

Filtros y consultas

ES desde la versión 2 no distingue entre filtros y consultas, en su lugar se introduce el concepto de contextos.
El contexto de la consulta se diferencia del contexto del filtro en que la consulta genera _score y no se almacena en caché. ¿Qué es _score? Lo mostraré más adelante.

Filtración por fecha

Usamos la consulta range en el contexto filter:

# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "filter": {
    "range": {
      "published_at": { "gte": "2014-09-01" }
    }
  }
}'

Filtración por etiquetas

Usamos consulta de término para buscar ids de documentos que contengan la palabra dada:

# найдем все документы, в поле tags которых есть элемент 'котята'
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": [
    "title",
    "tags"
  ],
  "filter": {
    "term": {
      "tags": "котята"
    }
  }
}'
{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "Gatitos divertidos",
        "tags" : [ "gatitos", "historia divertida" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Cómo llegó a mí un gatito",
        "tags" : [ "gatitos" ]
      }
    } ]
  }
}

Búsqueda de texto completo

Nuestros tres documentos contienen lo siguiente en el campo de contenido:

  • <p>Una historia divertida sobre gatitos<p>
  • <p>Una historia divertida sobre perros<p>
  • <p>Una historia desgarradora sobre un pobre gatito de la calle<p>

Usamos consulta de coincidencia para buscar ids de documentos que contengan la palabra dada:

# source: false означает, что не нужно извлекать _source найденных документов
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": false,
  "query": {
    "match": {
      "content": "история"
    }
  }
}'
{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.11506981,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "2",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 0.095891505
    } ]
  }
}

Sin embargo, si buscamos "historias" en el campo de contenido, no encontraremos nada, ya que el índice solo contiene las palabras originales y no sus raíces. Para realizar una búsqueda de calidad, es necesario configurar el analizador.

Campo _score muestra relevancia. Si la consulta se realiza en el contexto del filtro, el valor de _score siempre será 1, lo que indica una coincidencia total con el filtro.

Los analizadores

Los analizadores son necesarios para transformar el texto original en un conjunto de tokens.
Los analizadores consisten en uno Tokenizador y varios opcionales Filtros de Token. El tokenizador puede preceder a varios Filtros de Caracteres. El tokenizador divide la cadena original en tokens, por ejemplo, por espacios y signos de puntuación. El filtro de tokens puede modificar, eliminar o agregar nuevos tokens, como dejar solo la raíz de la palabra, eliminar preposiciones, agregar sinónimos. Los filtros de caracteres transforman toda la cadena original, por ejemplo, eliminando etiquetas html.

En ES hay varios analizadores estándar. Por ejemplo, el analizador ruso.

Hagamos uso de api y veamos cómo los analizadores estándar y ruso transforman la cadena "Cuentos divertidos sobre gatitos":

# используем анализатор standard       
# обязательно нужно перекодировать не ASCII символы
curl -XGET "$ES_URL/_analyze?pretty&analyzer=standard&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"
{
  "tokens" : [ {
    "token" : "divertidos",
    "start_offset" : 0,
    "end_offset" : 9,
    "type" : "",
    "position" : 0
  }, {
    "token" : "historias",
    "start_offset" : 10,
    "end_offset" : 18,
    "type" : "",
    "position" : 1
  }, {
    "token" : "sobre",
    "start_offset" : 19,
    "end_offset" : 24,
    "type" : "",
    "position" : 2
  }, {
    "token" : "gatitos",
    "start_offset" : 25,
    "end_offset" : 32,
    "type" : "",
    "position" : 3
  } ]
}
# используем анализатор russian
curl -XGET "$ES_URL/_analyze?pretty&analyzer=russian&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"
{
  "tokens" : [ {
    "token" : "divertido",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "historia",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "gato",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

El analizador estándar separó la cadena por espacios y convirtió todo a minúsculas; el analizador ruso eliminó las palabras insignificantes, convirtió a minúsculas y mantuvo la raíz de las palabras.

Veamos qué Tokenizers, TokenFilters y CharFilters utiliza el analizador ruso:

{
  "filter": {
    "russian_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "russian_keywords": {
      "type":       "keyword_marker",
      "keywords":   []
    },
    "russian_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "russian": {
      "tokenizer":  "standard",
      
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      
    }
  }
}

Describamos nuestro analizador basado en ruso, que eliminará las etiquetas HTML. Lo llamaremos default, ya que un analizador con este nombre se usará por defecto.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Primero se eliminarán todas las etiquetas HTML de la cadena original, luego se separará en tokens por el tokenizer estándar, los tokens obtenidos se convertirán a minúsculas, se eliminarán las palabras insignificantes y solo quedará la raíz de las palabras de los tokens restantes.

Creación del índice

Arriba hemos descrito el analizador default. Se aplicará a todos los campos string. Nuestra publicación contiene un array de etiquetas, por lo tanto, las etiquetas también serán procesadas por el analizador. Dado que buscamos publicaciones por coincidencia exacta de etiquetas, es necesario desactivar el análisis para el campo tags.

Crearemos un índice blog2 con analizador y mapeo, en el que se desactiva el análisis del campo tags:

curl -XPOST "$ES_URL/blog2" -d'
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "ru_stemmer": {
          "type": "stemmer",
          "language": "russian"
        }
      },
      "analyzer": {
        "default": {
          "char_filter": [
            "html_strip"
          ],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "ru_stop",
            "ru_stemmer"
          ]
        }
      }
    }
  },
  "mappings": {
    "post": {
      "properties": {
        "content": {
          "type": "string"
        },
        "published_at": {
          "type": "date"
        },
        "tags": {
          "type": "string",
          "index": "not_analyzed"
        },
        "title": {
          "type": "string"
        }
      }
    }
  }
}'

Añadiremos las mismas 3 publicaciones a este índice (blog2). Omitiré este proceso, ya que es análogo a la adición de documentos en el índice blog.

Búsqueda de texto completo con soporte para expresiones

Familiaricémonos con otro tipo de consultas:

# найдем документы, в которых встречается слово 'истории'
# query -> simple_query_string -> query содержит поисковый запрос
# поле title имеет приоритет 3
# поле tags имеет приоритет 2
# поле content имеет приоритет 1
# приоритет используется при ранжировании результатов
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "истории",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

Dado que utilizamos un analizador con stemming en ruso, esta consulta devolverá todos los documentos, aunque solo contengan la palabra ‘historia’.

La consulta puede contener caracteres especiales, por ejemplo:

""fried eggs" +(eggplant | potato) -frittata"

Sintaxis de la consulta:

+ significa operación AND
| significa operación OR
- niega un solo token
" rodea varios tokens para significar una frase de búsqueda
* al final de un término significa una consulta de prefijo
( y ) significan precedencia
~N después de una palabra significa distancia de edición (imprecisión)
~N después de una frase significa cantidad de deslizamiento
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

# получим 2 поста про котиков

Enlaces

PS

Si estás interesado en artículos lecciones similares, tienes ideas para nuevos artículos o sugerencias de colaboración, estaré encantado de recibir un mensaje en privado o por correo electrónico a m.kuzmin+habr@darkleaf.ru.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster