Fundamentos de Elasticsearch

Elasticsearch é un buscador con API json rest, que utiliza Lucene e está escrito en Java. Unha descrición de todas as vantaxes deste motor está dispoñible en sitio web oficial. No que segue faremos referencia a Elasticsearch como ES.

Úsanse motores similares para buscas complexas nunha base de datos de documentos. Por exemplo, buscar tendo en conta a morfoloxía da lingua ou buscar por xeocoordenadas.

Neste artigo falarei dos conceptos básicos de ES usando o exemplo de indexación de publicacións de blog. Vouche mostrar como filtrar, ordenar e buscar documentos.

Para non depender do sistema operativo, farei todas as solicitudes a ES usando CURL. Tamén hai un complemento para google chrome chamado sentido.

O texto contén ligazóns a documentación e outras fontes. Ao final hai ligazóns para acceder rapidamente á documentación. As definicións de termos descoñecidos pódense atopar en glosarios.

Instalación de ES

Para iso, primeiro necesitamos Java. Desenvolvedores recomendo instalar versións de Java máis recentes que a actualización 8 de Java 20 ou a actualización 7 de Java 55.

A distribución de ES está dispoñible en sitio para desenvolvedores. Despois de desempaquetar o arquivo, cómpre executar bin/elasticsearch. Tamén dispoñible paquetes para apt e yum. Ahí está imaxe oficial para docker. Máis información sobre a instalación.

Despois da instalación e do lanzamento, comprobemos a funcionalidade:

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

curl -X GET $ES_URL

Recibiremos algo así:

{
  "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

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

resposta do servidor:

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

ES creado automaticamente índice blog e тип publicación. Podemos debuxar unha analoxía condicional: un índice é unha base de datos e un tipo é unha táboa nesta base de datos. Cada tipo ten o seu propio esquema − mapeamento, igual que unha táboa relacional. A asignación xérase automaticamente cando se indexa o documento:

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

Na resposta do servidor, engadín os valores dos campos do documento indexado nos 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"
          }
        }
      }
    }
  }
}

Paga a pena notar que ES non diferencia entre un único valor e unha matriz de valores. Por exemplo, o campo de título simplemente contén un título e o campo de etiquetas contén unha matriz de cadeas, aínda que se representan do mesmo xeito na asignación.
Máis adiante falaremos sobre a cartografía.

Solicitudes

Recuperando un documento polo seu 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"
  }
}

Apareceron novas claves na resposta: _version и _source. En xeral, todas as claves comezan por _ están clasificados como oficiais.

Clave _version mostra a versión do documento. É necesario para que funcione o mecanismo de bloqueo optimista. Por exemplo, queremos cambiar un documento que ten a versión 1. Enviamos o documento modificado e indicamos que se trata dunha edición dun documento coa versión 1. Se alguén tamén editou un documento coa versión 1 e enviou cambios antes que nós, entón ES non aceptará os nosos cambios, porque almacena o documento coa versión 2.

Clave _source contén o documento que indexamos. ES non usa este valor para operacións de busca porque Os índices úsanse para buscar. Para aforrar espazo, ES almacena un documento de orixe comprimido. Se só necesitamos o identificador e non o documento de orixe completo, entón podemos desactivar o almacenamento de orixe.

Se non necesitamos información adicional, só podemos obter o contido 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"
}

Tamén pode seleccionar só certos campos:

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

Imos indexar algunhas publicacións máis e realizar consultas máis complexas.

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

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" : "Веселые котята",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Escollemos o último post. size limita o número de documentos a emitir. total mostra o número total de documentos correspondentes á solicitude. sort na saída contén unha matriz de enteiros polos que se realiza a ordenación. Eses. a data converteuse nun número enteiro. Pódese atopar máis información sobre a clasificación en documentación.

Filtros e consultas

ES desde a versión 2 non distingue entre filtros e consultas introdúcese o concepto de contextos.
Un contexto de consulta difire dun contexto de filtro en que a consulta xera un _score e non se almacena na memoria caché. Vouche mostrar cal é o _score máis tarde.

Filtrar por data

Usamos a solicitude alcance no contexto do filtro:

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

Filtrar por etiquetas

Usamos consulta de termos para buscar ID de documentos que conteñan unha 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" : [ "котята" ]
      }
    } ]
  }
}

Busca de texto completo

Tres dos nosos documentos conteñen o seguinte no campo de contido:

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

Usamos consulta de coincidencia para buscar ID de documentos que conteñan unha 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
    } ]
  }
}

Non obstante, se buscamos "historias" no campo de contido, non atoparemos nada, porque O índice contén só as palabras orixinais, non as súas raíces. Para facer unha busca de alta calidade, cómpre configurar o analizador.

Campo _score espectáculos relevancia. Se a solicitude se executa nun contexto de filtro, entón o valor _score sempre será igual a 1, o que significa unha coincidencia completa co filtro.

Analizadores

Analizadores son necesarios para converter o texto fonte nun conxunto de fichas.
Os analizadores consisten nun Tokenizador e varias opcionais TokenFilters. Tokenizer pode ir precedido de varios CharFilters. Os tokenizadores dividen a cadea de orixe en tokens, como espazos e caracteres de puntuación. TokenFilter pode cambiar tokens, eliminar ou engadir novos, por exemplo, deixar só o tallo da palabra, eliminar preposicións, engadir sinónimos. CharFilter: cambia toda a cadea de orixe, por exemplo, elimina as etiquetas html.

ES ten varias analizadores estándar. Por exemplo, un analizador ruso.

Aproveitemos api e vexamos como os analizadores estándar e ruso transforman a cadea "Historias divertidas sobre gatiños":

# используем анализатор 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
  } ]
}

O analizador estándar dividiu a cadea por espazos e converteu todo a minúsculas, o analizador ruso eliminou as palabras sen importancia, converteuno en minúsculas e deixou o tronco das palabras.

Vexamos que Tokenizer, TokenFilters, CharFilters usa o 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 отсутствуют */
    }
  }
}

Imos describir o noso analizador baseado en ruso, que eliminará as etiquetas html. Chamémoslle por defecto, porque un analizador con este nome empregarase por defecto.

{
  "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"
      ]
    }
  }
}

En primeiro lugar, todas as etiquetas HTML eliminaranse da cadea de orixe, despois o estándar do tokenizer dividirao en tokens, os tokens resultantes moveranse a minúsculas, as palabras insignificantes eliminaranse e os tokens restantes seguirán sendo a raíz da palabra.

Creando un índice

Arriba describimos o analizador predeterminado. Aplicarase a todos os campos de cadea. A nosa publicación contén unha serie de etiquetas, polo que as etiquetas tamén serán procesadas polo analizador. Porque Buscamos publicacións por coincidencia exacta cunha etiqueta, entón necesitamos desactivar a análise para o campo de etiquetas.

Imos crear un índice blog2 cun analizador e mapeo, no que a análise do campo de etiquetas está desactivada:

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

Imos engadir as mesmas 3 publicacións a este índice (blog2). Omitirei este proceso porque... é semellante a engadir documentos ao índice do blog.

Busca de texto completo con soporte de expresión

Vexamos outro tipo de solicitudes:

# найдем документы, в которых встречается слово 'истории'
# 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 a usar un analizador con derivación rusa, entón esta solicitude devolverá todos os documentos, aínda que só conteñen a palabra "historia".

A solicitude pode conter caracteres especiais, por exemplo:

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

Sintaxe de solicitude:

+ 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

Se estás interesado en artigos-leccións similares, tes ideas para novos artigos ou tes propostas de cooperación, estarei encantado de recibir unha mensaxe nunha mensaxe persoal ou por correo electrónico [protexido por correo electrónico].

Fonte: www.habr.com