Orama Full Text Search Engine Nedir, Nasıl Çalışır?
Uygulamalarımız içerisinde kullanıcılara daha iyi hizmet ve deneyim sağlayabilmek için birçok özellik geliştirmekteyiz, bunlardan bir tanesi ise arama/search fonksiyonalitesidir. Bu fonksiyonalite ne kadar gelişmiş ise kullanıcılara sağlayacağımız deneyim de paralel olarak artmaktadır. Aradıkları yapıyı/ürünü/ifadeyi uygulama içerisinde gezinip bulamaktansa onlara sağladığımız özellikler ile direkt erişmesini sağlayabilir ya da yardımcı olabiliriz tıpkı uygulamalarda sıklıkla gördüğümüz;
- Search (arama)
- Search Suggestions (arama önerileri)
- Filter and Sorting (filtreleme ve belirtilen değere göre sıralama)
- Typo Tolerance (yanlış olarak aratılsa bile uygun sonuçların getirilmesi)
- Autocorrection (yazım hatalarının otomatik düzeltilmesi)
gibi. Bu tarz özellikleri uygulamalarımıza entegre ederken halihazırda birçok ürün bulunmaktadır, bunlardan bazıları ise; Elasticsearch, Algolia ve Orama gibi ürünlerdir. Bu yazıda ise yukarıdaki özellikleri ve daha fazlasını bizlere sağlayan ve bir full-text search engine olan Orama’nın ne olduğunu, nasıl çalıştığını ve kullanıldığını inceleyeceğiz.
Orama Full Text Search Engine Nedir?
Orama, TypeScript ile yazılmış bir open source in-memory full-text search engine’dir.
Orama is a fast, batteries-included, full-text search engine entirely written in TypeScript, with zero dependencies.
— Orama Search Documentation
2022 yılında Oslo’daki NDC konferansında Michele Riva tarafından Writing a full-text search engine in TypeScript adlı sunumda bir full text search engine’in hangi özelliklere sahip olması gerektiğinden ve nasıl yazılabileceğinden bahsederken sunum sonlarında ise yazdığı full text search engine’den de bahsederek ürününü Lyra olarak tanıttı ve daha sonrasında ise Lyra ismini Orama olarak değiştirerek yeniden lanse etti.
Orama’nın performanslı ve hedef odaklı olmasını sağlayan özelliklere bakıldığında; arkasında yatan veri yapıları, sahip olduğu analyzer’lar, stemmer’lar ve daha fazlası yer almaktadır.
Diğer full text search ürünlerinde olduğu gibi Orama da aranmak istenen ifadeleri token’lara ayırdıktan sonra Inverted Index sayesinde hızlıca hangi dökümanda olduğunu bulabiliyor.
The inverted index is a data structure that allows efficient, full-text searches in the database. It is a very important part of information retrieval systems and search engines that stores a mapping of words (or any type of search terms) to their locations in the database table or document.
— educative.io (What is an inverted index?)
Veri yapılarına inverted index dışında bir örnek olarak da Orama, String ifadelerini Radix Tree üzerinde tutmaktadır, bu sayede exact-match (kelimenin tam olarak eşleşmesi) dışında prefix olarak node/düğüm üzerinde arama işlemi gerçekleştirebiliyor, bu sayede prefix search özelliğini de sağlayabiliyor bizlere. Aşağıda ise bununla ilgili bir örnek yer almaktadır.
Yukarıdaki Radix Tree yapısına bakıldığında; kullanıcı sahi
kelimesini aratacağı vakit sahi
ifadesini düğümleri gezerek bulduğumuzda, kendisinden sonra gelen düğümlere ve karşılığında yer alan dökümanlara bakıldığında da bir suggestion/öneri işlemi gerçekleştirilebiliyor tıpkı aşağıdaki gibi.
Yukarıdaki örneğe bakıldığında veri yapılarının önemine ve ne tür problemlere çözüm getirdiğine de iyi bir örnek oluyor.
Orama, Radix Tree’nin yanı sıra; AVL Tree, Boolean Index gibi veri yapılarını da kullanmaktadır.
Orama Full Text Search Engine Nasıl Kullanılır?
Installation
Orama’yı indirmek için; npm
,yarn
,pnpm
gibi paket yöneticileri kullanılabililir:
# npm
npm install @orama/orama
# yarn
yarn add @orama/orama
# pnpm
pnpm add @orama/orama
ya da direkt olarak module olarak da entegre edilebilir:
<html>
<body>
<script type="module">
import { create, search, insert } from 'https://unpkg.com/@orama/orama@latest/dist/index.js'
// ...
</script>
</body>
</html>
projeye import ederken de aşağıdaki gibi import edilebilir.
import type { Language } from '@orama/orama'
Veri Tabanı Oluşturulması | Create
Orama’daki özellikleri kullanmadan önce işlemleri gerçekleştireceğimiz bir adet veri tabanı oluşturmaya daha sonrasında da içerisine bir schema
tanımlamamız gerekmektedir. Buradaki schema
olarak bahsedilen ifade ise verilerin veri tabanında hangi formatta tutulacağını temsil etmektedir.
Aşağıda basit bir şekilde veri tabanının oluşturulması gösterilmektedir. Veri tabanını oluşturmak için create
metotu kullanılmaktadır.
import { create } from '@orama/orama'
const db = await create({
schema: {
word: 'string',
},
})
Bunun biraz daha kompleks hali ise aşağıdaki gibidir.
import { create } from '@orama/orama'
const movieDB = await create({
schema: {
title: 'string',
director: 'string',
plot: 'string',
year: 'number',
isFavorite: 'boolean',
},
})
Bunların yanı sıra nested properties içeren yapılar da aşağıdaki gibi tanımlanabilir.
const movieDB = await create({
schema: {
title: 'string',
plot: 'string',
cast: {
director: 'string',
leading: 'string',
},
year: 'number',
isFavorite: 'boolean',
},
})
Array gibi veri tipleri de aşağıdaki gibi tanımlanabilir.
const db = await create({
schema: {
name: 'string[]',
numbers: 'number[]',
booleans: 'boolean[]',
},
})
Veri Tabanına Kayıt Atılması | Insert
Yukarıdaki alt başlıkta veri tabanının oluşturulması gösterildi, bu kısımda ise ilgili veri tabanına kayıtların nasıl atılacağı bilgisi yer alacaktır.
Aşağıda film bilgilerini tutan bir veri tabanının oluşturulması yer almaktadır.
import { create, insert } from '@orama/orama'
const movieDB = await create({
schema: {
title: 'string',
director: 'string',
plot: 'string',
year: 'number',
isFavorite: 'boolean',
},
})
Veri tabanına ilgili kayıtların atılması için de insert
metotu kullanılmaktadır.
const thePrestigeId = await insert(movieDB, {
title: 'The prestige',
director: 'Christopher Nolan',
plot: 'Two friends and fellow magicians become bitter enemies after a sudden tragedy. As they devote themselves to this rivalry, they make sacrifices that bring them fame but with terrible consequences.',
year: 2006,
isFavorite: true,
});
const bigFishId = await insert(movieDB, {
title: 'Big Fish',
director: 'Tim Burton',
plot: 'Will Bloom returns home to care for his dying father, who had a penchant for telling unbelievable stories. After he passes away, Will tries to find out if his tales were really true.',
year: 2004,
isFavorite: true,
});
const harryPotterId = await insert(movieDB, {
title: 'Harry Potter and the Philosopher\'s Stone',
director: 'Chris Columbus',
plot: 'Harry Potter, an eleven-year-old orphan, discovers that he is a wizard and is invited to study at Hogwarts. Even as he escapes a dreary life and enters a world of magic, he finds trouble awaiting him.',
year: 2001,
isFavorite: false,
});
Orama’ya dökümanlar kayıt edilirken eğer döküman içerisinde ona özel id
değeri varsa Orama tarafından kullanılır yoksa da kendisi oluşturur.
import { create, search } from '@orama/orama'
const db = await create({
schema: {
id: 'string',
author: 'string',
quote: 'string',
},
})
await insert(db, {
id: '73cbcc79-2203-49b8-bb52-60d8e9a66c5f',
author: 'Fernando Pessoa',
quote: "I wasn't meant for reality, but life came and found me",
})
// the document will be indexed with the following id: 73cbcc79-2203-49b8-bb52-60d8e9a66c5f
Yukarıdaki örneğe bakıldığında döküman içerisinde id
değeri var ve Orama tarafındaki karşılığı da bu olacaktır. Tanımlanmamış ise de Orama tarafından otomatik olarak oluşturulacaktır.
If you try to insert two documents with the same ID, Orama will throw an error.
Veri Tabanından Kayıt Silinmesi | Remove
Orama’da veri tabanından kayıt silmek oldukça kolaydır, bunu yapmak için gereken şey remove
metotuna veri tabanını ve silinecek olan dökümanın id değerini vermektir.
Aşağıdaki örnekte filmlerin tutulacağı bir veri tabanı ve o veri tabanına eklenmiş 3 adet film yer almaktadır.
import { create, insert, remove, search } from '@orama/orama'
const movieDB = await create({
schema: {
title: 'string',
director: 'string',
plot: 'string',
year: 'number',
isFavorite: 'boolean',
},
})
const thePrestigeId = await insert(movieDB, {
title: 'The prestige',
director: 'Christopher Nolan',
plot: 'Two friends and fellow magicians become bitter enemies after a sudden tragedy. As they devote themselves to this rivalry, they make sacrifices that bring them fame but with terrible consequences.',
year: 2006,
isFavorite: true,
});
const bigFishId = await insert(movieDB, {
title: 'Big Fish',
director: 'Tim Burton',
plot: 'Will Bloom returns home to care for his dying father, who had a penchant for telling unbelievable stories. After he passes away, Will tries to find out if his tales were really true.',
year: 2004,
isFavorite: true,
});
const harryPotterId = await insert(movieDB, {
title: 'Harry Potter and the Philosopher\'s Stone',
director: 'Chris Columbus',
plot: 'Harry Potter, an eleven-year-old orphan, discovers that he is a wizard and is invited to study at Hogwarts. Even as he escapes a dreary life and enters a world of magic, he finds trouble awaiting him.',
year: 2001,
isFavorite: false,
});
Harry Potter’ı silmek için aşağıdaki işlemi gerçekleştirmek gerekmektedir.
await remove(movieDB, harryPotterId)
Veri Tabanındaki Kaydın Güncellenmesi | Update
Orama, yapısı itibarıyla immutable olarak geliştirilmiştir. Dökümantasyon üzerinde bir dökümanı güncellemek için önerilen işlemlerde;
- Veri tabanın baştan oluşturulması ve verilerin tekrar yüklenmesi
- İlgili dökümanın silinip tekrar veri tabanına eklenmesi
gibi yöntemler önerilmektedir, ilk bakışta 2. yöntemin daha sağlıklı olduğu da görülebilir.
Orama’da Yer Alan Yardımcı Metotlar | Utility Functions for Orama
Orama, temel CRUD işlemlerin yanı sıra birkaç metot daha sunmaktadır, bunlardan bazıları; getByID
ve count
‘tur.
getByID
, Orama veri tabanında yer alan dökümanı id
değerine göre vermeyi sağlar.
import { getByID } from '@orama/orama'
const thePrestige = await getByID(movieDB, 'tt0482571')
// Returns the original, full document
count
, Orama veri tabanında var olan toplam döküman sayısını vermektedir.
import { count } from '@orama/orama'
const docNumber = await count(movieDB)
// Returns the number of documents in the database
Aramaya Giriş | Introduction to Search
Temel Arama işlemleri
Orama, veri tabanındaki dökümanlar üzerindeki arama işlemlerimizi yapmamız için bizlere basit bir arayüz/metot/API sağlamaktadır. Bunun için search
metotunun kullanılması yeterlidir.
Elimizde filmlerin olduğu bir veri tabanı ve içerisinde de birkaç adet film olduğunu varsayalım, veri tabanının oluşturulması ve kayıtların eklenmesi aşağıdaki gibidir.
import { create, insert, search } from '@orama/orama';
const movieDB = await create({
schema: {
title: 'string',
director: 'string',
plot: 'string',
year: 'number',
isFavorite: 'boolean',
},
});
await insert(movieDB, {
title: 'The prestige',
director: 'Christopher Nolan',
plot: 'Two friends and fellow magicians become bitter enemies after a sudden tragedy. As they devote themselves to this rivalry, they make sacrifices that bring them fame but with terrible consequences.',
year: 2006,
isFavorite: true,
});
await insert(movieDB, {
title: 'Big Fish',
director: 'Tim Burton',
plot: 'Will Bloom returns home to care for his dying father, who had a penchant for telling unbelievable stories. After he passes away, Will tries to find out if his tales were really true.',
year: 2004,
isFavorite: true,
});
await insert(movieDB, {
title: 'Harry Potter and the Philosopher\'s Stone',
director: 'Chris Columbus',
plot: 'Harry Potter, an eleven-year-old orphan, discovers that he is a wizard and is invited to study at Hogwarts. Even as he escapes a dreary life and enters a world of magic, he finds trouble awaiting him.',
year: 2001,
isFavorite: false,
});
Aşağıda, döküman üzerindeki bütün alanlarda ‘Harry’ kelimesinin aranması işlemi yer almaktadır.
const searchResult = await search(movieDB, {
term: 'Harry',
properties: '*',
})
Yukarıdaki sorguya bakıldığında properties:'*'
olarak geçilmiş. Buradaki *
ifadesi ilgili kelimenin veri tabanı içerisinde tanımanan schema
'daki bütün field
'lar üzerinde aranmasını sağlamaktadır.
Temel Arama işlemleri — Search Properties
Özel olarak bir field üzerinde arama yapılmak isteniyorsa da ilgili properties’e array olarak geçilebilir, aşağıda bir örneği yer almaktadır.
const searchResult = await search(movieDB, {
term: 'Chris',
properties: ['director'],
})
nested property’ler için de aşağıdaki gibi kullanılabilir.
const searchResult = await search(movieDB, {
term: 'Chris',
properties: ['cast.director'],
})
Herhangi bir ifade girilmezse Orama default olarak bütün property’ler üzerinde arama yapmaktadır.
By default, Orama searches in all searchable properties.
— Orama Search Documentation
Temel Arama işlemleri — Exact Match
Orama saerch engine içerisinde arama yaparken default olarak prefix search işlemi gerçekleştiriliyor. Prefix search yerine direkt aranılan ifadeye denk gelen sonuçların gelinmesi isteniyorsa exact
olarak belirtilen alanın true
olması gerekmektedir.
The
exact
property finds all the document with an exact match of theterm
property.— Orama Search Documentation
const searchResult = search(movieDB, {
term: 'Chris',
properties: ['director'],
exact: true,
})
Yukarıdaki aramaya bakıldığında director
ifadesinin Chris
değerini olduğu gibi içermesi gerekiyor.
exact
alanı seçilmediğinde ise Christopher Nolan
gibi sonuçları da verecektir, çünkü Christopher
kelimesi Chris
kelimesini de içermektedir.
Temel Arama işlemleri — Typo Tolerance
Orama, yazım hatalarındaki düzeltmeleri ortadan kaldırmak için Levenshtein Distance algoritmasını kullanmaktadır.
Bilgisayar bilimlerinde Levenshtein mesafesi, iki String arasındaki farkı ölçmek için kullanılan bir ölçüdür. Diğer bir dille, iki kelime arasındaki Levenshtein mesafesi, bir kelimeyi diğerine değiştirmek için gereken minimum tek karakterli düzenleme sayısıdır.
The Levenshtein distance is a string metric for measuring the difference between two sequences. Informally, the Levenshtein distance between two words is the minimum number of single-character edits (insertions, deletions or substitutions) required to change one word into the other.
— Wikipedia
Aşağıda da matematiksel formülü yer almaktadır.
In computer science, the Wagner–Fischer algorithm is a dynamic programming algorithm that computes the edit distance between two strings of characters.
— Wikipedia
Aşağıda birkaç örnek yer almaktadır. Soldaki kelimelerin sağdaki kelimelere çevrilmesi için yapılması gereken işlem sayısı yer almaktadır.
- kitten → sitten (“k”’nın “s” olarak değişmesi, 1 işlem),
- sitten → sittin (“e”’nin “i” olarak değişmesi, 1 işlem),
- sittin → sitting (“g” harfinin eklenmesi, 1 işlem).
Aşağıda ise search
işlemindeki kullanımı yer almaktadır.
const searchResult = await search(movieDB, {
term: 'Chri',
properties: ['director'],
tolerance: 1,
})
// search will response 'Chris' because of 1 distance.
Yukarıdaki örnekte Chri
kelimesi aranacaktır ve arama işlemlerindeki mesafe de 1 olacaktır. Chri
için Chris
response’unu alacağız bu durumda, çünkü aralarındaki mesafe Chri -> Chris için “s” harfinin eklenmesidir ve 1 işlemdir.
Temel Arama işlemleri — Result Limits
Orama’da search
işlemindeki limit
ifadesi kaç adet kaydın alınacağı bilgisini içermektedir. Aşağıdaki gibi kullanımı söz konusudur.
const searchResult = await search(movieDB, {
term: 'Chris',
properties: ['director'],
limit: 1,
})
Yukarıdaki kodda ‘director’ alanında Chris kelimesi geçen ilk dökümanı verecektir.
Temel Arama işlemleri — Result Offset
Orama’da pagination/sayfalama işlemini gerçekleştirmek için offset
alanı kullanılmaktadır. Aşağıdaki gibi kullanımı söz konusudur.
const searchResult = await search(movieDB, {
term: 'Chris',
properties: ['director'],
offset: 1,
})
Yukarıdaki kodda ‘director’ alanında Chris kelimesi geçen dökümanlardan ilkini atlayarak sonuçları verecektir.
Orama’da default olarak limit=10, offset=0.
By default, Orama limits the search results to
10
, without any offset (so,0
as offset value).— Orama Search Documentation
Temel Arama işlemleri — Search Return Type
Orama’da search
işlemi gerçekleştikten sonra dökümanları ve beraberinde sorguya ait meta bilgilerini içeren bir response dönmektedir.
Aşağıda örnek bir sorgu ve beraberindeki sonuç yer almaktadır.
Query:
const searchResult = await search(movieDB, {
term: 'Cris',
properties: ['director'],
tolerance: 1,
})
Search Result:
{
elapsed: {
raw: 181208,
formatted: '181μs',
},
count: 2,
hits: [
{
id: '37149225-243',
score: 0.23856062735983122,
document: {
title: 'Harry Potter and the Philosopher\'s Stone',
director: 'Chris Columbus',
plot: 'Harry Potter, an eleven-year-old orphan, discovers that he is a wizard and is invited to study at Hogwarts. Even as he escapes a dreary life and enters a world of magic, he finds trouble awaiting him.',
year: 2001,
isFavorite: false
}
},
{
id: '37149225-5',
score: 0.21267890323564321,
document: {
title: 'The prestige',
director: 'Christopher Nolan',
plot: 'Two friends and fellow magicians become bitter enemies after a sudden tragedy. As they devote themselves to this rivalry, they make sacrifices that bring them fame but with terrible consequences.',
year: 2006,
isFavorite: true
}
}
]
}
Fields Boosting
Orama’da arama işlemi gerçekleştirirken ilgili kelimenin bulunduğu field’a göre önem sırası tanımlanabilir.
const searchResult = await search(movieDB, {
term: 'Harry',
properties: '*',
boost: {
title: 2,
director: 1,
},
})
Yukarıdaki örneğe bakıldığı vakit Harry kelimesinin title alanında bulunması director alanında bulunmasından 2 kat daha önemli ve buna göre response alacaktır.
Facets
Orama, filtreleme ve facet işlemleri için de bir API sağlamaktadır. Yukarıdaki örneklerde de olduğu gibi filmlere ait bir veri tabanımız olduğunu varsayalım ve içerisine de bir takım veriler aktarılsın.
import { create } from '@orama/orama'
const db = await create({
schema: {
title: 'string',
description: 'string',
categories: {
primary: 'string',
secondary: 'string',
},
rating: 'number',
isFavorite: 'boolean',
},
})
Arama anında facets
içerisine yapmak istediğimiz sorguyu bırakabiliyoruz. Aşağıda örnek bir sorgu yer almaktadır. Sorgu içerisinde de hangi alanların hangi verilere göre gruplayıp cevap döneceği bilgisi yer alıyor.
const results = await search(db, {
term: 'Movie about cars and racing',
properties: ['description'],
facets: {
'categories.first': {
size: 3,
order: 'DESC',
},
'categories.second': {
size: 2,
order: 'DESC',
},
rating: {
ranges: [
{ from: 0, to: 3 },
{ from: 3, to: 7 },
{ from: 7, to: 10 },
],
},
isFavorite: {
true: true,
false: true,
},
},
})
Yukarıdaki kodun çıktısı da aşağıdaki gibi olacaktır.
{
elapsed: ...,
count: ...,
hits: { ... },
facets: {
'categories.first': {
count: 14,
values: {
'Action': 4,
'Adventure': 3,
'Comedy': 2,
}
},
'categories.second': {
count: 14,
values: {
'Cars': 4,
'Racing': 3,
}
},
rating: {
count: 3,
values: {
'0-3': 5,
'3-7': 15,
'7-10': 80,
}
},
isFavorite: {
count: 2,
values: {
'true': 5,
'false': 95,
}
},
}
}
Yukarıdaki sorguda da fark edileceği gibi her bir property kendince bir konfigürasyona sahip. Integer için range, String için match, Boolean için de true/false gibi. Aşağıda veri tiplerine göre sorgu tiplerinin detayları yer almaktadır.
Facets — String Facets
Veri tabanı oluşturulurken girilen schema
alanında ilgili alan String
olarak tanımlanmış ise aşağıdaki gibi bir konfigürasyon yapılabilir.
Yukarıki tanımlara uygun bir facet sorugusu yazıldığında ilgili sorguya denk gelecek response da aşağıdaki gibi olacaktır.
{
count: 14, // Total number of values, now limited to 3 (size)
values: {
'Action': 4, // Number of documents that have this value
'Adventure': 3, // Number of documents that have this value
'Comedy': 2, // Number of documents that have this value
}
}
Facets — Number Facets
Veri tabanı oluşturulurken girilen schema
alanında ilgili alan number
olarak tanımlanmış ise aşağıdaki gibi bir konfigürasyon yapılabilir.
Yukarıdaki ranges alanına gelen kısım ise kendi içerisinde from
ve to
ifadelerini içermektedir.
Yukarıki tanımlara uygun bir facet sorugusu yazıldığında ilgili sorguya denk gelecek response da aşağıdaki gibi olacaktır.
{
count: 3, // Total number of ranges
values: {
'0-3': 5, // Number of documents that have a value between 0 and 3 (inclusive)
'3-7': 15, // Number of documents that have a value between 3 and 7 (inclusive)
'7-10': 80, // Number of documents that have a value between 7 and 10 (inclusive)
}
}
Facets — Boolean Facets
Veri tabanı oluşturulurken girilen schema
alanında ilgili alan boolean
olarak tanımlanmış ise aşağıdaki gibi bir konfigürasyon yapılabilir.
Yukarıki tanımlara uygun bir facet sorugusu yazıldığında ilgili sorguya denk gelecek response da aşağıdaki gibi olacaktır.
{
count: 2, // Total number of values
values: {
'true': 5, // Number of documents that have a `true` value
'false': 95, // Number of documents that have a `false` value
}
}
Filtreleme
Orama, facet query
dışında filtreleme de sağlamaktadır.
Filtreleme işlemi String alanı üzerinde exact match
olarak çalışmaktadır. Eğer String üzerinde bir filtreleme işlemi yapılacaksa stammer’ın devre dışı bırakılması önerilmektedir. Aşağıdaki örnekte de belirli property için tokenizer’daki stammer devre dışı bırakılmıştır. (Default tokenizer kullanıldığı vakit stemmerSkipProperties
alanına özel olarak property tanımlanabilir ya da stemming
alanı da ihtiyaca göre işaretlenebilir.)
On string properties it perform an exact matching on tokens so it is advised to disable stemming for the properties you want to use filters on (when using the default tokenizer you can provide the
stemmerSkipProperties
configuration property).— Orama Search Documentation
Yukarıdaki örneklerde de olduğu gibi filmlere ait bir veri tabanımız olduğunu varsayalım ve içerisine de bir takım veriler aktarılsın.
const db = await create({
schema: {
id: 'string',
title: 'string',
year: 'number',
meta: {
rating: 'number',
length: 'number',
favorite: 'boolean',
tags: 'string'
},
},
components: {
tokenizer: {
stemming: true,
stemmerSkipProperties: ['meta.tags']
}
}
})
Aşağıda da bir filtreleme işlemine ait örnek bir sorgu yer almaktadır. Filtreler search
içerisindeki where
alanına yazılmaktadır.
const results = await search(db, {
term: 'prestige',
where: {
year: {
gte: 2000,
},
'meta.rating': {
between: [5, 10],
},
'meta.favorite': true,
length: {
gt: 60,
}
},
})
Yukarıdaki sorguya bakıldığında,
prestige
kelimesinin, aranılabilir bütün field’lar üzerinde aranması,year
ifadesinin 2000'den büyük eşit olmasırating
ifadesinin 5–10 arasında olması,meta.favorite
alanının true olması,length
alanının ise 60'dan büyük olması
istenmektedir. Yukarıdaki sorguya bakıldığında String ve Boolean veri tipleri için filtreler görece basit ve tek. Numeric alanlar için ise birden fazla filtre mevcuttur, aşağıdaki tabloda ise Numeric alan için kullanılabilir komutlarının listesi yer almaktadır.
Aşağıda ise String ve Boolean alanlar için ayrı ayrı sorgu örnekleri de yer almaktadır.
Boolean:
const results = await search(db, {
term: 'prestige',
where: {
'meta.favorite': true,
},
})
String:
const results = await search(db, {
term: 'prestige',
where: {
'meta.tags': 'new',
},
})
String ifadeleri için aynı zamanda birden fazla değer Array formatında da belirtilebiliyor.
const results = await search(db, {
term: 'prestige',
where: {
'meta.tags': ['favorite', 'new'],
},
})
Sıralama
Orama’da yaptığımız arama isteği sonucunu istediğimiz property’e göre sıralamamız için aşağıdaki gibi search
API’si içerisine sortBy
ile tanım yapmak mümkündür.
const db = await create({
schema: {
title: 'string',
year: 'number',
inPromotion: 'boolean',
meta: {
tag: 'string',
rating: 'number',
favorite: 'boolean',
},
}
})
const results = await search(db, {
term: 'prestige',
sortBy: {
property: 'title', // or 'year', 'inPromotion'
}
})
Yukarıdaki sorguya bakıldığında arama sonucunu title
alanına göre sıralayacaktır.
Orama supports sorting on 'string', 'number' and 'boolean'. The arrays are not supported.
Sıralama işlemlerinde nested property
'ler de kullanılabilir, tıpkı; 'meta.tag'
, 'meta.rating'
ve 'meta.favorite'
gibi.
const results = await search(db, {
term: 'prestige',
sortBy: {
property: 'meta.rating',
}
})
Sıralama — Tersten Sıralama
Orama’da ilgili alana göre sıralama yaparken tersten de sıralayabiliyoruz, tıpkı aşağıdaki gibi.
const db = await create({
schema: {
title: 'string',
year: 'number',
inPromotion: 'boolean',
meta: {
tag: 'string',
rating: 'number',
favorite: 'boolean',
},
}
})
const results = await search(db, {
term: 'prestige',
sortBy: {
property: 'title', // or 'year', 'inPromotion'
order: 'DESC' // default is "ASC"
}
})
Sıralama — Bellek Optimizasyonu
Orama, veri tabanı oluşturulduğu vakit schema
içerisinde tanımlı olan bütün alanlar için sıralama işlemi gerçekleştirebiliyor ve bu alanlara ait bilgileri de kendi içerisinde bir cache
'te tutuyor. Bundan dolayıdır da bellek anlamında ortaya büyük bir maliyet çıkabiliyor. Burada sıralamak istemediğimiz özel bir alan olursa aşağıdaki gibi veri tabanı oluşturulurken unsortableProperties
alanına parametre olarak geçebiliyoruz. Bu sayede bellek anlamında bir optimizasyon da söz konusu oluyor.
const db = await create({
schema: {
title: 'string',
year: 'number',
inPromotion: 'boolean',
meta: {
tag: 'string',
rating: 'number',
favorite: 'boolean',
},
},
sortBy: {
unsortableProperties: ['year', 'meta.tag']
},
})
Sıralama — Özel Sıralama
Orama, default sıralamanın yanı sıra özel olarak da bir sıralama algoritması yazılmasına da olanak sağlar. Veri tabanı oluşturulurken tanımlanan sortBy
alanı için direkt bir sıralama metotu da geçilebiliyor, tıpkı aşağıdaki gibi.
const db = await create({
schema: {
title: 'string',
year: 'number',
inPromotion: 'boolean',
meta: {
tag: 'string',
rating: 'number',
favorite: 'boolean',
},
},
sortBy: (a, b) => {
// Implement the custom sort algorithm
return a[2].year - b[2].year
}
})
Yukarıdaki sortBy
` alanına bakıldığında a
ve b
parametreleri alan bir metot görülüyor, metota parametre olarak gelen ifadeler birer array ve index içerisinde temsil ettikleri ise;
0. Dökümana ait id,
- Dökümana ait score,
- Dökümanın kendisi
olarak karşımıza çıkmaktadır.
Sıralama — Sıralamayı Devre Dışı Bırakmak
Orama’da herhangi bir sıralama yapılamayacaksa bellek anlamında optimizasyonu sağlamak için aşağıdaki gibi devre dışı bırakılabilir.
const db = await create({
schema: {
// The schema
},
sortBy: {
enabled: false
},
})
Threshold
Orama, arama işlemini gerçekleştirirken girdiğimiz metne ait bütün token’ları tarar ve içeren sonuçları getirir. Bunun getirdiği bazı problemler söz konusudur. Aşağıdaki gibi bir veri tabanı ve içerisinde de 4 adet kayıt olsun.
import { create, insert, search } from '@orama/orama'
const db = await create({
schema: {
title: 'string',
}
})
await insert(db, { title: 'Blue t-shirt, slim fit' })
await insert(db, { title: 'Blue t-shirt, regular fit' })
await insert(db, { title: 'Red t-shirt, slim fit' })
await insert(db, { title: 'Red t-shirt, oversize fit' })
Yukarıdaki eklenen dökümanlara bakıldığı vakit hepsinde de ortak birçok kelime mevcut. Peki t-shirt
kelimesini arattığımızda nasıl bir sonuç bekliyor bizleri?
const results = await search(db, {
term: 't-shirt',
})
// results.count = 4
Bütün dökümanlar t-shirt
kelimesini içerdiğinden dolayı 4 adet dökümanı da bizlere geri verdi. Peki regular fit
aratıldığında?
const results = await search(db, {
term: 'regular fit',
})
// results.count = 4
regular fit
ifadesi 1 adet dökümanda geçmesine rağmen bizlere 4 adet döküman verdi. Bunun sebebi diğer gelen 3 dökümanın da fit
kelimesini içermesinden kaynaklı. Buradaki durum AND
değil de OR
sorgusu olarak da düşünülebilir.
Veri tabanındaki kayıt sayısı ve aranılan cümlenin de uzunluğu arttığı vakit bizler için oldukça maliyetli bir arama işlemi ortaya çıkmaktadır. Bu da performans anlamında istenilen sonucu vermeyebilir. Orama bu durumlarda bizler için threshold
kavramının kullanımını önermektedir.
threshold
ifadesi 0–1 arasında yüzdeli bir ifadedir.
The threshold property solves this problem by limiting (or maximizing) the number of results to return when performing a search operation. It must be a number between 0 and 1, and it represents the percentage of results to return.
— Orama Search Documentation
Threshold | Threshold Değerinin 1 Verilmesi
Orama, default olarak threshold
değerini 1 vermektedir. Bu da bütün dökümanların arama sonuca yansıyacağı anlamında gelmektedir.
const results = await search(db, {
term: 'slim fit',
})
Yukarıdaki ifadede hem slim
hem de fit
değerlerinden herhangi birini ya da ikisini de içeren dökümanları verecektir.
Threshold | Threshold Değerinin 0 Verilmesi
threshold
değeri 0 verildiği vakit aranılan bütün ifadelerin ilgili dökümanda bulunması gerekmektedir.
const results = await search(db, {
term: 'slim fit',
threshold: 0,
})
Yukarıdaki ifadede hem slim
hem de fit
değerlerinin ikisini de içeren dökümanları verecektir.
Preflight Search
Preflight Search, Orama’nın bizlere sunduğu oldukça faydalı bir API’dir. Amacı ise yapacak olduğumuz arama isteğinin sonuç sayısını vermesi.
Faydasının ve kullanım senaryosunun daha da netleştirebilmek için dökümantasyondaki bu örnek faydalı olacaktır.
İçerisinde 50,000 ürün kaydının olduğu büyük* bir veri tabanına sahip olduğumuzu var sayalım. Kullanıcı da veri tabanındaki az bulunan nadir bir ürüne detaylı bir filtre attığında muhtemelen ya çok az ya da hiç sonuç görmeyecektir. Buradaki deneyini artırmak için preflight search ile önce eşleşen arama sonuç sayısı alınabilir, sayının yetersiz olduğu durumda ise sorgunun threshold değeri ile oynanıp daha çok kayda hit edecek yeni bir sorgu atılabilir.
Aşağıda ise kullanımı yer almaktadır. Yapılması gereken tek şey, search isteğine preflight:true
olarak parametre geçmektedir.
import { create, insert, search } from '@orama/orama'
const db = await create({
schema: {
title: 'string'
}
})
await insert(db, { title: 'Red headphones' })
await insert(db, { title: 'Green headphones' })
await insert(db, { title: 'Blue headphones' })
await insert(db, { title: 'Yellow headphones' })
const results = await search(db, {
query: 'headphones',
preflight: true
})
console.log(results)
// {
// elapsed: {
// raw: 181208,
// formatted: '181μs'
// }
// hits: []
// count: 4
// }
Sorgudaki amaç sayıyı almak olduğu için Orama’nın verdiği response içerisindeki hits
alanı boş bir array olarak dönmektedir.
The
results
object will return a standard Orama response, but thehits
property will be an empty array.
Bu yazıda,
- Orama Full Text Search Engine nedir?
- Orama Full Text Search Engine nasıl çalışır?
- Orama Full Text Search Engine nasıl kullanılır?
- Orama Full Text Search Engine’in sağladığı özellikler nelerdir?
- Orama Full Text Search Engine’in kullandığı algoritmalar nelerdir?
gibi sorulara cevap arandı ve bilgiler edinildi.
Beni; Github, Twitter ve diğer sosyal ağlardan takip edebilirsiniz.