Teljes szöveges keresés implementálása PostgreSQL adatbázissal

A digitális világban az információ mennyisége exponenciálisan növekszik. Legyen szó weboldalak tartalmáról, dokumentumokról, termékleírásokról vagy blogbejegyzésekről, az adatok közötti hatékony keresés elengedhetetlen a felhasználói élmény és az üzleti hatékonyság szempontjából. A hagyományos, egyszerű adatbázis-keresési módszerek, mint például a LIKE operátor használata, gyakran lassúak, pontatlanok és nem képesek kezelni a természetes nyelv összetettségét. Itt jön képbe a teljes szöveges keresés, amely lehetővé teszi, hogy a felhasználók releváns eredményeket kapjanak, még akkor is, ha a pontos kifejezéseket nem ismerik.

Szerencsére nem kell bonyolult, külső rendszerekhez nyúlnunk azonnal. A PostgreSQL, a világ egyik legfejlettebb nyílt forráskódú relációs adatbázis-kezelő rendszere, beépített és rendkívül robusztus teljes szöveges keresési (Full-Text Search, FTS) képességekkel rendelkezik. Ezek a képességek lehetővé teszik számunkra, hogy hatékonyan, gyorsan és intelligensen keressünk nagy mennyiségű szöveges adatok között, mindezt az adatbázisunkon belül tartva. Ez a cikk egy átfogó útmutatót nyújt ahhoz, hogyan implementálhatjuk és optimalizálhatjuk a teljes szöveges keresést PostgreSQL adatbázissal.

Miért Válasszuk a PostgreSQL-t a Teljes Szöveges Kereséshez?

Mielőtt belemerülnénk a technikai részletekbe, érdemes megvizsgálni, miért érdemes a PostgreSQL FTS megoldását előnyben részesíteni:

  • Integrált Megoldás: Nem kell külön szervert vagy szolgáltatást üzemeltetnünk a keresési funkciókhoz. Minden az adatbázison belül marad, egyszerűsítve az architektúrát.
  • Rugalmasság és Erő: Támogatja a különböző nyelveket (stemmelés, stop szavak), a találatok rangsorolását, a kiemelést és a komplex boolean operátorokat.
  • Teljesítmény: Megfelelő indexeléssel (különösen a GIN indexekkel) rendkívül gyorsan képes hatalmas adathalmazokon keresni.
  • Költséghatékony: Nyílt forráskódú, így nincsenek licencköltségei, és a meglévő PostgreSQL infrastruktúrára épülhet.

A PostgreSQL Teljes Szöveges Keresés Alapjai

A PostgreSQL FTS két fő adattípusra épül: a tsvector-ra és a tsquery-re. Ezek a típusok teszik lehetővé a szöveges adatok hatékony tárolását és keresését.

tsvector: A Kereshető Dokumentum

A tsvector adattípus egy optimalizált reprezentációja egy szöveges dokumentumnak. Nem az eredeti szöveget tárolja, hanem annak tokenizált, normalizált és stop szavaktól megtisztított változatát. Ezt a folyamatot hívjuk parzsolásnak vagy feldolgozásnak. Lássuk a részleteket:

  • Tokenizálás (Tokenization): A szöveget kisebb egységekre, úgynevezett tokenekre (szavakra, számokra, szimbólumokra) bontja.
  • Normalizálás (Normalization): A tokeneket egységes formára hozza. Ide tartozik a kisbetűssé alakítás és a stemmelés (szótövesítés). A stemmelés lényege, hogy a szavak különböző alakjait (pl. „fut”, „futás”, „futtatott”) egyetlen gyökérformára (pl. „fut”) redukálja, így a keresés pontosabbá és átfogóbbá válik.
  • Stop Szavak (Stop Words): A nagyon gyakori, ám a keresési relevanciában kis szerepet játszó szavakat (pl. „a”, „az”, „és”, „vagy” – angolul „the”, „a”, „is”, „of”) eltávolítja. Ezzel csökkenti az index méretét és növeli a relevanciát.
  • Súlyozás (Weighting): Opcionálisan súlyokat (A, B, C, D) is hozzárendelhetünk a különböző részekhez (pl. cím: A, tartalom: B), ami később a találatok rangsorolásánál kulcsfontosságú lesz.

