Căutarea este una dintre cele mai importante caracteristici ale unei aplicații moderne, deoarece le permite utilizatorilor să identifice informațiile relevante pentru ei. MongoDB Atlas Search oferă funcționalitatea de căutare direct în cloud, împreună cu funcții mai avansate, precum căutarea parțială și căutarea fuzzy. În acest articol, vom configura Atlas Search și vom analiza conceptele tehnice care stau la baza acestuia. Funcționalitățile sunt exemplificate pe fragmente de cod.
Majoritatea aplicațiilor moderne conțin bare de căutare (searchbars), care permit utilizatorilor să caute date. Prin urmare, una dintre cele mai importante caracteristici ale unei aplicații moderne este capacitatea de a căuta și de a evidenția informații relevante.
MongoDB este o bază de date NoSQL populară, utilizată în multe aplicații moderne pentru a stoca seturi mari de date distribuite și pentru a efectua operații rapide. Se bazează pe modelul de date numit „document store” în care un document este stocat în format BSON. Formatul BSON este un format binar JSON care permite stocarea în aceeași colecție a documentelor cu o structură diferită.
MongoDB Atlas este o platformă de bază de date în cloud, Database-as-a-Service (DBaaS), care gestionează complexitatea și administrarea deploymenturilor și manipularea datelor. Ne permite să utilizăm baza noastră de date cu ușurință și, de asemenea, să îi facem deploy și să o scalăm mai rapid.
Unul dintre cele mai utile avantaje oferite de acesta este Atlas Search, care permite căutarea full-text asupra datelor noastre, în cloud. Elimină nevoia de a construi și rula un sistem de căutare separat alături de baza noastră de date. Atlas Full-Text Search se bazează pe Apache Lucene, care este o bibliotecă de căutare în text și cel mai popular proiect de limbaj open-source.
Atlas Full-Text Search este o funcționalitate adăugată recent ca înlocuitor pentru funcționalitatea „Text Search” existentă, utilizată pentru deploymenturile gestionate independent (non-Atlas). În metoda anterioară de căutare, MongoDB utilizează un index de text și un operator, pentru a susține interogările (queries) ce efectuează o căutare în conținutul de tip text.
Fig. 1 - Metodologia de lucru
Într-o instanță MongoDB gestionată independent, o căutare de tipul Text Search poate fi efectuată folosind operatorul $text. Acest operator compară termenii de căutare cu indexurile de text (text indexes) ale unuia sau mai multor câmpuri dintr-o colecție.
Un index de tip text (text index) realizează tokenuri și reduce numărul de cuvinte îndepărtând valorile duplicate pentru câmpurile indexate. Fiecare termen unic, în fiecare câmp indexat pentru fiecare document din colecție este stocat ca un index.
Indexul de tip text poate avea o dimensiune destul de mare și poate conține o înregistrare (entry) pentru fiecare cuvânt ce a derivat dintr-un câmp indexat, pentru fiecare document inserat. Construirea unui index de tip text este similară construirii unui index multi-cheie de dimensiuni mari și va dura mai mult decât construirea unui simplu index ordonat pe aceleași date.
Căutarea unui text poate fi efectuată odată ce indexul este creat. Operatorul $text
caută documente care conțin termenii de căutare specificați în câmpurile indexate și atribuie un scor fiecărui document în funcție de relevanța termenilor de căutare pentru document. Scorul reprezintă relevanța unui document pentru o anumită interogare de căutare.
Pentru a sorta documentele rezultate în funcție de relevanța căutării, puteți utiliza operatorul de agregare $meta
în etapa $sort
.
Îmbunătățește performanța interogărilor (queries), deoarece permite definirea Indexurilor de Căutare (Search Index) personalizate.
Eficientizează spațiul de stocare, întrucât oferă posibilitatea de a alege câmpurile care să fie indexate, în scopul maximizării relevanței căutării și minimizării spațiului de stocare care este utilizat.
Permite căutarea parțială, precum și criterii de căutare cu erori de scriere, cum ar fi caractere lipsă sau lipsa diacriticelor.
Fiind un serviciu bazat pe o platformă cloud, dezvoltatorii îl pot testa doar într-un mediu cloud, după deployment.
În timp ce descriem Atlas Search și exemplificăm caracteristicile sale, putem lua în considerare o aplicație ce caută stații de încărcare pentru vehicule electrice. Colecția MongoDB ar conține documente cu următoarea structură:
{
„id”: „1”,
„address”: {
„city”: „Cluj-Napoca”,
„street”: „str. Constanța nr. 30-34”
},
„plug_type”: „Mennekes”
}
Pentru a activa și utiliza Atlas Search, trebuie să creăm un Index de Căutare (Search Index) și să implementăm o Interogare de Căutare (Search Query). Precondiția este să avem o subscripție la Mongo Atlas și o bază de date configurată în cadrul Atlas. În imaginea de mai jos este ilustrat faptul că, atunci când efectuăm un query, va fi utilizat cel puțin un index, iar acest index va utiliza un analizator pentru a găsi datele stocate în baza noastră de date. Datele sunt împărțite în tokenuri, care sunt indexate.
Fig. 2 - Structura internă
Indexarea este o tehnică utilizată pentru a ridica performanța interogărilor (queries) dintr-o bază de date. Presupune stocarea informațiilor relevante în perechi cheie-valoare, astfel încât datele să fie găsite mai rapid în timpul interogării. Un Index de Căutare se bazează pe unul sau mai mulți analizatori, putând fi creat doar în cadrul Mongo Atlas.
Un analizator (Analyzer) oferit de Mongo este implementat utilizând biblioteca de căutare Apache „Lucene” și este compus dintr-un tokenizer și criteriile noastre de căutare. Tokenizerul este algoritmul care extrage tokenuri din textul dat. Luând în considerare metodologia de lucru, analizatorul creează tokenuri pe baza textului inițial. Apoi, mapează tokenurile către documentele care conțin aceste tokenuri. Astfel, construiește un Index Inversat (Inverted Index), unde tokenul pointează către ID-ul documentului care îl conține. Prin urmare, în timpul căutării vor fi găsite doar tokenurile indexate. Acest proces de indexare este ilustrat în imaginea de mai jos, considerând că avem aceste patru documente în colecția noastră:
Fig. 3 - Procesul de indexare
În Atlas sunt definite diverse tipuri de analizatori, iar noi putem alege care să fie utilizat în momentul indexării și căutării datelor. Analizatorul „standard” este utilizat în mod implicit în Mongo Atlas, deoarece este potrivit în majoritatea situațiilor obișnuite. Acest tip de analizator împarte textul în mai multe tokenuri, luând în considerare doar grupuri de litere și cifre. Astfel, elimină caracterele speciale (precum: . , ! ? | - ) și transformă literele în minuscule. Însă, în mod implicit, diacriticele sunt păstrate în cadrul tokenurilor generate. Tokenurile generate sunt, de asemenea, unice, astfel încât nu vor fi stocate duplicate. Luând în considerare exemplul ilustrat în imaginea anterioară, atunci când o valoare stocată în câmpul „street” este indexată, Analizatorul Standard va construi aceste tokenuri: „str”, „constanța”, „nr”, „30”, „34”. Dacă folosim acest analizator pentru a indexa textul „Cluj-Napoca”, vor fi create două tokenuri: „cluj”, „napoca”. Astfel, punctele și spațiile vor fi eliminate, iar toate tokenurile sunt stocate cu litere mici. Ulterior, Atlas actualizează Indexul Inversat pentru a facilita găsirea rapidă. Putem observa în imaginea de mai sus că Indexul Inversat conține tokenul pe post de cheie, iar valoarea este un array de ID-uri de documente care conțin aceste tokenuri. De exemplu, tokenul „str” se găsește în toate cele patru documente, deci vor fi stocate toate ID-urile lor. Însă, tokenul „30” poate fi găsit doar în primul document, deci va fi stocat doar ID-ul său. Prin urmare, dacă vom căuta după „str”, atunci vor fi returnate toate cele patru documente, dar, dacă vom căuta după „30*”, va fi returnat doar un singur document.
Un alt tip de analizator este cel „simplu” (simple). Acesta împarte textul numai pe baza literelor, adică elimină spațiile, cifrele și caracterele speciale. Astfel, un token reprezintă doar un grup de litere, până la apariția unui caracter care nu este o literă. Acesta transformă tokenurile în litere mici, astfel încât în timpul căutării este case-insensitive (ignoră capitalizarea literelor), dar diacriticele trebuie furnizate. Nu este potrivit pentru indexarea adreselor de email, numerelor de telefon sau ID-urilor. Astfel, dacă am indexa exemplul de mai sus cu acest analizator, vor fi create următoarele tokenuri: „str”, „constanța”, „nr”. Dacă am utiliza acest analizator pentru a indexa textul „Cluj-Napoca”, vor fi realizate două tokenuri: „cluj”, „napoca”.
Fig. 4 - Interfața din Altas pentru crearea unui index
Analizatorul „whitespace” împarte textul care trebuie indexat în funcție de spații. Astfel, toate caracterele consecutive sunt considerate un token, până când e întâlnit un spațiu. Prin urmare, acesta păstrează casingul original, caracterele speciale și diacriticele. Dar tokenurile nu sunt transformate în litere mici, deci la căutare se vor putea găsi documentele corespunzătoare doar dacă criteriile de căutare conțin și casingul din textul original utilizat pentru indexare. Astfel, dacă am indexa exemplul de mai sus cu acest analizator, vor fi create următoarele tokenuri: „str.”, „Constanța”, „nr.”, „30-34”. Putem observa că linia și punctele au fost păstrate. Dacă am utiliza acest analizator pentru a indexa textul „Cluj-Napoca”, va fi creat un singur token: „Cluj-Napoca”. Prin urmare, la căutarea acestuia, trebuie să folosim același casing.
Un alt tip de analizator este „keyword”. Acesta nu împarte textul atunci când creează tokenul, deoarece păstrează totul în cadrul textului original, returnând întotdeauna un singur token. Prin urmare, acesta păstrează toate caracterele, spațiile, casingul și diacriticele. Așadar, pentru a găsi documente potrivite în timpul căutării, trebuie furnizat întregul text original folosit pentru indexare. Dar vom vedea mai târziu în rândurile următoare cum putem folosi acest aparent dezavantaj ca pe un avantaj. Acest tip de analizator este recomandat pentru checkboxuri (casetele de selectare) sau dropdowns (meniurile derulante) din interfața utilizatorului. Astfel, dacă am indexa exemplul de mai sus cu acest analizator, va fi creat un singur token: „str. Constanța nr. 30-34”. Dacă am utiliza acest analizator pentru a indexa textul „Cluj-Napoca”, de asemenea, va fi creat un singur token: „Cluj-Napoca”.
Există și analizatoare optimizate pentru diferite limbaje, precum analizatorul „english”. Acesta structurează textul de indexat în grupuri de litere și cifre, dar păstrează doar rădăcina fiecărui cuvânt, ceea ce înseamnă că elimină pluralul și timpurile verbelor (-ed, -ing). De asemenea, păstrează cifrele și transformă toate tokenurile în litere mici. Însă, elimină cuvintele de legătură (the, of, a, it etc.) și caracterele speciale. Este recomandat să fie utilizat dacă documentele noastre din colecție ar avea un câmp care conține descrieri lungi cu fraze multiple, ca descrierile cărților sau a filmelor. Astfel, dacă am indexa exemplul de mai sus cu acest analizator, vor fi create următoarele tokenuri: „str”, „constanța”, „nr”, „30”, „34”. Dacă am utiliza acest analizator pentru a indexa textul „Cluj-Napoca”, vor fi create două tokenuri: „cluj”, „napoca”.
Atlas are și o platformă online pentru testarea analizoarelor și a funcției de căutare, utilizând diferite exemple predefinite. Dar acceptă și procesarea unui text scris de noi pentru testare și interogare (querying), permițându-ne să verificăm exact dacă un anumit analizator este potrivit pentru contextul nostru. Tabelul de mai jos prezintă particularitățile fiecărui tip de analizator.
Mongo Atlas ne permite și să creăm multi-analizoare pentru fiecare câmp care trebuie indexat. Acestea sunt analizatoare înlănțuite, astfel încât, dacă primul analizator nu găsește ceva, atunci se „verifică” următorul analizator dacă poate găsi o potrivire pentru criteriile de căutare scrise în bara de căutare (searchbar). Deopotrivă, oferă posibilitatea de a crea un analizor personalizat, însă acesta este mai complex și mai dificil de depanat (debug). Sau putem face mai multe indexuri de căutare individuale, dar constrângerea este că e necesar un cluster cu resurse mai puternice.
În cadrul Mongo Atlas, putem crea Indexul de Căutare din fila „Search” („Căutare”) conform explicațiilor din această documentație. Acolo trebuie să apăsăm butonul „Create Index” („Creează Index”) și butonul „JSON editor”, apoi selectăm numele colecției și tastăm numele pentru indexul nostru. Putem denumi indexul „CustomSearchIndex”.
Fig. 5 - Procesul de căutare
Putem construi un Index de Căutare precum cel ilustrat mai jos, folosind opțiunea de mapare „dinamică” (dynamic). Dar așa am indexa toate câmpurile din fiecare document. Adică, dacă un câmp ar stoca un document imbricat sau un șir de documente, toate aceste subcâmpuri vor fi indexate. Astfel ar crește spațiul de stocare și timpul de interogare. Deci această metodă de indexare ar fi o opțiune doar pentru teste inițiale.
{
„mappings”: {
„dynamic”: true
}
}
Pentru aplicația noastră care trebuie să caute stații electrice de încărcare, vom utiliza maparea statică, setând maparea dinamică pe false
. Procedând astfel, vom putea specifica explicit câmpurile care trebuie indexate, ce tip de date ar trebui să aibă și ce analizator ar trebui utilizat pentru indexarea lor. Un alt avantaj este că vom economisi spațiu de stocare. Apoi, în secțiunea „fields”
vom specifica fiecare câmp care trebuie indexat. Obiectele și vectorii sunt tratate în același mod. Așadar, dacă avem documente înglobate, putem face referire la câmpurile interne specifice folosind notația cu punct: numele_obiectului.câmpul_intern
. Indexul de căutare pe care îl putem utiliza pentru a suporta și căutarea parțială este următorul:
Pentru câmpul city, putem utiliza:
analizatorul simplu, pentru a fi luate în considerare doar literele, pentru că cifrele și caracterele speciale nu sunt relevante în numele unui oraș.
tipul de date autocomplete, pentru a activa autocompletarea și pentru a returna rezultate chiar și dacă nu există o potrivire completă între textul căutat și tokenul indexat. De exemplu, atunci când avem tokeni „napoca” și „napoli”, dar căutăm doar după „napo”, va returna documentele în care orașul este atât „Cluj-Napoca”, cât și „Napoli”. Dacă am avea un sub-document și ar trebui să fie indexate toate câmpurile din interiorul său, atunci putem folosi tipul de date „document”. Alte tipuri de date pot fi găsite aici: https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#data-types.
opțiunea de tokenizare nGram pentru a crea tokeni din subgrupuri de caractere, fiecare token având o lungime între valorile definite în minGrams și maxGrams. Astfel, vor fi create și indexate tokenuri cu diferite lungimi, pentru a se potrivi și căutărilor parțiale. De exemplu, dacă tokenul inițial este „napoca”, vor fi create alte tokenuri, cum ar fi: „nap”, „napo”, „napoc”, etc.
{
„mappings”: {
„dynamic”: false,
„fields”: {
„address”: {
„fields”: {
„city”: [
{
„type”: „autocomplete”,
„analyzer”: „lucene.simple”,
„tokenization”: „nGram”,
„minGrams”: 3,
„maxGrams”: 25,
„foldDiacritics”: true
}
],
„address”: [
{
„analyzer”: „lucene.keyword”,
// ... aceeași configurație ca mai sus,
// dar analizator diferit
}
]
}
},
„plug_type”: [
{
„analyzer”: „lucene.keyword”
// ... aceeași configurație ca mai sus, dar
// analizator diferit
}
]
}
}
}
După crearea indexului, putem scrie în searchbar criteriul de căutare str. Constanța nr. 34
și să executăm interogarea de căutare. Făcând asta, analizatorul MongoDB pe care l-am ales va construi tokenurile pentru acest criteriu de căutare și va căuta tokenurile în indexul inversat (Inverted Index) construit, pentru a găsi documentele care conțin aceste tokenuri. Prin urmare, va returna documentul cu ID-ul 1. Dacă am căuta str nr
, ar fi returnate toate documentele din colecție, pentru că toate conțin aceste tokenuri.
Pentru a folosi indexul de căutare (Search Index), trebuie utilizată funcția [$search](https://www.mongodb.com/docs/atlas/atlas-search/text/)
într-un pipeline de agregare dintr-o bază de date MongoDB. În Java, poate fi utilizat Mongo Driver SDK (de la versiunea 4.7 în sus). Fiecare document returnat are un scor de potrivire atașat, însă acesta este ascuns în mod implicit.
Interogarea este implementată folosind opțiunea compound
cu clauza „should”, care încearcă să găsească cel puțin un document. Componentele sunt:
tipul de date folosit pentru indexare. Funcția autocomplete
a fost folosită pentru toate câmpurile. Dacă ar fi fost indexat un document întreg imbricat, atunci ar fi putut fi utilizat tipul de date text.
path
conține calea către câmpul în care trebuie căutați tokenii indexați.
query
este textul introdus în bara de căutare. Astfel, textul introdus în bara de căutare este căutat în toate câmpurile specificate.
tokenorder
este opțiunea utilizată pentru a se asigura că analizatorul caută indicii într-o ordine relevantă (de exemplu: caută doar pentru indicii „Cluj Napoca” în această ordine, nu și în „Napoca Cluj”).
fuzz
y este o funcționalitate utilizată pentru a putea găsi un text potrivit, chiar dacă a fost introdus un criteriu de căutare cu greșeli. Se folosește maxEdits
de 1, pentru a permite doar o singură eroare, astfel încât, dacă criteriile de căutare sunt „zuruch”, vor fi returnate documentele care conțin „Zürich”.[
{
„$search”: {
„index”: „CustomSearchIndex”,
„compound”: {
„should”: [
{
„autocomplete”: {
„path”: „address.city”,
„query”: „text typed within searchbar”,
„tokenOrder”: „sequential”,
„fuzzy”: {
„maxEdits”: 1
}
}
},
{
„autocomplete”: {
„path”: „address.street”
// ... restul configurației ca mai sus
}
},
{
„autocomplete”: {
„path”: „plug_type”
// ... restul configurației ca mai sus
}
}
]
}
}
}
]
Aceasta este implementarea echivalentă în cod Java:
import static com.mongodb.client.model.search.FuzzySearchOptions.fuzzySearchOptions;
import static com.mongodb.client.model.search.SearchPath.fieldPath;
import org.bson.conversions.Bson;
private Bson searchQuery(String searchedCriteria) {
return Aggregates.search(
SearchOperator.compound().should(
Arrays.asList(
SearchOperator.autocomplete(fieldPath(„address.city”), searchedCriteria).sequentialTokenOrder().fuzzy(fuzzySearchOptions().maxEdits(1)),
SearchOperator.autocomplete(fieldPath(„address.street”), searchedCriteria).sequentialTokenOrder().fuzzy(fuzzySearchOptions().maxEdits(1)),
SearchOperator.autocomplete(fieldPath(„plug_type”), searchedCriteria).sequentialTokenOrder().fuzzy(fuzzySearchOptions().maxEdits(1))
)
),
SearchOptions.searchOptions().
index(„CustomSearchIndex”)
// numele indexului care trebuie utilizat
);
}
Dependinţa Maven folosită este mongodb-driver-sync:
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>4.9.0</version>
</dependency>
Nu a fost folosită biblioteca spring-boot-data-mongodb
, deoarece funcționalitatea de căutare în MongoDB Atlas nu a fost încă implementată.
Căutarea de text în MongoDB este o caracteristică puternică ce permite căutarea conținutului de tip string din datele unei colecții. Dacă se realizează deploymenturi gestionate independent (non-Atlas), atunci se poate utiliza operatorul $text
pentru a efectua interogări de tip text. Dacă este utilizată platforma MongoDB Atlas, se poate utiliza Atlas Search pentru a efectua căutare de text integral în colecțiile bazei de date. MongoDB Atlas Search este o soluție ușor de utilizat pentru toate nevoile de căutare și oferă o experiență plăcută de utilizare pentru dezvoltatori.
Atlas Search este diferit de Text Search, deoarece utilizează biblioteca Apache Lucene, ceea ce îl face mult mai rapid datorită modului în care sunt stocate datele. Acesta salvează toate cuvintele cheie sub forma unui glosar al unei cărți cu fiecare număr de pagină și cuvintele cheie corespunzătoare. Motoarele de căutare bazate pe Lucene sunt mai rapide decât motoarele de căutare text obișnuite și au multe funcții suplimentare, așa cum au fost descrise mai sus. Atlas Search este un motor de căutare puternic care poate căuta prin milioane de documente mulțumită implementării sale. Deci, Atlas Search este foarte diferit de Text Search, economisește mult timp, fără a utiliza motoare de căutare terțe, precum Elastic Search.
Atlas Search este benefic pentru proiecte de tip E-Commerce sau orice aplicație cu multe filtre și cerințe de căutare în milioane de documente și, de asemenea, pentru orice aplicație modernă care oferă o funcționalitate eficientă și rapidă de căutare.
de Péter Török