Fulltext se slovníky ve sdílené paměti

Pokud používáte fulltext zabudovaný do PostgreSQL a vzhledem k povaze jazyka vám nestačí řešení založené na snowballu (což funguje dobře pro angličtinu, ale například pro češtinu nic takového s rozumnou přesností neexistuje a asi ani existovat nebude), jste víceméně nuceni používat ispell slovníky. V tom případě jste si určitě všimli dvou nepříjemných vlastností.

Slovníky se načítají do paměti jednotlivých spojení (backendů), tj. každé spojení při prvním dotazu stráví netriviální čas parsováním souborů se slovníkem, a každé spojení si drží vlastní kopii. Pokud naparsovaný slovník zabere např. 25MB (a to není nic výjimečného) a máte třeba 20 spojení která se alespoň jednou dotkla fulltextu, rázem vám zmizí 500 MB paměti (což např. na VPS není málo).

Existují řešení jak toto řešit - např. použitím connection poolu s již inicializovaným fulltextem (tj. šetříte CPU ale plýtváte pamětí), vyhrazením několika persistentních spojení pro fulltext (ale s tím se zase hůř pracuje). Existují asi i další možnosti které mne teď nenapadají, ale nic z toho není ideální ...

Nedávné problémy právě s fulltextem mne vyprovokovaly k napsání extension která umožňuje sdílení slovníků ve sdílené paměti. Ani toto řešení není dokonalé (více na konci článku), ale rozhodně je to krok správným směrem. Takže jak to funguje a co to vlastně umí?

Tato extension je velice čerstvá - vyvinul jsem ji ani ne před dvěma dny, takže není tak otestovaná jak by měla být. Tudíž to asi není něco co byste chtěli instalovat na produkci, ale pokud můžete odzkoušejte ji na svém testovacím prostředí, s vašimi slovníky apod. a dejte mi vědět v případě jakýchkoliv problémů (pády, rozdíly oproti prostému ispellu atd.).

Základní funkčnost je velmi jednoduchá - extension si při startu databáze vyžádá prostor ve sdílené paměti (vedle shared buffers) pro uložení slovníků. To kolik paměti se vyhradí se určuje GUC proměnnou - default je 30 MB.

Extension následně definuje "search template" s názvem "shared_ispell", kterou lze následně použít pro definici vlastních slovníků - například takto:

CREATE TEXT SEARCH DICTIONARY czech_shared (
    TEMPLATE = shared_ispell,
    DictFile = czech,
    AffFile = czech,
    StopWords = czech
);

CREATE TEXT SEARCH CONFIGURATION public.czech_shared
                                 ( COPY = pg_catalog.simple );

ALTER TEXT SEARCH CONFIGURATION czech_shared
    ALTER MAPPING FOR asciiword, asciihword, hword_asciipart,
                      word, hword, hword_part
    WITH czech_shared;

Od této chvíle můžete konfiguraci "czech_shared" používat stejně jako ispell, tj. například

SELECT ts_query('czech_shared', 'hledané výrazy')

Veškeré kouzlo se skrývá právě v uvedené šabloně - ta totiž funguje velmi podobně jako "ispell" ale namísto aby slovníky načítala do paměti jednotlivých procesů, vyhledává je v shared segmentu. Pokud se zjistí že požadovaný slovník v paměti zatím není, standardně ho inicializuje a zkopíruje ho do sdílené paměti tak aby ho ostatní sessions mohly používat.

Velikost segmentu

Trochu problém je určení velikosti segmentu - je to statická hodnota (za běhu ji nelze měnit) a při startu databáze se samozřejmě neví které slovníky se tam budou načítat, o velikosti v paměti ani nemluvě. Pokud víte které slovníky budete používat můžete nejdříve nastavit větší hodnotu a po jejiich načtení zjistit kolik paměti je potřeba (pomocí funkce shared_ispell_mem_used()). Pokud seznam používaných slovníků neznáte, budete muset experimentovat.

Potřebný objem paměti pro daný slovník nelze zjistit bez jeho předchozího naparsování, a aktuální implementace se jednoduše pokusí zkopírovat výsledek parsování, takže je možné že v průběhu kopírování zjistí že v segmentu není dostatek volné paměti. V tom případě jednoduše vyhodí error s odpovídající hláškou a slovník nepřidá.

Není ale problém slovníky z paměti vyhodit - funkcí shared_ispell_reset() můžete jednoduše vše smazat a začít znovu - díky tomu lze například načíst z disku upravené slovníky apod (existující session to nerozhodí, při prvním následujícím dotazu provedou reinicializaci slovníku).

Rozdělení na části

Další zajímavá možnost na kterou jsem narazil se týká možnosti odděleného načítání jednotlivých částí (slovník, affixy a stop slova). Ty se sice načítají jednotlivě, v podstatě jsou to ale samostatné jednotky - není důvod proč by se jeden slovník (DicFile) nedal dle situace použít s více affixy (AffFile) nebo stop slovy. V tradiční implementaci je to tak že pokud se potřebuje jiná konfigurace, načte se vše znova (i když se třeba liší jenom jedním stop slovem).

Můj plán je že pokud zadáte dva slovníky sdílející soubor(y), například

CREATE TEXT SEARCH DICTIONARY czech_shared_1 (
    TEMPLATE = shared_ispell,
    DictFile = czech,
    AffFile = czech,
    StopWords = czech_stop_1
);

CREATE TEXT SEARCH DICTIONARY czech_shared_1 (
    TEMPLATE = shared_ispell,
    DictFile = czech,
    AffFile = czech,
    StopWords = czech_stop_2
);

budou se ty shodné sdílet (v tomto případě DictFile a AffFile). Takto oddělené jsou v tuto chvíli pouze StopWords soubory a funguje to myslím dobře.

Update 5/1/2012: Zjistil jsem že slovníky a affixy ve skutečnosti jsou provázané, takže jejich oddělení bohužel možné není.

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)