Ezeket a lépéseket egy nyelvi konfiguráció (pl. 'english', 'hungarian') alapján végzi a rendszer, amely tartalmazza a megfelelő tokenizálókat, szótárakat és stop szólistákat.

tsquery: A Keresési Kifejezés

A tsquery adattípus a keresési kifejezés parzsolt formáját reprezentálja. Ez a típus támogatja a boolean operátorokat (&: ÉS, |: VAGY, !: NEM) és a szótövesítést, így nagyon rugalmas keresési logikát valósíthatunk meg.

Kulcsfontosságú Függvények és Operátorok

A teljes szöveges keresés implementálásának gerincét néhány alapvető függvény és egy operátor alkotja:

to_tsvector(regconfig, text)

Ez a függvény konvertálja a sima szöveges adatot egy tsvector típusú objektummá. Az első paraméter a nyelvi konfiguráció, a második pedig a feldolozandó szöveg.


SELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog.');
-- Eredmény: 'brown':3 'dog':8 'fox':4 'jump':5 'lazi':7 'quick':2 'over':6

Látható, hogy a stop szavak (‘The’, ‘the’) eltűntek, a szavak stemmelve vannak (‘jumps’ -> ‘jump’, ‘lazy’ -> ‘lazi’), és a számok a szavak eredeti pozícióját jelölik.

to_tsquery(regconfig, text)

Ez a függvény alakítja át a felhasználó által bevitt keresési sztringet tsquery típusú objektummá. Szintén figyelembe veszi a nyelvi konfigurációt és a stemmelést.


SELECT to_tsquery('english', 'fox & dog');
-- Eredmény: 'fox' & 'dog'

SELECT to_tsquery('english', 'quick | lazy');
-- Eredmény: 'quick' | 'lazi'

@@ Operátor: Az Egyezés Ellenőrzése

A @@ operátor az, ami összeköti a tsvector és a tsquery objektumokat, és ellenőrzi, hogy a dokumentum (tsvector) tartalmazza-e a keresési kifejezést (tsquery).


SELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog.') @@ to_tsquery('english', 'fox & dog');
-- Eredmény: t (true)

SELECT to_tsvector('english', 'A quick brown cat.') @@ to_tsquery('english', 'fox | dog');
-- Eredmény: f (false)

Teljesítmény Optimalizálás: A GIN Index

A fenti műveletek önmagukban még nem lennének hatékonyak nagy adatbázisok esetén. A kulcs a sebességhez a megfelelő indexelés. A PostgreSQL a GIN (Generalized Inverted Index) indexet kínálja a tsvector típusú oszlopokhoz, ami drámaian gyorsítja a teljes szöveges kereséseket.

A GIN indexek invertált indexként működnek: minden egyedi lexémához eltárolják, hogy melyik dokumentumokban fordul elő. Ez rendkívül gyors keresést tesz lehetővé, mivel nem kell minden dokumentumot átvizsgálni. Két fő megközelítés létezik a GIN indexek használatára:

1. Indexelés Közvetlenül a to_tsvector Függvényre

Ez a legegyszerűbb módja a GIN index létrehozásának. Az index a függvény eredményét tárolja el.


CREATE TABLE cikkek (
    id SERIAL PRIMARY KEY,
    cim TEXT NOT NULL,
    tartalom TEXT NOT NULL
);

CREATE INDEX idx_cikkek_fts ON cikkek USING GIN (to_tsvector('hungarian', cim || ' ' || tartalom));

Ez működőképes, de van egy hátránya: minden egyes keresésnél újra meg kell hívni a to_tsvector függvényt a keresési oldalról is. Ezenfelül, ha a cim vagy tartalom oszlop frissül, az index nem frissül automatikusan. A jobb megoldás a perzisztált tsvector oszlop használata.

2. Perzisztált tsvector Oszlop Triggerrel

Ez az ajánlott legjobb gyakorlat. Létrehozunk egy külön tsvector típusú oszlopot (pl. tsv), amelyet egy trigger segítségével automatikusan frissítünk, amikor a releváns szöveges oszlopok (pl. cim, tartalom) megváltoznak. Az indexet ezután erre a tsv oszlopra építjük.


-- Hozzuk létre a táblát
CREATE TABLE termekek (
    id SERIAL PRIMARY KEY,
    nev TEXT NOT NULL,
    leiras TEXT NOT NULL,
    cikkszam TEXT UNIQUE,
    tsv TSVECTOR
);

-- Hozzuk létre a függvényt, ami generálja a tsvector-t
-- Fontos: itt súlyozhatjuk is a mezőket!
CREATE FUNCTION termek_tsv_update_trigger() RETURNS trigger AS $$
BEGIN
    NEW.tsv :=
        setweight(to_tsvector('hungarian', NEW.nev), 'A') ||
        setweight(to_tsvector('hungarian', NEW.leiras), 'B') ||
        setweight(to_tsvector('simple', NEW.cikkszam), 'C'); -- 'simple' konfiguráció számokhoz, azonosítókhoz
    RETURN NEW;
END
$$ LANGUAGE plpgsql;

-- Hozzuk létre a triggert
CREATE TRIGGER trg_termek_tsv_update
BEFORE INSERT OR UPDATE ON termekek
FOR EACH ROW EXECUTE FUNCTION termek_tsv_update_trigger();

-- Létrehozzuk az indexet a perzisztált tsv oszlopon
CREATE INDEX idx_termekek_tsv ON termekek USING GIN (tsv);

Ezzel a megközelítéssel a tsvector generálás csak akkor történik meg, amikor az adatokat módosítjuk, és az index közvetlenül a már feldolgozott adatokra mutat. Ez sokkal gyorsabb lekérdezéseket eredményez.

Haladó Keresési Funkciók

Találatok Rangsorolása (Ranking)

Gyakran nem elég csak megtalálni a dokumentumokat, hanem sorrendbe is kell őket állítani a relevancia alapján. Erre szolgál a ts_rank() és ts_rank_cd() függvény. Ezek figyelembe veszik, hogy hányszor szerepel a keresési kifejezés, milyen közel vannak egymáshoz a szavak, és milyen súlyú mezőkben (A, B, C, D) fordultak elő.


-- Szúrjunk be néhány adatot a termekek táblába
INSERT INTO termekek (nev, leiras, cikkszam) VALUES
('Okos TV', 'Smart TV 4K felbontással és Android operációs rendszerrel.', 'TV4K-001'),
('Okosóra', 'Vízálló okosóra pulzusmérővel és lépésszámlálóval.', 'ORA-002'),
('TV asztal', 'Modern TV asztal fa berakással, 120 cm széles.', 'ASZT-003');

-- Keressünk és rangsoroljunk
SELECT nev, leiras, ts_rank(tsv, to_tsquery('hungarian', 'okos & TV')) AS rank
FROM termekek
WHERE tsv @@ to_tsquery('hungarian', 'okos & TV')
ORDER BY rank DESC;

A ts_rank_cd() (cosine distance) egy alternatív rangsorolási algoritmust használ, ami általában jobban kezeli a nagyon rövid dokumentumokat.

Találatok Kiemelése (Highlighting)

A ts_headline() függvény segítségével kiemelhetjük a releváns találatokat a szöveges adatokban, ami jelentősen javítja a felhasználói élményt.


SELECT
    nev,
    ts_headline('hungarian', leiras, to_tsquery('hungarian', 'okos | pulzus'),
                'StartSel=, StopSel=, MaxFragments=1, FragmentDelimiter=..., MaxWords=15, MinWords=5') AS kiemelt_leiras
FROM termekek
WHERE tsv @@ to_tsquery('hungarian', 'okos | pulzus');

A fenti példában a StartSel és StopSel paraméterekkel adhatjuk meg a kiemeléshez használt HTML tageket, a MaxWords és MinWords pedig a kiemelt szövegrész hosszát szabályozza.

Boolean Operátorok és Prefix Keresés

A tsquery-ben használhatók a & (AND), | (OR), ! (NOT) operátorok a komplex lekérdezésekhez, valamint a <-> operátor a szavak egymáshoz való közelségének meghatározására.

  • 'kutya & macska': Ahol „kutya” és „macska” is előfordul.
  • 'kutya | macska': Ahol „kutya” vagy „macska” (vagy mindkettő) előfordul.
  • '!macska': Ahol „macska” nem fordul elő.
  • 'gyors <-> barna': Ahol „gyors” közvetlenül „barna” előtt áll.
  • 'gyors <2> barna': Ahol „gyors” és „barna” között maximum 2 szó van.

