Conceptes bàsics d'Elasticsearch

Elasticsearch és un motor de cerca amb API json rest, que utilitza Lucene i escrit en Java. Una descripció de tots els avantatges d'aquest motor està disponible a lloc web oficial. A continuació, ens referirem a Elasticsearch com a ES.

S'utilitzen motors similars per a cerques complexes en una base de dades de documents. Per exemple, cerca tenint en compte la morfologia de la llengua o cerca per coordenades geogràfiques.

En aquest article parlaré dels fonaments bàsics d'ES utilitzant l'exemple d'indexació d'entrades de bloc. Us mostraré com filtrar, ordenar i cercar documents.

Per no dependre del sistema operatiu, faré totes les peticions a ES mitjançant CURL. També hi ha un connector per a Google Chrome anomenat sentit.

El text conté enllaços a documentació i altres fonts. Al final hi ha enllaços per accedir ràpidament a la documentació. Les definicions de termes desconeguts es poden trobar a glossaris.

Instal·lació

Per fer-ho, primer necessitem Java. Desenvolupadors recomanar instal·leu versions de Java més noves que l'actualització 8 de Java 20 o l'actualització 7 de Java 55.

La distribució d'ES està disponible a lloc del desenvolupador. Després de desempaquetar l'arxiu, heu d'executar-lo bin/elasticsearch. També disponible paquets per a apt i yum. N’hi ha imatge oficial de Docker. Més informació sobre la instal·lació.

Després de la instal·lació i el llançament, comprovem la funcionalitat:

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

curl -X GET $ES_URL

Rebrem una cosa com aquesta:

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

Afegim una publicació 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 del servidor:

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

ES creat automàticament índex bloc i Escriviu publicació. Podem dibuixar una analogia condicional: un índex és una base de dades i un tipus és una taula en aquesta base de dades. Cada tipus té el seu propi esquema − cartografia, igual que una taula relacional. El mapeig es genera automàticament quan s'indexa el document:

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

A la resposta del servidor, he afegit els valors dels camps del document indexat als comentaris:

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

Val la pena assenyalar que ES no diferencia entre un sol valor i una matriu de valors. Per exemple, el camp de títol només conté un títol i el camp d'etiquetes conté una matriu de cadenes, tot i que es representen de la mateixa manera en el mapeig.
Més endavant parlarem de mapeig.

Sol·licituds

Recuperant un document pel seu identificador:

# извлечем документ с 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"
  }
}

Van aparèixer noves claus a la resposta: _version и _source. En general, totes les claus que comencen per _ estan classificats com a oficials.

Clau _version mostra la versió del document. És necessari perquè el mecanisme de bloqueig optimista funcioni. Per exemple, volem canviar un document que té la versió 1. Enviem el document modificat i indiquem que es tracta d'una edició d'un document amb la versió 1. Si algú altre també ha editat un document amb la versió 1 i ha enviat canvis abans que nosaltres, aleshores ES no acceptarà els nostres canvis, perquè emmagatzema el document amb la versió 2.

Clau _source conté el document que hem indexat. ES no utilitza aquest valor per a les operacions de cerca perquè Els índexs s'utilitzen per a la cerca. Per estalviar espai, ES emmagatzema un document font comprimit. Si només necessitem l'identificador, i no tot el document font, podem desactivar l'emmagatzematge d'origen.

Si no necessitem informació addicional, només podem obtenir el contingut 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"
}

També podeu seleccionar només determinats camps:

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

Anem a indexar unes quantes publicacions més i fer consultes més complexes.

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

Hem escollit l'últim post. size limita el nombre de documents a emetre. total mostra el nombre total de documents que coincideixen amb la sol·licitud. sort a la sortida conté una matriu d'enters mitjançant els quals es realitza l'ordenació. Aquells. la data es va convertir en un nombre enter. Podeu trobar més informació sobre la classificació a documentació.

Filtres i consultes

ES des de la versió 2 no distingeix entre filtres i consultes s'introdueix el concepte de contextos.
Un context de consulta difereix d'un context de filtre perquè la consulta genera una _puntuació i no s'emmagatzema a la memòria cau. Més endavant us mostraré quina és la _puntuació.

Filtra per data

Utilitzem la petició abast en el context del filtre:

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

Filtrat per etiquetes

Fem servir consulta de termini per cercar identificadors de documents que continguin una paraula 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" : [ "котята" ]
      }
    } ]
  }
}

Cerca de text complet

Tres dels nostres documents contenen el següent al camp de contingut:

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

Fem servir consulta de coincidència per cercar identificadors de documents que continguin una paraula 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
    } ]
  }
}

Tanmateix, si cerquem “històries” al camp de contingut, no trobarem res, perquè L'índex conté només les paraules originals, no les seves tiges. Per fer una cerca d'alta qualitat, cal configurar l'analitzador.

Camp _score espectacles pertinença. Si la sol·licitud s'executa en un context de filtre, el valor _score sempre serà igual a 1, la qual cosa significa una coincidència completa amb el filtre.

Analitzadors

Analitzadors són necessaris per convertir el text font en un conjunt de fitxes.
Els analitzadors consisteixen en un Tokenitzador i diversos opcionals TokenFilters. Tokenizer pot anar precedit de diversos CharFilters. Els tokenitzadors divideixen la cadena d'origen en fitxes, com ara espais i caràcters de puntuació. TokenFilter pot canviar fitxes, eliminar o afegir-ne de noves, per exemple, deixar només la tija de la paraula, eliminar preposicions, afegir sinònims. CharFilter: canvia tota la cadena d'origen, per exemple, elimina les etiquetes HTML.

ES en té diverses analitzadors estàndard. Per exemple, un analitzador rus.

Aprofitem api i vegem com els analitzadors estàndard i rus transformen la cadena "Històries divertides sobre gatets":

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

L'analitzador estàndard va dividir la cadena en espais i ho va convertir tot en minúscules, l'analitzador rus va eliminar les paraules sense importància, les va convertir en minúscules i va deixar la tija de les paraules.

Vegem quin Tokenizer, TokenFilters, CharFilters utilitza l'analitzador rus:

{
  "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 отсутствуют */
    }
  }
}

Descrivim el nostre analitzador basat en rus, que tallarà les etiquetes HTML. Diguem-ho per defecte, perquè s'utilitzarà per defecte un analitzador amb aquest nom.

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

Primer, totes les etiquetes HTML s'eliminaran de la cadena d'origen, després l'estàndard del tokenizer la dividirà en fitxes, les fitxes resultants passaran a minúscules, s'eliminaran les paraules insignificants i les fitxes restants seguiran sent la base de la paraula.

Creació d'un índex

Més amunt vam descriure l'analitzador predeterminat. S'aplicarà a tots els camps de cadena. La nostra publicació conté una sèrie d'etiquetes, de manera que les etiquetes també seran processades per l'analitzador. Perquè Estem buscant publicacions per coincidència exacta amb una etiqueta, llavors hem de desactivar l'anàlisi per al camp d'etiquetes.

Creem un índex blog2 amb un analitzador i mapeig, en el qual l'anàlisi del camp de les etiquetes 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"
        }
      }
    }
  }
}'

Afegim les mateixes 3 publicacions a aquest índex (blog2). Ometré aquest procés perquè... és semblant a afegir documents a l'índex del bloc.

Cerca de text complet amb suport d'expressió

Fem una ullada a un altre tipus de sol·licitud:

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

Perquè Estem utilitzant un analitzador amb derivació russa, aleshores aquesta sol·licitud retornarà tots els documents, encara que només continguin la paraula "història".

La sol·licitud pot contenir caràcters especials, per exemple:

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

Sintaxi de la sol·licitud:

+ 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 поста про котиков

Referències

PS

Si esteu interessats en articles-lliçons similars, teniu idees per a nous articles o teniu propostes de cooperació, estaré encantat de rebre un missatge en un missatge personal o per correu electrònic. [protegit per correu electrònic].

Font: www.habr.com