Információgyűjtés
Ezen szócikk a különböző információgyűjtési módszereket próbálja egy kalap alá gyűjteni, és egy koncepcionális „keretrendszerben“ tárgyalni. A szócikk nagyon messziről, rendkívül általános fogalmi térből indít, és innen halad specifikusabb problémakörök felé.
Ha valami elsőre nem érthető
A dokumentáció több ponton tartalmaz szemléltető példákat. Amennyiben valami nem érthető elsőre, mindenképp célszerű a kapcsolódó példá(ka)t tanulmányozni, az lehet, megvilágítja a fogalmak jelentését.
Magukkal az egzakt módszerekkel a szekción belüli szócikkek foglalkoznak. Ezek megpróbálják a módszerre nézve konkrétabban indítani a kérdéskört, és onnan indulva helyezik bele ebbe az általánosabb kontextusba a módszert.
Mi az az információgyűjtés?
Az információgyűjtés alapvetően egy rendkívül tág témakör, mely nem is nagy határolható körül annál jobban, mint ahogy azt a szó maga sugallja: hogyan gyűjtünk információt, ami jól jön a válaszadási folyamat javításához. Mindenesetre teszünk egy kísérletet a szabatos definíciókra és problémaleírásokra.
Információkeresés
Adott: Egy \(\mathcal{I}_{\text{raw}}\) nyers információhalmaz, egy \(\mathbb{S}\) keresési entitás séma, egy \(S\) sémának megfelelő entitás, és egy \(\mathbb{f}\) elvárt formátum.
Feladat: Adjuk vissza az \(S\) alapján releváns információt \(\mathcal{I}_{\text{raw}}\)-re támaszkodva, \(\mathbb{f}\) formátumban.
Ez egy on-line probléma, így segíthet, ha két részre bontjuk: egy előfeldolgozás és egy kikeresés.
Információfeldolgozás
Adott: Egy \(\mathcal{I}_{\text{raw}}\) nyers információhalmaz, egy \(\mathbb{S}\) keresési entitás séma, és egy \(\mathbb{f}\) elvárt formátum.
Feladat: Készítsünk egy olyan \(\mathcal{I}\) információhalmazt, mely \(\mathbb{S}\) séma és \(\mathbb{f}\) formátum szempontjából \(\mathcal{I}_{\text{raw}}\)-vel ekvivalens információkat tartalmaz, és \(\mathbb{S}\)-sémájú \(S\) entitásra hatékonyan előállítható belőle \(\mathbb{f}\) formátumú válasz.
Információ-kikeresés
Adott: Egy \(\mathfrak{p}\) feldolgozó eljárás, egy \(p\) által feldolgozott \(\mathcal{I}\) információhalmaz, egy \(S\) keresési entitás és egy \(\mathbb{f}\) elvárt formátum.
Feladat: Adjuk vissza az \(S\) alapján releváns információt \(\mathcal{I}\)-re támaszkodva, \(\mathbb{f}\) formátumban.
Zavaros nevezéktan
Itt 3 fogalmat vezetünk be. Sokszor az egész témakörre hivatkozunk „keresés“-ként, néha csak a „kikeresés“ részre. Ugyanakkor a gyakorlatban a feldolgozás és a kikeresés általában egymáshoz van tervezve, és egységként kezeljük őket.
Halmaz-e a halmaz?
Amikor információhalmazról beszélünk, nem kívánjuk meg, hogy ez matematikai értelemben halmaz legyen (azaz elemek struktúrálatlan összessége). Valójában ez lehet nagyon szigorúan struktúrált adat is.
Egyszerű példa információkeresésre
Vannak számaink 1-től 100-ig, és mindegyik lehet piros vagy kék. Szeretnénk elérni, hogy ha egy adott színre rákérdezünk, akkor arra gyorsan tudjunk válaszolni, hogy mely számok olyan színűek.
Erre egy off-line algoritmus a következőképp működhet:
type Color = Literal["red", "blue"]
NUMBERS_AND_COLORS: list[tuple[int, Color]] = [
(1, "red"),
...,
(100, "red")
]
def get_good_numbers(input_color: Color) -> list[int]
good_numbers = []
for number, color in NUMBERS_AND_COLORS:
if color == input_color:
good_numbers.append(number)
return good_numbers
get_good_numbers("red")
Az algoritmus nyilván megoldja a problémát. Mi itt a szereposztás?
| Fogalom | Kódbeli megfelelő |
|---|---|
| \(\mathcal{I}_{\text{raw}}\) | NUMBERS_AND_COLORS |
| \(\mathbb{S}\) | Literal["red", "blue"] azaz Color |
| \(\mathbb{f}\) | list[int] |
Egyszerű példa információfeldolgozásra és -kikeresésre
Vannak számaink 1-től 100-ig, és mindegyik lehet piros vagy kék. Szeretnénk elérni, hogy ha egy adott színre rákérdezünk, akkor arra gyorsan tudjunk válaszolni, hogy mely számok olyan színűek.
Erre egy off-line algoritmus a következőképp működhet:
type Color = Literal["red", "blue"]
NUMBERS_AND_COLORS: list[tuple[int, Color]] = [
(1, "red"),
...,
(100, "red")
]
def preprocess(raw_dataset: list[tuple[int, Color]]) -> dict[Color, int]:
nums_per_color: dict[Color, list[int]] = {}
for num, color in raw_dataset:
if color not in nums_per_color:
nums_per_color[color] = []
nums_per_color[color].append(num)
return nums_per_color
NUMS_PER_COLOR = preprocess(NUMBERS_AND_COLORS)
def get_good_numbers(input_color: Color) -> list[int]
return NUMS_PER_COLOR[input_color]
get_good_numbers("red")
Az algoritmus nyilván megoldja a problémát. Mi itt a szereposztás?
| Fogalom | Kódbeli megfelelő |
|---|---|
| \(\mathcal{I}_{\text{raw}}\) | NUMBERS_AND_COLORS |
| \(p\) | preprocess() |
| \(\mathcal{I}\) | NUMS_PER_COLOR |
| \(\mathbb{S}\) | Literal["red", "blue"] azaz Color |
| \(S\) | "red" |
| \(\mathbb{f}\) | thon list[int] |
Innentől kezdve amikor információkeresésről beszélünk, akkor általában (feldolgozási módszer, kikeresési módszer) párokról beszélünk, és ilyen formában is fogjuk vázolni azt. Továbbá a keresési módszereket \(\mathfrak{s}\), a feldolgozási módszereket \(\mathfrak{p}\), a kikeresési módszereket \(\mathfrak{f}\) betűkkel jelöljük.
Vitatható, hogy szükséges-e ez az általánossági szint. Látni fogjuk, hogy sok esetben \(p\) olyan, hogy egyértelmű hozzárendelést hoz létre \(\mathbb{f}\) formátumú objektumok és kereshető objektumok között. Továbbá \(\mathfrak{p}\) külön való említése is felesleges túlbonyolításnak tűnik. Ugyanakkor utóbbi mégis szerencsés olyan szempontból, hogy maga a kikeresési módszer mélyebben függhet \(\mathfrak{p}\)-től, mint ahogy az \(\mathcal{I}\) struktúrájából kiolvasható.
Miért szükséges \(\mathbb{f}\)?
Tekintsük a következő példát: álljon a tárolt információink halmaza SQL táblákból és adatpontokból. Ekkor ha \(\mathbb{f}=\)„egyetlen szöveg“, akkor teljesen másképp kell a keresés eredményét megkonstruálnunk, mint ha \(\mathbb{f}=\)„díjak listája“.
Az információ formátuma: \(\mathbb{I}\) és \(\mathbb{i}\)
\(\mathbb{f}\)-hez hasonlóan definiálható \(\mathbb{I}\) és \(\mathbb{i}\) is, ami a kimenet formátuma helyett a nyers információ formátumát határozza meg. Előbbi magának az információhalmaznak a formátumát írja le, utóbbit pedig akkor alkalmazzuk, amikor a nyers információ valóban halmaz (vagy legalábbis elemek valamiféle összessége); ekkor \(\mathbb{i}\) az elemek formátumát írja le.
Amikor egy keresésről beszélünk, ezeket külön definiálni annyira nem hasznos, hiszen a feldolgozási módszer bemenete ezt impliciten ugyan, de meghatározza. Ellenben amikor több keresési módszert vegyítünk, akkor hasznos lehet az információformátumokkal is tudatosan foglalkozni.
A leggyakoribb forma
Mivel a feladat rendkívül általános, megpróbáljuk azokat az eseteit definiálni a feladatnak, amelyekkel foglalkozunk. Ez persze nem jelenti, hogy kizárólag ilyen kereséseket tudunk értelmezni (pont ezért definiáltuk általánosan a rendszert), mindenesetre a RAG rendszerünk sajátosságai miatt első sorban így érdemes gondolni a módszerekre. További információ a RAG-ról itt található.
Szöveges chunk alapú információkeresés kérdéshez
Adott: Szöveges chunk-ok egy \(\mathcal{D}\) halmaza, egy \(q\) kérdés és \(C\) kérdést támogató segédinformációk („kontextus“).
Feladat: Keressük meg \(q\) és \(C\) segítségével azon \(\mathcal{D}\)-beli chunk-okat, melyek tartalmazzák a pontos válaszadáshoz szükséges információt.
Az egyszerűség kedvéért erre „chunk-keresés“ feladatként hivatkozunk, illetve felbontott formában „chunk-feldolgozás“ és „chunk-kikeresés“.
Alternatív elnevezések
A „szöveges chunk“ fogalma egyet jelent a „metaadattal ellátott szöveg“ fogalommal. Épp ezért amikor nem RAG rendszerben vizsgáljuk ezt a problémát, alternatívan hívhatjuk szövegkeresésnek is. Továbbá szintén RAG-ra alapozva a „retrieval“ szó megfelelőjeként a visszakeresés vagy szöveges visszakeresés név is használatos.
Vizsgáljuk meg, mi milyen szerepet tölt be ebben a definícióban:
| Fogalom | Jelenlegi megfelelő |
|---|---|
| \(\mathcal{I}_{\text{raw}}\) | \(\mathcal{D}\) |
| \(\mathbb{S}\) | (kérdés, kontextus) pár |
| \(S\) | \((q, C)\) |
| \(\mathbb{f}\) | chunk-ok listája |
Itt \(\mathfrak{p}\) és \(\mathcal{I}\) még nem meghatározott.
Ezen feladathoz szorosan kapcsolódik a „chunk-olás“ problémája. Ezt itt nem tárgyaljuk, kitér rá a RAG koncepcionális szócikk és az LLM alapú dokumentum chunk-olási modul leírása is
Dokumentum chunk-olás (szöveges)
Adott: dokumentumok egy \(\mathcal{D}_{\text{raw}}\) halmaza (melyekben nem csak szöveges tartalom szerepelhet), illetve egy \(r\) szöveges chunk alapú információkeresési módszer kérdéshez.
Feladat: Bontsuk a dokumentumot jól kezelhető szöveges egységekre úgy, hogy az \(r\) módszer képes hatékonyan megtalálni (ki)keresés során az adott kérdéshez releváns egységeket.
Miért \(\mathcal{D}\) a nyers információ?
Valójában tekinthető úgy is, hogy \(\mathcal{D}_{\text{raw}}\) a nyers információ a keresési módszer számára. Ugyanakkor ha így teszünk, vigyázzunk, hogy magát a chunk-olást az előfeldolgozás részének kell tekintenünk. Ez alapvetően nem okoz problémát, ugyanakkor azt érdemes észben tartani, hogy ha több keresési módszerünk van, akkor mindnek az előfeldolgozási része ugyanarra a chunkolásra „van rákötve“ (mivel ezt nem keresési módszerenként akarjuk elvégezni, hanem egyszer).
Megjegyezzük végül, hogy ez a feladat nem csak szöveges chunk-okkal értelmezhető, hanem multimodálisakkal is. Ugyanakkor ennek a változatnak a felírásától eltekintünk, mivel jelenleg nem is alkalmazunk ilyet, illetve majdnem pontosan megegyezik ezzel a felírással.
Keresés rendezéssel
Tegyük fel, hogy egy adott dokumentumhalmazt már fel-chunk-oltunk, azaz sikeresen szövegekké alakítottuk őket, és ezeket fogyasztható egységekre vágtuk. Legyen ezen chunk-ok halmaza a korábbiaknak megfelelően \(\mathcal{D}\). Keresési módszertől függően előfordulhat, hogy meg tudunk határozni sorrendet \(\mathcal{D}\) chunk-jain, amely sorrendben a korai elemek „közel“, a késői elemek „távol“ helyezkednek el \((q,C)\) bemenethez. Innentől a \((q,C)\) bemenetet az egyszerűség kedvéért (ismét) \(S\)-sel jelöljük.
Legyen tehát a \(\mathcal{D}\) chunk-ok közelség szerint rendezett sora \(L_S^{\mathfrak{s}}(\mathcal{D})\) (ahol \(\mathfrak{s}\) ugye a keresési módszert jelöli). Ekkor a chunk-keresési feladat átfogalmazható a következő formára:
Rendező chunk-keresés
Adott \(\mathcal{D}\) chunk-halmazra és \(S\) bemenetre találjuk meg \(L_S^\mathfrak{s}(\mathcal{D})\) első néhány elemét.
A rendezéshez létezéséhez nem kell ugyan feltennünk, de általában rendelkezésre áll egy \(S\)-től független mérték is, ami ezt a chunk-bemenet közelséget meghatározza: legyen ez \(m\). Ekkor az \(m(S, d)\) érték azt mondja meg, hogy \(S\) bemenethez milyen közel áll a \(d\in \mathcal{D}\) szöveg. Ekkor \(L_S^{\mathfrak{s}}(\mathcal{D})\) előáll úgy, mint a \(\mathcal{D}\) elemei az \(m(S, d)\) értékek szerint csökkenő sorrendbe rendezve.
És ha nem szöveges?
Itt valójában nem használjuk ki, hogy az adatunk szöveges. A fogalmak mindössze annyit igényelnek, hogy az \(\mathcal{I}_{\text{raw}}\) nyers információhalmaz elemeit kelljen megtalálni, és hogy tetszőleges \(S\) keresési entitás alapján sorba lehessen rendezni \(\mathcal{I}_{\text{raw}}\) elemeit. Ez már bőven elegendő, hogy az információkeresési feladatot rendezés segítségével tudjuk kezelni.
Keresési módszerek kombinálása
Keresési lehet (sőt sokszor érdemes is) mind „sorosan“, mind „párhuzamosan“ egymás után kötni. Ilyenkor kifejezetten fontos, hogy figyeljünk arra, mi egy-egy módszer milyen formátumokon és sémákon dolgozik, hiszen így tudunk biztosra menni, hogy azt végzi a módszer, amit várunk. Erre egy egyszerű példa a „szűrés“ műveletének keresésként való értelmezése.
Keresés szűréssel
Ismét számokat veszünk 1-től 100-ig, melyeket színekkel látunk el. Ugynakkor most már minden számról azt is megmondjuk, hogy szomorú-e vagy vidám.
type Color = Literal["red", "blue"]
type Emotion = Literal["sad", "happy"]
NUMBERS_COLORS_EMOTIONS: list[tuple[int, Color, Emotion]] = [
(1, "red", "sad"),
...,
(100, "red", "happy")
]
Emellett rendelkezésre áll a korábbi keresési módszerünk, ami megtalálta az adott színnel rendelkező számokat:
def get_good_numbers(input_color: Color) -> list[int]
good_numbers = []
for number, color in NUMBERS_AND_COLORS:
if color == input_color:
good_numbers.append(number)
return good_numbers
Most legyen az a feladat, hogy „keressük meg a piros számokat, melyek szomorúak“. Ennek ez első felére már van megoldásunk, így mindössze annyit kell csinálnunk, hogy „leszűrjük“ az adathalmazt érzelem alapján olyan formátumba, amit a már létező megoldásunk megkíván.
def filter_emotion(input_emotion: Emotion) -> list[tuple[int, Color]]:
good_numbers_and_colors = []
for number, color, emotion in NUMBERS_COLORS_EMOTIONS:
if emotion == input_emotion:
good_numbers_and_colors.append((number, color))
return good_numbers_and_colors
Ekkor a teljes kód így néz ki:
type Color = Literal["red", "blue"]
type Emotion = Literal["sad", "happy"]
NUMBERS_COLORS_EMOTIONS: list[tuple[int, Color, Emotion]] = [
(1, "red", "sad"),
...,
(100, "red", "happy")
]
def filter_emotion(input_emotion: Emotion) -> list[tuple[int, Color]]:
good_numbers_and_colors = []
for number, color, emotion in NUMBERS_COLORS_EMOTIONS:
if emotion == input_emotion:
good_numbers_and_colors.append((number, color))
return good_numbers_and_colors
NUMBERS_AND_COLORS = filter_emotion("sad")
def get_good_numbers(input_color: Color) -> list[int]
good_numbers = []
for number, color in NUMBERS_AND_COLORS:
if color == input_color:
good_numbers.append(number)
return good_numbers
get_good_numbers("red")
Ennek a kódnak a minősége nem túl magas olyan szempontból, hogy globális változókon dolgozunk folyamatosan. Ugyanakkor nem is ez a cél, hanem hogy minimálisan szemléltesse, miről van szó a szakaszban.
Ezzel tehát sorosan kapcsoltuk egymás után a szűrést és a már létező keresést. Mi itt a szereposztás?
Szűrésnél:
| Fogalom | Kódbeli megfelelő |
|---|---|
| \(\mathbb{I}^1\) | list[tuple[int, Color, Emotion]] |
| \(\mathbb{i}^1\) | tuple[int, Color, Emotion] |
| \(\mathcal{I}^1_{\text{raw}}\) | NUMBERS_COLORS_EMOTIONS |
| \(\mathbb{S}^1\) | Emotion |
| \(\mathbb{f}^1\) | list[tuple[int, Color]] |
Keresésnél:
| Fogalom | Kódbeli megfelelő |
|---|---|
| \(\mathbb{I}^2\) | list[tuple[int, Color]] |
| \(\mathbb{i}^2\) | tuple[int, Color] |
| \(\mathcal{I}^2_{\text{raw}}\) | NUMBERS_AND_COLORS |
| \(\mathbb{S}^2\) | Literal["red", "blue"] azaz Color |
| \(\mathbb{f}^2\) | list[int] |
Vegyük észre, hogy \(\mathbb{f}^1\equiv\mathbb{I}^2\), ami azért szükséges, hiszen sorosan kapcsoltuk egymás után a két keresési módszert, és a kettő között nem alkalmaztunk transzformációs eljárást (nyilván ez ilyen módon is kezelhető lenne).
Végül érdemes megmelíteni, hogy az eredeti keresésünk is valójában egy szűrés.
Keresési módszerek kombinálására gyakorlati példát szolgáltat a Hibrid keresés, ami összekapcsolja a Keyword Search, Semantic Search és Reranking keresési modulokat.