Elasticsearch pagrindai

Elasticsearch yra paieškos variklis su json rest api, naudojant Lucene ir parašyta Java. Visų šio variklio pranašumų aprašymą rasite adresu oficiali svetainė. Toliau Elasticsearch vadinsime ES.

Panašūs varikliai naudojami sudėtingoms paieškoms dokumentų duomenų bazėje. Pavyzdžiui, ieškoti atsižvelgiant į kalbos morfologiją arba ieškoti pagal geografines koordinates.

Šiame straipsnyje kalbėsiu apie ES pagrindus, naudodamas tinklaraščio įrašų indeksavimo pavyzdį. Parodysiu, kaip filtruoti, rūšiuoti ir ieškoti dokumentų.

Kad nepriklausyčiau nuo operacinės sistemos, visas užklausas pateiksiu ES naudodamas CURL. Taip pat yra google chromo įskiepis, vadinamas jausmas.

Tekste pateikiamos nuorodos į dokumentus ir kitus šaltinius. Pabaigoje yra nuorodos, leidžiančios greitai pasiekti dokumentaciją. Nepažįstamų terminų apibrėžimus galite rasti žodynėliai.

ES diegimas

Norėdami tai padaryti, pirmiausia turime „Java“. Kūrėjai rekomenduoju Įdiekite „Java“ versijas, naujesnes nei „Java 8“ naujinimas 20 arba „Java 7“ naujinimas 55.

ES platinimas pasiekiamas adresu kūrėjo svetainė. Išpakavus archyvą reikia paleisti bin/elasticsearch. Taip pat galima paketai apt ir yum. Yra oficialus docker vaizdas. Daugiau apie montavimą.

Įdiegę ir paleidę patikrinkime funkcionalumą:

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

curl -X GET $ES_URL

Gausime kažką panašaus:

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

Indeksavimas

Pridėkime įrašą prie 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"
}'

serverio atsakymas:

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

ES sukurta automatiškai rodiklis dienoraštį ir тип paštu. Galime nubrėžti sąlyginę analogiją: indeksas yra duomenų bazė, o tipas yra lentelė šioje duomenų bazėje. Kiekvienas tipas turi savo schemą - kartografavimas, kaip ir reliacinė lentelė. Atvaizdavimas sugeneruojamas automatiškai, kai dokumentas indeksuojamas:

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

Serverio atsakyme komentaruose pridėjau indeksuoto dokumento laukų reikšmes:

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

Verta paminėti, kad ES neskiria vienos reikšmės ir verčių masyvo. Pavyzdžiui, pavadinimo lauke tiesiog yra pavadinimas, o žymų lauke yra eilučių masyvas, nors jos vaizduojamos vienodai atvaizduojant.
Daugiau apie žemėlapių sudarymą kalbėsime vėliau.

Paklausimai

Dokumento gavimas pagal jo 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"
  }
}

Atsakyme pasirodė nauji raktai: _version и _source. Apskritai, visi klavišai prasideda _ priskiriami oficialiems.

Raktas _version rodoma dokumento versija. Jis reikalingas, kad veiktų optimistinis užrakto mechanizmas. Pavyzdžiui, norime pakeisti dokumentą, kurio versija yra 1. Pateikiame pakeistą dokumentą ir nurodome, kad tai yra 1 versijos dokumento redagavimas. Jei kas nors kitas taip pat redagavo 1 versijos dokumentą ir pateikė pakeitimus prieš mus, tada ES nepriims mūsų pakeitimų, nes jame saugomas 2 versijos dokumentas.

Raktas _source yra dokumentas, kurį indeksavome. ES nenaudoja šios reikšmės paieškos operacijoms, nes Indeksai naudojami paieškai. Siekdama sutaupyti vietos, ES saugo suglaudintą šaltinio dokumentą. Jei mums reikia tik ID, o ne viso šaltinio dokumento, galime išjungti šaltinio saugyklą.

Jei mums nereikia papildomos informacijos, galime gauti tik _source turinį:

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

Taip pat galite pasirinkti tik tam tikrus laukus:

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

Indeksuokime dar kelis įrašus ir vykdykime sudėtingesnes užklausas.

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

Rūšiuoti

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

Mes pasirinkome paskutinį įrašą. size riboja išduodamų dokumentų skaičių. total rodo bendrą užklausą atitinkančių dokumentų skaičių. sort išvestyje yra sveikųjų skaičių masyvas, pagal kurį atliekamas rūšiavimas. Tie. data buvo konvertuota į sveikąjį skaičių. Daugiau informacijos apie rūšiavimą rasite dokumentacija.

Filtrai ir užklausos

ES nuo 2 versijos neskiria filtrų ir užklausų pristatoma kontekstų sąvoka.
Užklausos kontekstas skiriasi nuo filtro konteksto tuo, kad užklausa generuoja _balą ir nėra saugoma talpykloje. Vėliau parodysiu, kas yra _balas.

Filtruoti pagal datą

Mes naudojame prašymą diapazonas filtro kontekste:

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

Filtruoti pagal žymas

Mes naudojame termino užklausa Norėdami ieškoti dokumentų ID, kuriuose yra nurodytas žodis:

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

Pilno teksto paieška

Trijų mūsų dokumentų turinio lauke yra ši informacija:

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

Mes naudojame atitikties užklausą Norėdami ieškoti dokumentų ID, kuriuose yra nurodytas žodis:

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

Tačiau jei turinio lauke ieškosime „istorijas“, nieko nerasime, nes Rodyklėje yra tik originalūs žodžiai, o ne jų kamienai. Norint atlikti kokybišką paiešką, reikia sukonfigūruoti analizatorių.

Laukas _score rodo aktualumą. Jei užklausa vykdoma filtro kontekste, tada _score reikšmė visada bus lygi 1, o tai reiškia visišką filtro atitiktį.

Analizatoriai

Analizatoriai reikalingi norint konvertuoti šaltinio tekstą į žetonų rinkinį.
Analizatoriai susideda iš vieno Tokenizatorius ir keli neprivalomi Žetonų filtrai. Prieš tokenizatorių gali būti keli CharFilters. Žetonai suskaido šaltinio eilutę į žetonus, tokius kaip tarpai ir skyrybos ženklai. TokenFilter gali keisti žetonus, ištrinti ar pridėti naujų, pavyzdžiui, palikti tik žodžio kamieną, pašalinti prielinksnius, pridėti sinonimų. CharFilter – pakeičia visą šaltinio eilutę, pavyzdžiui, išpjauna html žymas.

ES turi keletą standartiniai analizatoriai. Pavyzdžiui, analizatorius Rusijos.

Pasinaudokime API ir pažiūrėkime, kaip standartiniai ir rusiški analizatoriai transformuoja eilutę „Juokingos istorijos apie kačiukus“:

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

Standartinis analizatorius suskaidė eilutę į tarpus ir viską pavertė mažosiomis raidėmis, rusiškas analizatorius pašalino nesvarbius žodžius, pavertė mažosiomis raidėmis ir paliko žodžių kamieną.

Pažiūrėkime, kurį Tokenizer, TokenFilters, CharFilters naudoja rusiškas analizatorius:

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

Apibūdinkime savo analizatorių rusų kalba, kuris iškirps html žymas. Pavadinkime tai numatytuoju, nes pagal numatytuosius nustatymus bus naudojamas analizatorius tokiu pavadinimu.

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

Pirmiausia iš šaltinio eilutės bus pašalintos visos HTML žymės, tada žetonų standartas ją suskaidys į žetonus, gauti žetonai bus perkelti į mažąsias raides, nereikšmingi žodžiai bus pašalinti, o likę žetonai liks žodžio kamienu.

Indekso kūrimas

Aukščiau aprašėme numatytąjį analizatorių. Jis bus taikomas visiems eilutės laukams. Mūsų įraše yra daugybė žymų, todėl žymas taip pat apdoros analizatorius. Nes Ieškome įrašų pagal tikslią žymos atitiktį, tada turime išjungti žymų lauko analizę.

Sukurkime indekso dienoraštį2 su analizatoriumi ir atvaizdavimu, kuriame išjungta žymų lauko analizė:

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

Pridėkime prie šios rodyklės (3 dienoraštis) tuos pačius 2 įrašus. Aš praleisiu šį procesą, nes... tai panašu į dokumentų įtraukimą į dienoraščio rodyklę.

Viso teksto paieška su išraiškos palaikymu

Pažvelkime į kito tipo užklausą:

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

Nes Naudojame analizatorių su rusiška kamiene, tada ši užklausa grąžins visus dokumentus, nors juose yra tik žodis „istorija“.

Prašyme gali būti specialiųjų simbolių, pavyzdžiui:

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

Užklausos sintaksė:

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

Nuorodos

PS

Jei jus domina tokios straipsnių pamokos, turite idėjų naujiems straipsniams ar pasiūlymų bendradarbiauti, mielai gausiu žinutę asmenine žinute arba el. paštu m.kuzmin+habr@darkleaf.ru.

Šaltinis: www.habr.com