Orama Full Text Search Engine Nedir, Nasıl Çalışır?

Yusuf Yılmaz
19 min readJun 14, 2023
Orama | JavaScript Full Text Search Engine

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 — Search, Everywhere!

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.

Writing a full-text search engine in TypeScript — Michele Riva — NDC Oslo 2022

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.

Radix Tree with Inverted Index | Search Suggestion

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.

Search suggestion with Radix Tree by Orama

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 the term 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.

Wagner–Fischer Algorithm

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.

  1. kitten → sitten (“k”’nın “s” olarak değişmesi, 1 işlem),
  2. sitten → sittin (“e”’nin “i” olarak değişmesi, 1 işlem),
  3. 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.

Facets — String Facets

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.

Facets — Number Facets

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.

Facets — Boolean Facets

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.

Numeric Filter Queries

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,

  1. Dökümana ait score,
  2. 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 the hits 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.

--

--