Hogyan építs egy egyszerű keresőmotort a megfelelő adatszerkezet segítségével?

Képzeld el, hogy a digitális adatok hatalmas óceánjában úszol, és megpróbálsz megtalálni egyetlen, apró szigetet. Elveszettnek éreznéd magad, igaz? A mai világban az információ ereje kritikus, és éppen ezért olyan nélkülözhetetlenek a keresőmotorok. Gondoljunk csak a Google-re, Bingre, vagy akár a saját számítógépünk fájlkeresőjére. Mindegyik mögött bonyolult algoritmusok és optimalizált adatszerkezetek állnak, amelyek lehetővé teszik számukra, hogy másodpercek alatt találják meg, amire szükségünk van.

De mi van, ha nem a világ legfejlettebb keresőmotorját akarod megépíteni, hanem egy egyszerű, mégis működőképes rendszert, ami egy adott dokumentumhalmazban képes keresni? Ez a cikk pontosan erről szól. Megmutatjuk, hogyan építhetsz egy alapvető keresőmotort a nulláról, a megfelelő adatszerkezetek segítségével, amelyek a hatékony működés alapkövei.

Miért építsünk saját keresőmotort?

Lehet, hogy azon tűnődsz, miért foglalkoznál ilyesmivel, ha már léteznek kiváló keresőmotorok. Nos, ennek több oka is lehet:

  • Tanulás és megértés: Nincs jobb módja annak, hogy megértsd, hogyan működik egy komplex rendszer, mint az, ha magad építed meg. Bepillantást nyerhetsz az algoritmusok, az adatszerkezetek és a rendszermérnöki gondolkodásmód mélységeibe.
  • Specifikus igények: Lehet, hogy egy nagyon speciális adathalmazban szeretnél keresni – például egy belső vállalati tudásbázisban, egy személyes dokumentumgyűjteményben, vagy egy egyedi weboldal gyűjteményében, ahol a nyilvános keresőmotorok nem nyújtanak optimális megoldást.
  • Testreszabhatóság: Egy saját keresőmotorral teljes kontrollod van a rangsorolási algoritmusok, a szűrési mechanizmusok és a felhasználói felület felett.

Célunk egy olyan keresőmotor létrehozása, amely szöveges dokumentumokat indexel és képes reagálni egyszerű kulcsszavas lekérdezésekre, visszaadva azokat a dokumentumokat, amelyek tartalmazzák a keresett szavakat.

A keresőmotor alapvető komponensei

Mielőtt belevágunk az adatszerkezetekbe, nézzük meg, milyen fő részekből áll egy egyszerű keresőmotor:

  1. Crawler/Indexer (Feltérképező/Indexelő): Ez a rész felelős az adatok gyűjtéséért (pl. fájlok olvasása, weboldalak letöltése) és azok feldolgozásáért, hogy kereshetővé váljanak. Ez a folyamat létrehozza az indexet.
  2. Index (Adattár): Ez az a speciálisan strukturált adatgyűjtemény, amely lehetővé teszi a gyors keresést. Ideális esetben nem kell az összes dokumentumon végigmenni minden egyes keresésnél.
  3. Query Processor (Lekérdezésfeldolgozó): Ez a modul veszi át a felhasználó lekérdezését, feldolgozza azt (pl. normalizálja) és kikeresi a releváns dokumentumokat az indexből.
  4. Ranker (Rangsoroló): Ez a komponens értékeli a talált dokumentumok relevanciáját a lekérdezéshez képest, és sorrendbe rendezi őket, hogy a legfontosabbak jelenjenek meg először. Egy egyszerű keresőmotorban ez lehet egy nagyon alapvető algoritmus.

Az adatszerkezetek szerepe: A hatékony keresés alapkövei

A megfelelő adatszerkezetek kiválasztása kulcsfontosságú egy gyors és hatékony keresőmotor építéséhez. Nézzük meg a legfontosabbakat, amelyeket használni fogunk:

1. Fordított index (Inverted Index)

Ez a keresőmotorok abszolút lelke, a leghasznosabb adatszerkezet a szöveges kereséshez. Képzelj el egy hagyományos könyvindexet, ahol a szavak ábécérendben vannak, és mellettük látod azokat az oldalszámokat, ahol előfordulnak. A fordított index pontosan így működik, csak a dokumentumok (vagy azok azonosítói) a „helyszínek”.

Felépítése: Egy hash tábla (vagy szótár) ideális a fordított index megvalósítására. A kulcsok a dokumentumokban található egyedi szavak (termek/tokenek), az értékek pedig azoknak a dokumentumoknak a listái, amelyekben az adott szó előfordul. Ahhoz, hogy hatékonyabb legyen, a lista nem csak a dokumentum azonosítóját (DocumentID) tartalmazhatja, hanem a szó előfordulásainak számát is az adott dokumentumban (TermFrequency), sőt, akár az előfordulások pontos pozícióit is (Positions).

Példa:

  • Dokumentum 1: „A macska alszik a szőnyegen.”
  • Dokumentum 2: „A kutya ugat, a macska figyel.”

Fordított Index (egyszerűsítve):

    "a": [Doc1, Doc2]
    "macska": [Doc1, Doc2]
    "alszik": [Doc1]
    "szőnyegen": [Doc1]
    "kutya": [Doc2]
    "ugat": [Doc2]
    "figyel": [Doc2]

A fejlettebb változatok a DocumentID mellett tárolhatják a szó gyakoriságát és pozícióit is, ami segíti a relevancia számítását és a fázis alapú keresést.

2. Dokumentumtár (Document Store)

Amikor a keresőmotor talál egy releváns dokumentumot (az DocumentID alapján), valahogyan meg kell jelenítenie annak tartalmát. A dokumentumtár egyszerűen az eredeti dokumentumok tárolója. Ez lehet egy lista, egy tömb, vagy egy másik hash tábla, ahol a kulcs a DocumentID, az érték pedig maga a dokumentum szövege (vagy egy hivatkozás az eredeti fájlra).

Felépítése: Egy Python listában tárolhatod a dokumentumokat, ahol az index a DocumentID-nek felel meg, vagy egy szótárban, ahol explicit kulcsokat használsz.

3. Trie (Prefix Tree) – Opcionális, de hasznos

Bár egy egyszerű keresőmotorhoz nem feltétlenül szükséges, a Trie adatszerkezet rendkívül hasznos lehet olyan funkciókhoz, mint az automatikus kiegészítés (autocomplete) vagy a prefix alapú keresés („kuty” -> „kutya”, „kutyus”, „kutyatej”). Egy Trie minden csomópontja egy karaktert reprezentál, és a csomópontok útvonalai szavakat alkotnak. Gyorsan ellenőrizhető vele, hogy egy adott prefix létezik-e, vagy milyen szavak kezdődnek az adott prefix-szel.

4. Min-Heap / Max-Heap – Rangsoroláshoz

Amikor több ezer találat van, de csak a legrelevánsabb TOP-K eredményt akarjuk megjeleníteni, a min-heap vagy max-heap (prioritásos sor) adatszerkezet rendkívül hasznos. Segítségével hatékonyan kiválaszthatjuk a K legfontosabb elemet anélkül, hogy az összes elemet rendeznünk kellene. Például, ha a rangsorolási pontszámokat tároljuk a heapben, könnyedén megkaphatjuk a legjobb N találatot.

Az egyszerű keresőmotor építési lépései

Most, hogy megismerkedtünk a kulcsfontosságú adatszerkezetekkel, lássuk, hogyan áll össze a keresőmotor lépésről lépésre:

1. Adatgyűjtés és Dokumentum azonosítás

Először is szükségünk van a feldolozandó dokumentumokra. Ez lehet egy mappában lévő szöveges fájlok gyűjteménye, vagy akár egy előre definiált szövegek listája.


# Példa: Dokumentumok betöltése
documents = [
    "A macska alszik a szőnyegen.",
    "A kutya ugat, a macska figyel.",
    "A madár énekel a fán.",
    "A szőnyeg puha és meleg."
]
document_store = {} # Ez lesz a dokumentumtár
for i, doc_text in enumerate(documents):
    document_store[i] = doc_text # Index = DocumentID

Minden dokumentumhoz hozzárendelünk egy egyedi DocumentID-t (általában egy egész számot).

2. Szövegfeldolgozás (Preprocessing)

A nyers szöveg nem alkalmas közvetlenül az indexelésre. Szükséges néhány lépés a tisztítására és normalizálására:

  • Tokenizálás: A szöveget kisebb egységekre, ún. tokenekre (általában szavakra) bontjuk. Ehhez elválasztjuk a szavakat a szóközök, írásjelek mentén.
  • Kisbetűsítés: Minden szót kisbetűsre alakítunk, hogy ne számítson külön találatnak az „Alma” és az „alma”.
  • Stop szó eltávolítás: A gyakori, kevés információt hordozó szavakat (pl. „a”, „az”, „és”, „de”) eltávolítjuk. Ezek a stop szavak rengeteg helyet foglalnának az indexben, de ritkán segítenek a releváns dokumentumok megtalálásában.
  • Stemming/Lemmatizálás (opcionális): Ez egy fejlettebb lépés, ami a szavakat gyökerükre redukálja (pl. „futó”, „futás”, „futott” -> „fut”). Ez segít megtalálni azokat a dokumentumokat is, amelyek a keresett szó különböző alakjait tartalmazzák. Egy egyszerű keresőmotorhoz elhagyható.

import re
from collections import Counter

# Nagyon egyszerű stop szó lista
stop_words = {"a", "az", "és", "de", "vagy", "is", "egy"}

def preprocess_text(text):
    text = text.lower() # Kisbetűsítés
    tokens = re.findall(r'bw+b', text) # Tokenizálás (szavak kivonása)
    filtered_tokens = [token for token in tokens if token not in stop_words] # Stop szavak eltávolítása
    return filtered_tokens

3. Az Indexelés: A Fordított Index felépítése

Ez a kulcsfontosságú lépés. Iterálunk az összes dokumentumon, feldolgozzuk a szövegüket, és felépítjük a fordított indexet.


inverted_index = {} # A fordított index

for doc_id, doc_text in document_store.items():
    tokens = preprocess_text(doc_text)
    token_counts = Counter(tokens) # Számoljuk meg az előfordulásokat
    for token, count in token_counts.items():
        if token not in inverted_index:
            inverted_index[token] = []
        # Tároljuk a DocumentID-t és a TermFrequency-t
        inverted_index[token].append({'doc_id': doc_id, 'term_frequency': count})

# Példa az index egy részére:
# {
#   "macska": [{'doc_id': 0, 'term_frequency': 1}, {'doc_id': 1, 'term_frequency': 1}],
#   "alszik": [{'doc_id': 0, 'term_frequency': 1}],
#   "szőnyegen": [{'doc_id': 0, 'term_frequency': 1}],
#   ...
# }

Minden szóhoz hozzárendelünk egy listát, amely azon dokumentumok azonosítóit és a szó előfordulási gyakoriságát tartalmazza, ahol az adott szó megtalálható. Ez teszi lehetővé a gyors lekérdezést.

4. Lekérdezés feldolgozása

Amikor egy felhasználó beír egy keresőkifejezést, azt is feldolgozzuk ugyanazokkal a lépésekkel, mint a dokumentumokat.


def search(query):
    query_tokens = preprocess_text(query)
    results = {} # Itt gyűjtjük a releváns dokumentumokat és pontszámaikat

    for token in query_tokens:
        if token in inverted_index:
            # Iterálunk azon dokumentumokon, amelyek tartalmazzák az aktuális tokent
            for entry in inverted_index[token]:
                doc_id = entry['doc_id']
                term_freq = entry['term_frequency']

                # Egyszerű pontozás: minden talált szóért növeljük a dokumentum pontszámát
                # Itt lehetne sokkal bonyolultabb rangsorolást is bevezetni (pl. TF-IDF)
                if doc_id not in results:
                    results[doc_id] = 0
                results[doc_id] += 1 # Egy pont minden találatért
                # results[doc_id] += term_freq # Vagy a szó gyakorisága alapján
                
    return results # {'doc_id': score, ...}

5. Rangsorolás és Eredmények megjelenítése

A search függvény egy szótárt ad vissza, ahol a kulcsok a DocumentID-k, az értékek pedig az egyszerű relevanciális pontszámok. Most ezeket sorba rendezhetjük, és megjeleníthetjük az eredeti dokumentumokat.


def display_results(search_results, document_store, top_k=3):
    # Rendezés csökkenő pontszám szerint
    sorted_results = sorted(search_results.items(), key=lambda item: item[1], reverse=True)

    print(f"nTalálatok ({len(sorted_results)}):")
    if not sorted_results:
        print("Nincs találat.")
        return

    for i, (doc_id, score) in enumerate(sorted_results):
        if i >= top_k:
            break
        print(f"  Dokumentum ID: {doc_id}, Pontszám: {score}")
        print(f"  Tartalom: "{document_store[doc_id]}"")
        print("-" * 30)

# Futtassuk a keresőmotort!
query = "macska szőnyegen"
found_docs = search(query)
display_results(found_docs, document_store)

query2 = "madár dal"
found_docs2 = search(query2)
display_results(found_docs2, document_store)

Az egyszerű pontozás helyett használhatunk kifinomultabb algoritmusokat, mint például a TF-IDF (Term Frequency-Inverse Document Frequency), ami figyelembe veszi, hogy egy szó mennyire gyakori egy adott dokumentumban, és mennyire ritka az összes dokumentumban. Ez segít kiszűrni a kevésbé releváns, de gyakori szavakat, és előnyben részesíteni a speciálisabbakat.

Fejlesztési lehetőségek és kihívások

Ez az egyszerű keresőmotor egy nagyszerű kiindulópont. Íme néhány irány, amerre továbbfejlesztheted:

  • Fejlettebb rangsorolás: Implementáld a TF-IDF-et, vagy akár a BM25 algoritmust a relevancia pontozáshoz.
  • Fázis alapú keresés: Keresés pontos kifejezésekre (pl. „kutya és macska” egyben, nem csak a két szó külön-külön). Ehhez a szópozíciókat is tárolni kell a fordított indexben.
  • Fuzzy keresés és helyesírás-javítás: Használj Levenshtein távolságot vagy más algoritmusokat a hasonló szavak megtalálásához.
  • Felhasználói felület: Egy egyszerű webes felület Python Flask vagy Django segítségével, vagy egy asztali alkalmazás.
  • Scalability (Skálázhatóság): Nagyobb adatmennyiség esetén a memóriában tartott index már nem elegendő. Ekkor adatbázisokat (SQL vagy NoSQL) vagy elosztott rendszereket (pl. Elasticsearch) kell bevetni.
  • Adattípusok: Képzelj el keresést képek, videók vagy strukturált adatok között. Ez egészen másfajta indexelést igényelne.
  • Valós idejű indexelés: A dokumentumok változásainak azonnali kezelése és az index frissítése.

Összefoglalás

Ahogy láthatod, egy egyszerű keresőmotor építése rendkívül tanulságos feladat, amely bemutatja, hogyan válnak az absztrakt adatszerkezetek és algoritmusok valós, működő rendszerekké. A fordított index, a megfelelő szövegfeldolgozás és az alapvető rangsorolási logika megértése alapvető fontosságú. Bár a valódi, nagyméretű keresőmotorok óriási komplexitásúak, a mögöttük álló alapelvek gyakran éppen azokkal az egyszerű, elegáns megoldásokkal kezdődnek, amiket itt bemutattunk.

Ne habozz kísérletezni, bővíteni ezt az alapot! A programozás szépsége abban rejlik, hogy egy kis tudással és a megfelelő eszközökkel hatalmas rendszereket lehet építeni. Sok sikert a saját keresőmotorod megalkotásához!

Leave a Reply

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