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.
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.
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.
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.
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) {
}
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.
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
}
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.
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.
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.
ORGANIZATION ↔ DRUG) szigorúan irányított, és
az LLM néha felcserélve adja vissza a két végpontot.
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.
_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.
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.