Přejít k navigační liště

Zdroják » Databáze » Elasticsearch: Vyhledáváme hezky česky (a taky slovensky)

Elasticsearch: Vyhledáváme hezky česky (a taky slovensky)

Články Databáze

V prvním díle jsme si představili knihovnu Lucene a vysvětlili jsme si, jakou nabízí „out-of-the-box“ podporu fulltextového vyhledávání pro češtinu. Ve druhém díle budeme pokračovat a ukážeme si hunspell token filter. Praktickou část opět demonstrujeme s použitím Elasticsearch.

Hned na úvod bych rád podotknul, že hunspell token filter nabízí podporu i pro slovenštinu. V průběhu článku se k tomu několikrát vrátíme.

(Použité verze: Elasticsearch 0.90.3, Lucene 4.4)

Lucene

Jakým způsobem analyzuje Lucene textová data jsme si podrobněji vysvětlili v předchozím díle. Už víme, že těžištěm podpory češtiny v Lucene jsou především token filtry. Dnes se budeme věnovat hunspell token filtru.

Hunspell token filter

Hunspell token filter je založený na jazykových slovnících používaných spell-checkerem Hunspell. Systémů pro spell-checking exituje celá řada. Kromě Hunspellu to je např. MySpell nebo Ispell. Nejsem odborníkem na tuto problematiku, ale zdá se mi, že Hunspell patří mezi systémy nejmladší a (snad i) nejpropracovanější. Jak samotný název napovídá, původně vzniknul pro podporu maďarštiny, protože ostatní systémy nebyly pro tento jazyk dostatečně flexibilní. Dnes podporuje mnoho dalších jazyků. Vývojově na starší systémy navazuje a je s mini často kompatibilní co do formátu slovníkových souborů, to znamená, že Hunspell může používat i slovníky pro Ispell nebo MySpell.

Ač je Hunspell určen především pro identifikaci a opravu překlepů, dají se jeho slovníky dobře použít pro analýzu slov (tokenů) v Lucene.

Hunspell potřebuje pro každý podporovaný jazyk slovníkové soubory dvou typů:

  • Soubor s příponou .dic. Jedná se o dictionary. Je to seznam slov daného jazyka, ideálně v kořenovém tvaru. Obecně může být slovník rozdělen do více takových souborů.
    Jak se lze dočíst v manu, ve slovníku (.dic) může být pro každé slovo dále definována řada morfologických informací. Ty ovšem nejsou v Lucene implementaci používány, proto se jim nebudeme věnovat. Ostatně český ani slovenský slovník, který budeme používat, takové morfologicé informace neobsahuje.
  • Soubor s příponou .aff. Jedná se o affixová pravidla, která lze aplikovat na vybraná slova ze slovníku. Pravidla určují, jak lze k těmto slovům přidávat předpony a přípony a vygenerovat tak správné slovní tvary v daném jazyce.

Například pro slovo hledat lze pomocí affixových pravidel vygenerovat následující slovní tvary – viz.: http://www.pravidla.cz/hledej.php?qr=hledat.

Služba pravidla.cz uvádí, že používá Ispell slovník. Ve skutečnosti i my budeme pro češtinu používat Ispell slovník.

Pro potřeby Lucene analýzy je však zajímavý opačný případ, kdy pro libovolný z těchto slovních tvarů dokážeme zpětně určit slovo základní (schválně neříkám kořen slova, protože velmi záleží na obsahu slovníku, ten totiž nemusí obsahovat pouze slova v kořenovém tvaru).

Algoritmus je takový, že Lucene zkusí pro každý vstupní token vytipovat affixová pravidla, která by mohla být použita pro tvorbu tohoto tokenu a zpětně vytvoří kandidáta na výchozí slovo. Pokud je kandidát nalezen ve slovníku (.dic) a zároveň je pro kandidáta povoleno použití daného pravidla (.aff), nalezli jsme základní slovo. Tento algoritmus se pro nalezeného kandidáta dále rekurzivně opakuje.

Až do verze Lucene 4.3 byla úroveň rekurzivního opakování stanovena na hodnotu 2. Od Lucene 4.4 (a Elasticsearch 0.90.3) je počet úrovní rekurze volitený. Pro každý jazyk (a každý slovník) je potřeba volit vhodnou hodnotu rekurzivního opakování. Češtině a slovenštině dle mého názoru nejvíce svědčí hodnota 0. To vychází z předpokladu, že na kořen slova stačí aplikovat affixová pravidla pouze jednou a můžeme získat úplnou množinu slovních tvarů. Pro jiné jazyky (či jinak konstruované slovníky) to ovšem platit nemusí.

Odkud Hunspell slovníky vzít?

  • Slovenský slovník: Projekt sk-spell se aktivně stará o řadu slovenských slovníků, konkrétně Hunspell najdeme zde: http://www.sk-spell.sk.cx/hunspell-sk.
  • Český slovník: Ukážeme si, jak získat potřebné soubory z OpenOffice balíku českých slovníků. Jedná se o Ispell slovník a zdá se mi, že není tak aktivně udržovaný jako slovenský hunspell (to ale nemusí nutně znamenat, že je méně kvalitní).
  • Nenašli jste slovník, který potřebujete? Tak si napište vlastní nebo upravte některý z existujících slovníků. Možná to zní jako šílenost, ale zkusím ukázat, že to může být užitečné.

České slovníky z OpenOffice

dict-cs-2.0

Stáhněte extension „České slovníky (cs-CZ)“ a rozbalte jej:

$ unzip dict-cs-2.0.oxt 
Archive:  dict-cs-2.0.oxt
  inflating: cs_CZ.aff     # <- affixová pravidla          
  inflating: cs_CZ.dic     # <- seznam slov          
  inflating: description.xml         
  inflating: Dictionaries.xcu        
  inflating: hyph_cs_CZ.dic          
   creating: META-INF/
  inflating: META-INF/manifest.xml   
  inflating: README_cs.txt           
  inflating: README_en.txt           
  inflating: th_cs_CZ_v3.dat         
  inflating: th_cs_CZ_v3.idx

České slovníky pro OpenOffice obsahují tyto části:

  • Kontrolu pravopisu
  • Slovník synonym
  • Slovník dělení slov

Nás v tuto chvíli zajímá pouze první ze slovníků. Jedná se o soubory cs_CZ.aff acs_CZ.dic, který obsahuje přes 160.000 českých slov a hesel. Oba slovníkové soubory jsou licencované pod GPL (viz README_cs.txt).

Jak jsem už zmínil, jedná se o český slovník pro Ispell. Jeho tvůrcem je Petr Kolář. Bohužel se mi nepodařilo zjistit, zda Petr Kolář stále slovník aktivně udržuje. Za jakékoliv informace o Petru Kolářovi, či vůbec o aktuálním stavu správy českého slovníku, budu vděčný.

Slovenské slovníky z sk-spell

Stáhněte a rozbalte nejnovější verzi Hunspell slovníku:

$ wget http://www.sk-spell.sk.cx/files/hunspell-sk-20110228.zip
$ unzip hunspell-sk-20110228.zip
Archive:  hunspell-sk-20110228.zip
   creating: hunspell-sk-20110228/
   creating: hunspell-sk-20110228/doc/
  inflating: hunspell-sk-20110228/doc/AUTHORS  
  inflating: hunspell-sk-20110228/doc/Copyright  
  inflating: hunspell-sk-20110228/doc/Flagy.AUTHORS  
  inflating: hunspell-sk-20110228/doc/Flagy.Changelog  
  inflating: hunspell-sk-20110228/doc/Flagy.README  
  inflating: hunspell-sk-20110228/doc/Flagy.TODO  
  inflating: hunspell-sk-20110228/doc/GPL  
  inflating: hunspell-sk-20110228/doc/LGPL  
  inflating: hunspell-sk-20110228/doc/MPL  
  inflating: hunspell-sk-20110228/sk_SK.aff  # <- affixová pravidla
  inflating: hunspell-sk-20110228/sk_SK.dic  # <- seznam slov

Licenční podmínky slovníkových souborů najdete v doc/Copyright.

Slovníky bychom měli, dále pokračujeme praktickou částí a ukážeme si, jak je použít v Elasticsearch.

Elasticsearch

Pokud ještě nemáte nainstalovaný Elasticsearch, nainstalujte jej teď. V případě, že process Elasticsearch zrovna běží, je nutné jej nejdříve zastavit. V obou případech můžete postupovat podle návodu z předchozího dílu.

Instalace Hunspell slovníků

Předpokládejme, že jsme Elasticsearch nainstalovali do adresáře elasticsearch-0.90.3. Ten obsahuje adresář config. V něm je potřeba vytvořit adresář hunspell, v tom dále vytvoříme adresáře cs_CZ a sk_SK. Do těch je potřeba nakopírovat patřičné slovníkové soubory. Výsledná souborová struktura by měla vypadat následovně:

elasticsearch-0.90.3  # <- zde byl nainstalován Elasticsearch
  ∟ config
      ∟ hunspell  
          ∟ cs_CZ
              ⊢ cs_CZ.aff
              ⊢ cs_CZ.dic
              ∟ settings.yml
          ∟ sk_SK
              ⊢ sk_SK.aff
              ⊢ sk_SK.dic
              ∟ settings.yml

Soubory settings.yml byly vytvořeny následovně:

echo 'strict_affix_parsing: false' > settings.yml

V některých případech se může stát, že affixové soubory používají „nestandardní“ zápis pro určitá pravidla. Nastavení strict_affix_parsing řídí, jak má Lucene v takovém případě pravidlům rozumět. Slovníky, které v našem případě používáme, nestandardní zápis sice nepoužívají, ale není na škodu na tohle nastavení upozornit.

Další možnosti konfigurace jsou popsány v dokumentaci hunspell tokenfilter.

Look Ma, I’m a Hacker!!!

Abychom vůbec mohli Elasticsearch teď nastartovat, bude třeba udělat jeden „hot-fix“ v souboru českého slovníku cs_CZ.aff. Jinak start Elasticsearch ukončí Java Exception.

Could not load hunspell dictionary [cs_CZ]
java.util.regex.PatternSyntaxException: Unclosed character class near index 45
.*[aeiouyáéíóúýůěr][^aeiouyáéíóúýůěrl][^aeiouy
                                             ^

Na řádku 2119 je toiž suffixové pravidlo s neúplným regexp výrazem. Celý řádek by měl správně obsahovat následující pravidlo:

SFX A   nout        l          [aeiouyáéíóúýůěr][^aeiouyáéíóúýůěrl][^aeiouyáéíóúýůěrl]nout

„Drsoni“ by mohli udělat něco jako:

# ----------------------------------
# 1) convert .aff file to UTF-8
# 2) do `sed` magic
# 3) convert also .dic file to UTF-8
# 4) cleanup
# ----------------------------------
$ iconv -f ISO-8859-2 -t UTF-8 cs_CZ.aff > cs_CZ.aff.utf8
$ sed "1s/ISO8859-2/UTF-8/" cs_CZ.aff.utf8 > cs_CZ.aff.utf8.1
$ sed "2119s/$/áéíóúýůěrl\]nout/" cs_CZ.aff.utf8.1 > cs_CZ.aff.utf8
$ iconv -f ISO-8859-2 -t UTF-8 cs_CZ.dic > cs_CZ.dic.utf8
$ rm cs_CZ.aff.utf8.1
$ mv cs_CZ.aff.utf8 cs_CZ.aff
$ mv cs_CZ.dic.utf8 cs_CZ.dic

Výsledkem je opravený soubor .aff a oba slovníkové soubory zkonvertované do kódováníUTF-8. Upravené kódování se bude hodit později, až budeme slovník rozšiřovat.

„Měkkoni“ si můžou oba upravené soubory stáhnou například z Lucene-4311.

Konfigurace hunspell token filtru pro češtinu a slovenštinu

Nyní Elasticsearch nastartujeme a vytvoříme konfiguraci custom analyzérů pro index i, který bude používat hunspell token filter.

curl -X DELETE 'localhost:9200/i'

curl -X PUT 'localhost:9200/i' -d '{
  "settings" : {
    "analysis" : {
      "analyzer" : {
        "cestina_hunspell" : {
          "type" : "custom",
          "tokenizer" : "standard",
          "filter" : [ "stopwords_CZ", "cs_CZ", "lowercase", "stopwords_CZ", "remove_duplicities" ]
        },
        "slovenstina_hunspell" : {
          "type" : "custom",
          "tokenizer" : "standard",
          "filter" : [ "stopwords_SK", "sk_SK", "lowercase", "stopwords_SK", "remove_duplicities" ]
        }
      },
      "filter" : {
        "stopwords_CZ" : {
          "type" : "stop",
          "stopwords" : [ "právě", "že", "_czech_" ],
          "ignore_case" : true
        },
        "stopwords_SK" : {
          "type" : "stop",
          "stopwords" : [ "ako", "sú", "a", "v" ],
          "ignore_case" : true
        },
        "cs_CZ" : {
          "type" : "hunspell",
          "locale" : "cs_CZ",
          "dedup" : true,
          "recursion_level" : 0
        },
        "sk_SK" : {
          "type" : "hunspell",
          "locale" : "sk_SK",
          "dedup" : true,
          "recursion_level" : 0
        },
        "remove_duplicities" : {
          "type" : "unique",
          "only_on_same_position" : true
}}}}}'

Konfigurace vytváří analyzéry pro češtinu (cestina_hunspell) a slovenštinu (slovenstina_hunspell). Oba používají podobný seznam token filtrů. Pojdmě se podrobněji podívat, jakou úlohu jednotlivé token filtry při analýze mají.

Hunspell token filter

V konfiguraci hunspell token filtru pro češtinu (cs_CZ) a slovenštinu (sk_SK) bych upozornil na dvě nastavení:

  • recursion_level je nastaven na hodnotu 0.

Můžete vyzkoušet i jiné nezáporné hodnoty recursion_level. Defaultně je nastavena hodnota 2, což není podle mě ideální (i když určitou logiku to má). Pro češtinu (a předpokládám i pro slovenštinu) budete s rostoucí hodnotou dostávat horší výsledky, navíc platí, že čím kratší vstupní token, tím horší výsledky lze očekávat.

Horším výsledkem mám na mysli to, že kromě správného základního slova může být vygenerováno jedno či více jiných základních slov, které nemají shodný kořen se vstupním tokenem (falešná základní slova). Z pohledu fulltextového vyhledávání je takové chování zcela nevhodné.

Experimenty s nenulovou hodnotou recurion_level ponechávám na čtenáři a nebudu se touto problematikou v článku příliš zabývat.

  • dedup je aktivní. Používá se pro odstranění duplicitních výstupních tokenů.

Obecně lze použít různá affixová pravidla pro odvození stejného základního slova. Řečeno jinak: k jednomu základnímu slovu se lze dostat různou cestou. V takovém případě jsou obě (identická) základní slova výstupem hunspell token filtru (obzvláště při nenulových hodnotáchrecursion_level se pravděpodobnost tohoto jevu zvyšuje). Tyto duplicitní tokeny je užitečné odfiltrovat.

Remove duplicities

Ačkoliv je pro hunspell token filter volba dedup aktivní, narazil jsem na případy, kdy mi nefungovala podle očekávání (zatím nevím, jestli je chyba na mojí straně, nebo v Lucene implementaci). Z toho důvodu jsem si pojistil odstranění duplicitních tokenů pomocí unique token filteru.

Stopwords

Stopwords pro češtinu jsme definovali jako rozšíření defaultního seznamu slov, který je součástí Lucene. Na defaultní seznam se odkazujeme pomocí _czech_ a přidali jsme další dvě slova: právěže.

Protože Lucene zatím nenabízí defaultní stopwords pro slovenštinu, musíme celý seznam slov nadefinovat v konfiguraci. Pro naše potřeby bude stačit malý výběr inspirovaný tímto seznamem.

Další možnosti konfigurace jsou popsány v dokumentaci stop tokenfilter.

Pořadí token filtrů

Token filtry jsou na jednotlivé tokeny aplikovány v tom pořadí, v jakém jsou uvedeny v analyzéru. Proč jsem zvolil právě následné pořadí?

[ "stopwords_CZ", "cs_CZ", "lowercase", "stopwords_CZ", "remove_duplicities" ]
  • Aplikace hunspell token filtru může být drahá operace. Pro každý vstupní token se provádí řada lookup operací do paměti a i když je lookup operace rychlá, stále to je operace, kterou je lepší neprovádět vůbec, je-li to možné. Proto nejdříve odstraníme co nejvíce stopwords.
  • Následuje aplikace hunspell token filtru. Všimněte si, že vstupní tokeny doposud nebyly převedeny na malá písmena. To je proto, že hunspell slovníky mohou být „case-sensitive“ (například u jmen a zkratek). Má to však i své důsledky. Podrobnosti dále.
  • Převedeme tokeny na malá písmena.
  • Podruhé (!) aplikujeme tentýž stopwords filter. Velmi totiž záleží na konkrétním seznamu stopwords slov. Například defaultní česká stopwords neobsahují všechny slovní tvary. Kupříkladu obsahuje slovo který ale už neobsahuje slovo kterému (mimochodem, tuto skutečnost by si měli uvědomit všichni, kdo používají CzechAnalyzer představený v minulém díle). Řešením je buď rozšíření stopwords, vytvoření zcela nového seznamu nebo si elegantně vypomůžeme hunspell slovníkem a druhým průchodem stopwords filtrem (což je případ konfigurace analýzy v této ukázce).
  • Na závěr odstraníme případné duplicitní tokeny.

Takto definovaný seznam token filtrů berte pochopitelně jen jako ukázku. V praxi může být nastavení analýzy mnohem sofistikovanější.

Komu se „kouzla“ se stopwords zdají příliš krkolomná, toho bych rád upozornil nacommons terms query a doporučuji přečíst si tento článek. Stopwords token filtrům se dá vyhnout.

Ještě si dovolím malou poznámku k defaultnímu seznamu českých stopwords. Skutečnost, že seznam neobsahuje všechny slovní tvary, může vypadat jako nedostatek, ale pokud vím, tak Lukáš Zapletal (autor českých stopwords pro Lucene) používal Ispell slovník pro analýzu českých textů, a to by mohl být důvod, proč mu neúplný seznam nevadil (stejně tak to nevadí teď nám, pokud stopwords token filteru předchází hunspell token filter).

Hunspell token filter v akci!

Nejrychlejší cesta, jak vyzkoušet funkci hunspell token fitleru je přes analyze API.

Příklad #1: „Právě se mi zdálo, že se kolem okna něco mihlo.“

curl 'localhost:9200/i/_analyze?analyzer=cestina_hunspell&pretty=true' -d 'Právě se mi zdálo, že se kolem okna něco mihlo.'

Po odstranění stopwords: [právěsemiže] a aplikaci hunspell token filteru jsou výstupem následující tokeny:

  • zdát
  • kolemkolo
  • okno
  • něco
  • mihnout

Všimněte si, že z tokenu kolem se nám zrodily tokeny dva (oba na stejné pozici), neboť se také může jednat o Instrumentál, tedy 7. pád podstatného jména kolo.

Příklad #2: „On se nejvíc zajímal o nejnesnesitelnější způsob, jak co nejvíce nezlobit.“

curl 'localhost:9200/i/_analyze?analyzer=cestina_hunspell&pretty=true' -d 'On se nejvíc zajímal o nejnesnesitelnější způsob, jak co nejvíce nezlobit.'
  • víc
  • zajímat
  • snesitelnější
  • způsobzpůsoba
  • zlobit

Zde vidíme, že hunspell umí odstranit i předpony (ne, nej, nejne). Na konci článku se k této vlastnosti ještě vrátíme, neboť to trochu zlobí. Všimněte si, že zatímco z nejvíc se urodilovícnejvíce zmizelo úplně. Proč? Protože více je v seznamu stopwords, kdežtovíc není.

A co je to způsoba? No, to asi bude ten … přechodník. To je něco jako Werichův Příchozí Vejda, nebo ten druhej – Úprka (ne, to už je vlastně jméno, a k těm se dostaneme později také). Nebo … že by to byl tento způsoba?

Příklad #3: Zkusíme slovenštinu – „Ako vieme, lacné parochne a bokombrady sú klasikou v mužskej móde.“

curl 'localhost:9200/i/_analyze?analyzer=slovenstina_hunspell&pretty=true' -d 'Ako vieme, lacné parochne a bokombrady sú klasikou v mužskej móde.'
  • vedieť
  • lacný
  • parochňa
  • bokombrada
  • klasika
  • mužský
  • móda

Tak schválně, že nevíte, co je to parochňa nebo bokombrada?

Rozšíření slovníku

Doposud jsme si ukázali, že hunspell slovník je načten z jediného souboru s příponou .dic. Výsledný slovník však může být vytvořen z více souborů. Elasticsearch se pokusí načíst a sloučit všechny soubory s příponou .dic, které v adresáři pro daný jazyk nalezne.

Pořadí, v jakém jsou soubory načteny není IMO specifikován, takže pozor v případě, kdy se pokusíte nějaké slovo z jednoho souboru „přepsat“ novou definicí ve druhém souboru. Kódování všech souborů s příponou .dic musí být stejné a je specifikováno prvním příkazem v souboru .aff.

Soubor s příponou .aff může být pouze jediný, pokud byste náhodou měli v adresáři pro daný jazyk více souborů s touto příponou, Elasticsearch načte („náhodně“) pouze jeden z nich.

V naší ukázce se pokusíme existující slovník obohatit o pár moravských výrazů.

Vytvořte soubor elasticsearch-0.90.3/config/hunspell/cs_CZ/morava.dic s následujícím obsahem:

9
čupnout/JTN
čupnutí/SN
čupnutý/YRN
čupět/AN
čupění/SN
čupící/YN
zavazející/YN
zavazet/AJTN
zavazení/SN

Číslo na prvním řádku udává počet slov ve slovníku (Elasticsearch však touto hodnotu nepoužívá). Kódování souboru musí být ve shodě s prvním příkazem v souboru .aff. V našem případě tedy UTF-8 (viz. odstavec „Look Ma, I’m a Hacker!!!“, kde jsme kódování takto změnili).

Připravený soubor si můžete stáhnout zde nebo přes wget (nezapomeňte jej uložit do správného adresáře).

wget https://gist.github.com/lukas-vlcek/6341611/raw/485078f9bb5dfbbca16fd7529e21bf64d6cac9e1/morava.dic

Nyní restartujte Elasticsearch process.

Příklad: „Ty, zavazíš mi ve výhledu, čupni si!“. Tak jsem si čupnul.

curl 'localhost:9200/i/_analyze?analyzer=cestina_hunspell&pretty=true' -d '"Ty, zavazíš mi ve výhledu, čupni si!". Tak jsem si čupnul.'
  • zavazet
  • výhled
  • čupnout
  • čupnout

Modifikace existujících slovníků či vytváření nových a doplňujících slovníků je podle mě jeden z nejzajímavějších přínosů hunspell token filtru.

Vady na kráse

Až potud funguje hunspell token filter velmi nadějně. Dokonce lépe, než jsem vůbec očekával. Když jsem totiž začal na článku pracovat, tak ještě nešlo konfigurovat hodnotu  recursion_level a hunspell byl pro češtinu prakticky nepoužitelný (jediná cesta vedla přes vlastní rozšíření Lucene nebo implementaci vlastního plugin pro Elasticsearch). Přesto má hunspell implementace stále pár vad na kráse.

Ne, Nej, Nejne … a další předpony

S českým i slovenským Hunspell slovníkem lze rozpoznat několik prefixů, které mají být odstraněny, aby bylo možné slovo správně převést na základní tvar. Typickým příkladem jsou předpony: „ne“, „nej“ a „nejne“. Bohužel současná Lucene implementace má v tomto směru nepříjemná omezení.

Hunspell token filter například správně převede slovo „nezavazet“ na „zavazet“, zároveň správně rozpozná slova, u kterých se tyto předpony odstranit nedají (např. „nemocnice“, „nejapný“). Problém však nastává u slov, u kterých je potřeba odstranit předponu a zároveň upravit či odstranit i příponu. Například „nezavazela„. Aby hunspell převedl i takové slovo na „zavazet“, už by bylo potřeba dvou průchodů algoritmem, tedy museli bychom nastavitrecursion_level na hodnotu 1. A to v mnoha případech nebude dělat dobrotu s ohledem na falešná základní slova, která mohou být generována ze slov v textu.

Možná by se dal najít nějaký šikovný workaround, ale správným řešením bude jedině úprava Lucene implementace.

Case-sensitivity

Hunspell slovníky můžou být case-sensitive. Například český slovník rozlišuje jména, zkratky… atd. Umí například z „Karla Čapka“ udělat „Karel Čapek“. Takže pokud budete hledat cokoli o „Čapkovi“, „Čapcích“, „Čapkům“ atd, tak to může dopadnout dobře. To je výborná vlastnost.

Problém ale nastává, pokud chcete zároveň použít i lowercase token fitler a umožnit tak uživatelům hledat i přes „čapek“, „čapka“, „čapkovi“, „čapkcích“, „čapkům“ … pokud totiž odstraníte velká písmena dřív, než se token dostane do hunspell token filteru, tak o tuto vlastnost přicházíte. Na druhou stranu, pokud aplikujete lowercase až po hunspellu, tak máte problém s velkými písmeny na začátku vět (takové tokeny nebudou správně analyzované).

Příklad #1: „Potkal jsem Karla Čapka“ – lowercase až za hunspellem.

curl 'localhost:9200/i/_analyze?analyzer=cestina_hunspell&pretty=true' -d 'Potkal jsem Karla Čapka'

Výstupem jsou následující tokeny:

  • potkal (tenhle token není převeden na základní tvar)
  • karlakarel (ženská a můžská varianta téhož jména)
  • čapek

Příklad #2: „Potkal jsem Karla Čapka“ – lowercase před hunspellem. (V našem případě nebudeme definovat nový analyzátor, ale převedeme celou větu do lowercase „ručně“.)

curl 'localhost:9200/i/_analyze?analyzer=cestina_hunspell&pretty=true' -d 'potkal jsem karla čapka'

Výstupem jsou následující tokeny:

  • potkat (teď je to už správně)
  • karla (ostaní odvozené slovní tvary od karel nebudou „matchovat“)
  • čapka (totéž platí zde, navíc, čapka je také pokrývka hlavy)

Zatím jsem moc nepřemýšlel nad tím, jestli pro tento problém existuje jednoduché řešení, ale domnívám se, že jednou z cest by mohlo být přímé rozšíření Lucene implementace hunspellu tak, aby umožnil aplikovat lowercase interně. To znamená, že vstupem by byl token před aplikací lowercase a pokud by hunspell nedokázal takové slovo převést na základní tvar, zkusil by interně aplikovat lowercase a pak znovu převést slovo na základní tvar. Takové řešení by zřejmě vyřešilo oba problémy se jmény i tokeny na začátku vět.

Pokud jde o workaround, nabízí se možnost rozšíření slovníku tak, aby slova, která obsahují velká písmena, existovala i ve variantě v lowercase. Takový slovník by nemělo být obtížné vytvořit. Z pohledu analýzy by to znamenalo aplikovat lowercase před hunspellem.

Lucene-5057

Další problém se jmenuje: „Hunspell stemmer generates multiple tokens“ a má svůj JIRA ticket: LUCENE-5057.

Už jsme viděli, že hunspell token filter může pro jeden vstupní token vygenerovat více výstupních tokenů (i když hodnota recursion_level je 0). Například:

  • kolem -> kolemkolo
  • den -> dendnadno

Je to užitečná a správná vlastnost. Zádrhel však může nastat, pokud chcete při hledání použít v uživatelském dotazu operátor AND.

Ukážeme si příklad. Nejdříve vytvoříme mapping a následně zaindexujeme dokument:

curl -X PUT 'localhost:9200/i/t/_mapping' -d '
{
  "t" : {
    "properties" : {
      "text" : { "type" : "string", "analyzer" : "cestina_hunspell"}
    }
  }
}'

curl -X POST 'localhost:9200/i/t?refresh=true' -d '
{ "text" : "Auto má malé kolo" }'

Pro tento dokument jsou zaindexovány následující tokeny:

  • auto
  • malý
  • kolo

A teď zkusíme vyhledávat podle dotazu „auto s malým kolem“. Aby se problém projevli, použijeme match query:

curl -X GET 'localhost:9200/i/t/_search' -d '{
  "query" : {
    "match" : {
      "text" : {
        "query" : "auto s malým kolem",
        "operator" : "AND"
}}}}'

Takové query dokument nenajde. Je to proto, že se hledá dokument, který obsahuje všechny následující termy: [ automalýkolemkolo]. V tom našem dokumentu chybí termkolem.

Při použití query_string je dokument už nalezen správně.

curl -X GET 'localhost:9200/i/t/_search' -d '{
  "query" : {
    "query_string" : {
      "query" : "auto s malým kolem",
      "default_field" : "text",
      "default_operator" : "AND"
}}}'

Na závěr

Cesta k dokonalému vyhledávání zde nekončí. Dalším krokem může být integrace pokročilého lematizátoru. Na akademické půdě existují zajímavé implementace českých lematizátorů, např. Morfo. Určitě by nebylo marné prozkoumat, jak jej lze integrovat s Lucene. A podívat se blíže na Apache openNLP by také nebylo na škodu.

U dalších článků na téma Lucene a Elasticsearch se těším nashledanou.

Komentáře

Subscribe
Upozornit na
guest
10 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
jjjjj

skvely clanok, dakujem. mam vsak otazku, ako je to v takomto pripade keby hladam text bez diakritiky?

Miroslav Hruška

Narazil jsem na problém s diakritikou, konkrétně se jedná například o výraz „dámský“ vs „damsky“. Můj analyzer vypadá zhruba takto:

    "cestina": {
      "type": "custom",
      "char_filter": [
        "html_strip"
      ],
      "tokenizer": "standard",
      "filter": [
        "stopwords_CZ",
        "lowercase",
        "lemmagen_cs_CZ",
        "stopwords_CZ",
        "icu_folding",
        "remove_duplicities"
      ]
    }

Slovníku (hunspell ani lemmagen) se ale nedaří výrazy spojit, pokud provedu příkaz:

curl -X POST 'localhost:9200/products_v5/_analyze?analyzer=cestina' -d 'dámská dámské dámským damskym damska damske'

S výsledkem:

{"tokens": [
{
  "token": "damsky",
  "start_offset": 0,
  "end_offset": 6,
  "type": "<ALPHANUM>",
  "position": 0
},
{
  "token": "damsko",
  "start_offset": 30,
  "end_offset": 36,
  "type": "<ALPHANUM>",
  "position": 1
},
{
  "token": "damske",
  "start_offset": 37,
  "end_offset": 43,
  "type": "<ALPHANUM>",
  "position": 2
}

]
}

Žádný slovník to prostě nezvládne převést na správný tvar a při vyhledávání je to problém. Máte někdo zkušenosti jak tohle řešit? Do vlastního slovníku se mi moc nechce, protože bych musel neustále reinsexovat X tisíc produktů a upravovat konfigurace, vytvářet nové indexy apod …

Díky

Jan Pobořil

Vypadá to, že jste to docela vyladil. Nechcete výslednou českou konfiguraci i se slovníky zveřejnit na GitHubu?

Vítek

Zkusil jsem popisovanou konfiguraci nad ElasticSearch 1.2.1, který aktuálně používáme a nedaří se mi to rozchodit. Když jsem si nainstaloval 0.9.0 tak samozřejmě funguje. Nevíte, Lukáši, jaké zásadní změny se v ES přihodily a co je třeba upravit? Zatím se mi nedaří nic rozumného dohledat.

Vítek

Díky, zítra si s tím budu hrát, o výsledky se do mailu podělím.

Birkof

Zdravím, prosím co znamenají ta označení /JTN, /SN, /YRN, /AN …. Respektive kde k nim dohledat nějaké informace. Díky

Martin Hassman

To je z formátů oněch slovníků. Když si stáhnete české vloníky viz sekce České slovníky z OpenOffice, tak se skládají z dvou důležitých souborů:

cs_CZ.aff # <- affixová pravidla
cs_CZ.dic # <- seznam slov

Aff obsahuje definice oněch JTN, SN, YRN, což jsou vzásadě skloňovací pravidla (možné předpony, přípony a koncovky, které to slovo může mít).

Když se do něj teď dívám, tak hned na začáku je definice N, vypadá takhle:

PFX N Y 1
PFX N   0           ne         .

Neznám přesně ten formá, ale definuje to předponu ne-. A znamená to, že ke všem slovům, které v souboru .dif budou mít /N lze přidat ne-.

Ještě pro ukázku přiložím definici pro R, ta je:

SFX R Y 25
SFX R   ý           ě          [bpvmfdnt]ý
SFX R   ý           e          [lsz]ý
SFX R   hý          ze         [^c]hý
SFX R   chý         še         chý
SFX R   rý          ře         rý
SFX R   ý           y          [sc]ký
SFX R   ký          ce         [^sc]ký
SFX R   í           ě          [dnt]í
SFX R   í           e          [zž]í
SFX R   í           eji        čí
SFX R   ší          i          [eě]jší
SFX R   0           ě          [bdfmnptvw]
SFX R   g           ze         g
SFX R   ch          še         ch
SFX R   h           ze         [^c]h
SFX R   k           ce         k
SFX R   0           e          [cčjlřsšzž]
SFX R   r           ře         r
SFX R   o           ě          [bdfmnptvw]o
SFX R   go          ze         go
SFX R   cho         še         cho
SFX R   ho          ze         [^c]ho
SFX R   ko          ce         ko
SFX R   o           e          [cčjlřsšzž]o
SFX R   ro          ře         ro

Ta definuje koncovky kategorie R. Čili všem slovům, ktéré mají v .dic slovníku /NR lze přidat předpony kategorie N a přípony kategorie R.

V článku je příklad čupnutý/YRN, takže k němu patří i nečupnutý, čupnutě, nečupnutě atd.

Dokumentace asi tady https://www.systutorials.com/docs/linux/man/4-hunspell/#lbAQ

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.