A prefix keresés (előtag alapú keresés) is lehetséges, például 'telefon:*' megtalálja a „telefon”, „telefonszám”, „telefonálás” szavakat is.


SELECT nev, leiras
FROM termekek
WHERE tsv @@ to_tsquery('hungarian', 'okos & (óra | telefon):*');

Nyelvi Konfigurációk Testreszabása

A PostgreSQL alapértelmezetten számos nyelvi konfigurációt tartalmaz (pl. english, hungarian). Ezek a konfigurációk határozzák meg a stop szólistákat, a stemmelő algoritmusokat és a tokenizátorokat. Néha szükség lehet azonban egyedi konfigurációra, például ha speciális szavakat nem szeretnénk stop szóként kezelni, vagy saját szinonima szótárakat szeretnénk használni.


-- Új konfiguráció létrehozása egy meglévő alapján
CREATE TEXT SEARCH CONFIGURATION my_hungarian (COPY = hungarian);

-- Egyéni stop szólista hozzáadása
ALTER TEXT SEARCH CONFIGURATION my_hungarian
ALTER MAPPING FOR asciiword, hword, hword_asciipart, hword_part
WITH simple; -- Itt beállíthatjuk, hogy ne stemmeljen, vagy saját szótárt adjunk hozzá.

-- Esetleg egyéni stop szólista beállítása
ALTER TEXT SEARCH CONFIGURATION my_hungarian
ALTER MAPPING FOR asciiword, hword, hword_asciipart, hword_part
WITH hungarian_stem, stop; -- ahol 'stop' egy egyéni stop szólista.

A simple konfiguráció hasznos lehet, ha nem akarunk stemmelést vagy stop szó szűrést alkalmazni (pl. cikkszámok vagy azonosítók esetén).

Összefoglalás és Legjobb Gyakorlatok

A PostgreSQL beépített teljes szöveges keresési funkciói egy rendkívül erős és rugalmas eszköztárat kínálnak a szöveges adatok közötti hatékony kereséshez. Ahhoz, hogy a legtöbbet hozza ki belőle, érdemes betartani a következő legjobb gyakorlatokat:

  • Használjon Perzisztált tsvector Oszlopot: Mindig hozzon létre egy külön tsvector típusú oszlopot, amelyet triggerekkel frissít, és erre építse a GIN indexet. Ez biztosítja a legjobb teljesítményt és konzisztenciát.
  • Mindig Használjon GIN Indexet: A GIN index elengedhetetlen a gyors lekérdezésekhez nagy adathalmazok esetén.
  • Válassza Ki a Megfelelő Nyelvi Konfigurációt: Használja az adatok nyelvét tükröző konfigurációt a pontos stemmelés és stop szó szűrés érdekében. Szükség esetén testreszabhatja a konfigurációkat.
  • Súlyozza a Különböző Mezőket: Rendeljen különböző súlyokat (A, B, C, D) a fontosabb mezőkhöz (pl. cím, kulcsszavak) a relevánsabb rangsorolás érdekében.
  • Optimalizálja a Lekérdezéseket: Használja ki a boolean operátorokat, a prefix keresést és a ts_rank() függvényt a felhasználói igényeknek megfelelő, releváns találatok érdekében.
  • Tesztelje és Figyelje a Teljesítményt: Mint minden adatbázis-optimalizálásnál, itt is fontos a rendszeres tesztelés és a teljesítmény monitorozása.

Habár léteznek dedikált külső keresőmotorok (pl. Elasticsearch, Apache Solr), amelyek specifikus, extrém skálázhatósági vagy nagyon összetett facetált keresési igények esetén kiválóak, a PostgreSQL teljes szöveges keresése a legtöbb webalkalmazás és adatvezérelt projekt számára több mint elegendő, egy integrált, robusztus és költséghatékony megoldást nyújtva. Ne habozzon kihasználni a benne rejlő lehetőségeket!

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük