Elementele de bază ale Elasticsearch

Elasticsearch este un motor de căutare cu json rest api, folosind Lucene și scris în Java. O descriere a tuturor avantajelor acestui motor este disponibilă la site-ul oficial. În cele ce urmează ne vom referi la Elasticsearch ca ES.

Motoare similare sunt folosite pentru căutări complexe într-o bază de date de documente. De exemplu, căutați ținând cont de morfologia limbii sau căutați după coordonatele geografice.

În acest articol voi vorbi despre elementele de bază ale ES folosind exemplul de indexare a postărilor de blog. Vă voi arăta cum să filtrați, să sortați și să căutați documente.

Pentru a nu depinde de sistemul de operare, voi face toate cererile către ES folosind CURL. Există, de asemenea, un plugin pentru google chrome numit sens.

Textul conține link-uri către documentație și alte surse. La sfârșit există link-uri pentru acces rapid la documentație. Definițiile termenilor nefamiliari pot fi găsite în glosare.

Instalarea ES

Pentru a face acest lucru, avem nevoie mai întâi de Java. Dezvoltatori recomanda instalați versiuni Java mai noi decât actualizarea Java 8 20 sau actualizarea Java 7 55.

Distribuția ES este disponibilă la site-ul dezvoltatorului. După despachetarea arhivei, trebuie să rulați bin/elasticsearch. Deasemenea disponibil pachete pentru apt și yum. Există imagine oficială pentru docker. Mai multe despre instalare.

După instalare și lansare, să verificăm funcționalitatea:

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

curl -X GET $ES_URL

Vom primi ceva de genul:

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

indexare

Să adăugăm o postare în 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"
}'

răspunsul serverului:

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

ES creat automat index blog și тип post. Putem trage o analogie condiționată: un index este o bază de date, iar un tip este un tabel din această bază de date. Fiecare tip are propria sa schemă − cartografiere, la fel ca un tabel relațional. Maparea este generată automat când documentul este indexat:

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

În răspunsul serverului, am adăugat valorile câmpurilor documentului indexat în comentarii:

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

Este demn de remarcat faptul că ES nu face diferența între o singură valoare și o serie de valori. De exemplu, câmpul titlu conține pur și simplu un titlu, iar câmpul etichete conține o matrice de șiruri, deși acestea sunt reprezentate în același mod în mapare.
Vom vorbi mai multe despre cartografiere mai târziu.

cereri

Preluarea unui document după id-ul său:

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

Au apărut chei noi în răspuns: _version и _source. În general, toate cheile începând cu _ sunt clasificate drept oficiale.

Ключ _version arată versiunea documentului. Este necesar pentru ca mecanismul optimist de blocare să funcționeze. De exemplu, dorim să schimbăm un document care are versiunea 1. Trimitem documentul modificat și indicăm că aceasta este o editare a unui document cu versiunea 1. Dacă altcineva a editat și un document cu versiunea 1 și a transmis modificări înaintea noastră, atunci ES nu va accepta modificările noastre, pentru că stochează documentul cu versiunea 2.

Ключ _source conține documentul pe care l-am indexat. ES nu folosește această valoare pentru operațiuni de căutare deoarece Indecșii sunt folosiți pentru căutare. Pentru a economisi spațiu, ES stochează un document sursă comprimat. Dacă avem nevoie doar de id, și nu de întregul document sursă, atunci putem dezactiva stocarea sursă.

Dacă nu avem nevoie de informații suplimentare, putem obține doar conținutul _source:

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

De asemenea, puteți selecta doar anumite câmpuri:

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

Să mai indexăm câteva postări și să executăm interogări mai complexe.

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

triere

# найдем последний пост по дате публикации и извлечем поля 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 ]
    } ]
  }
}

Am ales ultimul post. size limitează numărul de documente care trebuie eliberate. total arată numărul total de documente care corespund cererii. sort în ieșire conține o matrice de numere întregi prin care se realizează sortarea. Acestea. data a fost convertită într-un număr întreg. Mai multe informații despre sortare pot fi găsite în documentație.

Filtre și interogări

În schimb, ES din versiunea 2 nu face distincție între filtre și interogări se introduce conceptul de contexte.
Un context de interogare diferă de un context de filtru prin faptul că interogarea generează un _score și nu este stocată în cache. Vă voi arăta mai târziu ce este _score.

Filtrați după dată

Folosim cererea gamă în contextul filtrului:

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

Filtrați după etichete

Folosim interogare pe termen pentru a căuta ID-uri de document care conțin un anumit cuvânt:

# найдем все документы, в поле 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" : [ "котята" ]
      }
    } ]
  }
}

Căutare text integral

Trei dintre documentele noastre conțin următoarele în câmpul de conținut:

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

Folosim interogare de potrivire pentru a căuta ID-uri de document care conțin un anumit cuvânt:

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

Totuși, dacă căutăm „povestiri” în câmpul de conținut, nu vom găsi nimic, pentru că Indexul conține doar cuvintele originale, nu tulpinile lor. Pentru a efectua o căutare de înaltă calitate, trebuie să configurați analizorul.

Câmp _score spectacole relevanţă. Dacă cererea este executată într-un context de filtru, atunci valoarea _score va fi întotdeauna egală cu 1, ceea ce înseamnă o potrivire completă cu filtrul.

analizoare

analizoare sunt necesare pentru a converti textul sursă într-un set de jetoane.
Analizoarele constau dintr-un singur Tokenizer si mai multe optionale TokenFilters. Tokenizer poate fi precedat de mai multe CharFilters. Tokenizatoarele împart șirul sursă în simboluri, cum ar fi spații și caractere de punctuație. TokenFilter poate schimba jetoane, șterge sau adăuga altele noi, de exemplu, lăsați doar tulpina cuvântului, eliminați prepozițiile, adăugați sinonime. CharFilter - modifică întregul șir sursă, de exemplu, decupează etichetele html.

ES are mai multe analizoare standard. De exemplu, un analizor Rusă.

Să profităm api și să vedem cum analizoarele standard și ruse transformă șirul „Povești amuzante despre pisoi”:

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

Analizatorul standard a împărțit șirul în spații și a convertit totul în minuscule, analizatorul rus a eliminat cuvintele neimportante, l-a convertit în minuscule și a lăsat tulpina cuvintelor.

Să vedem ce Tokenizer, TokenFilters, CharFilters folosește analizatorul 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 отсутствуют */
    }
  }
}

Să descriem analizatorul nostru bazat pe limba rusă, care va elimina etichetele html. Să-i spunem implicit, pentru că va fi folosit implicit un analizor cu acest nume.

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

În primul rând, toate etichetele HTML vor fi eliminate din șirul sursă, apoi standardul tokenizer îl va împărți în token-uri, token-urile rezultate se vor muta în litere mici, cuvintele nesemnificative vor fi eliminate, iar token-urile rămase vor rămâne tulpina cuvântului.

Crearea unui index

Mai sus am descris analizatorul implicit. Se va aplica tuturor câmpurilor de șir. Postarea noastră conține o serie de etichete, astfel încât etichetele vor fi, de asemenea, procesate de analizor. Deoarece Căutăm postări după potrivirea exactă a unei etichete, apoi trebuie să dezactivăm analiza pentru câmpul de etichete.

Să creăm un index blog2 cu un analizor și mapare, în care analiza câmpului etichetelor este dezactivată:

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

Să adăugăm aceleași 3 postări la acest index (blog2). Voi omite acest proces pentru că... este similar cu adăugarea de documente la indexul blogului.

Căutare text integral cu suport pentru expresii

Să aruncăm o privire la un alt tip de solicitare:

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

Deoarece Folosim un analizor cu derivație rusă, atunci această solicitare va returna toate documentele, deși acestea conțin doar cuvântul „istorie”.

Solicitarea poate conține caractere speciale, de exemplu:

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

Sintaxa cererii:

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

referințe

PS

Dacă sunteți interesat de articole-lecții similare, aveți idei pentru articole noi sau aveți propuneri de cooperare, atunci voi fi bucuros să primesc un mesaj într-un mesaj personal sau prin e-mail [e-mail protejat].

Sursa: www.habr.com