Big Data

Apache CouchDB – Deo I

Imate problem koji je teško definisati pomoću standardne relacione sheme? Za razliku od relacionih baza podataka, ni ranije opisani MongoDB ni CouchDB ne koriste tabele za čuvanje podataka i relacija između njih, već kolekcije nezavisnih dokumenata. Obe ove baze se kategorišu kao document oriented NoSQL. Fiksna shema podataka ne postoji, već svaki dokument sadrži sopstvenu, samoopisujuću shemu. CouchDB koristi JSON zapis za skladištenje podataka unutar dokumenata. Upiti i razne operacije sa podacima se mogu izvršiti preko programskog jezika JavaScript. Sa bazom se može komunicirati direktno, preko RESTful API-ja.

Uvod i osnovni pojmovi

CouchDB ima moto “relax” (“opusti se”). Dizajniran je imajući u vidu računarske mreže i sve njihove mane, greške, otkaze i kvarove. Postavljen je fokus na lakoću korišćenja. Primetna je izuzetna otpornost na greške, a posebna pažnja je posvećena brizi o podacima i skalabilnosti. CouchDB nudi alate za upravljanje konfliktima, master-master replikaciju baze, eventualnu konzistentnost i viševerzionu kontrolu konkurentnosti. CouchDB se može instalirati i pokrenuti gotovo svuda: na kućnim računarima, serverima, masivnim klasterima, ali i mobilnim uređajima, i to na gotovo svim novijim verzijama modernih operativnih sistema.

Sam CouchDB je napisan u programskom jeziku Erlang. Razvija se od strane Apache softverske fondacije. Detaljno uputstvo za instalaciju se može naći na adresi. Evo nekih osnovnih pojmova koje ćemo koristiti:

  • CRUDCreate, Read, Update, Delete – operacije koje kreiraju, čitaju, ažuriraju i brišu podatke,
  • cURL – alat komandne linije za primanje ili slanje podataka koristeći URL sintaksu i razne protokole,
  • REST/HTTPREpresentational State Transfer/Hyper Text Transfer Protocol – arhitekturalni stil i pristup komunikacijama koji se najčešće koristi za razvoj veb servisa. Njegova najpoznatija implementacija je HTTP. Koristi metode kao što su GET, PUT, POST, DELETE, HEAD itd.
  • API – aplikacioni programski interfejs, određuje rečnik konvencije pozivanja koje programer treba da primeni kako bi koristio eksponirane servise.

Dokumenti

Kao što je već naglašeno, CouchDB koristi dokumente umesto tabela kao osnovne jedinice za čuvanje podataka. Za razliku od MongoDB-a, koji dokumente čuva u BSON formatu, CouchDB koristi standardni JSON.

O dokumentima

Dokumenti su u mnogo čemu prirodniji za korišćenje od tabela – to je zato što se modeluju prema “opipljivim” dokumentima koje koristimo svakodnevno. Dok se u relacionim bazama model podataka mora znati unapred, u bazama orijentisanimm na dokumente striktna shema ne postoji, pa se model podataka može definisati u hodu.

Na primer, faktura sadrži sve značajne informacije o jednoj transakciji: kupca, prodavca, datum, listu proizvoda ili usluga koji su prodati itd. Dokumenti su samoodrživi, nema apstraktnih referenci na papiru koje pokazuju na neke druge papire. Većina vizit karti sadrži približno iste informacije – nečiji identitet, zaposlenje i kontakt informacije. Dok tačna forma vizit karti može varirati, opšte informacije ostaju iste i uvek možemo prepoznati da se radi o vizit karti. Zato se vizit karta može smatrati za dokument iz realnog sveta. Neko na vizit karti može imati i broj telefona i broj faksa, dok neko drugi može imati samo broj telefona. U tom slučaju nećemo pisati Faks: NULL, već ćemo samo izostaviti taj podatak. Dokumenti iz realnog sveta, poput vizit karti, su obično semantički vrlo slični – sadrže skoro isti tip informacija. Međutim, karte se sintaksno ili po strukturi informacija mogu izuzetno razlikovati. Nama kao ljudskim bićima nije teško da se prilagodimo ovim varijacijama.

CouchDB dokumenti

Osvrnimo se sada na interno funkcionisanje dokumenata u CouchDB-u. Dokument prima JSON objekat koji sadrži parove ključ/vrednost. Ti parovi se nazivaju polja. Svaki dokument sadrži identifikaciono polje _id, koje mora biti jedinstveno na nivou baze i nikad se ne menja.

Dokumenti nakon kreiranja dobijaju i _rev polje koje se naziva polje revizije. Ovo polje će dobiti novu vrednost svaki put kada se dokument modifikuje. Format ovog polja se sastoji od celog broja koji označava broj promena dokumenta, zatim crtice i na kraju pseudonasumičnog niza karaktera.

Imena polja koja počinju sa donjom crtom imaju posebno značenje i _id i _rev su posebno bitna. Da bi se dokument obrisao ili modifikovao, potrebno je obezbediti i _id i _rev vrednost dokumenta. Ako se neka od ove dve vrednosti ne poklapa, CouchDB će odbiti operaciju. Ovako se sprečavaju konflikti – osiguravanjem da samo najnovije verzije dokumenta mogu menjati.

CouchDB ne omogućava transakcije ili zaključavanja. Da bi se sadržaj dokumenta modifikovao, prvo se mora pročitati, pritom vodivši računa o _id i _rev vrednostima. Zatim se traži promena, tako što se prilože vrednosti dokumenta u celosti, uključujući i _id i _rev. Prva operacija koja dođe na red će se prva i izvršiti. Time što traži identični _rev, CouchDB se osigurava da dokument koji se ažurira nije već ažuriran ranije.

Futon (Fauxton)

CouchDB u sebi sadrži Futon (u najnovijim verzijama Fauxton), veb grafički interfejs koji olakšava rad sa bazom. Nakon instalacije i pokretanja CouchDB-a, potrebno je otvoriti internet pregledač i u adresnu liniju ukucati sledeće:

http://localhost:5984/_utils

Odlazak na označenu adresu će otvoriti glavnu stranu Futon-a:

Za početak je potrebno napraviti bazu podataka sa kojom želimo da radimo. Baza u ovom primeru će biti zasnovana na muzičarima i njihovim izdatim albumima i pesmama. Klik na Create Database… otvara formu za kreiranje baze. U iskačućem prozoru uneti music i kliknuti Create. Baza je kreirana i korisnik je automatski preusmeren na stranu za upravljanje upravo napravljenom bazom. Odatle se mogu kreirati novi dokumenti ili otvoriti već postojeći.

CRUD operacije preko Futon-a

Grafičko okruženje omogućava jednostavno izvršavanje osnovnih operacija nad podacima. Za kreiranje novog dokumenta, potrebno je kliknuti na dugme New Document. Svaki dokument mora imati _id polje, koje korisnik može imenovati po želji ili ostaviti bazi generisanje vrednosti.

Unutar dokumenta se mogu dodati nova polja, i to preko Add Field dugmeta. Potrebno je uneti par ključ/vrednost – unutar Field kolone unećemo name, a unutar Value kolone unećemo The Beatles. Izmene se čuvaju pritiskom na zeleni taster, a poništavaju pritiskom na crveni taster. Primetiti da je _rev polje izmenjeno i da sada počinje sa brojem 2.

Za kasnije potrebe, dodaćemo još par vrednosti u dokument. Pošto smo odlučili da će dokument predstavljati grupu The Beatles, dodaćemo sledeći JSON niz kao vrednost ključa albums istim postupkom:

[{
    "title": "Help!",
    "year": 1965
},{
    "title": "Sgt. Pepper's Lonely Hearts Club Band",
    "year": 1967
},{
    "title": "Abbey Road",
    "year": 1969
}]

Dokument se može obrisati klikom na dugme Delete Document.

REST API i HTTP CURL

Sva komunikacija sa CouchDB je zasnovana na REST-u, što u praksi znači davanje komandi preko HTTP-a. Komande će se zadavati preko konzole. Za početak, potrebno je pokrenuti sledeću komandu:

$ curl http://localhost:5984/
{"couchdb":"Welcome","version":"1.1.1"}

Zadavanje GET zahteva (podrazumevan u cURL-u) vraća informacije o naznačenom resursu unutar URL-a. U ovom slučaju je zahtev izdat serveru, i kao odgovor stižu informacije da li je CouchDB pokrenut, verzija operativnog sistema, verzija baze, poruka pozdrava itd. Sada šaljemo zahtev bazi koju smo ranije napravili:

$ curl http://localhost:5984/music/
{
    "db_name":"music",
    "doc_count":1,
    "doc_del_count":0,
    "update_seq":4,
    "purge_seq":0,
    "compact_running":false,
    "disk_size":16473,
    "instance_start_time":"1326845777510067",
    "disk_format_version":5,
    "committed_update_seq":4
}

Ovo vraća informacije o tome koliko ima dokumenata u bazi, koliko dugo server radi, koliko operacija je izvršeno itd.

Čitanje dokumenta (GET)

Da bi se povratio određeni dokument, potrebno je dodati njegov _id na URL do baze:

$ curl http://localhost:5984/music/74c7a8d2a8548c8b97da748f43000ac4
{
    "_id":"74c7a8d2a8548c8b97da748f43000ac4",
    "_rev":"4-93a101178ba65f61ed39e60d70c9fd97",
    "name":"The Beatles",
    "albums": [
    {
        "title":"Help!",
        "year":1965
    },{
        "title":"Sgt. Pepper's Lonely Hearts Club Band",
        "year":1967
    },{
        "title":"Abbey Road",
        "year":1969
    }
    ]
}

U CouchDB-u, slanje GET zahteva je uvek bezbedno, jer GET zahtev neće izvršiti promene na dokumentu. Da bi se izvršile promene, neophodno je iskoristiti druge HTTP komande, poput PUT, POST i DELETE.

Kreiranje dokumenta (POST)

Za kreiranje novog dokumenta se koristi POST. Neophodno je u zaglavlju zahteva postaviti Content-Type: application/json; u suprotnom će CouchDB odbiti zahtev.

$ curl -i -X POST "http://localhost:5984/music/" \
-H "Content-Type: application/json" \
-d '{ "name": "Wings" }'
HTTP/1.1 201 Created
Server: CouchDB/1.1.1 (Erlang OTP/R14B03)
Location: http://localhost:5984/music/74c7a8d2a8548c8b97da748f43000f1b
Date: Wed, 18 Jan 2012 00:37:51 GMT
Content-Type: text/plain;charset=utf-8
Content-Length: 95
Cache-Control: must-revalidate
{
    "ok":true,
    "id":"74c7a8d2a8548c8b97da748f43000f1b",
    "rev":"1-2fe1dd1911153eb9df8460747dfe75a0"
}

HTTP kôd i poruka 201 Created nam govore da je zahtev za kreiranjem bio uspešan. Telo odgovora sadrži JSON objekat sa korisnim informacijama, kao što su _id i _rev vrednosti dokumenta.

Ažuriranje dokumenta (PUT)

PUT komanda se koristi da ažurira postojeći dokument ili da kreira novi dokument sa određenom vrednošću _id polja. Isto kao i GET, URL PUT komande se sastoji od imena baze i _id vrednosti polja dokumenta.

$ curl -i -X PUT \
"http://localhost:5984/music/74c7a8d2a8548c8b97da748f43000f1b" \
-H "Content-Type: application/json" \
-d '{
"_id": "74c7a8d2a8548c8b97da748f43000f1b",
"_rev": "1-2fe1dd1911153eb9df8460747dfe75a0",
"name": "Wings",
"albums": ["Wild Life", "Band on the Run", "London Town"]
}'
Location: http://localhost:5984/music/74c7a8d2a8548c8b97da748f43000f1b
Etag: "2-17e4ce41cd33d6a38f04a8452d5a860b"
Date: Wed, 18 Jan 2012 00:43:39 GMT
Content-Type: text/plain;charset=utf-8
Content-Length: 95
Cache-Control: must-revalidate
{
    "ok":true,
    "id":"74c7a8d2a8548c8b97da748f43000f1b",
    "rev":"2-17e4ce41cd33d6a38f04a8452d5a860b"
}

Bitna stvar koju je neophodno napomenuti: CouchDB ne menja vrednosti određenog polja već prepisuje vrednosti celog dokumenta da bi napravio promenu. Futon interfejs možda omogućava korisniku jednostavnu promenu vrednosti jednog polja, ali u pozadini se zapravo brišu sve vrednosti ključeva u dokumentu i ponovo postavljaju.

Kao što je ranije napomenuto, vrednosti _id i _rev zahteva moraju da se poklapaju sa vrednostima u dokumentu koji se ažurira ili operacija neće biti uspešna. Da bi se videlo na primeru, dovoljno je poslati isti PUT zahtev kao i u prethodnom primeru:

HTTP/1.1 409 Conflict
Server: CouchDB/1.1.1 (Erlang OTP/R14B03)
Date: Wed, 18 Jan 2012 00:44:12 GMT
Content-Type: text/plain;charset=utf-8
Content-Length: 58
Cache-Control: must-revalidate
{"error":"conflict","reason":"Document update conflict."}

Prethodni zahtev je promenio _rev vrednosti dokumenta, pa će se javiti greška.

Brisanje dokumenta (DELETE)

Za kraj, može se iskoristiti DELETE operacija za brisanje dokumenta iz baze:

$ curl -i -X DELETE \
"http://localhost:5984/music/74c7a8d2a8548c8b97da748f43000f1b" \
-H "If-Match: 2-17e4ce41cd33d6a38f04a8452d5a860b"
HTTP/1.1 200 OK
Server: CouchDB/1.1.1 (Erlang OTP/R14B03)
Etag: "3-42aafb7411c092614ce7c9f4ab79dc8b"
Date: Wed, 18 Jan 2012 00:45:36 GMT
Content-Type: text/plain;charset=utf-8
Content-Length: 95
Cache-Control: must-revalidate
{
    "ok":true,
    "id":"74c7a8d2a8548c8b97da748f43000f1b",
    "rev":"3-42aafb7411c092614ce7c9f4ab79dc8b"
}

DELETE operacija će obezbediti novi broj revizije, iako je dokument izbrisan. To se dešava zato što dokument nije zapravo izbrisan sa diska, već je prazan dokument nalepljen preko njega, označivši ga kao obrisanog.

Upiti nad dokumentima

Primarni alat za postavljanje upita nad CouchDB dokumentima su pogledi (views). Postoje dve različite vrste pogleda:

  1. Trajni pogledi se čuvaju unutar specijalnih dokumenata koji se nazivaju dokumenti dizajna (design documents). Njima se može pristupiti slanjem HTTP GET zahteva na URI /{imebaze}/{dokumentid}/{imepogleda}, gde {dokumentid} ima prefix _design/ da bi CouchDB prepoznao da se radi o dokumentu dizajna, i gde {imepogleda} ima prefix _view/, da bi CouchDB prepoznao da se radi o pogledu.
  2. Privremeni pogledi se ne čuvaju u bazi, već se izvršavaju na licu mesta. Da bi se izvršio privremeni pogled, šalje se HTTP POST zahtev na URI /{imebaze}/_temp_view, gde telo zahteva sadrži kôd funkcije pogleda i Content-Type zaglavlje ima vrednost application/json. Obe vrste pogleda su definisane kao JavaScript funkcija koja mapira ključeve i vrednosti.

Pristupanje dokumentima preko pogleda

Pogled koristi MapReduce model koje se sastoji od funkcije mapiranje (map) i opcione funkcije redukovanja (reduce). On generiše uređenu listu parova ključ/vrednost, gde i ključevi i vrednosti moraju biti JSON formata. Najjednostavniji pogled se naziva _all_docs koji je ugrađen, a sadrže ga sve baze unutar CouchDB-a i u rezultatima sadrži stavku za svaki dokument u bazi.

Da bi se pogledale informacije za sve dokumente u bazi, treba poslati GET zahtev _all_docs pogledu:

$ curl http://localhost:5984/music/_all_docs
{
    "total_rows":1,
    "offset":0,
    "rows":[{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"74c7a8d2a8548c8b97da748f43000ac4",
        "value":{
            "rev":"4-93a101178ba65f61ed39e60d70c9fd97"
        }
    }]
}

Rezultat zahteva sadrži jedini dokument koji smo kreirali. Odgovor je JSON objekat koji sadrži rows niz. Svaki član niza je JSON objekat sa tri polja:

  • id je _id vrednost dokumenta,
  • key je vrednost ključa po kom se pretražuje; u ovom slučaju se id i key vrednosti poklapaju,
  • value je vrednost za taj ključ pretrage; u ovom slučaju sadrži _rev vrednost dokumenta.

Podrazumevano ponašanje je da pogledi ne prikazuju kompletne sadržaje dokumenata unutar value vrednosti. To se može postići dodavanjem include_docs=true parametra na zahtev:

$ curl http://localhost:5984/music/_all_docs?include_docs=true
{
    "total_rows":1,
    "offset":0,
    "rows":[{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"74c7a8d2a8548c8b97da748f43000ac4",
        "value":{
            "rev":"4-93a101178ba65f61ed39e60d70c9fd97"
        },
        "doc":{
            "_id":"74c7a8d2a8548c8b97da748f43000ac4",
            "_rev":"4-93a101178ba65f61ed39e60d70c9fd97",
            "name":"TheBeatles",
            "albums":[{
                "title":"Help!",
                "year":1965
            },{
                "title":"Sgt. Pepper's Lonely Hearts Club Band",
                "year":1967
            },{
                "title":"AbbeyRoad",
                "year":1969
            }]
        }
    }]
}

Konstruisanje privremenih pogleda i funkcije mapiranja

Kreiranje sopstvenih pogleda se može uraditi preko Futon-a. Na strani baze treba u padajućoj listi izabrati stavku Temporary view… , koja će otvoriti novu stranu za pisanje pogleda:

Napomena: Pisanje privremenih pogleda je moguće u trenutnoj stabilnoj verziji (1.6.1), dok su u najnovijim verzijama (2.x) privremeni pogledi u potpunosti izbačeni.

Kôd unutar dela za funkciju mapiranja (Map Function) trebalo bi da izgleda ovako:

function(doc) {
    emit(null, doc);
}

Klikom na dugme Run funkcija će se izvršiti, a rezultati mogu izgledati ovako:

Key Value
null {_id: „74c7a8d2a8548c8b97da748f43000ac4“, _rev: „4-93a101178ba65f61ed39e60d70c9fd97“, name: „The Beatles“, albums: [{title: „Help!“, year: 1965}, {title: „Sgt. Pepper’s Lonely Hearts Club Band“, year: 1967}, {title: „Abbey Road“, year: 1969}]}

Funkcija mapiranja prima parametar doc koji označava dokument. Funkcija će se izvršiti jednom za svaki dokument u bazi podataka. Ključni deo funkcije mapiranja unutar svih pogleda je funkcija emitovanja:

emit(key, value);

Funkcija emitovanja prima dva argumenta: ključ i vrednost. Svako pozivanje funkcije emitovanja dodaje jednu stavku u rezultatima izvršavanja pogleda. Funkcija emitovanja se može pozvati više puta za isti dokument.

Sada ćemo napisati pogled koji simulira ponašanje ugrađenog pogleda _all_docs. Setimo se da ovaj pogled sadrži _id vrednost dokumenta kao ključ i _rev vrednost dokumenta kao vrednost. Funkcija emitovanja treba da izgleda ovako:

function(doc) {
    emit(doc._id, { rev: doc._rev });
}

Privremeni pogledi ne moraju da se izvršavaju u grafičkom okruženju. Moguće je i poslati POST zahtev sa funkcijom mapiranja na _temp_view:

$ curl -X POST \
http://localhost:5984/music/_temp_view \
-H "Content-Type: application/json" \
-d '{"map":"function(doc){emit(doc._id,{rev:doc._rev});}"}'
{
    "total_rows":1,
    "offset":0,
    "rows":[{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"74c7a8d2a8548c8b97da748f43000ac4",
        "value":{
            "rev":"4-93a101178ba65f61ed39e60d70c9fd97"
        }
    }]
}

Čuvanje trajnih pogleda kao dokument dizajna i B+ stabla

Privremeni pogledi su dobri kao brzo rešenje za testiranje, ali su veoma spori i njihovo korišćenje u produkciji se ne preporučuje. Zašto je to tako? Odgovor na ovo pitanje se krije u načinu na koji CouchDB barata sa pogledima.

Funkcije mapiranja i redukovanja pogleda se izvršavaju kada se pogledu pošalje upit (što u praksi znači slanje HTTP zahteva tom pogledu). Kada se pogled prvi put pozove, CouchDB će proći kroz svaki dokument tačno jednom i konstruisati B+ stablo sa rezultatima upita. Svaki sledeći upit poslat pogledu će koristiti prethodno konstruisano B+ stablo. Ukoliko se naprave novi dokumenti, ažuriraju ili izbrišu postojeći, pogled će izvršiti funkcije mapiranja i redukcije samo za dokumente obuhvaćene promenama. Tom prilikom će se indeksi u B+ stablu ažurirati. MapReduce model koji koristi CouchDB se zato još naziva i inkrementalni.

B+ stablo nudi izuzetno brze pretrage rezultata po ključu ili po rasponu između dva ključa. Pogledi bi zato trebalo da koriste ključeve pretrage koje bi imalo smisla sortirati u semantičkom smislu.

Pogledi se čuvaju unutar dokumenata dizajna. Dokumenti dizajna su kao i ostali dokumenti u bazi koji čuvaju podatke, osim što kao podatke čuvaju potpise funkcija pogleda i ostalih funkcija aplikacije. Njihovi identifikacioni ključevi počinju sa _design/ da bi CouchDB mogao da ih razdvoji od običnih dokumenata. Funkcije pogleda se čuvaju u polju sa vrednošću ključa views/. Dokument dizajna može sadržati jedan ili više pogleda. Svaki put kada se vrši upit nad pogledom, indeksi svih pogleda koji se nalaze u istom dokumentu će se ažurirati. Zato je dobra praksa da se pogledi grupišu prema sličnosti manipulacije podacima.

Pogledi se mogu sačuvati klikom na Save As…. Za svaki pogled se unosi ime dokumenta dizajna u kome će pogled biti smešten i ime pogleda.

Primeri pogleda i parametri pretrage

Baza podataka na kojoj je do sada rađeno je baza za čuvanje podataka vezanih za muziku. Koristeći _all_docs/ pogled moguće je pristupiti dokumentima koristeći njihov _id/, ali zanimljivije je vršiti pretragu prema imenima bendova:

function(doc) {
    if ('name' in doc) {
        emit(doc.name, doc._id);
    }
}

Funkcija mapiranja proverava da li dokument sadrži sadrži name/ polje i, ako je tako, emituje to ime kao ključ, a _id/ polje emituje kao vrednost ključa.

Key Value
„The Beatles“ „74c7a8d2a8548c8b97da748f43000ac4“

Pogled ćemo sačuvati: ime dokumenta će biti artists/, a ime pogleda by_name/. Sledeći primer će omogućiti pretragu prema imenima albuma:

function(doc) {
    if ('name' in doc && 'albums' in doc) {
        doc.albums.forEach(function(album) {
            var
                key = album.title || album.name,
                value = { by: doc.name, album: album };
            emit(key, value);
        });
    }
}

Prvo proveravamo da li u dokumentu postoje polja name i albums/; ako je tako, emitovaće se ime albuma kao ključ i par podataka kao kompozitna vrednost ključa.

Key Value
„Abbey Road“ {by: „The Beatles“, album: {title: „Abbey Road“, year: 1969}}
„Help!“ {by: „The Beatles“, album: {title: „Help!“, year: 1965}}
„Sgt. Pepper’s Lonely Hearts Club Band“ {by: „The Beatles“, album: {title: „Sgt. Pepper’s Lonely Hearts Club Band“, year: 1967}}

Pogled ćemo sačuvati u dokumentu albums/, a ime pogleda će biti by_name/.

Kao što je već pomenuto, trajnim pogledima se može pristupiti slanjem HTTP GET zahteva na URI /{imebaze}/{dokumentid}/{imepogleda}, gde {dokumentid} ima prefiks _design/ i gde {imepogleda} ima prefiks _view/. Upite nad pogledima ćemo vršiti preko komandne linije.

Pretraga umentika po imenu:

$ curl http://localhost:5984/music/_design/artists/_view/by_name
{
    "total_rows":1,
    "offset":0,
    "rows":[{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"The Beatles",
        "value":"74c7a8d2a8548c8b97da748f43000ac4"
    }]
}

Pretraga albuma po imenu:

$ curl http://localhost:5984/music/_design/albums/_view/by_name
{
    "total_rows":3,
    "offset":0,
    "rows":[{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"Abbey Road",
        "value":{
            "by":"The Beatles",
            "album":{
                "title":"Abbey Road",
                "year":1969
            }
        }
    },{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"Help!",
        "value":{
            "by":"The Beatles",
            "album":{
                "title":"Help!",
                "year":1965
            }
        }
    },{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"Sgt. Pepper's Lonely Hearts Club Band",
        "value":{
            "by":"The Beatles",
            "album":{
                "title":"Sgt. Pepper's Lonely Hearts Club Band",
                "year":1967
            }
        }
    }]
}

Pretraga umetnika po imenu sa navedenom vrednošću ključa:

$ curl 'http://localhost:5984/music/_design/albums/_view/by_name?key="Help!"'
{
    "total_rows":3,
    "offset":1,
    "rows":[{
        "id":"74c7a8d2a8548c8b97da748f43000ac4",
        "key":"Help!",
        "value":{
            "by":"The Beatles",
            "album":{"title":"Help!","year":1965}
        }
    }]
}

U ovom primeru treba primetiti vrednosti total_rows i offset polja. Total_rows prikazuje ukupan broj rezultata u celom pogledu, a ne samo u podskupu vraćenom ovim zahtevom. Polje offset prikazuje redni broj pozicije prvog rezultata vraćenog zahtevom.

Pretraga umetnika sa parametrom limitlimit nam govori koliko ukupno rezultata želimo. Rezultati bi mogli da izgledaju ovako:

$ curl http://localhost:5984/music/_design/artists/_view/by_name?limit=5
{"total_rows":100,"offset":0,"rows":[
    {"id":"370255","key":"\"\"ATTIC\"\"","value":"370255"},
    {"id":"353262","key":"10centSunday","value":"353262"},
    {"id":"367150","key":"abdielyromero","value":"367150"},
    {"id":"276","key":"AdHoc","value":"276"},
    {"id":"364713","key":"Adversus","value":"364713"}
]}

Pretraga umetnika sa navođenjem početnog karaktera u ključu pretrage – koristi se parametar startkey:

$ curl http://localhost:5984/music/_design/artists/_view/by_name?\
limit=5\&startkey=%22C%22
{"total_rows":100,"offset":16,"rows":[
    {"id":"340296","key":"CalexB","value":"340296"},
    {"id":"353888","key":"carsten may","value":"353888"},
    {"id":"272","key":"Chroma","value":"272"},
    {"id":"351138","key":"Compartir D\u00f3na Gustet","value":"351138"},
    {"id":"364714","key":"Daringer","value":"364714"}
]}

Pretraga umetnika sa navođenjem krajnjeg karaktera u ključu pretrage – koristi se parametar endkey:

$ curl http://localhost:5984/music/_design/artists/_view/by_name?\
startkey=%22C%22\&endkey=%22D%22
{"total_rows":100,"offset":16,"rows":[
    {"id":"340296","key":"CalexB","value":"340296"},
    {"id":"353888","key":"carsten may","value":"353888"},
    {"id":"272","key":"Chroma","value":"272"},
    {"id":"351138","key":"Compartir D\u00f3na Gustet","value":"351138"}
]}

Pretraga umetnika sa sortiranjem ključa pretrage u obrnutom redosledu – koristi se parametar descending=true:

$ curl http://localhost:5984/music/_design/artists/_view/by_name?\
startkey=%22D%22\&endkey=%22C%22\&descending=true
{"total_rows":100,"offset":16,"rows":[
    {"id":"351138","key":"Compartir D\u00f3na Gustet","value":"351138"},
    {"id":"272","key":"Chroma","value":"272"},
    {"id":"353888","key":"carsten may","value":"353888"},
    {"id":"340296","key":"CalexB","value":"340296"}
]}

Kada se koristi descending=true za pretragu, potrebno je zameniti vrednosti startkey i endkey parametara. Razlog ovome je zato što CouchDB obilazi stablo pretrage u tzv. „pre-order“ maniru – gleda svaki element u svakom čvoru počevši od leve strane, spušta se u svaki podčvor u koji može, čita njegove elemente, a zatim se vraća u nadčvor kada pročita sve elemente podčvora. Pri obrtanju redosleda, neće se invertovati stablo, već samo redosled čitanja čvorova. Ako navedemo početni karakter ključa pretrage, pretraga će početi od ključa koji ispunjava taj uslov. Pretraga se završava kada se dođe do kraja stabla ili do ključa koji ne sadrži karakter naveden u završnom parametru.

Funkcija redukovanja

Funkcije redukovanja (reduce) su opcione u pogledima i služe za agregaciju rezultata dobijenih funkcijom mapiranja. Kada se vrši upit nad pogledom koji sadrži funkciju redukovanja, CouchDB će načiniti sledeće korake tokom izvršavanja:

  1. Poslaće sve dokumente funkciji mapiranja,
  2. Sakupiće sve emitovane vrednosti,
  3. Sortiraće emitovane redove po ključu,
  4. Poslaće redove sa istim ključem funkciji redukovanja,
  5. Ako je podataka previše i sve redukcije se ne mogu obraditi u jednom pozivu funkcije, pozvaće ponovo funkciju redukovanja, ali ovaj put sa prethodno redukovanim vrednostima,
  6. Rekurzivno će pozivati funkciju redukovanja dok je to neophodno, sve dok ne ostane bez duplikata ključeva.

Funkcije redukovanja primaju tri argumenta: key, values i rereduce. Prvi argument, key, je niz čiji su elementi takođe nizovi sa dva člana: ključem koji se emituje i _id poljem dokumenta čiji je ključ član. Drugi argument, values, je niz vrednosti koje odgovaraju ključevima. Treći argument, rereduce, je logički tip podatka koji ima tačnu vrednost ako je poziv funkcije reredukujući, tj. ako se funkcija redukovanja poziva sa prethodno redukovanim vrednostima. U tom slučaju, key argument će biti null, dok će values argument biti niz koji sadrži prethodno redukovane vrednosti.

Pretpostavimo da smo u muzičku bazu dokumentima dodali ključeve po imenu genre i da neki dokumenti imaju vrednost tog ključa ambient. Pretpostavimo i da smo napisali funkciju mapiranja koja za ključ/vrednost emituje ambient/1. U jednom trenutku, dovoljno parova vrednosti će biti emitovano i CouchDB će pozvati redukciju koja vrši sumiranje vrednosti ključeva. Taj poziv može izgledati ovako:

reduce (
    [["ambient", id1], ["ambient", id2], ...], // kljucevi su isti
    [1, 1, ...],                               // sve vrednosti su 1
    false                                      // rereduce je false (netacan)
)

Pošto su sve vrednosti 1, suma će zapravo biti prebrojavanje koliko puta se pojavljuje ambient kao muzički žanr. Posle nekog vremena, CouchDB će odlučiti da sumira sve međuvrednosti koje je dobio redukcijama:

reduce (
    null,        // niz kljuceva je null
    [10, 10, 8], // vrednosti niza su dobijene prethodnim pozivima redukcije
    true         // rereduce je true, znaci da se vrsi reredukcija
)

Većina drugih sistema koji koriste MapReduce odbacuju rezultat izvršavanja funkcija mapiranja i redukovanja nakon što završe sa poslom. Sa CouchDB to nije slučaj. Već je objašnjeno kako se B+ stablo koristi za čuvanje rezultata pogleda. Ako se uključi i redukcija, CouchDB će pored ključeva pretrage indeksirati i međurezultate redukcije u odgovarajućim čvorovima prethodno kreiranog stabla. Ovo stvara dodatno ubrzanje, jer često neće biti potrebno vršiti redukciju nad određenim delom stabla, već će biti dovoljno samo pročitati vrednost iz čvora koji se nalazi više u stablu.

Napravimo sada privremenu funkciju mapiranja. Funkcija će za svaki album u dokumentu emitovati ime benda kao ključ i 1 kao njegovu vrednost:

function (doc) {
    if ('name' in doc && 'albums' in doc) {
        doc.albums.forEach(function(album) {
            var
                key = doc.name,
                value = 1;
            emit( key, value );
        });
    }
}

Dodajmo i funkciju redukcije koja će vršiti sumiranje vrednosti:

function (key, values, rereduce) {
    return sum(values);
}

Pokretanjem pogleda dobijamo sledeće vrednosti:

Key Value
„The Beatles“ 1
„The Beatles“ 1
„The Beatles“ 1

Međutim, sada u tabeli sa rezultatima imamo polje sa nazivom Reduce. Ako ga obeležimo, dobićemo sledeće:

Key Value
„The Beatles“ 3

Obeležavanjem polja Reduce označavamo da želimo da primenimo funkciju redukovanja na dobijene rezultate.

U sledećem nastavku više o osobini CouchDB-a koja ovu NoSQL bazu izdvaja od sličnih rešenja – mehnaizmu replikacije.

Autor: Andreja Vulovic

Tagged , , , , , , , , , , , ,