TSearch2

Z PostgreSQL
Skočit na navigaci Skočit na vyhledávání

Chybějící vestavěná podpora fulltextu je vážným argumentem proti používání PostgreSQL. Modul tsearch2 nepředstavuje obvyklé a ani úplně dokonalé řešení problému vyhledávání v textových položkách, nicméně nepochybně představuje dostatečně robusní a efektivní řešení odpovídající běžným fulltextovým nástrojům v ostatních RDBMS. Na rovinu je třeba říci, že specializované fulltextové systémy jsou se svými schopnostmi někde úplně jinde. Nejedná se ale o obvyklé řešení fulltextu, tj. rozšíření možností indexace a vyhledávání nad textovými položkami, ale o implementaci nového datového typu tsvector, podporujícího fulltextové vyhledávácí operace s podporou indexu (využívá možností GiST indexů v PostgreSQL).

Projekt tsearch2 úzce souvisí s projektem OpenFTS, tj. s projektem fulltextového systému ukládajícího metainformace o dokumentech v klasické relační databázi, resp. představuje databázový backend tohoto projektu.

Samotná instalace je jednoduchá a neliší se od instalace jiných doplňků (Contrib modulů) v PostgreSQL. Stačí v adresáři /contrib/tsearch2 jako root zadat:

make
make install
ldconfig

Poté je třeba otevřít databázi, ve které chceme mít podporu typu tsvector, a naimportovat soubor /usr/local/pgsql/share/contrib/tsearch2.sql. (Jeho umístění může záležet na distribuci. Zmíněná cesta platí pro instalaci ze zdrojových textů.)

createdb ts
psql ts
ts=# \i /usr/local/pgsql/share/contrib/tsearch2.sql

Samotný indexovaný dokument se zpracovává v několika krocích. Nejdříve parser rozdělí daný text na tzv. tokeny: slova, číslice, mezery, html značky. Pomocí slovníku se každý token převede na tzv. lexém (pro slovesa se hledá infinitiv, podstatná jména se převádějí do jednotného čísla v prvním pádu atd). Lexikální analýzu můžeme provést buď nad slovníkem ispellu, nebo tzv. stemmer funkcí (Funkcionalita je stejná, nepotřebujeme ale slovník. Pro češtinu bohužel tato funkce není vytvořená, nebo alespoň jsem o ní nenašel na internetu jedinou zmínku.) Sloučením všech lexémů dokumentu dostaneme vektor (typu tsvector), s kterým se dále pracuje, resp. se uloží a lze jej indexovat a fulltextově prohledávat. Vektor kromě vlastních lexémů obsahuje i polohu lexémů v dokumentu.

Velikost slovníku má vliv na kvalitu a rychlost redukce textu na lexémy. Je podstatný rozdíl v odezvě prvního volání funkce převodu na lexém (lexize()), pokud má slovník 226 KB, nebo 2MB. Lexikální analýza probíhá jak při vkládání a modifikaci záznamů, tak při dotazování (viz konec textu).

Když token není v daném slovníku nalezen, je výsledkem analýzy prázdný řetězec. Tato vlastnost by mohla diskvalifikovat tsearch2 v praxi - např. některá příjmení nebo názvy v žádném slovníku nenajdeme. Díky genialitě tvůrců máme ale k dispozici tzv. simple slovník, který pouze převádí velká písmena na malá, a můžeme řadit za sebe více slovníků (slovník simple zařadíme na konec). Každý slovník tsearch2 může obsahovat seznam tzv. stop-words (blokovaných slov), tj. slov, která se neindexují (např. je zbytečné indexovat spojky a předložky). Neměl by být problém vytvořit vlastní slovník, např. oborový slovník, třídník součástek atd.

ts=# select dict_name, dict_comment, dict_initoption from pg_ts_dict;

dict_name                dict_comment                              dict_initoption
simple           Simple example of dictionary.
en_stem          English Stemmer. Snowball.                        /usr/local/pgsql/share/contrib/english.stop
ru_stem          Russian Stemmer. Snowball.                        /usr/local/pgsql/share/contrib/russian.stop
ispell_template  ISpell interface. Must have .dict and .aff files
synonym          Example of synonym dictionary
cz_ispell
                                                                   DictFile="/usr/local/pgsql/share/contrib/czech.dict",
                                                                   AffFile="/usr/local/pgsql/share/contrib/czech.aff",
                                                                   StopFile="/usr/local/pgsql/share/contrib/czech.stop"</code></div>

Poznámka: Slovník synonym je textový soubor obsahující na každém řádku dvojici slov – slovo a jedno z jeho synonym, např.

eroplán	     letadlo
éro	     letadlo

Hodnota dict_initoption obsahuje cestu k tomuto souboru - obdoba en_stem nebo ru_stem (podrobnosti).

Za předpokladu, že máme dict, aff a stop soubory v adresáři /usr/local/pgsql/share/contrib, zaregistrujeme český slovník následujícím sql příkazem:

-- pro latin2
INSERT INTO pg_ts_dict (
   SELECT 'cz_ispell', dict_init,
   'DictFile="/usr/local/pgsql/share/contrib/czech.dict",
    AffFile="/usr/local/pgsql/share/contrib/czech.aff",
    StopFile="/usr/local/pgsql/share/contrib/czech.stop"', dict_lexize
     FROM pg_ts_dict where dict_name='ispell_template')

-- pro utf8
INSERT INTO pg_ts_dict (
   SELECT 'cz_ispell', dict_init,
   'DictFile="/usr/local/pgsql/share/contrib/czech-utf8.dict",
    AffFile="/usr/local/pgsql/share/contrib/czech-utf8.aff",
    StopFile="/usr/local/pgsql/share/contrib/czech-utf8.stop"', dict_lexize
     FROM pg_ts_dict where dict_name='ispell_template');

Soubory dict a aff lze dohledat na internetu (jedná se o slovníky k ispellu) - není nutné instalovat samotný ispell. Doplněk obsahuje Makefile my2ispell převádějící slovníky z formátu OpenOffice MyIspell do formátu ispellu. Stačí si stáhnout příslušný slovník, zkopírovat jej do adresáře contrib/tsearch2/my2ispell a v adresáři sputit konverzi

make ZIPFILE=cs_CZ LANGUAGE=czech

Zkonvertované soubory včetně mnou vytvořeného seznam blokovaných slov naleznete na adrese http://postgresql.ok.cz/download/tsearch2cz.tar.gz nebo http://www.pgsql.cz/data/tsearch2cz-utf8.tar.gz (pro kódování utf8).

Počínaje verzí 8.2 TSearch2 podporuje formát MySpell, takže není nutná konverze do ispell formátu. V případě, že používáte kódování UTF8, tak vás konverze nemine. Slovníky je třeba převést do tohoto kódování. Dále je třeba změnit v souboru cs_CZ.aff první řádek na SET UTF8.

 iconv -f iso-8859-2 -t utf8 cs_CZ.dic -o cs_CZ-utf8.dic
 iconv -f iso-8859-2 -t utf8 cs_CZ.aff -o cs_CZ-utf8.aff

Slovník se zaregistruje obdobně jako v předchozích případech:

INSERT INTO pg_ts_dict (
   SELECT 'cz_myspell', dict_init,
   'DictFile="/usr/local/pgsql/share/contrib/cs_CZ-utf8.dic",
    AffFile="/usr/local/pgsql/share/contrib/cs_CZ-utf8.aff",
    StopFile="/usr/local/pgsql/share/contrib/czech-utf8.stop"', dict_lexize
     FROM pg_ts_dict where dict_name='ispell_template');

Pokud máme nainstalovaný český slovník, můžeme vyzkoušet lexikální analýzu. V případě, že vše nechodí tak, jak by se zdálo, že by chodit mělo, autoři včetně mne doporučují restart klienta (pomůže to pouze po změnách v systémových tabulkách tsearch2).

SELECT lexize('cz_ispell','jablka');     => {jablko} 
SELECT lexize('cz_ispell','jablkům');    => {jablko} 
SELECT lexize('cz_ispell','jablek');     => {jablko} 
SELECT lexize('cz_ispell','čekal');      => {čekal,čekat,čekat} 
SELECT lexize('cz_ispell','počká');      => {počkat} 
SELECT lexize('cz_ispell','počkala');    => {počkat,počkat} 
SELECT lexize('cz_ispell','pravidelně'); => {pravidelný}

Zpět k parseru. Parser v tsearch2 slouží k jednoduchému rozdělení textu (zvládá i html) na jednotlivé tokeny - slova, číslice atd. Pokud je třeba, můžete použít jiný parser, musíte si jej ale napsat.

ts=# select * from parse('<h1>Nadpis</h2>Příliš žluťoučký kůň');
  tokid |   token
 -------+-----------
     13 |
      1 | Nadpis
     13 |
      3 | Příliš
     12 |
      3 | žluťoučký
     12 |
      3 | kůň

Parser rozlišuje mezi slovy bez diakritiky a obsahujícími diakritiku - resp. mezi ascii psaným textem a ostatním textem. Zajímavé je chování parseru při zadání odkazu. Rozloží URL na protokol, url, adresu a stránku:

ts=# select * from parse('http://postgresql.ok.cz/index.html');
  tokid |            token
 -------+-----------------------------
     14 | http://
      5 | postgresql.ok.cz/index.html
      6 | postgresql.ok.cz
     18 | /index.html

Tsearch2 obsahuje několik připravených konfigurací, tj. záznamů v tabulce pg_ts_cfg určujících locale a parser. Záznamy v tabulce pg_ts_cfgmap určují, který slovník se použije pro určitý typ tokenů (tokenid). Vzhledem k původu tsearch2 jsou připraveny pouze konfigurace default, default_russian a simple. Podporu češtiny si musíme do zmíněných tabulek doplnit sami (stačí nechat provést následující sql příkazy - předpokladem je funkční český slovník).

INSERT INTO pg_ts_cfg VALUES ('default_czech','default','cs_CZ'); 
INSERT INTO pg_ts_cfgmap
   SELECT 'default_czech',tok_alias,dict_name
     FROM pg_ts_cfgmap WHERE ts_name='default_russian'; 
UPDATE pg_ts_cfgmap SET dict_name='{cz_ispell,simple}'
   WHERE ('ru_stem_koi8'=ANY(dict_name) OR 'en_stem' = ANY(dict_name))
     AND ts_name='default_czech';

Pokud vše je nastaveno, můžeme testovat redukci tokenů a převod na lexémy

ts=# select to_tsvector('default_czech',
 'Příliš žluťoučký kůň se napil žluté vody');
                           to_tsvector
 ---------------------------------------------------------------
 'kůň':3 'voda':7 'napít':5 'žlutý':6 'příliš':1 'žluťoučký':2

Funkcí set_curcfg aktivujeme vybranou konfiguraci:

ts=# SELECT set_curcfg('default_czech');
 set_curcfg
 ------------
 (1 řádka)

Nastavená konfigurace se použije jen pro explicitní konverzní funkce to_tsvector(), to_tsquery() (prvním nepovinným parametrem funkcí může být specifikace konfigurace, viz výše uvedený příklad) a pro funkci ts_debug(). Implicitní konverze používají stále konfiguraci 'default':

ts=# select tsvector 'Příliš žlutý kůň se napil žluté vody';
                       tsvector
 ----------------------------------------------------
 'se' 'kůň' 'vody' 'napil' 'žluté' 'žlutý' 'Příliš'
 (1 řádka)

Funkce ts_debug zobrazí podrobnější informace o převodu slov do tsearch2 vektoru:

ts=# select * from ts_debug('Příliš žluťoučký kůň se napil žluté vody');
    ts_name    | tok_type | description |   token   |     dict_name      |  tsvector
 --------------+----------+-------------+-----------+ -------------------+ ------------
 default_czech | word     | Word        | Příliš    | {cz_ispell,simple} | 'příliš'
 default_czech | word     | Word        | žluťoučký | {cz_ispell,simple} | 'žluťoučký'
 default_czech | word     | Word        | kůň       | {cz_ispell,simple} | 'kůň'
 default_czech | lword    | Latin word  | se        | {cz_ispell,simple} |
 default_czech | lword    | Latin word  | napil     | {cz_ispell,simple} | 'napít'
 default_czech | word     | Word        | žluté     | {cz_ispell,simple} | 'žlutý'
 default_czech | lword    | Latin word  | vody      | {cz_ispell,simple} | 'voda'
 (7 řádek)

Dále se s hodnotami typu tsvector bude zacházet stejně jako s hodnotami jiných datových typů. Vytvoříme tabulku se sloupcem tsvector a nad ním vytvoříme index.

CREATE TABLE foo(
   id SERIAL PRIMARY KEY,
   t  text,
   v  tsvector );

 CREATE INDEX idxFTI_idx ON foo USING gist(v);
 VACUUM FULL ANALYZE;
 CREATE TRIGGER tsvectorupdate BEFORE UPDATE OR INSERT ON foo
   FOR EACH ROW EXECUTE PROCEDURE tsearch2(v, t);

Pokud chceme do tsvector spojit více atributů (sloupečků) tabulky najednou, zadáme tsearch2(v, t1, t2, t3), atd.

Pak

ts=# insert into foo(t) VALUES ('Příliš žluťoučký kůň se napil žluté vody');
 INSERT 
 ts=# \x Rozšířené zobrazení zapnuto.
 ts=# SELECT * from foo;
 -[ RECORD 1 ]-----------------------------------------------------
 id | 1
 t  | Příliš žluťoučký kůň se napil žluté vody
 v  | 'kůň':3 'voda':7 'napít':5 'žlutý':6 'příliš':1 'žluťoučký':2

 ts=# SELECT t from foo where v @@ to_tsquery('default_czech','(napil&žlutý)|!cotunení');
 -[ RECORD 1 ]-------------------------------
 t | Příliš žluťoučký kůň se napil žluté vody

Význam operátorů je klasický: & – AND, | – OR, ! – negace. Binární operátor @@ provádí fulltextové vyhledávání.

Typ tsquery je jakoby duální k tsvectoru. Oba obsahují lexémy. Jestliže tsvector představuje pouze posloupnost lexémů, pak tsquery představuje kombinaci lexémů a klasických boolovských operátorů ~ logický výraz.

ts=# SELECT to_tsquery('(napil&žluté)|!xx');
        to_tsquery
---------------------------
 'napít' & 'žlutý' | !'xx'
(1 řádka)

Fulltextové vyhledávání je nyní triviální záležitostí. Použijeme operátor @@, kde je na jedné straně hodnota typu tsvector a na druhé straně typu tsquery.