Conceptos básicos de búsqueda elástica

Elasticsearch es un motor de búsqueda con json rest api, que utiliza Lucene y está escrito en Java. Una descripción de todas las ventajas de este motor está disponible en el sitio web oficial. En lo que sigue nos referiremos a Elasticsearch como ES.

Se utilizan motores similares para búsquedas complejas en una base de datos de documentos. Por ejemplo, buscar teniendo en cuenta la morfología del idioma o buscar por coordenadas geográficas.

En este artículo hablaré sobre los conceptos básicos de ES usando el ejemplo de indexación de publicaciones de blog. Le mostraré cómo filtrar, ordenar y buscar documentos.

Para no depender del sistema operativo, realizaré todas las solicitudes a ES mediante CURL. También hay un complemento para Google Chrome llamado sentido.

El texto contiene enlaces a documentación y otras fuentes. Al final hay enlaces para un acceso rápido a la documentación. Las definiciones de términos desconocidos se pueden encontrar en glosarios.

Instalación de ES

Para hacer esto, primero necesitamos Java. Desarrolladores Recomendar Instale versiones de Java más recientes que Java 8 actualización 20 o Java 7 actualización 55.

La distribución ES está disponible en sitio del desarrollador. Después de descomprimir el archivo, debe ejecutar bin/elasticsearch. También disponible paquetes para apt y yum. Hay imagen oficial de Docker. Más sobre la instalación.

Después de la instalación y el lanzamiento, verifiquemos la funcionalidad:

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

curl -X GET $ES_URL

Recibiremos algo como esto:

{
  "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>Смешная история про котят<p>",
  "tags": [
    "котята",
    "смешная история"
  ],
  "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 creado automáticamente índice blog y тип correo. Podemos hacer una analogía condicional: un índice es una base de datos y un tipo es una tabla en esta base de datos. Cada tipo tiene su propio esquema: cartografía, como una tabla relacional. El mapeo se genera automáticamente cuando se indexa el documento:

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

En la respuesta del servidor, agregué los valores de los campos del documento indexado en los comentarios:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Смешная история про котят<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"
          }
        }
      }
    }
  }
}

Vale la pena señalar que ES no diferencia entre un valor único y una matriz de valores. Por ejemplo, el campo de título simplemente contiene un título y el campo de etiquetas contiene una matriz de cadenas, aunque se representan de la misma manera en el mapeo.
Hablaremos más sobre el mapeo más adelante.

solicitudes

Recuperar 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" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

Nuevas claves aparecieron en la respuesta: _version и _source. En general, todas las claves que comienzan con _ están clasificados como oficiales.

Llave _version muestra la versión del documento. Es necesario para que funcione el mecanismo de bloqueo optimista. Por ejemplo, queremos cambiar un documento que tiene la versión 1. Enviamos el documento modificado e indicamos que se trata de una edición de un documento con la versión 1. Si alguien más también editó un documento con la versión 1 y envió los cambios antes que nosotros, entonces ES no aceptará nuestros cambios, porque almacena el documento con la versión 2.

Llave _source contiene el documento que indexamos. ES no utiliza este valor para operaciones de búsqueda porque Los índices se utilizan para realizar búsquedas. Para ahorrar espacio, ES almacena un documento fuente comprimido. Si solo necesitamos la identificación y no el documento fuente completo, entonces podemos desactivar el almacenamiento de origen.

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

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
  "title" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

También puede 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" : "Веселые котята"
  }
}

Indexemos algunas publicaciones más y ejecutemos consultas más complejas.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Веселые щенки",
  "content": "<p>Смешная история про щенков<p>",
  "tags": [
    "щенки",
    "смешная история"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Как у меня появился котенок",
  "content": "<p>Душераздирающая история про бедного котенка с улицы<p>",
  "tags": [
    "котята"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Сортировка

# найдем последний пост по дате публикации и извлечем поля 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" : "Веселые котята",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Elegimos el último post. size limita el número de documentos a emitir. total muestra el número total de documentos que coinciden con la solicitud. sort en la salida contiene una matriz de números enteros mediante los cuales se realiza la clasificación. Aquellos. la fecha se convirtió a un número entero. Puede encontrar más información sobre la clasificación en documentación.

Filtros y consultas

ES desde la versión 2 no distingue entre filtros y consultas, en cambio Se introduce el concepto de contextos..
Un contexto de consulta se diferencia de un contexto de filtro en que la consulta genera una puntuación _score y no se almacena en caché. Te mostraré qué es _score más adelante.

Filtrar por fecha

Usamos la solicitud distancia en el contexto del filtro:

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

Filtrar por etiquetas

Uso consulta de término para buscar identificadores de documentos que contengan una palabra determinada:

# найдем все документы, в поле 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" : "Веселые котята",
        "tags" : [ "котята", "смешная история" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Как у меня появился котенок",
        "tags" : [ "котята" ]
      }
    } ]
  }
}

Búsqueda de texto completo

Tres de nuestros documentos contienen lo siguiente en el campo de contenido:

  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

Uso consulta de coincidencia para buscar identificadores de documentos que contengan una palabra determinada:

# 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, porque El índice contiene sólo las palabras originales, no sus raíces. Para realizar una búsqueda de alta calidad, debe configurar el analizador.

Campo _score espectáculos Relevancia. Si la solicitud se ejecuta en un contexto de filtro, entonces el valor _score siempre será igual a 1, lo que significa una coincidencia completa con el filtro.

Analizadores

Analizadores son necesarios para convertir el texto fuente en un conjunto de tokens.
Los analizadores constan de uno Tokenizer y varios opcionales Filtros de tokens. El tokenizador puede ir precedido de varios Filtros de carbón. Los tokenizadores dividen la cadena de origen en tokens, como espacios y caracteres de puntuación. TokenFilter puede cambiar tokens, eliminar o agregar otros nuevos, por ejemplo, dejar solo la raíz de la palabra, eliminar preposiciones, agregar sinónimos. CharFilter: cambia toda la cadena fuente, por ejemplo, recorta etiquetas html.

ES tiene varios analizadores estándar. Por ejemplo, un analizador ruso.

usemos abejas y veamos cómo los analizadores estándar y ruso transforman la cadena “Historias divertidas 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" : "веселые",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истории",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "про",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "<ALPHANUM>",
    "position" : 2
  }, {
    "token" : "котят",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "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" : "весел",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истор",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "кот",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

El analizador estándar dividió la cadena por espacios y convirtió todo a minúsculas, el analizador ruso eliminó las palabras sin importancia, las convirtió a minúsculas y dejó la raíz de las palabras.

Veamos qué Tokenizer, TokenFilters, 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",
      /* TokenFilters */
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      /* CharFilters отсутствуют */
    }
  }
}

Describamos nuestro analizador basado en ruso, que recortará etiquetas html. Llamémoslo predeterminado, porque De forma predeterminada se utilizará un analizador con este nombre.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* добавляем удаление html тегов */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Primero, todas las etiquetas HTML se eliminarán de la cadena de origen, luego el estándar del tokenizador la dividirá en tokens, los tokens resultantes se moverán a minúsculas, las palabras insignificantes se eliminarán y los tokens restantes seguirán siendo la raíz de la palabra.

Creando un índice

Arriba describimos el analizador predeterminado. Se aplicará a todos los campos de cadena. Nuestra publicación contiene una serie de etiquetas, por lo que el analizador también procesará las etiquetas. Porque Buscamos publicaciones por coincidencia exacta con una etiqueta, luego debemos desactivar el análisis para el campo de etiquetas.

Creemos un blog2 de índice con analizador y mapeo, en el que el análisis del campo etiquetas esté deshabilitado:

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"
        }
      }
    }
  }
}'

Agreguemos las mismas 3 publicaciones a este índice (blog2). Omitiré este proceso porque... es similar a agregar documentos al índice del blog.

Búsqueda de texto completo con soporte de expresiones.

Veamos otro tipo de solicitud:

# найдем документы, в которых встречается слово 'истории'
# 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"
      ]
    }
  }
}'

Porque Estamos utilizando un analizador con derivación rusa, entonces esta solicitud devolverá todos los documentos, aunque solo contengan la palabra "historia".

La solicitud puede contener caracteres especiales, por ejemplo:

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

Sintaxis de solicitud:

+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

referencias

PS

Si está interesado en artículos-lecciones similares, tiene ideas para nuevos artículos o tiene propuestas de cooperación, estaré encantado de recibir un mensaje en un mensaje personal o por correo electrónico. [email protected].

Fuente: habr.com