Hogyan joinoljunk Elasticsearchben?

Oké, ennyire hülye címet is már régen adtam bármilyen postnak. De lássuk csak, hogy miről is akar ez szólni. Ugye a JOIN egy közkedvelt eszköze a relációs (RDBMS) adatbáziskezelőknek, ami pont az a tulajdonságot használja ki, hogy az adatbázis relációs és annak tartalma normalizált ÉS feltételezhetően vannak benne relációk. Ezen relációk adják az RDBMS rendszerek valódi lépéselőnyét a saját pályájukon. A JOIN lényege az, hogy egy vagy több tábla között létrehozunk logikai kapcsolatot és ezen kapcsolatokon keresztül bonyolult lekéréseket tudunk létrehozni. (vagy éppen ki tudjuk nyírni az egész RDBMS-t egy gyönyörű kaszkád szorzással…)
Namost az Elasticsearch az sem nem RDBMS, sem nincsennek benne reálciók (óóó, dehogy nincs… lásd a cikk végén a parent/child relációt…) és úgy egyébként a JOIN-nak sincs semmi értelme benne. Miért is nincs értelme a JOIN-nak? Az Elasticsearch (és igaz ez a legtöbb key=value alapú NoSQL motorra) pont arra az elvre épül fel, hogy minden egyes dokumentum önálló elem melyek önmagukban értemezhetők és esetleg valamilyen statisztikai elemzést vagy aggregációt akarhatunk rajta végrehajta. Mivel erre van optimalizálva az egész motor, ezért semmi értelme az olyan relációs adatmodellnek, ahol egy indexben tárolunk mondjuk dokumentumokat és felhasználói ID-kat, egy másik indexben pedig tárolunk a felhasználói ID-khoz tartozó neveket és egyéb adatokat. Sokkal egyszerűbb minden releváns adatot minden dokumentum esetén tárolni, hiszen a háttérben a NoSQL motor gondoskodik arról, hogy ezek az adatok ne redundánsan tárolódjanak. Ráadásul az ilyen erőltetett kereszthivatkozások csak lassítják is a keresési performanciát.

Ha ennek ennyire nincs értelme, akkor miért kell mégis foglalkozni a JOIN kérdéssel? Ennek az oka igen prózai. Nagyon sok cég ismeri fel a NoSQL megoldások előnyeit. Ennek megfelelően szeretnék ezt minél gyorsabban és minél egyszerűbben kiaknázni. A full-text searching és a near-realtime indexing, továbbá a végtelenségig skálázható működés miatt sokat szeretnék az átállást egyszerűen megúszni. Ennek a legtriviálisabb megoldása a meglévő RBDMS alapú adatbázis struktúra “smartcopy”-ja. Tehát egy egyszerű adatbázis export után minimális átalakítással betölteni a nosql szempontjából releváns adatokat pl. elasticsearchbe, majd hibrid módon használni vagy mind az RDBMS-t, mind a NoSQL-t, vagy ha az adatbázis struktúra engedi, akkor akár csak a NoSQL-t.
Itt találkozik az egyszeri integrátor először azzal a kihívással, hogy az adott adatbázist használó alkalmazás kőkeményen támaszkodik a primary key-eken keresztül normalizált relációkra az egyes táblák között. Alábbiakban bemutatok néhány egyszerű megoldást ezen probléma hatékony kezelésére:

Adat denormalizálás

Talán ez a leglogikusabb lépés és hosszú távon ez fizetődik is ki legegyszerűbben. Mit is jelent ez: Ott ahol szükségünk lenne ID lookupra egy RDBMS-ben, ott NoSQL-ben egyszerűen az ID helyett, vagy az ID mellett rakjuk be a joinolandó mező értékét az hivatkozott helyre. Mint fentebb írtam, ez valójában sem tárhely, sem egyéb szempontból nem hátrányos, sőt iszonyatosan gyorsítja a keresési performanciát. Mit jelent ez a gyakorlatban, alább egy konkrét denormalizálási példa:
PUT /test/user/1
{
"name": "John Smith",
"email": "john@smith.com",
"dob": "1970/10/24"
}
PUT /test/blogpost/2
{
"title": "Relationships",
"body": "It's complicated...",
"user": {
"id": 1,
"name": "John Smith"
}
}

Látható, hogy bár megvan a reláció a blogpost és az users index-type között, de a blogpost typeon belül az egyes dokumentumokba bekerül redundánsan user.id mellé a user.name mező is. Így szabadon lehet erre keresni direktben a konkrét (vagy pont hogy nem annyira konkrét…) nevekre anélkül, hogy JOIN-olni kellene.

commercial break...

Alkalmazás oldali joinolás

Oké, az előző példa talán túlságosan is egyszerű és különben is, miért akarnánk a szerzőnek a lába méretét is elátolni az összes dokumentuma mellett. Lássuk be imádjuk a relációkat és amúgy is van egy csomó olyan eset, amikor logikus a relációk használata. (jól példa erre a láb méret…)
Erre egy nagyon hatékony megoldás lehet az alkalmazás oldali joinolás használata. Erre egy konkrét példa:
GET /test/user/_search
{
"query": {
"match": {
"name": "John"
}
}
}
GET /test/blogpost/_search
{
"query": {
"filtered": {
"filter": {
"terms": { "user": [1] }
}
}
}
}

A második requestben szereplő “[1]” tag automatikusan jelzi az elasticsearchnek, hogy oda helyettesítse be az első keresés eredményét. Tehát itt valójában nem egy JOIN hajtódik végre, hanem egy soros végrehajtás, ahol az első query eredménye behelyettesítésre kerül a második querybe. (ezért is hívják alkalmazás oldali joinnak…)

Nested objektumon keresztüli relációk

Az előző pontban említett adat denormalizációnak azonban van egy nagyon fontos hátulütője. Vegyük azt a példát, hogy szerenténk a fenti blogbejegyzésekhez tartozó hozzászólásokat is az adott blogbejegyzés alatt tárolni egy comment alobjektum alatt. Ilyenkor az összes comment egy dokumentumon belül lesz elérhető, azonban a commentek alatti dokumetum struktúra nem lesz független, tehát ha van a commentnek egy olyan fix mezője, hogy name és egy másik fix mezője, hogy “dob” (date of birth), majd szerenténk rákereni az összes olyan commentre, amit az 1970/10/24 születésű John-ok írtak:

"query": {"bool": {
"must": [
{ "match": { "user.name": "John" }},
{ "match": { "user.dob": "1970/10/24" }}
]
} }

Akkor eredményként meg fogjuk kapni az ÖSSZES olyan commentet aminek a szerzőjének a nevében szerepel a John VAGY 1970/10/24-ei születésű író írta. Ez azért alakulhat így ki, mert egy dokumentumon belül az elasticsearch már nem kezel relációkat. Ezen probléma feloldására találták ki a “nested” objektum típust. A nested type jelöli az elasticsearchnek, hogy az az alatti objektumok önálló beágyazott objektumok, amik egymástól függetlenek. Ezzel a módszerrel könnyedén lehet kezelni a denormalizálás nagatív hatásait, hogy megmaradnak a relációk és pl egy dokumentumhoz kapcsolódó adatok (pl. egy email attachmentjei, vagy egy blogbejegyzés commentjei, vagy egy megrendeléshez kapcsolódó szállítási történet) könnyedén törölhető úgy, hogy a fő dokumentumot töröljük, nem fog ilyenkor szemét maradni az adatbázisban.

Szülő-gyermek (parent-child) reláció használata

Na ez egy olyan konkrét megoldás, amiről ugyan hallottam, viszont még soha nem szorultam a használatára, így nagyon nem elmélkednék olyasmiről amihez nem értek. Lényegében egyébként ugyanaz mint a nested object, azzal a különbséggel, hogy míg a nested beágyazásnál ugyanabban a konkrét dokumentumban történik a tárolás, addig a parent/child relációnál a szülő és gyermek dokumentumok fizikailag is külön dokumentumban találhatók, csak éppen tartalmaznak egy konkrét hivatkozást, amivel az ES-nek jelik, hogy ők bizony összefüggő elemek. A jellegéből fakadóan ez ugye “one-to-many” reláció, tehát egy parenthez tartozhat sok child dokumentum. Ez eddig ugye ugyanaz az elv, mint a nested object, akkor miért is van erre szükség?
A választ az adat felhasználásának jellege adja. Mivel a nested-nél a nagy dokumentum tárolja az összes nested objectet, így ha pl updatelni akarjuk az egyik beágyazott doksit, akkor az egész nagy dokumentum válatozik (tehát annak a verziója updatelődik), ugyanígy ha pl sűrűn szerenténk újabb beágyazott dokumentumokat hozzáadni vagy törölni, akkor szintén a nagy dokumentum updatelődik folyamatosan. Tehát ilyen esetekben érdemesebb inkább a parent-child relációt használni. A hogyanra, majd visszatérek egy későbbi postban, ha rászorulok ennek a használatára. Addig is, ha valakinek pont ez kellene, az mehet is az elastic.co-ra a részletekért.

Bookmark the permalink.

Szólj hozzá: