Elasticsearch: Vyhledáváme hezky česky

Dobré fulltextové vyhledávání pro češtinu nemusí nutně znamenat investici do proprietárních knihoven a slovníků. Nevěříte? Zkusím vás přesvědčit! Ukážeme si, jak nakonfigurovat Elasticsearch pro fulltextové vyhledávání v českých textech.

Seriál: Elasticsearch: Vyhledáváme hezky česky (2 díly)

  1. Elasticsearch: Vyhledáváme hezky česky 1.7.2013
  2. Elasticsearch: Vyhledáváme hezky česky (a taky slovensky) 4.9.2013

Nejdříve si ale představíme knihovnou Lucene a vysvětlíme si, jakou nabízí podporu pro češtinu (trochu suché teorie). Pak se podíváme na Elasticsearch, který je nad Lucene postavený a uvedeme si konkrétní konfigurace pro vyhledávání v češtině.

(Použité verze: Elasticsearch 0.90.0, Lucene 4.2.1)

Lucene

Lucene je výkonná open source knihovna pro fulltextové vyhledávání implementovaná v Javě. Mezi vývojáři je ceněná především pro svoji vyspělost a širokou komerční i komunitní podporu. Je to low-level knihovna, přímé použití není pro každého. Nabízí velmi detailní, nízkoúrovňové API, ale řadu problémů spojených s implementací rozsáhlejších systémů neřeší.

Lucene

Analýza textu

Fulltextové vyhledávače ukládají data do invertovaných indexů. Aby Lucene mohl vytvořit invertovaný index, potřebuje vstupní text rozdělit na jednotlivá slova a případně tato slova dále upravit (například převést na základní tvar). Tento proces se nazývá analýza.

  • Analyzér sestává z char filtrů, jednoho tokenizéru a několika token filtrů.
  • Chart filter může měnit, přidávat nebo odebírat jednotlivé znaky vstupního textu. Jeho vstupem i výstupem je stream znaků.
  • Tokenizér je zodpovědný za rozsekání vstupního textu (streamu znaků) na jednotlivá slova. V Lucene se pro „slovo“ užívá označení token nebo term – v závislosti na kontextu. Výstupem tokenizéru je stream tokenů.
  • Token filtry přicházejí na řadu jako poslední. Každý vstupní token je postoupen jednotlivým token filtrům v předem definovaném pořadí. Token filtry mohou vstupní token nějak zpracovat či modifikovat. Výstupem token filtru může být žádný, jeden či více tokenů.

Hezkým příkladem chart filtru je HTML strip filter, který z textu odstraňuje HTML značky. Umí se postarat i o překlad HTML entit.

Příkladem tokenizéru, který je vhodný pro slovanské jazyky (včetně češtiny) je StandardTokenizer, který za oddělovače slov považuje mezery, čárky, tečky, pomlčky a podobně. Složitější situace nastává u germánských jazyků (např. u němčiny kvůli „složeným slovům“) nebo u jazyků označovaných jako CJK (činština, japonština, korejština), ve kterých se jednotlivá slova v textu neoddělují. Pro takové případy existují v Lucene specializované tokenizéry.

Příkladem token filtru může být např:

  • zjištění kořene slova (hledání -> [hledat])
  • expanze na synonyma (hledat -> [hledat, pátrat, shánět])
  • ngramy (např. 3-gramy: hledat -> [hle, led, eda, dat])
  • odstranění diakritiky … atd.

Podpora češtiny v Lucene

Těžištěm podpory češtiny v Lucene jsou především token a char filtry, které umožňují zohlednit pravidla platná pro češtinu. Lucene nabízí „out of the box“ následující komponenty:

  1. Czech Stopwords token filtr
  2. Czech Stemmer token filtr
  3. Podporu ICU
  4. Hunspell token filtr (probereme ve druhém díle článku)

Czech Stopwords token filtr

Stopwords (či Stopslova) je seznam tokenů (slov), které se neindexují, token filter tyto tokeny zahodí. Nejsou tedy zahrnuty ve výsledném invertovaném indexu a nelze podle nich ani vyhledávat. Vhodnými kandidáty na stopslova jsou taková slova, která se v textu vyskytují velmi často a nejsou nositelem informační hodnoty. Vyloučení těchto slov z indexování má za následek menší velikost výsledného indexu a může pozitivně ovlivnit přesnost vyhledávání.

Seznam českých stopwords je součástí Lucene někdy od roku 2005. Jejich autorem je Lukáš Zapletal — timto zdravím kolegu z Red Hatu :-). Samozřejmě Lucene, stejně jako Elasticsearch, umožňuje nadefinovat vlastní seznam stopslov.

Czech Stemmer token filtr

Stematizace je proces nalezení základu slova (tzv. stem). Nejedná se tedy o Lematizaci, což je nalezení kořene slova (tzv. lemma). Stemmery jsou často implementovány jako „algoritmická ořezávátka koncovek slov“. Jejich výstupem nemusí být validní slovo v daném jazyce.

Od roku 2009 nabízí Lucene jeden stemmer pro češtinu (tento stemmer nenavrhli ani neimplementovali Češi). V budoucnu se možná dočkáme i stemmeru pro češtinu založeném na Snowballu. Existuje i implementace stemmeru pro Snowball od českých autorů (paper je zde). Uvidíme někdy v Lucene i tento?

Czech Analyzer

Všimli jste si, že v tuto chvíli už umíme sestavit první analyzér pro češtinu? Spojením Standard tokenizéruCzech Stopwords a Czech Stemmeru totiž vznikne CzechAnalyzer. Za chvíli si ho vyzkoušíme.

Podpora ICU

Vzpomínáte si na okamžik, kdy jste se naučili, že v češtině má posloupnost znaků c a h specifický význam? Ano… písmeno Ch! Nejen, že se jedná o jedno písmeno, ale je třeba si pamatovat, že má i určité místo v abecedě.

A přesně tohle je úkol pro Lucene analýzu využívající ICU. Obecně se jedná o podporu národních formátu a unicode standardu pro kódování znaků. Ve stručnosti se dá říci, že s ICU můžete v Lucene správně porovnávat řetězce, odstraňovat diakritiku a překládat speciání znaky, použít normalizované kódování národních znaků a v neposlední řadě použít ICU tokenizér. Za chvíli si ukážeme nějaké příklady.

Tolik teorie a teď pojďme na praktickou část.

Elasticsearch

Existuje několik cest, kudy se vývojář může vydat, pokud chce Lucene použít, ale nechce se „dotýkat“ detailních API či psát si (a tedy i udržovat) vlastní Lucene rozšíření. Za všechny můžeme zmínit třeba Hibernate SearchSolrCompass nebo Elasticsearch. V další části použijeme Elasticsearch a hlavní pozornost zaměříme na konfiguraci analýzy.

Elasticsearch

Poznámka: Vývoj knihovny Compass byl ukončen a její použití proto nelze obecně doporučit. Milovníky historie však může zaujmout skutečnost, že Compass je přímým předchůdcem Elasticsearch.

Budeme používat výhradně REST API pro komunikaci s běžícím serverem. Veškeré ukázky lze pohodlně předvést na příkazovém řádku (předpokládám užití Linuxu nebo Mac OS).

Nainstalujte a spusťte Elasticsearch

Elasticsearch 0.90.0 můžete stáhnout pomocí wget:

wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.90.0.zip

Archiv rozbalte a nastavte aktuální cestu do nově vzniklého adresáře.

unzip elasticsearch-0.90.0.zip
cd elasticsearch-0.90.0

Pokud máte správně nainstalovanou Javu (doporučuji SUN JDK 1.7), tak nastartuje elasticsearch.

./bin/elasticsearch -f

Přepínač -f říká, že Elasticsearch má běžet v popředí (foreground). Znamená to, že v konzoli můžeme ihned sledovat logy (ty jsou standardně ukládány do ./logs/elasticsearch.log).

Jakmile v konzoli (nebo v logu) uvidíme něco jako

[2013-05-26 16:26:15,135][INFO ][node     ] [Marrow] {0.90.0}[1202]: started
[2013-05-26 16:26:15,190][INFO ][gateway  ] [Marrow] recovered [0] indices into cluster_state

tak jsme připraveni k další práci.

Pro jistotu ještě můžeme ověřit dostupnost REST end pointu pomocí curl příkazu. Otevřete si druhou konzoli a na příkazový řádek zadejte:

curl localhost:9200
#
# Výstupem je:
# {
#   "ok" : true,
#   "status" : 200,
#   "name" : "Marrow",
#   "version" : {
#     "number" : "0.90.0",
#     "snapshot_build" : false
#   },
#   "tagline" : "You Know, for Search"
# }

Běžící Elasticsearch můžete zastavit několika způsoby. Pokud jeho proces běží v popředí (-f), tak klasicky Ctrl+C, jinak je možné použít kill -9 <pid> (ačkoliv kill vypadá násilně, Elastic je na takovou situaci připravený a jedná se o běžné ukončení).

Konfigurace Czech Analyzéru

Začneme ukázkou použití Czech Analyzéru. V Elasticsearch je analyzér vázán na konkrétní index. Proto nejrychlejší cesta vede přes REST API pro vytváření indexů.

Poznámka: Pokud už máte s Elasticsearch nějakou zkušenost, tak možná víte, že analyzéry lze definovat i pro index templates nebo v konfiguračním souboru elasticsearch.yml, ale tím se teď nebudeme zabývat.

curl -X PUT 'localhost:9200/sbornik' -d '{
  "settings" : {
    "analysis" : {
      "analyzer" : {
        "cestina" : {
          "type" : "czech"
}}}}}'

Tímto příkazem jsme vyrobili index sbornik s nadefinovaný analyzérem cestina, který používá Czech Analyzer z Lucene.

Pomocí Analyze API můžeme analyzér ihned vyzkoušet.

curl 'localhost:9200/sbornik/_analyze?analyzer=cestina&pretty=true' -d 'Bankovní poplatky jsou nehorázné!'

Výstupem analýzy textu „Bankovní poplatky jsou nehorázné!“ jsou tři tokeny (v tomto případě stemy): [„bankovn“,“poplatk“,“nehorázn“]. Jinými slovy: pokud bychom indexovali dokument, který by obsahoval právě tento text a použili přitom analyzátor cestina, pak by invertovaný index obsahoval právě tyto tři termy.

Czech Analyzér nabízí možnost konfigurace stopslov a stemmer exclusion – tedy umožňuje vyjmout vyjmenované termy ze stemming procesu (může se hodit např. pro jména osob, měst a podobně).

Někdy se může hodit, že si sestavíte vlastní analyzér, který je Czech Analyzérů podobný, ale některé filtry nebo tokenizér chcete upravit. Inspirací nechť je gist ukázka, která naznačuje, jak na to.

Instalace a použití ICU pluginu

Pojďme se teď podívat, jakou podporu nabízí Elasticsearch pro ICU. Konkrétně si ukážeme třídění a case folding (který mimo jiné odstraňuje diakritiku).

Zastavte Elasticsearch a pak nainstalujte Elasticsearch ICU plugin.

./bin/plugin -install elasticsearch/elasticsearch-analysis-icu/1.9.0
# Počkejte na hlášku: Installed analysis-icu

Opět nastartujte Elasticsearch. Při startu si v logu ověřte, že analysis-icu plugin byl nalezen.

[2013-06-18 23:53:13,527][INFO ][plugins  ] [Atalanta] loaded [analysis-icu], sites []

ICU Collation

ICU collation se používá pro správné setřídění řetězců. Výstupem fulltextového vyhledávání bývá zpravidla seznam dokumentů, které jsou setříděné podle relevance. Elasticsearch však umožňuje setřídit výsledky vyhledávání i podle mnoha jiných kritérií. Pokud potřebujete třídit podle pole, ve kterém se vyskytují národní znaky, tak byste měli ICU collation vyzkoušet.

Mějme následující množinu názvů: [„Celer“,“Cizrna“,“Cypřiš“,“Chřest“]. Podívejme se, jak můžeme tuto množinu setřídit podle abecedy.

# Pokud index existuje, tak jej nejdříve odstraníme (včetně mappingu a analyzátorů)
curl -X DELETE 'localhost:9200/i/'

curl -X POST 'localhost:9200/i/' -d '{
  "settings" : {
    "analysis" : {
      "analyzer" : {
        "cs_icu_analyzer" : {
          "type" : "custom",
          "tokenizer" : "standard",
          "filter" : ["cs_icu_collation"]
        }
      },
      "filter" : {
        "cs_icu_collation" : {
          "type" : "icu_collation",
          "language" : "cs"
  }}}},
  "mappings" : {
    "t" : {
      "properties" : {
        "text_icu" : { "type" : "string", "analyzer" : "cs_icu_analyzer" },
        "text"     : { "type" : "string" }
}}}}'

Takto jsme připravili index i s typem t, který má nadefinované dvě pole text a text_icu. Následně zaindexujeme dokumenty.

curl -X POST 'localhost:9200/i/t/' -d '{ "text_icu" : "Celer", "text" : "Celer" }'
curl -X POST 'localhost:9200/i/t/' -d '{ "text_icu" : "Cypřiš", "text" : "Cypřiš" }'
curl -X POST 'localhost:9200/i/t/' -d '{ "text_icu" : "Cizrna", "text" : "Cizrna" }'
curl -X POST 'localhost:9200/i/t/' -d '{ "text_icu" : "Chřest", "text" : "Chřest" }'

Výsledek třídění dokumentů bez použití ICU a s použitím ICU můžeme jednoduše porovnat.

echo "Třídíme bez ICU \n"
curl -X GET 'localhost:9200/_search?pretty=true' -d '{
  "query" : { "match_all" : {} },
  "sort" : [ "text" ]
}'

# Celer, Chřest, Cizrna, Cypřiš

echo "Třídíme s ICU \n"
curl -X GET 'localhost:9200/_search?pretty=true' -d '{
  "query" : { "match_all" : {} },
  "sort" : [ "text_icu" ]
}'

# Celer, Cizrna, Cypřiš, Chřest

Gist: Kompletní ukázka ICU collation.

ICU Folding

Pokud chcete odstranit diakritiku, tak můžete použít asciifolding token filter. ICU však nabízí alternativu, která umí mnohem víc (a je také „dražší“). ICU folding totiž není jen o diakritice, ale obecně provádí mnohem sofistikovaňější transformace znaků, které jsou detailně popsány v dokumentaci ICU.

curl -X DELETE 'localhost:9200/i/'

curl -X POST 'localhost:9200/i/' -d '{
  "settings" : {
    "analysis" : {
      "analyzer" : {
        "icu_folding" : {
          "type" : "custom",
          "tokenizer" : "whitespace",
          "filter" : ["icu_folding"]
        },
        "ascii_folding" : {
          "type" : "custom",
          "tokenizer" : "whitespace",
          "filter" : ["asciifolding","lowercase"]
}}}}}'

Takto jsme připravili index i se dvěma analyzátory, první používá ICU folding a druhý asciifolding token filter.

Výsledek analýzy textu „Běloučký kůň úpěl ódy!“ je pro oba analyzátory shodný.

curl 'localhost:9200/i/_analyze?analyzer=icu_folding&pretty=true' -d 'Běloučký kůň úpěl ódy!'
curl 'localhost:9200/i/_analyze?analyzer=ascii_folding&pretty=true' -d 'Běloučký kůň úpěl ódy!'

Ovšem v následujícím případě už narazíme na první rozdíly.

curl 'localhost:9200/i/_analyze?analyzer=icu_folding&pretty=true' -d 'dž ¼ № ℃ ™ Æ Ȣ ffi '
curl 'localhost:9200/i/_analyze?analyzer=ascii_folding&pretty=true' -d 'dž ¼ № ℃ ™ Æ Ȣ ffi '

Vidíme, že ICU folding si lépe poradí se znaky: ¼ № ℃ ™

V poslední ukázce je ovšem rozdíl ještě výraznější.

curl 'localhost:9200/i/_analyze?analyzer=icu_folding&pretty=true' -d 'º o ª a ℹ i ℇ e'
curl 'localhost:9200/i/_analyze?analyzer=ascii_folding&pretty=true' -d 'º o ª a ℹ i ℇ e'

V této se totiž naplno projevuje pravý účel ICU foldingu.

Gist: Kompletní ukázka ICU Folding.

Pro další studium ICU může sloužit článek Three Principles for Multilingal Indexing in Elasticsearch.

Na závěr

V tomto článku jsem se pokusil načrtnout přehled pro ty, kteří se potřebují zorientovat v základní problematice podpory češtiny ve fulltextovém vyhledávání založeném na Lucene a odpovídající konfiguraci pro Elasticsearch.

V navazujícím díle se seznámíme s token filtrem, který používá Hunspell, s českým Ispell slovníkem.

Lukáš Vlček pracuje v pro společnost Red Hat. V poslední době používá především Elasticsearch a (z nouze i) JavaScript (a díky Closure Tools se to dá vydržet), ve skutečnosti se ale považuje za Java vývojáře. Tajemstvím ovšem zůstává, že Lukáš je především ilustrátor, který za poslední roky nic kloudného nevytvořil a sní o tom, že jednou bude hrát v pořádném big bandu na pozoun.

Komentáře: 15

Přehled komentářů

Vašek Detailnější popis konfigurace ala článek Sphinx Search API
bodo Re: Detailnější popis konfigurace ala článek Sphinx Search API
Lukáš Vlček Ad: Detalnější popis konfigurace a API
DavidDvorak Re: Ad: Detalnější popis konfigurace a API
Lukáš Vlček Re: Ad: Detalnější popis konfigurace a API
Lukáš Vlček Re: Ad: Detalnější popis konfigurace a API
martin.bazik Slovencina
Lukáš Vlček Slovencina
tomas.fejfar Třídění vs. Řazení :)
Lukáš Vlček Ad: Třídění vs. Řazení :)
Martin Lutonský Praktické ukázky
Lukáš Vlček Re: Praktické ukázky
Michal Samek
Juraj Chlebec Sort pre celú vetu
Jan Kalina Content-Type header [application/x-www-form-urlencoded] is not supported
Zdroj: https://www.zdrojak.cz/?p=9066