Osnove Elasticsearch

Elasticsearch — iskalni mehanizem z json rest api, ki uporablja Lucene in je napisan v Javi. Opišemo vse prednosti tega mehanizma na uradni spletni strani. Kasneje v besedilu bomo Elasticsearch imenovali ES.

Taki mehanizmi se uporabljajo pri zahtevnem iskanju po bazi dokumentov. Na primer, iskanje ob upoštevanju morfologije jezika ali iskanje po geo koordinatah.

V tem članku bom predstavil osnove ES na primeru indeksiranja objav bloga. Prikazal bom, kako filtrirati, razvrstiti in iskati dokumente.

Da ne bi bili odvisni od operacijskega sistema, bom vse poizvedbe k ES izvajal s pomočjo CURL. Obstaja tudi razširitev za Google Chrome z imenom sense.

V besedilu so postavljene povezave na dokumentacijo in druge vire. Na koncu so vključene povezave za hiter dostop do dokumentacije. Opredelitve neznanih izrazov lahko preberete v glosarju.

Namestitev ES

Za to potrebujemo najprej Javo. Razvijalci priporočajo namestitev različic Jave, novejših od Java 8 update 20 ali Java 7 update 55.

Distribucija ES je na voljo na spletni strani razvijalca. Po razpakiranju arhiva je potrebno zagnati bin/elasticsearch. Prav tako so na voljo paketi za apt in yum. Na voljo je uradna slika za docker. Podrobnosti o namestitvi.

Po namestitvi in zagonu preverimo delovanje:

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

curl -X GET $ES_URL

Prejeli bomo približno takšen odgovor:

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

Indeksiranje

Dodajmo objavo v ES:

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

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Smešna zgodba o mačkih<p>",
  "tags": [
    "mački",
    "smešna zgodba"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

odgovor strežnika:

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

ES je samodejno ustvaril indeks blog in tip post. Lahko naredimo analogijo: indeks je baza podatkov, tip pa je tabela v tej bazi. Vsak tip ima svojo shemo — mapping, prav tako kot relacijska tabela. Mapping se samodejno generira pri indeksiranju dokumenta:

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

V odgovoru strežnika sem dodal v komentarjih vrednosti polj indeksiranega dokumenta:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Smešna zgodba o mačkih<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"
          }
        }
      }
    }
  }
}

Pomembno je poudariti, da ES ne razlikuje med enojno vrednostjo in poljem vrednosti. Na primer, polje title vsebuje zgolj naslov, polje tags pa je seznam nizov, čeprav sta v mappingu predstavljena enako.
Kasneje se bomo bolj podrobno pogovorili o mappingu.

Poizvedbe

Pridobitev dokumenta po njegovem 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": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>Sme\u0161na zgodba o ma\u010dkih<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"
    }
}

V odgovoru so se pojavili novi ključi: _version in _source. Na splošno vsi ključi, ki se začnejo z _ pripada službenim.

Opcija _version prikazuje verziju dokumenta. Potreban je za rad mehanizma optimističkih zaključavanja. Na primer, ako želimo da promenimo dokument koji ima verziju 1, šaljemo izmenjeni dokument i navodimo da je to izmena dokumenta sa verzijom 1. Ako je neko drugi takođe uređivao dokument sa verzijom 1 i poslao izmene pre nas, ES neće prihvatiti naše izmene, jer čuva dokument sa verzijom 2.

Opcija _source sadrži onaj dokument koji smo indeksirali. ES ne koristi ovu vrednost za pretraživačke operacije, jer se za pretraživanje koriste indeksi. U cilju štednje prostora, ES čuva komprimovani izvorni dokument. Ako nam je potreban samo id, a ne ceo izvorni dokument, možemo isključiti čuvanje izvora.

Ako nam ne treba dodatna informacija, možemo dobiti samo sadržaj _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>Sme\u0161na zgodba o ma\u010dkih<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"
}

Takođe možemo odabrati samo određena polja:

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

Hajde da indeksiramo još nekoliko postova i izvršimo složenije upite.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Veseli psi",
  "content": "<p>Smešna zgodba o psih<p>"
  "tags": [
    "psi",
    "smešna zgodba"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Kako je prišel moj maček",
  "content": "<p>Srce parajoča zgodba o ubogem mučku z ulice<p>",
  "tags": [
    "mački"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Razvrščanje

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

Odabrali smo poslednji post. size ograničava broj dokumenata u rezultatu. total prikazuje ukupni broj dokumenata koji odgovaraju upitu. sort u rezultatu sadrži niz celih brojeva, po kojima se vrši sortiranje. To jest, datum je preveden u celo broj. Više o sortiranju možete pročitati u dokumenti.

Filtri i upiti

ES od verzije 2 ne razlikuje filtre i upite, umesto toga uvodi se pojam konteksta.
Kontekst upita se razlikuje od konteksta filtra po tome što upit generiše _score i ne kešira se. Šta je _score ću prikazati kasnije.

Filtriranje po datumu

Koristimo upit range u kontekstu filter:

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

Filtriranje po tagovima

Uporabljamo term upit za pretragu id dokumenata koji sadrže zadatu reč:

# найдем все документы, в поле 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" : "Veseli mačkoti",
        "tags" : [ "mačkoti", "smešna istorija" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Kako sam dobio mačića",
        "tags" : [ "mačkoti" ]
      }
    } ]
  }
}

Potpuno pretraživanje

Tri naša dokumenta sadrže u polju content sledeće:

  • <p>Smešna zgodba o mačkih<p>
  • <p>Smešna zgodba o psih<p>
  • <p>Srce parajoča zgodba o ubogem mučku z ulice<p>

Uporabljamo match query za pretragu id dokumenata koji sadrže zadatu reč:

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

Međutim, ako tražimo „historije“ u polju content, nećemo ništa pronaći, jer indeks sadrži samo originalne reči, a ne njihova osnova. Da bismo omogućili kvalitetno pretraživanje, potrebno je postaviti analizator.

Field _score prikazuje relevantnost. Ako se zahtjev izvršava u filter kontekstu, vrednost _score će uvek biti 1, što označava potpuno usklađivanje sa filtrom.

Analizatori

Analizatori su potrebni za pretvaranje izvornog teksta u skup tokena.
Analizatori se sastoje od jednog Tokenizera i nekoliko opcionalnih TokenFilters. Tokenizer može prethoditi nekoliko CharFilters. Tokenizer razdvaja izvorni niz na tokene, na primer, po razmacima i interpunkcijskim simbolima. TokenFilter može menjati tokene, brisati ili dodavati nove, na primer, ostavljajući samo osnovu reči, uklanjajući predloge, dodajući sinonime. CharFilter menja ceo izvorni niz, na primer, uklanja HTML oznake.

U ES postoji nekoliko standardnih analizatora. Na primer, analizator russian.

Iskoristimo api i pogledajmo kako analizatori standard i russian pretvaraju niz „Veseli istorije o mačkama“:

# используем анализатор 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" : "veseli",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "istorije",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "o",
    "start_offset" : 16,
    "end_offset" : 17,
    "type" : "",
    "position" : 2
  }, {
    "token" : "mačkama",
    "start_offset" : 18,
    "end_offset" : 25,
    "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" : "kot",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Standardni analizator je podelio string na reči i sve preveo u malo slovo, analizator russian je odstranio nebitne reči, preveo u malo slovo i sačuvao osnovu reči.

Pogledajmo koje Tokenizer, TokenFilters, CharFilters koristi analizator russian:

{
  "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 nedostaju */
    }
  }
}

Opisujemo svoj analizator na osnovu russian, koji će ukloniti html tagove. Nazvaćemo ga default, jer će se analizator sa tim imenom koristiti po defaultu.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* dodajemo uklanjanje html tagova */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Prvo će iz izvorne stringa biti uklonjeni svi html tagovi, zatim će ga razdvojiti na tokene tokenizer standard, dobijeni tokeni će preći u mala slova, biće uklonjene nebitne reči i od preostalih tokena će ostati osnova reči.

Kreiranje indeksa

Gore smo opisali default analizator. On će se primeniti na sva string polja. Naš post sadrži niz tagova, tako da će i tagovi biti obrađeni analizatorom. Pošto tražimo postove po tačnom podudaranju taga, neophodno je isključiti analizu za polje tags.

Kreirajmo indeks blog2 sa analizatorom i mapiranjem, u kojem je isključena analiza polja tags:

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

Dodajmo iste 3 objave u ovaj indeks (blog2). Preskočiću ovaj proces, jer je sličan dodavanju dokumenata u indeks blog.

Potpuno pretraživanje sa podrškom za izraze

Upoznajmo još jedan tip upita:

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

Budući da koristimo analizer s ruskim stemingom, ovaj upit će vratiti sve dokumente, iako se u njima pojavljuje samo reč 'istorija'.

Upit može sadržavati specijalne simbole, na primer:

""pržena jaja" +(patlidžan | krompir) -frittata"

Sintaksa upita:

+ označava AND operaciju
| označava OR operaciju
- negira jedan token
" obavija više tokena da označi frazu za pretragu
* na kraju termina označava pretragu prefiksa
( i ) označavaju prioritet
~N nakon reči označava udaljenost uredbe (nepreciznost)
~N nakon fraze označava količinu nerednosti
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Povezave

PS

Ako vas zanimaju slični članci, imate ideje za nove članke ili predloge za saradnju, biću srećan da primim poruku u ličnoj ili na e-mail m.kuzmin+habr@darkleaf.ru.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster