Indexování a fulltextové vyhledávání v dokumentech s Elasticsearch

27. 9. 2016
Doba čtení: 20 minut

Sdílet

Popíšeme si snadný způsob konfigurace fulltextového vyhledávacího systému s využitím nástroje Elasticsearch. Pro indexaci dokumentů uložených v různých formátech je použita knihovna Apache Tika.

Obsah

1. Indexování a fulltextové vyhledávání v dokumentech s využitím nástroje Elasticsearch

2. Lokální instalace nástroje Elasticsearch

3. Instalace pluginu určeného pro indexování příloh (dokumentů) v různých formátech

4. Základní nastavení mapování

5. Přidání dokumentů do fulltextové databáze

6. Vyhledávání dokumentů na základě hledané fráze

7. První vylepšení indexování: uložení metadat dokumentů

8. Vyhledávání v dokumentech se zobrazením základních metadat

9. Druhé vylepšení indexování: uživatelsky definovaná metadata

10. Vyhledávání v dokumentech se zobrazením uživatelských metadat

11. Omezení počtu vyhledaných dokumentů i počtu zobrazených řetězců se zvýrazněním

12. Jednoduché GUI k vyhledávacímu Engine naprogramované v jazyce Clojure

13. Spuštění a použití GUI

14. Odkazy na Internetu

1. Indexování a fulltextové vyhledávání v dokumentech s využitím nástroje Elasticsearch

Mezi jeden z nejznámějších a pravděpodobně i nejrozšířenějších nástrojů určených pro fulltextové vyhledávání patří fulltextový vyhledávač nazvaný Elasticsearch. Ten je, společně s podpůrnými knihovnami a některými pluginy, naprogramovaný kompletně v Javě, což mj. znamená možnost jeho snadné instalace na různých platformách a operačních systémech; vzácností nejsou ani heterogenní clustery složené z různých platforem. Mezi přednosti Elasticsearche patří velmi dobrá škálovatelnost (tou se však dnes nebudeme zabývat), vysoká dostupnost spočívající v automatickém sledování a řízení jednotlivých uzlů v clusteru (taktéž se jedná o téma přesahující rozsah tohoto článku) a v neposlední řadě i vysoká rychlost vyhledávání, která může ty uživatele, kteří jsou zklamáni z výkonu Javy například u některých desktopových aplikací, velmi příjemně překvapit.

Elasticsearch dokáže indexovat čistě textová data, popř. strukturované údaje složené opět z textových dat. To je sice pěkné, ovšem v reálném světě se v mnoha případech setkáme s dokumenty uloženými do formátu PDF, Open/Libre Office, MS Office, s knihami ve formátu Epub a samozřejmě i s dokumenty používajícími značkovací jazyky HTML a XML. I pro indexaci a vyhledávání v takových dokumentech je možné Elasticsearch použít, ovšem je nutné tento nástroj doplnit o vhodný zásuvný modul (plugin), který dokáže se soubory různých typů více či méně úspěšně pracovat a parsovat je. Tento plugin skutečně existuje a jmenuje se mapper-attachment. Interně se pro rozpoznání formátu dokumentů a pro jejich indexaci používá knihovna nazvaná Apache Tika. Na stránce http://tika.apache.org/1.13/for­mats.html jsou dostupné informace o podporovaných formátech; užitečná je jak podpora formátů používaných v balíčcích „office“, tak i podpora pro zmíněný PDF a Epub.

V dalším textu se ve stručnosti seznámíme s:

  1. Lokální instalací nástroje Elasticsearch
  2. Instalací pluginu mapper-attachment
  3. Konfigurací mapování a indexací dokumentů
  4. Vyhledávání v dokumentech se zvýrazněním vyhledaných výrazů
  5. Použitím automaticky zjišťovaných metadat
  6. Použitím uživatelsky nastavených metadat
  7. Jednoduchou aplikací s webovým GUI, která komunikuje s Elasticsearchem pomocí RESTful API

Tip: naučte se ELK na našich školeních ELK Stack školení a Kurz ELK stack – instalace a provoz

2. Lokální instalace nástroje Elasticsearch

Důležité upozornění: nástroj Elasticsearch sice nalezneme v repositářích mnoha Linuxových distribucí, ovšem v tomto článku budeme předpokládat, že se používá jedna konkrétní verze 2.4.0. Je to důležitá informace, protože především dále popsaný plugin mapper-attachment se postupně vyvíjí a mění, takže příklady jsou upraveny tak, aby fungovaly právě v Elasticsearchi 2.4.0 a k němu příslušné verzi pluginu (zejména Elasticsearch 1.x.y se bude chovat odlišně!).

Lokální instalace Elasticsearche je velmi jednoduchá a vyžaduje vlastně pouze dva kroky. Předpokladem je, že máte v systému JVM alespoň verze 7. Pokud je JVM nainstalována (lze zkontrolovat například přes java -version), postačuje si do pracovního adresáře stáhnout archiv s verzí 2.4.0 a rozbalit ho:


wget https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/zip/elasticsearch/2.4.0/elasticsearch-2.4.0.zip
unzip elasticsearch-2.4.0.zip

Můžeme si zkusit Elasticsearch pustit, prozatím bez dat:


cd elasticsearch-2.4.0/bin
./elasticsrach

Na standardní výstup by se měly vypsat přibližně následující zprávy (jen se nelekněte, že jméno vlákna je zvoleno náhodně):


[2016-09-25 21:30:56,815][INFO ][node                     ] [Alexander Goodwin Pierce] version[2.4.0], pid[4826], build[ce9f0c7/2016-08-29T09:14:17Z]
[2016-09-25 21:30:56,816][INFO ][node                     ] [Alexander Goodwin Pierce] initializing ...
[2016-09-25 21:30:57,393][INFO ][plugins                  ] [Alexander Goodwin Pierce] modules [lang-groovy, reindex, lang-expression], plugins [], sites []
[2016-09-25 21:30:57,443][INFO ][env                      ] [Alexander Goodwin Pierce] using [1] data paths, mounts [[/ (/dev/sda1)]], net usable_space [98.5gb], net total_space [113.5gb], spins? [no], types [ext4]
[2016-09-25 21:30:57,443][INFO ][env                      ] [Alexander Goodwin Pierce] heap size [990.7mb], compressed ordinary object pointers [true]
[2016-09-25 21:30:57,443][WARN ][env                      ] [Alexander Goodwin Pierce] max file descriptors [4096] for elasticsearch process likely too low, consider increasing to at least [65536]
[2016-09-25 21:30:59,367][INFO ][node                     ] [Alexander Goodwin Pierce] initialized
[2016-09-25 21:30:59,367][INFO ][node                     ] [Alexander Goodwin Pierce] starting ...
[2016-09-25 21:30:59,441][INFO ][transport                ] [Alexander Goodwin Pierce] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300}, {[::1]:9300}
[2016-09-25 21:30:59,446][INFO ][discovery                ] [Alexander Goodwin Pierce] elasticsearch/uQfXGZ70QNah5K387Oqu-g
[2016-09-25 21:31:02,522][INFO ][cluster.service          ] [Alexander Goodwin Pierce] new_master {Alexander Goodwin Pierce}{uQfXGZ70QNah5K387Oqu-g}{127.0.0.1}{127.0.0.1:9300}, reason: zen-disco-join(elected_as_master, [0] joins received)
[2016-09-25 21:31:02,538][INFO ][http                     ] [Alexander Goodwin Pierce] publish_address {127.0.0.1:9200}, bound_addresses {127.0.0.1:9200}, {[::1]:9200}
[2016-09-25 21:31:02,538][INFO ][node                     ] [Alexander Goodwin Pierce] started
[2016-09-25 21:31:02,579][INFO ][gateway                  ] [Alexander Goodwin Pierce] recovered [0] indices into cluster_state

Ve zprávách je mj. uvedeno, že na URL 127.0.0.1:9200 nebo též localhost:9200 je možné s Elasticsearchem komunikovat přes RESTful API. Ostatně můžeme si to vyzkoušet:


$ curl localhost:9200
 
{
  "name" : "Alexander Goodwin Pierce",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.4.0",
    "build_hash" : "ce9f0c7394dee074091dd1bc4e9469251181fc55",
    "build_timestamp" : "2016-08-29T09:14:17Z",
    "build_snapshot" : false,
    "lucene_version" : "5.5.2"
  },
  "tagline" : "You Know, for Search"
}

Poznámka: opět platí, že jméno (zde „Alexander Goodwin Pierce“) se s velkou pravděpodobností bude na vašem počítači lišit.

3. Instalace pluginu určeného pro indexování příloh (dokumentů) v různých formátech

Druhým důležitým krokem je instalace pluginu, který se bude starat o parsování a indexování dokumentů uložených v různých formátech. Nejprve pro jistotu běžící Elasticsearch ukončíme (Ctrl+C) a nainstalujeme plugin k tomu připraveným skriptem:


cd elasticsearch-2.4.0/bin
./plugin install mapper-attachments

Po chvíli by se měla zobrazit zpráva o úspěšné (doufejme!) instalaci pluginu. Pokud se nyní pokusíme Elasticsearch znovu spustit, objeví se ve zprávách nový řádek, který je v následujícím výpisu zvýrazněn:


./elasticsrach
 
[2016-09-26 20:17:39,804][INFO ][node                     ] [Anti-Phoenix Force] version[2.4.0], pid[9150], build[ce9f0c7/2016-08-29T09:14:17Z]
[2016-09-26 20:17:39,804][INFO ][node                     ] [Anti-Phoenix Force] initializing ...
[2016-09-26 20:17:40,572][INFO ][plugins                  ] [Anti-Phoenix Force] modules [lang-groovy, reindex, lang-expression], plugins [mapper-attachments], sites []
[2016-09-26 20:17:40,604][INFO ][env                      ] [Anti-Phoenix Force] using [1] data paths, mounts [[/ (/dev/sda1)]], net usable_space [98.5gb], net total_space [113.5gb], spins? [no], types [ext4]
[2016-09-26 20:17:40,604][INFO ][env                      ] [Anti-Phoenix Force] heap size [990.7mb], compressed ordinary object pointers [true]
[2016-09-26 20:17:40,605][WARN ][env                      ] [Anti-Phoenix Force] max file descriptors [4096] for elasticsearch process likely too low, consider increasing to at least [65536]
[2016-09-26 20:17:42,479][INFO ][node                     ] [Anti-Phoenix Force] initialized
[2016-09-26 20:17:42,479][INFO ][node                     ] [Anti-Phoenix Force] starting ...
[2016-09-26 20:17:42,540][INFO ][transport                ] [Anti-Phoenix Force] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300}, {[::1]:9300}
[2016-09-26 20:17:42,546][INFO ][discovery                ] [Anti-Phoenix Force] elasticsearch/rcWn1VueQpaPlnGDpCO8xg
[2016-09-26 20:17:45,606][INFO ][cluster.service          ] [Anti-Phoenix Force] new_master {Anti-Phoenix Force}{rcWn1VueQpaPlnGDpCO8xg}{127.0.0.1}{127.0.0.1:9300}, reason: zen-disco-join(elected_as_master, [0] joins received)
[2016-09-26 20:17:45,622][INFO ][http                     ] [Anti-Phoenix Force] publish_address {127.0.0.1:9200}, bound_addresses {127.0.0.1:9200}, {[::1]:9200}
[2016-09-26 20:17:45,622][INFO ][node                     ] [Anti-Phoenix Force] started
[2016-09-26 20:17:45,768][INFO ][gateway                  ] [Anti-Phoenix Force] recovered [0] indices into cluster_state

Poznámka: pokud se zvýrazněný řádek neobjeví, nebudou dále popisované příklady fungovat a je nutné zjistit, proč instalace selhala. Zejména u starších verzí Elasticsearchu se to v některých případech stávalo, takže je kontrola důležitá.

4. Základní nastavení mapování

Před vlastním parsováním a indexací dokumentů je nutné Elasticsearch nakonfigurovat a nastavit mapování. Veškerá komunikace s Elasticsearchem probíhá přes RESTful API, což mj. znamená, že si vystačíme s nástrojem curl. Existují sice i nadstavby pro webové prohlížeče či integrovaná vývojová prostředí, které mohou práci s RESTful API zjednodušit, nicméně prozatím si skutečně vystačíme s velmi flexibilní příkazovou řádkou. Nastavení mapování provedeme následujícím způsobem:


curl -XPUT "localhost:9200/docs/"
curl -XPUT "localhost:9200/docs/document/_mapping" -d @create_mapping.json

Poznámka: zkontrolujte, zda Elasticsearch odpověděl stylem {acknowledged a nikoli chybovým hlášením.

Druhé volání REST API do Elasticsearche přenáší obsah JSON souboru nazvaného create_mapping.json. V tomto souboru je definováno, že se mají indexovat přílohy (zpracovávané pluginem mapper-attachment) a při indexování se má interně vytvořit datová struktura umožňující rychlé hledání slov a sousloví v parsovaných dokumentech:


{
  "document" : {
    "properties" : {
      "file" : {
        "type" : "attachment",
        "fields" : {
          "content" : { "type":"string","term_vector":"with_positions_offsets", "store":"yes" }
        }
      }
    }
  }
}

Interně se zpracovávané soubory nejdříve převedou do textové podoby:

A následně se pro jednotlivá slova vytvoří (přibližně) následující struktura, která je interně uložena efektivním způsobem umožňujícím rychlé vyhledávání a současně i poměrně malé nároky na diskový prostor:

Více informací se můžete v případě zájmu dozvědět na http://blog.jpountz.net/pos­t/41301889664/putting-term-vectors-on-a-diet, pro samotné provozování Elasticsearche to však není nutné.

5. Přidání dokumentů do fulltextové databáze

Nyní nastává důležitý okamžik – začneme do fulltextové databáze přidávat dokumenty. Ty se musí přenášet přes RESTful API ve formátu JSON. To zdánlivě představuje problém, protože například PDF je binární formát, který může obsahovat všechny znaky (včetně znaku pro konec souboru, konec řetězce apod.). Aby se předešlo zmatkům, vyžaduje plugin mapper-attachment, aby se obsah všech souborů před přenosem zakódoval s použitím Base64. Předností je, že při použití Base64 se binární data převedou na sekvenci znaků A-Z, a-z, 0–9, + a / (navíc ještě =), takže se zde nevyskytují žádné potenciálně problematické znaky, které by mohly způsobit chybu při zpracování JSONu na straně Elasticsearche. Pro zjednodušení přidávání souborů do databáze lze použít následující skript nazvaný add_file.sh:

#!/bin/sh
 
coded=`cat $1 | perl -MMIME::Base64 -ne 'print encode_base64($_)'`
json="{\"file\":\"${coded}\"}"
echo "$json" > file.json
curl -X POST "localhost:9200/docs/document/" -d @file.json

Příklad použití:

./add_file.sh index.html
./add_file.sh TopSecret.odt
./add_file.sh InstallationGuide.pdf

Poznámka: skript na disku ponechá soubor file.json určený pro studijní účely.

6. Vyhledávání dokumentů na základě hledané fráze

I vyhledávání v dokumentech je zabezpečeno přes RESTful API, přičemž specifikace, co a jak se má vyhledat, je přenesena v JSON:

#!/bin/sh
 
curl -XGET "http://localhost:9200/_search?pretty" -d @search.json

V souboru search.json je (pro testovací účely) ukázáno, jak hledat například slovo „network“:

{
  "fields" : [],
  "query" : {
    "match" : {
      "file.content" : "network"
    }
  },
  "highlight" : {
    "fields" : {
      "file.content" : {}
    }
  }
}

Elasticsearch odpoví – jak jinak – taktéž strukturou reprezentovanou v JSONu:

{
  "took" : 39,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.0817802,
    "hits" : [ {
      "_index" : "docs",
      "_type" : "document",
      "_id" : "AVdnaVFvU0d6k-GWgQp5",
      "_score" : 0.0817802,
      "highlight" : {
        "file.content" : [
            "configuration and administration of <em>network</em> interfaces, networks and <em>network</em> services in Red Hat Enterprise",
            "NetworkManager\n\n\n\t1.5. <em>Network</em> Configuration Using a Text User Interface (nmtui)\n\t1.6. <em>Network</em> Configuration Using",
            "Using NetworkManager's CLI (nmcli)\n\t1.7. <em>Network</em> Configuration Using the Command-Line Interface (CLI)",
            "(CLI)\n\t1.8. NetworkManager and the <em>Network</em> Scripts\n\t1.9. <em>Network</em> Configuration Using sysconfig Files\n\t1.10",
            "Interface Settings\n\t\t2.1.1. When to Use Static <em>Network</em> Interface Settings\n\t2.1.2. When to Use Dynamic" ]
      }
    }
}

Povšimněte si, že se vrátily jak informace o průběhu vyhledávání (čas v milisekundách atd.), tak i část indexovaného dokumentu, v němž je nalezený řetězec zvýrazněn značkou <em> (to lze ovšem změnit). Taktéž si všimněte, že se nalezlo celkem pět bloků v dokumentu s výskytem hledaného slova, takže pod klíčem file.content je uloženo pole, jehož obsah jsem pro jistotu zdůraznil přidáním konců řádků za jednotlivé řetězce (ty Elasticsearch kupodivu sám nepřidává).

7. První vylepšení indexování: uložení metadat dokumentů

K indexovaným dokumentům může plugin mapper-attachment přidat i některá automaticky zjištěná metadata, například typ souboru, délku dat, titulek (nemusí být dostupný vždy) či datum vytvoření (opět závisí na typu dokumentu). Pojďme si to vyzkoušet. Nejprve vytvoříme a nakonfigurujeme nové mapování. Povšimněte si použití docs2, aby nedošlo k chybovému hlášení, že mapování pro docs již existuje:

curl -XPUT "localhost:9200/docs2/"
curl -XPUT "localhost:9200/docs2/document/_mapping" -d @create_mapping2.json

Tentokrát je již soubor create_mapping2.json nepatrně složitější, protože v něm specifikujeme další atributy, které se mají ukládat do databáze:

{
  "document" : {
    "properties" : {
      "file" : {
        "type" : "attachment",
        "fields" : {
          "title" :          {"store" : "yes" },
          "date" :           {"store" : "yes" },
          "content" :        {"type":"string","term_vector":"with_positions_offsets", "store":"yes" },
          "content_type" :   {"store" : "yes"},
          "content_length" : {"store" : "yes"},
          "language" :       {"store" : "yes"}
        }
      }
    }
  }
}

Poznámka: většinou se při konfiguraci lze spolehnout na úzus, že hodnota „yes“ je totožná s hodnotou „true“.

8. Vyhledávání v dokumentech se zobrazením základních metadat

Pomocný skript add_file.sh si upravte, aby používal odlišné URL a následně zkuste do databáze přidat další dokumenty:

#!/bin/sh
 
coded=`cat $1 | perl -MMIME::Base64 -ne 'print encode_base64($_)'`
json="{\"file\":\"${coded}\"}"
echo "$json" > file.json
curl -X POST "localhost:9200/docs2/document/" -d @file.json

I skript pro vyhledání je zapotřebí nepatrně upravit:

#!/bin/sh
 
curl -XGET "http://localhost:9200/docs2/_search?pretty" -d @search2.json

Po těchto úpravách je možné při vyhledávání požadovat, aby se kromě vlastního textu s nalezenými slovy vrátily i metainformace o dokumentu, tj. konkrétně titulek, datum vytvoření/modifikace, délka a typ původního souboru:

{
  "fields" : ["file.content_type", "file.title", "file.date", "file.content_length"],
  "query" : {
    "match" : {
      "file.content" : "network"
    }
  },
  "highlight" : {
    "fields" : {
      "file.content" : {}
    }
  }
}

V praxi může výsledek vyhledávání vypadat například takto:

{
  "took" : 17,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.011244276,
    "hits" : [ {
      "_index" : "docs2",
      "_type" : "document",
      "_id" : "AVdnbbR-42uB_K-xw_ex",
      "_score" : 0.011244276,
      "fields" : {
        "file.content_length" : [ "391905" ],
        "file.content_type" : [ "application/xhtml+xml; charset=UTF-8" ],
        "file.title" : [ "Fedora 22 Release Notes" ]
      },
      "highlight" : {
        "file.content" : [
            "Hardware Enablement. \n\n\t\t\t\n\n\t\n\t\t\t\tController Area Network (CAN) device drivers are now supported, see Chapter 12",
            "possible to read information about the system network interfaces from the operating system. It has been",
            "discovery to find the nearest servers, and to use the network in an optimized way. As a result, administrators",
            "certificate can be used. For example, a Virtual Private Network (VPN) server can be configured to only accept certificates",
            "option -p for the arpwatch command of the arpwatch network monitoring tool. This option disables promiscuous" ]
      }
    } ]
  }
}

Povšimněte si, že u souborů HTML nebylo možné zjistit datum vytvoření. Například u PDF souborů je to však možné:

{
  "took" : 19,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.028263604,
    "hits" : [ {
      "_index" : "docs2",
      "_type" : "document",
      "_id" : "AVdoKXkSdBFvurTfS8dM",
      "_score" : 0.028263604,
      "fields" : {
        "file.content_length" : [ "958776" ],
        "file.content_type" : [ "application/pdf" ],
        "file.date" : [ "2013-09-16T15:55:06Z" ],
        "file.title" : [ "VideoCore® IV 3D Architecture Reference Guide" ]
      },
      "highlight" : {
        "file.content" : [ "to je ted nezajimave :-)" ]
      }
    } ]
  }
}

9. Druhé vylepšení indexování: uživatelsky definovaná metadata

Kromě automaticky zjišťovaných metadat je samozřejmě možné k dokumentům přidat i uživatelská metadata. Opět si to vyzkoušejme; nejdříve však nakonfigurujeme další (už poslední) mapování:

curl -XPUT "localhost:9200/docs3/"
curl -XPUT "localhost:9200/docs3/document/_mapping" -d @create_mapping2.json

Nyní můžeme u každého dokumentu specifikovat produkt, verzi produktu a URL s výsledným dokumentem (samozřejmě si můžete definovat vlastní metadata, například zda už byl dokument nahrán do čtečky, zda ho už indexuje taktéž NSA atd. atd.). K touto účelu postačuje nepatrně rozšířit JSON strukturu předávanou přes RESTful API při parsování a indexování dokumentu. Upravíme si tedy soubor add_file.sh, aby akceptoval čtyři parametry: jméno souboru, jméno produktu, jeho verzi a URL:

#!/bin/sh
 
coded=`cat $1 | perl -MMIME::Base64 -ne 'print encode_base64($_)'`
json="{\"product\":\"$2\",\"version\":\"$3\",\"url\":\"$4\",\"file\":\"${coded}\"}"
echo "$json" > file.json
curl -X POST "localhost:9200/docs3/document/" -d @file.json

Příklad použití:

./add_file.sh InstallationGuide.pdf "Fedora" "23" "https://docs.fedoraproject.org/en-US/Fedora/23/pdf/Installation_Guide/Fedora-23-Installation_Guide-en-US.pdf"

Poznámka: zajisté jste si všimli, že může dojít k problémům ve chvíli, kdy například název produktu obsahuje nějaké znaky, které by mohly způsobit chybu při zpracování JSONu. Úpravu skriptu ponechám na váženém čtenáři (není to nic složitého).

10. Vyhledávání v dokumentech se zobrazením uživatelských metadat

Opět je možné rozšířit možnosti vyhledávání o zobrazení nových metadat (produktu, jeho verze a URL):

#!/bin/sh
 
curl -XGET "http://localhost:9200/docs2/_search?pretty" -d @search3.json

Jména atributů s metadaty se jednoduše přidají do pole dostupné pod klíčem fields:

{
  "fields" : ["product", "version", "url", "file.content_type", "file.title", "file.date", "file.content_length"],
  "query" : {
    "match" : {
      "file.content" : "network"
    }
  },
  "highlight" : {
    "fields" : {
      "file.content" : {}
    }
  }
}

Výsledek může vypadat takto:

{
  "took" : 26,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.042208605,
    "hits" : [ {
      "_index" : "docs3",
      "_type" : "document",
      "_id" : "AVdneznq42uB_K-xw_e_",
      "_score" : 0.042208605,
      "fields" : {
        "product" : [ "Fedora" ],
        "file.content_length" : [ "903404" ],
        "file.content_type" : [ "application/xhtml+xml; charset=UTF-8" ],
        "url" : [ "http://root.cz" ],
        "version" : [ "22" ],
        "file.title" : [ "Installation Guide" ]
      },
      "highlight" : {
        "file.content" : [ "ted nezajimave" ]
      }
    } ]
  }
}

Poznámka: metadata jsou vrácena ve formě polí! Proto pozor na způsob jejich přečtení.

11. Omezení počtu vyhledaných dokumentů i počtu zobrazených řetězců se zvýrazněním

Elasticsearch taktéž umožňuje omezení počtu vyhledávaných dokumentů. To lze zajistit atributem size, který je možné kombinovat s atributem from (tento druhý atribut je však možné vynechat). Příkladem z praxe může být (mezi uživateli nepříliš oblíbené) rozdělení výsledků do více stránek, mezi kterými se lze přepínat. Dále je možné omezit i počet řetězců nalezených v jednom dokumentu. Pozor: nejedná se o počet celkově nalezených slov či sousloví, protože pokud je slovo/sousloví umístěno například v jedné větě několikrát, je to stále považováno za jeden nález (například když budete v anglických dokumentech hledat slovo „the“). Ukažme si použití obou omezení v praxi:

#!/bin/sh
 
curl -XGET "http://localhost:9200/docs2/_search?pretty" -d @search4.json
{
  "size" : 1,
  "fields" : ["product", "version", "url", "file.content_type", "file.title", "file.date", "file.content_length"],
  "query" : {
    "match" : {
      "file.content" : "network"
    }
  },
  "highlight" : {
    "fields" : {
      "file.content" : {"number_of_fragments":5}
    }
  }
}

Počet dokumentů byl omezen na jeden, počet tzv. fragmentů na pět.

12. Jednoduché GUI k vyhledávacímu Engine naprogramované v jazyce Clojure

Popis základní instalace jednoho uzlu Elasticsearche a jeho konfigurace pro fulltextové vyhledávání tedy máme za sebou, takže se pro ukázku komunikace aplikace s Elasticsearchem přes RESTful API podívejme na velmi jednoduchou webovou aplikaci naprogramovanou v Clojure. Jedná se o aplikaci postavenou nad knihovnami clojure-ring a hiccup, s nimiž jsme se seznámili v seriálu o programovacím jazyku Clojure. V aplikaci lze zadat vyhledávanou frázi, limit na počet vrácených dokumentů i limit na počet bloků s vyhledanou frází. Veškerá kontrola zadaných dat je provedena jen na straně serveru, přidání JS (nebo přenos celé aplikace například do Angularu) není příliš složité.

Struktura projektu vygenerovaná nástrojem Leiningen je následující:

.
├── doc
│   └── intro.md
├── LICENSE
├── project.clj
├── README.md
├── resources
├── src
│   └── es
│       ├── core.clj
│       └── server.clj
└── test
    └── es
        └── core_test.clj

Důležité jsou jen soubory project.clj, src/es/core.clj a src/es/server.clj.

project.clj

V tomto souboru je důležitá především specifikace všech použitých knihoven:

(defproject es "0.1.0-SNAPSHOT"
    :description "FIXME: write description"
    :url "http://example.com/FIXME"
    :license {:name "Eclipse Public License"
              :url "http://www.eclipse.org/legal/epl-v10.html"}
    :dependencies [[org.clojure/clojure "1.6.0"]
                   [org.clojure/data.json "0.2.5"]
                   [org.clojure/tools.cli "0.3.1"]
                   [ring/ring-core "1.3.2"]
                   [ring/ring-jetty-adapter "1.3.2"]
                   [clj-http "2.0.0"]
                   [hiccup "1.0.4"]]
    :dev-dependencies [[lein-ring "0.8.10"]]
    :plugins [[lein-ring "0.8.10"]]
    :ring {:handler es.core/app}
    :main ^:skip-aot es.core
    :target-path "target/%s"
    :profiles {:uberjar {:aot :all}})

core.clj

V souboru core.clj je umístěn programový kód, který aplikaci spustí jako webový server na zadaném portu. Pokud port není zadán, použije se výchozí hodnota 3000:

(ns es.core)
 
(require '[ring.adapter.jetty      :as jetty])
(require '[clojure.tools.cli       :as cli])
(require '[ring.middleware.params  :as http-params])
(require '[ring.middleware.session :as http-session])
(require '[es.server               :as server])
 
(def default-port
    "3000")
 
(def cli-options
    "Definitions of all command line options currenty supported."
    [["-p" "--port   PORT"    "port number"   :id :port]])
 
(def app
    "Definition of a Ring-based application behaviour."
    (-> server/handler            ; handle all events
        http-session/wrap-session ; we need to work with HTTP sessions
        http-params/wrap-params)) ; and to process request parameters, of course
 
(defn start-server
    "Start the HTTP server on the specified port."
    [port]
    (println "Starting the server at the port: " port)
    (jetty/run-jetty app {:port (read-string port)}))
 
(defn get-port
    "Returns specified port or default port if none is specified on the command line."
    [specified-port]
    (if (or (not specified-port) (not (string? specified-port)) (empty? specified-port))
        default-port
        specified-port))
 
(defn -main
    "Entry point to the Jenkins dashboard."
    [& args]
    ; Load configuration from the provided INI file
    (let [all-options      (cli/parse-opts args cli-options)
          options          (all-options :options)
          port             (options :port)]
    (start-server (get-port port))))

server.clj

Nejdůležitější část aplikace, která na základě uživatelem zadaných dat (do dynamicky generované webové stránky) vytvoří strukturu JSON, tu předá do Elasticsearche a zpracuje výsledek (tedy další JSON). Pro komunikaci se používá knihovna http-client, obousměrnou transformaci dat Clojure-JSON zajišťuje knihovna clojure.data.json:

(ns es.server)
 
(require 'clojure.string)
 
(require '[hiccup.core        :as hiccup])
(require '[hiccup.page        :as page])
(require '[hiccup.form        :as form])
(require '[clj-http.client    :as http-client])
(require '[ring.util.response :as http-response])
(require '[clojure.data.json  :as json])
 
(def default-fulltext-limit
    10)
 
(def default-highlighted-lines
    10)
 
(def search-url
    "http://localhost:9200/docs3")
 
(defn render-highlight
    "Converts output from ElasticSearch into HTML"
    [highlight]
    (-> (reduce #(str %1 "<hr/>" %2) highlight)
        (.replaceAll "\n" "<br>")
        (.replaceAll "\t" "")))
 
(defn render-search-page
    [text-to-search search-results limit highlight]
    (page/xhtml
        [:head
            [:title "Elastic Search GUI"]
            [:meta {:name "Generator"  :content "Clojure"}]
            [:meta {:http-equiv "Content-type" :content "text/html; charset=utf-8"}]
        ]
        [:body
            [:div
                (form/form-to [:get "/"]
                [:h2 "Fulltext search"]
                [:table
                    [:tr
                        [:td (form/text-field {:size "50" :class "-form-control" :id "text_to_search" :value text-to-search} "text_to_search") " "]
                        [:td (form/submit-button {:class "btn btn-primary" :id "submit"} "Search")]
                        [:td "Limit max " (form/text-field {:size "5" :value limit} "limit") " guide(s)"]
                        [:td "Highlight " (form/text-field {:size "5" :value highlight} "highlight") " part(s)"]
                    ]
                ])
                (if search-results
                [:table
                    [:tr
                        [:td "Finished in:"]
                        [:td (:took search-results) "ms"]
                        [:td " "]
                    ]
                    [:tr
                        [:td "Found:"]
                        [:td (:total (:hits search-results)) " guides"]
                        [:td {:colspan "4"} " "]
                    ]
                    [:tr
                        [:td {:colspan "6"} " "]
                    ]
                    [:tr
                        [:th "Score"]
                        [:th "Product"]
                        [:th "Guide"]
                        [:th "Date"]
                        [:th "Type"]
                        [:th "Size"]
                    ]
                        (for [hit (:hits (:hits search-results))]
                        [:tpart
                            [:tr [:td (-> hit :_score)]
                                 [:td (-> hit :fields :product first)]
                                 [:td [:a {:href (-> hit :fields :url first)} (-> hit :fields :file.title first)]]
                                 [:td (-> hit :fields :file.date first)]
                                 [:td (-> hit :fields :file.content_type first)]
                                 [:td (-> hit :fields :file.content_length first)]
                            ]
                            [:tr
                                 [:td {:colspan "6"} (render-highlight (-> hit :highlight :file.content))]
                            ]
                            [:tr
                                 [:td {:colspan "6"} " "]
                            ]
                        ])
                ])]]))
 
(defn post-method-to-call-elastic-search
    [url postdata]
    (println url)
    (try
        (http-client/post url {
            :body postdata
            :content-type "application/json"})
        (catch Exception e
             (.printStackTrace e))))
 
(defn fulltext-search-json
    [text-to-search limit highlight-items]
    {
        :fields ["title" "url" "product" "name" "file.title" "file.content_type" "file.date" "file.content_length"]
        :size limit
        :query {
             :match {
                  :file.content text-to-search
             }
        }
        :highlight {
             :fields {
                 :file.content {:number_of_fragments highlight-items}
             }
        }
    })
 
(defn fulltext-search
    [text-to-search limit highlight-items]
    (if (and text-to-search (not (empty? (clojure.string/trim text-to-search))))
        (let [search-json (json/write-str (fulltext-search-json text-to-search limit highlight-items))
              response    (post-method-to-call-elastic-search (str search-url "/_search?pretty") search-json)]
              (if (and response (:body response))
                  (json/read-str (:body response) :key-fn keyword)))))
 
(defn parse-int-parameter
    "Try to parse parameter into integer, nil is returned in case of any error."
    [params param-name]
    (try
        (Integer/parseInt (get params param-name))
        (catch Exception e
            nil)))
 
(defn handler
    "Handler for all HTTP requests."
    [request]
    (println "request URI: " (request :uri))
    (let [uri            (:uri    request)
          params         (:params request)
          text-to-search (get params "text_to_search")
          limit          (or (parse-int-parameter params "limit")     default-fulltext-limit)
          highlight      (or (parse-int-parameter params "highlight") default-highlighted-lines)
          search-results (fulltext-search text-to-search limit highlight)
          page-content   (render-search-page text-to-search search-results limit highlight)]
         (-> (http-response/response page-content)
             (http-response/content-type "text/html; charset=utf-8"))))

13. Spuštění a použití GUI

Aplikace (přesněji webový server) se spustí příkazem:

ict ve školství 24

lein run

Obrázek 1: rozhraní aplikace po spuštění.

Obrázek 2: výsledek vyhledání.

14. Odkazy na Internetu

  1. Elasticsearch home page
    https://www.elastic.co/pro­ducts/elasticsearch
  2. elasticsearch-mapper-attachments (GitHub)
    https://github.com/elastic/e­lasticsearch-mapper-attachments
  3. Elasticsearch (EN Wikipedia)
    https://en.wikipedia.org/wi­ki/Elasticsearch
  4. Elasticsearch (CZ Wikipedia)
    https://cs.wikipedia.org/wi­ki/Elasticsearch
  5. Apache Lucene home page
    https://lucene.apache.org/
  6. Apache Lucene Core
    https://lucene.apache.org/core/
  7. Apache Tika home page
    http://tika.apache.org/
  8. Apache Tika: Supported Document Formats
    http://tika.apache.org/1.13/for­mats.html
  9. Apache Tika (Wikipedia)
    https://en.wikipedia.org/wi­ki/Apache_Tika
  10. Leiningen: nástroj pro správu projektů napsaných v Clojure
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/
  11. Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/
  12. Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/
  13. Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/
  14. Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/
  15. Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/
  16. Clojure home page
    http://clojure.org/
  17. Clojure (downloads)
    http://clojure.org/downloads
  18. Clojure – Functional Programming for the JVM
    http://java.ociweb.com/mar­k/clojure/article.html
  19. Clojure quick reference
    http://faustus.webatu.com/clj-quick-ref.html
  20. Putting term vectors on a diet
    http://blog.jpountz.net/pos­t/41301889664/putting-term-vectors-on-a-diet
  21. What is REST API
    http://restfulapi.net/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.