Laravel Scout ve Elasticsearch Entegrasyonu

Turan Karatuğ
4 min readJul 12, 2020

--

Laravel’in resmi paketlerinden biri olan Scout, sürücü tabanlı olarak Eloquent Model’lerine full-text search özelliği kazandırmamıza olanak sağlar. Yerleşik olarak sadece Algolia sürücüsü ile birlikte gelir. Onun dışındaki servislerle entegrasyon için var olan paketleri kullanmamız ya da ihtiyaçlarımıza ve entegre edeceğimiz servise göre kendi sürücümüzü yazmamız gerekir.

Geçtiğimiz haftalarda diğer developer arkadaşlarla beraber projemize Elasticsearch ile arama özelliği eklemek istedik. Laravel ekosisteminde uygulanan yöntemleri araştırırken, yukarıda bahsettiğim gibi Scout için resmi bir elasticsearch sürücüsünün olmadığını öğrendik ve işimizi görecek bir paket aramaya başladık.

Sonuç olarak Laravel 7.x sürümleri ile uyumlu olan, babenkoivan adlı bir kullanıcının geliştirdiği elastic-scout-driver paketini bulduk.

https://github.com/babenkoivan/elastic-scout-driver

Elastic Scout Driver

Bu paket, yine aynı kullanıcı tarafından yazılan elastic-client adlı bir paket ile birlikte geliyor. Temel olarak Elasticsearch’ün php sdk’sını kullanarak, belirtilen elasticsearch servisine bağlanan bir client oluşturuyor. Scout driver da bu client’ı kullanarak, Eloquent modellerini elasticsearch üzerinde indexleyebiliyor.

Şimdi kurulum aşamasından itibaren Laravel Scout’u Elasticsearch ile nasıl kullanacağımızı görelim.

1- Kurulum

İlk olarak Laravel projemize Scout paketini kuruyoruz;

composer require laravel/scout

Daha sonra elastic-scout-driver paketini kuruyoruz.

composer require babenkoivan/elastic-scout-driver

Herhangi bir elasticsearch servisine bağlanabilmek için, paketin içinde gelen elastic-client paketinin konfigürasyon dosyasını projemize dahil etmemiz ve servis bağlantı bilgilerini bu dosyada belirtmemiz gerekiyor.

Bunun için aşağıdaki komut ile dosyayı projemizin config dizinine çıkarıyoruz;

php artisan vendor:publish --provider="ElasticClient\ServiceProvider"

Komut çalıştıktan sonra config dizini içerisinde aşağıdaki içeriğe sahip, elastic.client.php adlı bir dosya oluşuyor.

return [
'hosts' => [
env('ELASTICSEARCH_HOST', 'localhost:9200'),
]
];

Buradan elasticsearch servisimize ait host ve port bilgilerini girerek bağlantı sağlıyoruz.

Son olarak .env dosyasına SCOUT_DRIVER=elastic satırını ekliyoruz.

2- Model Index’lerini Yapılandırma

Laravel modelleri içerisinde, Scout paketine ait Searchable traitini kullanarak, o modeldeki verileri elasticsearch üzerinde index’leyebiliyoruz. Bu trait, modele yeni bir kayıt eklendiğinde ve var olan bir kayıt güncellendiğinde ya da silindiğinde, ModelObserver kullanarak bu değişiklikleri elasticsearch üzerine yansıtıyor.

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Post extends Model { use Searchable; protected $fillable = ['title', 'content', 'keywords', 'slug']; public function searchableAs(): string
{
return 'posts_index';
}
}

3- Index’lenecek Verilerin Belirlenmesi

Scout paketinde varsayılan olarak modeldeki tüm veriler index’lenir. Ancak çoğu zaman buna ihtiyaç olmayabilir. Bu yüzden yalnızca belirlediğimiz alanların index’lenmesini istiyorsak toSearchableArray() methodunu kullanabiliriz.

public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
];
}

Yukarıdaki konfigürasyon sayesinde Post modeline eklenen her bir kaydın sadece id, title ve content alanları index’lenecektir.

Model’in ilişkili olduğu diğer modellere ait verileri de bu listeye ekleyebiliriz.

Örneğin Post modelindeki bir kaydı oluşturan kullanıcıların ad ve soyad bilgilerini de index’lemek istersek, User modeli ile ilişkiyi kurduktan sonra bu modeldeki fullname kolonunu dahil edebiliriz.

