SDK
10. fejezet

NER ingestion és a tudásgráf

A NER ingestion tool nem csupán egy feltöltési segédeszköz – ez a domain-specifikus tudás egyetlen, kanonikus forrása. Amit itt definiálsz, azt a platform mind a gráfépítéskor, mind a kérdés-megválaszoláskor használja. Ez a fejezet a tágabb képet mutatja: hogyan illeszkedik a tool a pipeline-ba, és miért van rá szüksége a platform query-oldali eszközeinek is.

10.1   A NER tool kettős szerepe

A AbstractNERIngestionTool leszármazottja egyszerre két feladatot lát el:

Ingestion időben a tool definiálja, mit kell az LLM-nek kinyernie a dokumentumokból: milyen entitástípusokat keressen, milyen relációkat ismerjen fel, és milyen kiegészítő szabályok vonatkozzanak a kinyerésre. A platform ezeket az információkat automatikusan juttatja el a BatchedNERTransformer-nek, amely Apache Spark-on keresztül, párhuzamosan dolgozza fel a dokumentumokat, és az eredményt az ArangoDB tudásgráfba írja.

Kérdés-megválaszoláskor ugyanezek a definíciók vezérlik a platform gráf-alapú eszközeit: az entitástípus-leírások bekerülnek az LLM instrukciókba, hogy a lekérdezési oldal pontosan ugyanazon fogalomrendszert értse, amellyel a gráf fel lett építve. Nincs konfigurációs duplikáció – egyetlen forrásfájl, két helyen felhasználva.

💡
A paraméteres implementáció részletei – defineEntityTypes(), defineRelationTypes(), getAdditionalRules(), getRelationTypesForPromptType() és a többi hook – az 5. fejezet 5.8 alfejezetében találhatók. Ez a fejezet a tágabb architektúrát és a pipeline integrációt tárgyalja.

10.2   Az ingestion pipeline – hogyan hívódik meg a tool?

A platform az ingestion pipeline legvégén automatikusan futtatja a domain-hez regisztrált összes IIngestionTool implementációt – a vektorizálási és indexelési lépések befejezése után. A NER tool ekkor kapja meg a feldolgozott dokumentumok mondatszintű adatait, és ezek alapján, LLM-alapú batch feldolgozással kinyeri az entitásokat és relációkat, majd beírja a tudásgráfba.

A JSON sémát és a prompt instrukciót a platform a defineEntityTypes() és defineRelationTypes() visszatérési értékéből automatikusan generálja – ezeket nem kell kézzel megadni.

ℹ️
Az executeIngestion() negyedik paramétere, a boolean pageLevelMode, azt jelzi, hogy a jelen ingestion futás a domain szokásos, mondat-szintű módjában vagy a nagy korpuszokhoz szánt page-level módban zajlik-e – ez utóbbi a mention-kulcs képzését érinti. Részletek lentebb, a 10.5 alfejezetben.

Idempotencia

Az ingestion idempotens: ha egy batch már feldolgozásra került, az LLM-alapú kinyerési lépés kihagyódik, és a tool egyből a gráf-írással folytatja. A gráf-írás maga is idempotens – újrafuttatás esetén nem keletkeznek duplikált csúcsok, csak az alias-listák bővülnek.

⚠️
A gráf-írás hozzáfűz az aliasokhoz – nem cseréli le őket. Ha az entitástípus-definíciók változtak és tiszta újra-ingestion szükséges, a tudásgráf gyűjteményeit manuálisan kell törölni és újra létrehozni. Erről a 10.4 alfejezet szól részletesebben.

10.3   A platform gráf-alapú eszközei – miért van szükségük a NER tool-ra?

A következő platform alaposztályok mindegyike konstruktor paraméterként kapja meg a domain NER ingestion tool-ját:

Platform alaposztály Hogyan használja a NER tool-t
AbstractEntityAnalysisTool defineEntityTypes() – az entitástípus-leírások bekerülnek az LLM instrukciójába, így az entitáskinyerés kérdezéskor ugyanazon fogalomrendszer szerint osztályoz, mint az ingestion.
AbstractContextSearchTool defineEntityTypes() – a kontextuselőállításhoz szükséges entitáskinyerés instrukciója a NER tool típuslistájából épül fel.
AbstractSummaryTool Domain és system azonosítás a NER tool-on keresztül (getDomain(), getEligibleSystem()).
AbstractPageAnalysisTool Az összes fenti tool közös alapja; tárolja a NER tool referenciát, és delegál a típuslistához.

A konstruktorban átadott NER tool tehát nem csak kényelmi megoldás – nélküle a gráf-alapú eszközök nem tudják, milyen entitástípusok léteznek a domainben, és az LLM instrukciójuk hiányos lenne.

ℹ️
Ha egy domainhez több NER tool is van (pl. eltérő system-ekhez), a gráf-alapú eszközökbe és az orchestrátorba az elsődleges NER tool kerül – az, amelyik a domainre jellemző fő entitástípusokat definiálja. A többi NER tool (pl. weboldalakhoz) párhuzamosan fut az ingestion pipeline-ban, de a kérdés-megválaszoló eszközök nem kapják meg.

10.4   Tiszta újra-ingestion

Ha az entitástípus- vagy relációtípus-definíciók megváltoztak, a korábbi gráf inkonzisztens lesz az új definíciókkal. Ebben az esetben a platform adminisztrátor törli és újra létrehozza a tudásgráf gyűjteményeit, majd újraindítja az ingestion-t.

A vektorizálási gyűjtemények (oldal- és összefoglaló vektorok) ilyenkor nem kell újraszámolni – azok az entitástípus-definícióktól függetlenek, és változatlanul felhasználhatók az újrafuttatásban.

Az ingestion cache-t is törölni kell a batch könyvtárból, hogy az LLM-alapú kinyerési lépés valóban újra lefusson az új definíciókkal.

10.5   Page-level ingestion mód – nagy korpuszokhoz

Nagyon nagy dokumentum-korpusznál (több tízezer dokumentum, milliós nagyságrendű oldalszám) a mondat-szintű vektorizálás és indexelés mérete és költsége aránytalanná válhat a retrieval-minőségi haszonhoz képest. Ilyen esetekre a platform egy alternatív ingestion/retrieval módot kínál: mondatonkénti vektor és LLM-alapú oldalösszefoglaló helyett kizárólag az oldal pontos szövegének egyetlen vektora és BM25-bejegyzése készül el.

A tudásgráf-építés (jelen fejezet tárgya, a NER ingestion tool executeIngestion() hívása) ettől függetlenül, változatlanul lefut page-level módban is – az egyetlen különbség, hogy a mention a PageEmbedding1024 rekord kulcsára mutat "_page" végződéssel, nem a szokásos, LLM-alapú oldalösszefoglalóra mutató "_summary" végződéssel.

A döntés – hogy egy adott domain+system kombináció page-level vagy mondat-szintű módban ingestál/retrievel-e – nem a NER ingestion tool felelőssége. A platform ezt az orchestrátor domain+system alapú hookjából vezeti le: AbstractOrchestrator.usePageLevelIngestion(String system) (→ 5.6). A NER ingestion tool ezt a döntést kizárólag mechanikus jelzésként, az executeIngestion() negyedik paraméterén keresztül kapja meg – saját maga nem dönt, és nem is tárolja tartós (instance-szintű) állapotként, mert a tool Spring singleton, egyetlen példánya szolgálja ki az összes domain+system kombinációt.

@Override
public void executeIngestion(String domain, String basePath, String progressBasePath,
        Dataset<Embeddings> lemmatizedEmbeddings, boolean pageLevelMode,
        IngestionProgressReporter progressReporter) {
}
ℹ️
A legtöbb domain-specifikus NER tool az AbstractNERIngestionTool örökölt executeIngestion() implementációját használja, és nem írja felül – a pageLevelMode paramétert a platform már kezeli. Felülírás csak akkor indokolt, ha a mention-kulcs képzésének egyedi domain-specifikus logikára van szüksége.

A progressBasePath és a progressReporter paraméter az executeIngestion() saját, közvetlen paramétere – nincs hozzá külön, kevesebb paraméteres változat. A platform minden hívásnál mindkettőt átadja; az AbstractNERIngestionTool saját implementációja a progressReporter-en keresztül több, egymást követő szakaszra bontva jelzi az előrehaladást, elem-szintű élő számlálással a hosszabb, kinyerési és gráfírási szakaszokban – a leszármazottnak ehhez nincs saját teendője.

10.6   Bemeneti szöveg testreszabása – buildPageTextDataset() és getMaxCompletionTokens()

Alapértelmezés szerint a NER lépés bemenete oldalanként, dokumentumonként csoportosított, mondatsorrendben összefűzött szöveg – az AbstractNERIngestionTool ezt a bemenetet egy felülírható hook-on keresztül építi fel:

protected Dataset<Row> buildPageTextDataset(Dataset<Embeddings> filteredEmbeddings) {
    // alapértelmezés: relevant == 1 mondatok, documentId/page/system szerint
    // csoportosítva, mondatsorrendben összefűzve
}

Felülírás nélkül a teljes ingestion anyagra a fenti, dokumentumoldal-szintű alapértelmezett viselkedés fut. Felülírva a leszármazott a domain ismeretében szűrheti, hogy mely dokumentumok (pl. csak egy adott dokumentumtípus-végződésű oldalak) és milyen tartalommal kerüljenek be az LLM-ágba, mielőtt a BatchedNERTransformer egyáltalán megkapná a szöveget – tehát nem az NER kimenetét szűri, hanem a bemenetét válogatja/alakítja át.

💡
Tipikus felhasználás: ha egy domain dokumentumtípusai fix, szabványosított szakaszszerkezettel rendelkeznek (pl. jogszabályi vagy hatósági sablon), a hook a mondatszintű bemenetből dokumentumonként egy vagy több, csak a releváns szakaszokat tartalmazó, összefűzött szöveget állíthat elő – ezzel elkerülhető, hogy irreleváns szakaszok (pl. adminisztratív részek) feleslegesen növeljék az LLM-hívás méretét, vagy hogy egy releváns szakasz két fizikai dokumentumoldal határán töredezetten kerüljön az LLM elé.

A visszaadott Dataset<Row>-nak documentId, page, system és text oszlopot kell tartalmaznia – a page oszlop nem feltétlenül valódi fizikai oldalszám, lehet a hook saját, szintetikus csoportosítási sorszáma is. Ez esetben a mention documentSentenceKey-e is ezt a szintetikus sorszámot tartalmazza majd, vagyis a mention nem egy konkrét fizikai oldalra, hanem csak a forrás-dokumentumra mutat vissza egyértelműen.

A NER lépés kimeneti JSON méretét (és ezzel a finish_reason=length csonkolás kockázatát) a bemeneti szöveg mérete közvetlenül befolyásolja – ha a buildPageTextDataset() felülírása jelentősen nagyobb egységeket hoz létre, mint az alapértelmezett, egy fizikai oldalnyi szöveg, érdemes vele együtt a completion-token plafont is megemelni:

protected int getMaxCompletionTokens() {
    return 16384; // alapértelmezett
}
⚠️
A token-plafon emelése önmagában nem old meg minden méretproblémát – nagy, egyetlen LLM-hívásba összevont bemenet a GPU KV-cache-t is jobban terheli, ami a kiszolgáló ütemezőjén preempciót és megnövekedett várakozási időt okozhat. Ha a buildPageTextDataset() felülírása változó méretű egységeket hoz létre, célszerűbb a bemenetet egy karakterszám-alapú küszöb szerint több, kisebb egységre bontani (pl. mohó bin-packing algoritmussal, szakaszhatárok mentén), mintsem egyetlen, tetszőlegesen nagy egységre emelt token-plafonnal kiszolgálni.

10.7   Relációtípus végpont-megkötés – validFromTypes/validToTypes

A defineRelationTypes() által visszaadott RelationTypeDescriptor opcionálisan megadhatja, hogy egy relációtípus két végpontján milyen entitástípus fogadható el:

new RelationTypeDescriptor(
    "TREATS",
    "készítmény vagy hatóanyag az adott javallatra/tünetre alkalmazható",
    Set.of("DRUG", "ACTIVE_SUBSTANCE"),   // validFromTypes
    Set.of("INDICATION", "SYMPTOM")       // validToTypes
)

Üres halmaz (vagy a kételemű, validFromTypes/validToTypes nélküli konstruktor) azt jelenti, hogy az adott relációtípus végpontjaira nincs megkötés – ez az alapértelmezett, visszafelé kompatibilis viselkedés minden olyan domainre, amely nem ad meg típus-megkötést.

A megkötés nem az LLM promptjába kerülő szabály, hanem íráskor, kódban érvényesített szűrés: a gráf-jelöltek előállítása minden relációnál ellenőrzi, hogy a forrás- és cél-entitás type mezője szerepel-e a megfelelő halmazban. Ha nem, a reláció – az LLM tényleges kimenetétől függetlenül – nem kerül a tudásgráfba, csak a naplóba, kihagyás jelzéssel.

ℹ️
Ez a szűrés determinisztikus és a promptból kihagyva is érvényesül – akkor is működik, ha az LLM egy adott futásnál a definiált szabályok ellenére rossz típusú entitást kapcsolna a relációhoz (pl. egy gyártót/hatóságot jelölő entitást kezelési kapcsolat forrásaként). A típus-vokabulárium (defineEntityTypes()) és a relációtípusok saját leírása (defineRelationTypes() description mezője) önmagában adja a megkötések forrását – nem kell külön, párhuzamos szabálylistát karbantartani.
💡
A RELATED_TO-szerű, általános célú, "ha más típus nem illik" relációtípusokra jellemzően nem érdemes megkötést megadni – ezek pont azért léteznek, hogy a fel nem sorolt típuskombinációkat is le tudják fedni.

Ha egy relációtípushoz mindkét végponton van megkötés (sem validFromTypes, sem validToTypes nem üres), és a bemeneti sorrend nem felel meg a megkötésnek, de a megfordított sorrend igen, a gráf-jelöltek előállítása nem dobja el a relációt, hanem a forrás- és cél-entitást megcserélve írja be – a naplóba ilyenkor iránymegfordítás kerül, nem kihagyás jelzés.

ℹ️
Ez a viselkedés azért csak akkor lép életbe, ha mindkét végponton van tényleges megkötés: ha az egyik oldal üres halmazzal, korlátozás nélkül van megadva, a "megfordítás" nem egyértelmű, ezért ilyenkor a szűrés a szokásos módon dönt. Tipikus eset, amikor ez segít: a forgalomba hozatali engedély jogosultját azonosító reláció, ahol a leírás (ORGANIZATION ↔ DRUG) szigorúan irányított, és az LLM néha felcserélve adja vissza a két végpontot.
💡
Egy aszinkron gráf-bővítő feladatban (→ 12.5 alfejezet), ahol a keresendő entitástípusok listája a teljes domain vokabulárium egy szűkített részhalmaza, a validFromTypes/ validToTypes megkötés nem csak utólagos szűrésre szolgál: a kinyerés automatikusan felveszi a hiányzó végpont-entitástípust a listába, ha az a szűkített listából kimaradt, de a domain teljes vokabuláriumában megtalálható.

10.8   Domain-specifikus utófeldolgozás hook

Az ingestion pipeline mention-írási lépése után az AbstractNERIngestionTool meghív egy felülírható, alapból üres hook-ot:

protected void postProcessDomainSpecificData(String domain, String pageEmbeddingCollectionName) {
    // alapértelmezés szerint nincs teendő
}

Ez a hook olyan feldolgozáshoz való, amelyet nem LLM-alapú NER-kinyeréssel, hanem determinisztikus szövegmintázat-illesztéssel vagy egyéb kódalapú logikával érdemes elvégezni – például egy fix, sablonos szövegrészletből (pl. jogszabályi vagy sablon-eredetű, mindig azonos szerkezetű mondatból) kinyerhető strukturált adat esetén, ahol egy regex megbízhatóbban adja vissza a pontos szöveget, mint egy LLM-alapú kinyerés.

ℹ️
A hívás _SUCCESS-fájllal védett, ugyanúgy, mint az ingestion többi lépése – egy már feldolgozott batch-hez tartozó hívás automatikusan kimarad, ismételt futtatás esetén sem fut le kétszer.

Oldalak lekérdezése névminta alapján

A hook-hoz gyakran szükség van a batch már ingestált oldalszövegeinek elérésére. Az ArangoDB-kapcsolat és a tárolt dokumentum-szerkezet nem publikus SDK felület – ehelyett az AbstractNERIngestionTool egy SDK-biztos, a tárolt szerkezetet elrejtő metódust ad:

protected List<PageTextEntry> findPagesByNameSuffix(String pageEmbeddingCollectionName,
        String system, String nameSuffix)

A visszaadott PageTextEntry(String name, String text) rekord az oldal nevét és teljes mondatszövegét tartalmazza. A metódus a névvégződés alapján szűr – akkor hasznos, ha egy adott dokumentumtípus egységes, felismerhető névutótaggal kerül ingestálásra.

💡
Példa: egy domain-specifikus, fix szerkezetű besorolási mondatot tartalmazó dokumentumtípus minden oldalán ugyanaz a névutótag szerepel. A hook findPagesByNameSuffix()-szal lekéri ezeket az oldalakat, egy regex-szel kinyeri a mondatot, majd upsertConcept()/ upsertRelation() hívásokkal beírja a talált adatot a tudásgráfba – önálló, domain-specifikus fogalom- és relációtípusként, amely nincs benne az LLM-alapú kinyerés defineEntityTypes()/defineRelationTypes() vokabuláriumában.

10.9   Összefoglalás – a NER tool mint a domain tudásának forrása

AbstractNERIngestionTool leszármazott
    │
    ├─ Ingestion pipeline (platform automatikusan futtatja)
    │       LLM-alapú entitás- és relációkinyerés
    │       → tudásgráf
    │
    ├─ AbstractPageAnalysisTool (+ leszármazottai)
    │       AbstractEntityAnalysisTool  ← defineEntityTypes() → LLM instrukció
    │       AbstractContextSearchTool   ← defineEntityTypes() → LLM instrukció
    │       AbstractSummaryTool         ← getDomain() / getEligibleSystem()
    │
    └─ AbstractOrchestratorTool
            getGraphRelationTypesForPromptType()  ← nerTool.getRelationTypesForPromptType()
            isGraphBoostEnabled()                 ← nerTool.isGraphBoostEnabled()

A NER tool az egyetlen hely, ahol a domain-specifikus tudásreprezentáció él. Ha egy entitástípust hozzáadsz, törölsz vagy átnevezed, az automatikusan megjelenik az ingestion pipeline-ban, az LLM instrukciókban és a gráfbejárási szűrőkben egyaránt – sehol máshol nem kell változtatni.