Fundamentele Elasticsearch

Elasticsearch — un motor de căutare cu API REST json care folosește Lucene și este scris în Java. Descrierea tuturor avantajelor acestui motor este disponibilă pe site-ul oficial. În continuare, ne vom referi la Elasticsearch ca ES.

Astfel de motoare sunt utilizate pentru căutări complexe în baza de date de documente. De exemplu, căutarea ținând cont de morfologia limbii sau căutarea după coordonate geografice.

În acest articol, voi vorbi despre bazele ES folosind exemplul indexării postărilor de blog. Voi arăta cum să filtrăm, să sortăm și să căutăm documente.

Pentru a nu depinde de sistemul de operare, toate cererile către ES le voi face utilizând CURL. De asemenea, există un plugin pentru Google Chrome numit sense.

În text sunt plasate linkuri către documentație și alte surse. La final sunt incluse linkuri pentru acces rapid la documentație. Definițiile termenilor necunoscuți pot fi citite în glosar.

Instalarea ES

Pentru aceasta, mai întâi avem nevoie de Java. Dezvoltatorii se recomandă trebuie să instaleze versiuni de Java mai noi decât Java 8 update 20 sau Java 7 update 55.

Distribuția ES este disponibilă pe site-ul dezvoltatorului. După desfășurarea arhivei, trebuie să porniți bin/elasticsearch. Sunt disponibile și pachete pentru apt și yum. Există imagine oficială pentru docker. Mai multe despre instalare.

După instalare și pornire, vom verifica funcționalitatea:

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

curl -X GET $ES_URL

Vom primi un răspuns aproximativ astfel:

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

Vom adăuga o postare în ES:

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

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>O poveste amuzantă despre pisici<p>",
  "tags": [
    "pisici",
    "poveste amuzantă"
  ],
  "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 a creat automat indexul blog și tipul post. Se poate face o analogie condiționată: indexul este o bază de date, iar tipul este o tabelă în această bază de date. Fiecare tip are propriul său schema — mapping, la fel ca o tabelă relațională. Mapping-ul este generat automat la indexarea documentului:

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

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

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>O poveste amuzantă despre pisici<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 de menționat că ES nu face distincții între o valoare unică și un array de valori. De exemplu, câmpul title conține pur și simplu titlul, iar câmpul tags este un array de șiruri, deși sunt reprezentate în mapping în mod identic.
Mai târziu vom discuta despre mapping mai detaliat.

Cererile

Extracția documentului 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": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>O poveste amuzant\u0103 despre pisici<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"
    }
}

În răspuns au apărut chei noi: _version și _source. În general, toate cheile care încep cu _ se referă la serviciu.

Cheia _version arată versiunea documentului. Este necesar pentru funcționarea mecanismului de blocare optimistă. De exemplu, dorim să modificăm un document cu versiunea 1. Trimitem documentul modificat și specificăm că aceasta este o revizuire a documentului cu versiunea 1. Dacă altcineva a editat, de asemenea, documentul cu versiunea 1 și a trimis modificările înainte de noi, atunci ES nu va accepta modificările noastre, deoarece păstrează documentul cu versiunea 2.

Cheia _source conține acel document pe care l-am indexat. ES nu folosește această valoare pentru operațiuni de căutare deoarece pentru căutare se folosesc indici. Pentru a economisi spațiu, ES păstrează documentul sursă comprimat. Dacă avem nevoie doar de id, și nu de întregul document sursă, putem dezactiva păstrarea sursei.

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": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
    "content": "<p>O poveste amuzant\u0103 despre pisici<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"
}

De asemenea, putem 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" : "Pisicuțe jucăușe"
  }
}

Să indexăm încă câteva posturi și să efectuăm interogări mai complexe.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Câini veseli",
  "content": "<p>O poveste amuzantă despre căței<p>",
  "tags": [
    "căței",
    "poveste amuzantă"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Cum am primit o pisicuță",
  "content": "<p>O poveste emoționantă despre o pisicuță sărmană de pe stradă<p>",
  "tags": [
    "pisicuțe"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Sortare

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

Am selectat ultimul post. dimensiune limitează numărul documentelor în rezultat. total arată numărul total de documente potrivite cererii. sort rezultatul conține un array de numere întregi, pe baza cărora se efectuează sortarea. Adică data a fost transformată într-un număr întreg. Mai multe detalii despre sortare pot fi găsite în documentation.

Filtre și interogări

ES, începând cu versiunea 2, nu face distincție între filtre și interogări, în schimb se introduce conceptul de contexte.
Contextul unei interogări se diferențiază de contextul unui filtrul prin faptul că interogarea generează _score și nu este cache-uită. Ce este _score vă voi arăta mai târziu.

Filtrarea după dată

Folosim interogarea range în contextul filter:

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

Filtrarea după etichete

Vom folosi term query pentru a căuta id-uri de documente care conțin cuvântul dat:

# найдем все документы, в поле 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" : "Pisici amuzante",
        "tags" : [ "pisici", "poveste amuzantă" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Cum am avut un pisicuț",
        "tags" : [ "pisici" ]
      }
    } ]
  }
}

Căutare completă

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

  • <p>O poveste amuzantă despre pisici<p>
  • <p>O poveste amuzantă despre căței<p>
  • <p>O poveste emoționantă despre o pisicuță sărmană de pe stradă<p>

Vom folosi interogare de tip match pentru a căuta id-uri de documente care conțin cuvântul dat:

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

Cu toate acestea, dacă căutăm „povesti” în câmpul content, nu vom găsi nimic, deoarece în index sunt conținute doar cuvintele originale, nu formele lor de bază. Pentru a efectua o căutare de calitate, trebuie configurat analizerul.

Câmp _score arată relevanță. Dacă interogarea este efectuată în contextul filter, valoarea _score va fi întotdeauna 1, ceea ce denotă o corespondență totală cu filtrul.

Analizatori

Analizatori sunt necesari pentru a transforma textul inițial într-un set de tokenuri.
Analizatorii constau dintr-un Tokenizer și mai multe opționale TokenFilters. Tokenizer poate fi precedat de mai multe CharFilters. Tokenizer împarte șirul inițial în tokenuri, de exemplu, în funcție de spații și semne de punctuație. TokenFilter poate modifica tokenurile, elimina sau adăuga altele noi, de exemplu, păstrează doar forma de bază a cuvântului, elimină prepozițiile, adaugă sinonime. CharFilter — modifică întreaga șir inițial, de exemplu, elimină etichetele html.

În ES există mai mulți analizatori standard. De exemplu, analizatorul russian.

Vom folosi api și să vedem cum analizoarele standard și russian transformă șirul „Povești amuzante despre pisici”:

# используем анализатор 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" : "amuzante",
    "start_offset" : 0,
    "end_offset" : 8,
    "type" : "",
    "position" : 0
  }, {
    "token" : "povesti",
    "start_offset" : 9,
    "end_offset" : 16,
    "type" : "",
    "position" : 1
  }, {
    "token" : "despre",
    "start_offset" : 17,
    "end_offset" : 23,
    "type" : "",
    "position" : 2
  }, {
    "token" : "pisici",
    "start_offset" : 24,
    "end_offset" : 30,
    "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" : "vesel",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "istor",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "cot",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Analizatorul standard a împărțit șirul în funcție de spații și a convertit totul în litere mici. Analizatorul russian a eliminat cuvintele insignifiante, a transformat în litere mici și a păstrat baza cuvintelor.

Să vedem ce Tokenizer, TokenFilters, CharFilters folosește analizatorul russian:

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

Să ne descriem propriul analizator bazat pe russian, care va elimina etichetele html. Îi vom da numele default, deoarece analizatorul cu acest nume va fi utilizat implicit.

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

Mai întâi, toate etichetele html vor fi eliminate din șirul inițial, apoi va fi împărțită în tokeni folosind tokenizer standard, tokenii obținuți vor fi convertiți în litere mici, cuvintele insignifiante vor fi eliminate, iar din tokenii rămași va rămâne baza cuvântului.

Crearea indexului

Mai sus am descris analizatorul default. Acesta va fi aplicat la toate câmpurile de tip șir. Postarea noastră conține un array de etichete, prin urmare, etichetele vor fi de asemenea procesate de analizator. Deoarece căutăm postări după o potrivire exactă a etichetei, este necesar să dezactivăm analiza pentru câmpul tags.

Vom crea un index blog2 cu analizatorul și mapping-ul, în care analiza câmpului tags 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"
        }
      }
    }
  }
}'

Vom adăuga aceleași 3 postări în acest index (blog2). Voi sări peste acest proces, deoarece este similar cu adăugarea documentelor în indexul blog.

Căutare full-text cu suport pentru expresii

Să explorăm un alt tip de interogare:

# найдем документы, в которых встречается слово 'истории'
# 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 analizator cu stemming în limba rusă, această interogare va returna toate documentele, chiar dacă în ele apare doar cuvântul 'istorie'.

Interogarea poate conține simboluri speciale, de exemplu:

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

Sintaxa interogării:

+ semnifică operația AND
| semnifică operația OR
- neagă un singur token
" învăluie un număr de tokeni pentru a semnifica o frază de căutare
* la sfârșitul unui termen semnifică o interogare prefixată
( și ) semnifică precedența
~N după un cuvânt semnifică distanța de editare (fuzziness)
~N după o frază semnifică nivelul de slop
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Linkuri

PS

Dacă ești interesat de articole-tutoriale similare, ai idei pentru articole noi sau sugestii de colaborare, aș fi bucuros să primesc mesaje prin privat sau pe e-mail m.kuzmin+habr@darkleaf.ru.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster