KnowledgeGraphService – saját gráf-ingestion külső forrásból
A 10. fejezet a platform beépített útját mutatja be:
a AbstractNERIngestionTool a platform saját, már feldolgozott és
beágyazott (embedding) szöveges dokumentumaiból építi a tudásgráfot, LLM-alapú
entitáskinyeréssel. Ez a fejezet a másik esetről szól: amikor a forrásadat nem a
platform dokumentum-pipeline-ján megy át – például egy külső rendszer API-jából,
adatbázisából vagy egyéb strukturált exportjából érkezik –, de a plugin mégis
szeretné ugyanazt a domain-izolált gráf-reprezentációt felépíteni belőle, hogy egy
másik tool később fel tudja használni.
Mikor van szükség erre?
A KnowledgeGraphService egy @Service singleton – bármely
plugin tool vagy saját service osztály @Autowired-del injektálhatja,
függetlenül attól, hogy van-e a domainhez NER ingestion tool. Közvetlen használata
akkor indokolt, ha:
- a forrásadat nem szöveges dokumentum, amit a platform document/embedding pipeline-ja dolgozott volna fel (pl. strukturált rekordok, egy külső rendszer REST API válasza, egy másik adatbázis export-ja)
- a plugin ennek ellenére gráf-reprezentációt akar építeni belőle – fogalmakat (csomópontokat) és a köztük lévő kapcsolatokat –, hogy egy másik tool ezt lekérdezhesse
Ilyenkor a plugin a AbstractNERIngestionTool egész gépezetét (Spark
batch, LLM-alapú entitáskinyerés, mondatszintű mention-ek) megkerülve, közvetlenül
hívja a KnowledgeGraphService UPSERT metódusait – a saját, már ismert
struktúrájú adatból nincs szükség LLM-es kinyerésre, a mezők közvetlenül leolvashatók.
KnowledgeGraphService @SdkApi(strip=true) jelölésű –
az SDK JAR-ban stub formában szerepel (az aláírások megvannak, a metódustörzsek
nem). Fordítási időben ez elég a plugin lefordításához; futásidőben a
PluginClassLoader parent-first betöltése miatt mindig a platform
valódi implementációja fut.
A gráf modell – collection-ok és named graph
A gráf három, domain-előtaggal ellátott ArangoDB collection-ből áll, ugyanazzal a névkonvencióval, mint a dokumentum-embedding collection-öknél:
| Collection | Típus | Szerepe |
|---|---|---|
{domain}KnowledgeConcept |
document (vertex) | Egy fogalom/csomópont: label, normalizedLabel,
type, aliases, systems. |
{domain}ConceptRelation |
edge | Két fogalom közti irányított él: _from, _to,
relationType, weight, systems. |
{domain}ConceptMention |
document | Egy fogalom előfordulása egy konkrét, visszakereshető dokumentum-egységben (opcionális – lásd lentebb). |
Domain nélkül (domain = null) a prefix üres – ez egy alapértelmezett
domain collection-jeit jelenti, amelyet lekérdezéskor nem kell külön megjelölni.
A három collection-t és a rájuk épülő named graph-ot az initCollections(domain)
hozza létre, idempotens módon (ha már léteznek, nem csinál semmit).
domain = null eset a platform korai, egy-domain-es időszakából
maradt meg fallback-ként. A platform adminisztrációs felületén ma már nem hozható
létre névtelen domain – minden domaint kötelező elnevezni. A null-ág továbbra is
működik (visszafelé kompatibilitásból), de új domain létrehozásakor mindig kapsz
egy konkrét domain nevet, amit a KnowledgeGraphService hívásaiban
(és minden más domain-szűrt API-ban) explicit át kell adnod.
upsertConcept / upsertRelation / saveMention
egyike sem hozza létre automatikusan a collection-öket – ha egyik sem létezik,
az ArangoDB hibát dob. Ha a domainhez tartozik már egy AbstractNERIngestionTool
(az a saját ingestion futásakor mindig meghívja az initCollections-t),
a collection-ök már léteznek, és a direkt gráf-írás rájuk épülhet. Ha a domainben
nincs semmilyen NER ingestion tool, a pluginnak magának kell
meghívnia az initCollections(domain)-t legalább egyszer, mielőtt az
első upsertConcept-et hívja (pl. egy IPluginConfigurer.configure()-ben
vagy a saját ingestion service első futásakor).
Írás: a gráf felépítése
upsertConcept(domain, system, label, type, aliases)
Felvisz vagy frissít egy fogalmat. A deduplikálás a label normalizált
formája (normalizeLabel() – kisbetűsítés + ékezet-eltávolítás) és a
type párosa alapján történik: ha már létezik ugyanilyen
normalizedLabel + type kombinációjú csomópont, a hívás
frissíti azt UPSERT-tel ahelyett, hogy duplikálná. Azonos label, de eltérő type
esetén két külön csomópont jön létre. Visszatérési érték: a
csomópont ArangoDB _key-e – ezt kell továbbadni az upsertRelation
és saveMention hívásoknak.
String patientKey = knowledgeGraphService.upsertConcept(
domain, system,
"Kovács Mária", // label
"PATIENT", // type – szabadon választott, saját taxonómia
List.of("K. Mária") // aliases – kereséskor is találatot ad
);
upsertConcept hívás nem cseréli le az
aliases / systems
listákat, hanem hozzáfűzi (APPEND) az újakat a régiekhez. Ha egy
teljesen tiszta újraingestion-t szeretnél (a régi állapot törlésével), előbb
hívd meg a deleteBySystem(domain, system)-et.
upsertRelation(domain, system, fromConceptKey, toConceptKey, relationType, bidirectional)
Felvisz vagy frissít egy irányított élt két korábban upsertConcept-tel
létrehozott csomópont között. A dedup kulcsa (_from, _to, relationType)
– ismételt előfordulás esetén a weight nő (kezdetben 1.0, minden
ismétlésnél +0.5). A bidirectional
flag csak metaadat – a gráf-bejárás iránya mindig _from → _to, a flag
a lekérdező logikának ad jelzést arra, hogy az élt visszafelé is érdemes-e követni.
knowledgeGraphService.upsertRelation(
domain, system,
patientKey, conditionKey,
"DIAGNOSED_WITH", // relationType – saját taxonómia
false // bidirectional
);
saveMention(domain, system, conceptKey, documentSentenceKey, documentCollection, mentionContext) – opcionális
Ez a metódus köti össze a fogalmat egy konkrét, visszakereshető szöveges
dokumentum-egységgel – a platform szövegalapú retrieval pipeline-jában ez
egy PageEmbedding1024 bejegyzés kulcsa. Ha a forrásadatod nem ilyen
szöveges oldal (mint egy strukturált külső rekord), nyugodtan kihagyhatod
– a fogalmak és relációk enélkül is felépülnek és lekérdezhetők label alapján
(lásd lentebb). A saveMention csak akkor szükséges, ha azt szeretnéd,
hogy a findPageKeysByLabels ezen fogalmakhoz retrievelhető oldal-kulcsokat
is visszaadjon a RAG kontextushoz.
deleteBySystem(domain, system) – tiszta újraingestion
Törli az adott system összes bejegyzését a domain gráfjából: a csak
ehhez a system-hez tartozó fogalmakat és éleket teljesen törli, a több system
között megosztottaknál csak a system-jelölést veszi le. Idempotens – ismételt
hívás hibátlanul lefut akkor is, ha már nincs mit törölni. A bevett minta egy
teljes újraingestion elején:
knowledgeGraphService.deleteBySystem(domain, system);
for (ExternalRecord record : source.fetchAll()) {
// ... upsertConcept / upsertRelation minden rekordra
}
Teljes példa – saját ingestion service külső forrásból
Az alábbi minta egy plugin saját, nem-IIngestionTool service osztálya,
amely egy külső rendszer strukturált rekordjaiból épít gráfot – nincs LLM-hívás,
nincs Spark, a mezők közvetlenül a forrásból olvashatók:
@Service
public class MyExternalDataGraphIngestionService {
@Autowired
private KnowledgeGraphService knowledgeGraphService;
public void ingest(String domain, String system, List<ExternalRecord> records) throws Exception {
// Tiszta újraingestion: előző állapot törlése erről a system-ről
knowledgeGraphService.deleteBySystem(domain, system);
for (ExternalRecord record : records) {
String parentKey = knowledgeGraphService.upsertConcept(
domain, system,
record.getParentLabel(), record.getParentType(),
record.getParentAliases());
String childKey = knowledgeGraphService.upsertConcept(
domain, system,
record.getChildLabel(), record.getChildType(),
List.of());
knowledgeGraphService.upsertRelation(
domain, system,
parentKey, childKey,
record.getRelationType(), false);
}
}
}
Ezt a service-t egy IPluginConfigurer.configure() triggerelheti induláskor,
vagy egy saját, ütemezett/manuálisan hívható tool – attól függően, hogy a külső
forrás milyen gyakran változik.
Olvasás: a gráf lekérdezése egy másik tool-ból
A felépített gráfot bármelyik tool lekérdezheti, amely szintén @Autowired-eli
a KnowledgeGraphService-t. A kész lekérdező metódusok mindegyike
normalizeLabel()-lel normalizált kulcsszavakat vár bemenetként.
findAliasesByLabels,
findConceptInfoByLabels, findConceptsByFuzzyLabel,
findPageKeysByLabels, findConnectedConcepts) automatikusan
figyelmen kívül hagyja a logikailag törölt ("kuka") állapotú fogalmakat - ezek úgy
viselkednek a hívó szempontjából, mintha nem is léteznének a gráfban. Ha egy korábban
megtalált fogalom váratlanul eltűnik a találatokból, ennek egyik lehetséges oka, hogy
valamelyik gráf-karbantartó feladat logikailag törölte - lásd a
Logikai törlés (kuka) szakaszt.
findAliasesByLabels(domain, normalizedLabels)
A megadott label-ekhez egyező fogalmak összes ismert (eredeti, ékezetes) alias-át
adja vissza – az input label-ekkel megegyezőket kizárva. Csak a
KnowledgeConcept collection-t nézi, ConceptMention
függetlenül is működik.
findConceptInfoByLabels(domain, normalizedLabels)
A megadott label-ekhez egyező fogalmak strukturált metaadatát adja vissza:
label, type, aliases – Map-ek listájaként.
Ugyanúgy ConceptMention-független; ez a fő belépési pont, ha a
forrásadatod nem szöveges, és egyszerűen csak "van-e ilyen fogalom, és mi a típusa,
milyen alias-okon ismert" kérdésre keresel választ.
findConceptsByFuzzyLabel(domain, conceptTypes, normalizedTerm, maxDistance, limit)
Elgépelés-toleráns fogalom-keresés: a normalizedLabel mezőt és a
normalizedAliases tömb egyes elemeit külön-külön Levenshtein-szerkesztési
távolság alapján hasonlítja össze a keresett névvel (betűcsere/transzpozíció is egyetlen
szerkesztési lépésnek számít), és a maxDistance-en belüli találatokat adja
vissza key/label párokként. Arra való, amikor a pontos vagy
szuffix-alapú névkeresés (findConceptInfoByLabels, vagy egy saját
executeReadOnlyQuery-alapú lekérdezés) nulla találatot ad, és ennek indoka
valószínűleg a felhasználó elgépelése, nem a fogalom tényleges hiánya.
normalizedAliases önálló ellenőrzése szándékos, nem csak kiegészítés: egy
fogalom teljes label-je gyakran a rövid nevet egy hosszú, kiszerelést/
hatáserősséget leíró szöveggel együtt tartalmazza (pl. "Flamborin 500 mg/ml belsőleges
oldatos cseppek"), aminek a szerkesztési távolsága egy rövid, elgépelt névtől már a
hosszkülönbség miatt is jóval meghaladja a reális küszöböt. A rövidebb, önálló nevet
tartalmazó alias viszont közvetlenül összevethető - ha csak a normalizedLabel-t
vizsgálnánk, egy ilyen összetett label-ű fogalom gyakorlatilag sosem adna fuzzy találatot.
A visszaadott jelöltek - pontosan úgy, mint egy alias-egyezés - nem garantáltan a keresett fogalomhoz tartoznak: egy kis szerkesztési távolság véletlenül egy teljesen más fogalomra is illeszkedhet. A bevett minta ezért a fuzzy találatokat egy LLM-alapú relevancia-szűrőn engedi át, mielőtt elfogadná őket - ugyanazon a szűrőn, amit egy alias-alapú találatra is használnál:
List<Map<String, Object>> directHits = findByExactOrSuffixMatch(domain, normalizedTerm);
if (directHits.isEmpty()) {
int maxDistance = Math.min(3, Math.max(1, (normalizedTerm.length() + 3) / 4));
List<Map<String, Object>> fuzzyHits = knowledgeGraphService.findConceptsByFuzzyLabel(
domain, List.of("DRUG"), normalizedTerm, maxDistance, 10);
// fuzzyHits minden elemét LLM-mel ellenőrizd, mielőtt elfogadnád találatnak
}
findPageKeysByLabels(domain, systems, normalizedLabels, allowedRelationTypes, minRelationWeight)
A megadott label-ekhez tartozó fogalmakhoz, majd (opcionálisan) az 1-hop gráf-szomszédaikhoz
tartozó ConceptMention.documentSentenceKey értékeket adja vissza –
ezek PageEmbedding1024 kulcsok, tehát a RAG kontextushoz retrievelhető
oldalakat azonosítanak. Ha nem hívtál saveMention-t az
adott fogalmakra, ez a metódus üres listával tér vissza rájuk – ez esetben a
findConceptInfoByLabels / findAliasesByLabels a releváns
lekérdező eszköz.
allowedRelationTypes és minRelationWeight paraméterek
null értékkel nincsenek korlátozva; üres Set az
allowedRelationTypes-nak letiltja az 1-hop bejárást (csak a
közvetlen egyezés oldalai jönnek vissza).
KnowledgeConcept csúcsok domain-szinten (nem system-szinten) megosztottak –
egy fogalmat több system NER futása is bővítheti ugyanazon a csúcson. A systems
paraméterrel a visszaadott oldalkulcsok a megadott system-ekhez tartozó
ConceptMention rekordokra szűkíthetők (null vagy üres lista =
nincs szűrés, minden system mention-je számít). System-szűrés nélkül egy megosztott
fogalom más system-ekhez tartozó oldalai is bekerülhetnek a találatba.
findConceptPagesByLabels(domain, systems, normalizedLabels, allowedRelationTypes, minRelationWeight)
Ugyanazokat a fogalmakat járja be, mint a findPageKeysByLabels, de az oldalakat
nem egyesíti: fogalmanként ad vissza egy ConceptPages rekordot (a fogalom kulcsa,
normalizált címkéje, az, hogy közvetlen egyezés vagy szomszéd, és az említéseinek oldalkulcsai).
Akkor hasznos, ha a hívó maga dönti el, mely fogalmaknak van megkülönböztető ereje – például
kihagyja azokat, amelyek szinte minden oldalon szerepelnek –, vagy azt vizsgálja, hány fogalom
említi ugyanazt az oldalt. Csak az említéssel rendelkező fogalmak szerepelnek; hiba esetén üres
listát ad. A paraméterek jelentése megegyezik a findPageKeysByLabels-ével.
countMentionedPages(domain, systems)
Azoknak az oldalaknak a száma, amelyeken a megadott system-ekben legalább egy fogalom-említés szerepel – a fogalmak oldalgyakoriságának viszonyítási alapja. Az eredmény domain és system-lista szerint néhány percig gyorsítótárazott, így egy frissen ingestált dokumentum rövid késéssel jelenik meg benne. Hiba esetén 0.
findConnectedConcepts(domain, normalizedLabels, relationType, reverse, targetType)
A megadott label-ekhez egyező fogalmakból kiindulva, egy megadott relationType
értékű élen 1 lépésben elérhető fogalmakat adja vissza – opcionálisan a célfogalom
type mezőjére szűrve. Általános, tetszőleges relációtípusra alkalmazható
bejárás: a visszatérési érték a talált fogalmak key/label/
type metaadata (nem oldalkulcs, mint a findPageKeysByLabels-nél).
List<Map<String, Object>> results = knowledgeGraphService.findConnectedConcepts(
domain,
List.of(normalizedLabel),
"DIAGNOSED_WITH", // relationType
true, // reverse
"PATIENT" // targetType – null = nincs típusszűrés
);
Az irány paraméterezhető: reverse=false esetén a bejárás a matched fogalom
felől kimenő élen megy (a matched fogalom az él _from oldalán áll),
reverse=true esetén a bejárás a matched fogalom felé bejövő élen megy
(a matched fogalom az él _to oldalán áll).
AbstractNERIngestionTool-on keresztül,
LLM-alapú kinyeréssel hozták létre, a reláció iránya (melyik végpont került a
from, melyik a to oldalra) kinyerésenként eltérhet – a
platform sem a promptban, sem kódban nem kényszeríti ki egy adott entitástípus
rögzített oldalra kerülését. Egy ilyen relációtípusra épülő bejárásnál emiatt érdemes
mindkét irányban lekérdezni (reverse=false és reverse=true),
és a találatokat a key mező alapján deduplikálni – különben a "másik"
irányban tárolt találatok kimaradnak. Ez a kockázat nem áll fenn a saját, kódból
hívott upsertRelation-nel létrehozott relációkra, ott az irány mindig a
hívó kódban rögzített, garantált.
executeReadOnlyQuery(domain, aql, bindVars)
Tetszőleges, a hívó által összeállított, csak olvasó AQL lekérdezést futtat a domain
tudásgráfján – olyan egyedi, több lépésben láncolt bejárási mintákhoz, amelyeket a
findPageKeysByLabels (egy ugrás) és a findConnectedConcepts
(egy relationType) nem fed le, például amikor egy fogalomhalmazból egy adott
relációtípuson át elért fogalmakból egy második, eltérő relációtípusú
ugrással kell tovább bejárni a gráfot.
A lekérdezés szövegében a három tudásgráf collection-re kizárólag négy lefoglalt placeholder néven keresztül lehet hivatkozni – a metódus ezeket a domainhez tartozó valódi collection-névre cseréli futtatás előtt, a hívó kód sosem szerez tudomást a tényleges elnevezési konvencióról:
| Placeholder | Feloldás | Használat az AQL szövegében |
|---|---|---|
@@concept | a domain KnowledgeConcept collection-je | collection bind-változóként, pl. FOR c IN @@concept |
@@relation | a domain ConceptRelation collection-je | collection bind-változóként, pl. FOR r IN @@relation |
@@mention | a domain ConceptMention collection-je | collection bind-változóként, pl. FOR m IN @@mention |
@graph | a domain named graph neve | egyszeres kukac, string bind-változóként egy GRAPH-záradékban |
@@concept-et és @@relation-t használja, a @@mention
és a @graph nem kerül bekötésre – nem kell (és nem is szabad) a nem használt
placeholder-eket "biztonságból" megemlíteni a lekérdezésben.
Map<String, Object> bindVars = Map.of(
"labels", List.of("fejfajas"),
"minWeight", 2.0
);
String aql = """
FOR c IN @@concept
FILTER c.normalizedLabel IN @labels
FOR r IN @@relation
FILTER r._to == c._id OR r._from == c._id
FILTER r.weight >= @minWeight
LET other = r._from == c._id ? DOCUMENT(r._to) : DOCUMENT(r._from)
RETURN { label: other.label, type: other.type, weight: r.weight }
""";
List<Map<String, Object>> rows = knowledgeGraphService.executeReadOnlyQuery(domain, aql, bindVars);
bindVars egyik kulcsa sem kezdődhet "@" karakterrel – ez a
négy lefoglalt placeholder kizárólagosságát kényszeríti ki: a hívó nem deklarálhat
saját collection bind-változót. A lekérdezés szövege emellett nem tartalmazhatja
(szóhatáron, kis- és nagybetűt nem megkülönböztetve) az INSERT,
UPDATE, REPLACE, REMOVE, UPSERT
kulcsszavak egyikét sem, sem pontosvesszőt, sem a fenti négytől eltérő
"@@" collection-hivatkozást. A négy lefoglalt placeholder közül legalább
az egyiknek szerepelnie kell a lekérdezésben – enélkül a metódus elutasítja, hogy a
csatorna kizárólag a tudásgráffal kapcsolatos lekérdezésekre legyen felhasználható, ne
egy általános célú, tetszőleges AQL-futtató legyen. Bármelyik feltétel megsértése
IllegalArgumentException-t vált ki, még a lekérdezés elindítása előtt. A
ténylegesen lefutó lekérdezésre futásidő- és memóriakorlát vonatkozik.
FOR x IN kifejezés szintaxisa ugyanabban a szintaktikai pozícióban fogad
el collection-nevet és egy korábban LET-tel számolt helyi listát is, ezt a
két esetet egy egyszerű szöveg-ellenőrzés nem tudja megbízhatóan megkülönböztetni
valódi AQL-parser nélkül. A hívó felelőssége, hogy collection-re kizárólag a lefoglalt
placeholder-eken keresztül hivatkozzon.
A lekérdezés RETURN záradékának objektumot kell visszaadnia (pl.
RETURN { label: c.label }), nem primitív értéket – a visszaadott
List<Map<String, Object>> minden eleme egy-egy visszaadott sor
mező-érték párjait tartalmazza.
Logikai törlés (kuka)
Egy fogalom nem csak a deleteBySystem-mel távolítható el fizikailag: a
KnowledgeGraphService egy logikai törlési ("kuka") állapotot is támogat,
amelyet tipikusan egy gráf-karbantartó feladat állít be olyan fogalmakra, amelyekről
utólag kiderül, hogy tévesen kerültek a gráfba (pl. egy kinyerési hiba miatt). Logikailag
törölt állapotban a fogalom-dokumentum, a hozzá kapcsolódó élek és mention-ök változatlanul
megmaradnak, csak a fenti lekérdező metódusok elől válnak láthatatlanná - az adat nem vész
el, a döntés visszavonható.
listDeletedConcepts(domain)
Az adott domainben jelenleg logikailag törölt állapotú összes fogalmat adja vissza -
key, label, type, a törlés időpontja és a törlés
mellett rögzített indoklás mezőkkel. Arra való, hogy egy adminisztrációs felület vagy
karbantartó tool át tudja tekinteni, mi került a kukába, mielőtt döntene a visszaállításról
vagy a végleges eltávolításról.
restoreConcept(domain, conceptKey)
Visszavonja egy fogalom logikai törlését - a fogalom ismét megjelenik a lekérdező metódusok találatai között, pontosan úgy, mintha sosem lett volna törölve. A hozzá kapcsolódó élek és mention-ök a törlés alatt is érintetlenek maradtak, így nincs mit helyreállítani rajtuk.
purgeDeletedConcepts(domain)
Az adott domainben logikailag törölt állapotú összes fogalmat, valamint a hozzájuk
kapcsolódó éleket és mention-öket véglegesen, visszavonhatatlanul eltávolítja a
tudásgráfból. Visszatérési érték: a véglegesen eltávolított fogalmak száma. Ezt a
metódust csak akkor érdemes hívni, ha a listDeletedConcepts alapján
megerősítést nyert, hogy a kukában lévő fogalmakra a jövőben sincs szükség - utána a
restoreConcept már nem tudja visszahozni őket.
Viszonya a NER ingestion pipeline-hoz
A KnowledgeGraphService ugyanaz az alaprétege mindkét útnak: a
10. fejezetben tárgyalt AbstractNERIngestionTool
valójában ugyanezeket az UPSERT metódusokat hívja a Spark-alapú, LLM-es entitáskinyerés
eredményéből, plusz minden feldolgozott mondathoz saveMention-t is hív
(mert ott a forrás mindig egy retrievelhető szöveges oldal). A különbség tehát nem a
gráf-modellben van, hanem abban, hogy ki és honnan tölti fel:
| Helyzet | Ajánlott megközelítés |
|---|---|
| A forrás a platform által ingesztált, embedding-elt szöveges dokumentum | AbstractNERIngestionTool leszármaztatása – LLM-alapú entitáskinyerés,
automatikus saveMention, beépül a standard ingestion pipeline-ba
(10. fejezet) |
| A forrás nem a platform szöveg-pipeline-ján megy át (külső API, struktúrált rekord, más adatbázis) | Saját service/tool, amely közvetlenül hívja a KnowledgeGraphService
UPSERT metódusait – nincs szükség LLM-es kinyerésre, a mezők ismert struktúrából
olvashatók |
| A direkt úton felépített gráf fogalmait egy másik tool-nak metaadatként kell elérnie (típus, alias-ok) | findConceptInfoByLabels / findAliasesByLabels –
ConceptMention nélkül is működnek |
| A direkt úton felépített fogalmakat retrievelhető szöveges oldalakhoz is szeretnéd kötni | saveMention hívása minden fogalom-előforduláshoz, majd
findPageKeysByLabels a lekérdezéshez |