Hlavní navigace

Fulltextování v PostgreSQL - modul tsearch2

6. 1. 2004
Doba čtení: 8 minut

Sdílet

Chybějící 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 jistý pokrok, alespoň co se týká funkcionality PostgreSQL.

Chybějící 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 jistý pokrok, alespoň co se týká funkcionality PostgreSQL, a navíc, jak jsem si uvědomil při debatě v konferenci, asi jen málokdo tady má alespoň malou představu, jak to funguje. Na rovinu je třeba říci, že specializované fulltextové systémy jsou se svými schopnostmi někde úplně jinde.

Modul z doplňků tsearch2 představuje původní nekomerční podporu pro fulltext v RDBMS PostgreSQL 7.3.X a vyšší. 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 souvisi s projektem OpenFTS, tj. fullttextové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/sh­are/contrib/tse­arch2.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" 

Poznámka: Slovník synonym je textový soubor obsahují 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/sh­are/contrib, zaregistrujeme český slovník následujícím sql příkazem:

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') 

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/tsear­ch2/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 postgresql.ok­.cz/download/tse­arch2cz.tar.gz.

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'=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); 

pak

ts=# insert into foo(t) VALUES ('Příliš žluťoučký kůň se napil žluté vody');
INSERT 154239 1
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.

UX DAy - tip 2

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.

Jelikož jsem nikdy nepoužil žádný jiný fulltextový systém, nemohu na závěr napsat porovnání s ostatními. Určitě by se našlo, co by se dalo vylepšit. Namátkou: použití frází – více slovních výrazů, použití zástupných symbolů. Pro někoho může být překážkou existence dalšího sloupce (tsvecor) v tabulce. V každém případě je použitelnost a funkčnost PostgreSQL opět o krok dál.

Byl pro vás článek přínosný?

Autor článku

Pavel Stěhule je odborníkem na relační databázový systém PostgreSQL, pracuje jako školitel a konzultant.