public function user(): belongsTo
{
$this->belongsTo(User::class);
}
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'user' => $this->user->fullname,
'title' => $this->title,
'content' => $this->content,
];
}

4- Koşullu Index’leme

Bazı durumlarda yalnızca belirli koşullara uyan kayıtların index’lenmesi gerekebilir. Örneğin Post modeline eklenen yazıların varsayılan olarak taslak durumunda kaydedildiğini ve ancak yayınlandı durumuna geçtiği zaman ulaşılabilir olduğunu düşünelim. Bu durumda yalnızca yayınlanmış yazıların index’lenmesini isteyebiliriz.

Böyle durumlarda koşullu index’leme methodu olan shouldBeSearchable() yardımımıza yetişiyor.

public function shouldBeSearchable(): bool
{
return $this->is_published;
}

Yukarıdaki kodda görüleceği gibi, is_published kolonu true değilse index’leme gerçekleşmeyecektir.

5- Index Güncelleme

Yazının başında belirttiğim gibi, Model Observer sayesinde bir kayıt üzerinde güncelleme yapıldığında, Scout paketi, bu değişiklikleri elasticsearch üzerine de yansıtır. Bunu yaparken de index’lenen kolonlarda bir değişiklik olup olmadığına bakmaz. Index’leme listesinde olmayan bir kolon güncellenmiş olsa dahi elasticsearch üzerindeki kaydı da günceller. Tabi ki boş yere yapılan bu request masrafından kaçınmak gerekir.

Bunun için ilk olarak Scout paketinin ModelObserver sınıfını extend edip saved() methodunu ezen yeni bir observer sınıfı yaratıyoruz.

namespace App;use Laravel\Scout\ModelObserver;class OnlySearchableModelObserver extends ModelObserver
{
public function saved($model): void
{
if (static::syncingDisabledFor($model)) {
return;
}
if (! $model->shouldBeSearchable()) {
$model->unsearchable();
return;
}
if (empty(array_intersect_key($model->toSearchableArray(), $model->getDirty()))) {
return;
}
$model->searchable();
}
}

Burada önce ilgili model için senkronizasyonun kapalı olup olmadığını kontrol ediyoruz. Sonrasında eklenen ya da güncellenen kaydın index’leme koşullarına uyup uymadığına bakıyoruz. Son olarak da update edilen alanlardan herhangi biri, index’lenecek alanlar içerisinde yer almıyorsa return edip index’leme işlemini atlıyoruz.

Kendi observer sınıfımızı yarattıktan sonra artık model içerisinde default observer yerine kullanmak üzere tanımlamamız gerekiyor. Bunun için Searchable trait’i içerisinde yer alan bootSearchable() methodunu override ediyoruz.

namespace App;use App\OnlySearchableModelObserver;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Laravel\Scout\SearchableScope;
class Post extends Model
{
use Searchable;
public static function bootSearchable(): void
{
static::addGlobalScope(new SearchableScope);
static::observe(new OnlySearchableModelObserver()); (new static)->registerSearchableMacros();
}
}

Sonuç

Yukarıdaki işlemler sonrasında model sınıfımız aşağıdaki gibi bir yapıya kavuşuyor.

namespace App;use App\OnlySearchableModelObserver;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Laravel\Scout\SearchableScope;
class Post extends Model
{
use Searchable;
protected $fillable = ['title', 'content', 'keywords', 'slug']; public static function bootSearchable(): void
{
static::addGlobalScope(new SearchableScope);
static::observe(new OnlySearchableModelObserver()); (new static)->registerSearchableMacros();
}
public function searchableAs(): string
{
return 'posts_index';
}
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
];
}
public function shouldBeSearchable(): bool
{
return $this->is_published;
}

}

Arama

Index’leme için gerekli ayarlamaları yaptıktan sonra artık arama işlemine geçebiliriz. Bunun için Scout paketinin bize sunduğu search() methodunu kullanıyoruz.

$searchResponse = App\Post::search('Lorem ipsum')->raw();

Yukarıdaki raw() methodu dikkatinizi çekmiştir. Bu method, arama sonuçlarını, elasticsearch servisinin döndürdüğü yapıda vermektedir.

Sonuçları Eloquent Collection tipinde almak için ise normalde olduğu gibi get() methodunu kullanabiliriz.

$searchResponse = App\Post::search('Lorem ipsum')->get();

Kaynaklar

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Turan Karatuğ
Turan Karatuğ

Responses (1)

Write a response