M. Niyazi Alpay
M. Niyazi Alpay
M. Niyazi Alpay

Çok küçük yaştan itibaren bilgisayar sistemleriyle ilgileniyorum ve 2005 yılından beri programlama ile uğraşıyorum, PHP, MySQL, Python, MongoDB ve Linux konularında bilgi sahibiyim

 

about.me/Cryptograph

  • admin@niyazi.org

Laravel - Meilisearch ve MongoDB Entegrasyonu

Laravel - Meilisearch ve MongoDB Entegrasyonu

Bir önceki yazımda Laravel ve Meilisearch entegrasyonundan, bir önceki yazımda da Laravel ve MongoDB entegrasyonundan bahsetmiştim. Bu yazımda ise bu üçünü birlikte nasıl kullanacağımızdan bahsedeceğim.

Önceki yazılarımda belirttiğim gibi MongoDB ve Meilisearch entegrasyonlarını yapıyoruz. Aslında her şey normal çalışıyor, eklediğim içerik Meilisearch tarafında da oluşuyor, güncelleme yapınca güncelleniyor ya da sildiğimde aynı şekilde Meilisearch tarafında da siliniyor ancak tek bir sorun var, arama yaptığımda herhangi bir sonuç gelmiyor.

Burada yapılan arama işlemi sonrası çalıştırılan veritabanı sorgusunu debugbar çıktısından incelediğimde sorunun MongoDB tarafında çalıştırılan arama sorgusundan kaynaklandığını gördüm.

Meilisearch, Mysql ile geliştirilen projede yapılan arama için aşağıdaki örnek sorguyu çalıştırıyor

select * from `products` where `products`.`id` in (2, 1)

products isimli tabloda yapılan aramaya göre id değerleri 1 ve 2 olan içerikleri çağırmış oluyor bu şekilde ancak where şartında tablo adı bir daha belirtilmiş. MongoDB tarafında da olay işte burada başlıyor :)

MongoDB tarafında sorgu şu şekilde çalıştırılıyor.

products.find({"products._id": {"$in":["656d9ac0ab082026280db1a4","656d9a78ab082026280db193"]}},{"typeMap":{"root":"array","document":"array"}})

products._id isimli bir field yoktur, sql tarafında table.column_name şeklinde sorgu yazabiliriz ancak MongoDB tarafında bu şekilde sorgu yazamıyoruz.

/vendor/laravel/scout/src/Searchable.php dosyasında sorguların Laravel’in kendi query builder yapısından geldiği görülüyor. Bu dizindeki dosyalara maalesef müdahale edemeyiz. Bu yüzden Searchable.php dosyasının kopyasını alarak düzenleme yapacağız ve searcahble olarak tanımlamak istediğimiz modellere bunu çağıracağız.

/vendor/laravel/scout/src/Searchable.php bu dosyayı app dizini altında Traits dizini oluşturarak kopyalıyoruz, app dizini altında istediğiniz yere de koyabilirsiniz ancak dosya düzeni bozulmaması için ben bu yolu tercih ettim. Kopyaladıktan sonra dosyanın içeriğini aşağıdaki gibi değiştiriyoruz.

<?php

namespace App\Traits;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Database\Eloquent\Collection;
use Laravel\Scout\Builder;
use Laravel\Scout\EngineManager;
use Laravel\Scout\ModelObserver;
use Laravel\Scout\Scout;
use Laravel\Scout\SearchableScope;

trait Searchable
{
    /**
     * Additional metadata attributes managed by Scout.
     *
     * @var array
     */
    protected array $scoutMetadata = [];

    /**
     * Boot the trait.
     *
     * @return void
     */
    public static function bootSearchable(): void
    {
        static::addGlobalScope(new SearchableScope);

        static::observe(new ModelObserver);

        (new static)->registerSearchableMacros();
    }

    /**
     * Register the searchable macros.
     *
     * @return void
     */
    public function registerSearchableMacros(): void
    {
        $self = $this;

        BaseCollection::macro('searchable', function () use ($self) {
            $self->queueMakeSearchable($this);
        });

        BaseCollection::macro('unsearchable', function () use ($self) {
            $self->queueRemoveFromSearch($this);
        });
    }

    /**
     * Dispatch the job to make the given models searchable.
     *
     * @param  Collection  $models
     * @return void
     */
    public function queueMakeSearchable($models)
    {
        if ($models->isEmpty()) {
            return;
        }

        if (! config('scout.queue')) {
            return $models->first()->makeSearchableUsing($models)->first()->searchableUsing()->update($models);
        }

        dispatch((new Scout::$makeSearchableJob($models))
            ->onQueue($models->first()->syncWithSearchUsingQueue())
            ->onConnection($models->first()->syncWithSearchUsing()));
    }

    /**
     * Dispatch the job to make the given models unsearchable.
     *
     * @param  Collection  $models
     * @return void
     */
    public function queueRemoveFromSearch($models)
    {
        if ($models->isEmpty()) {
            return;
        }

        if (! config('scout.queue')) {
            return $models->first()->searchableUsing()->delete($models);
        }

        dispatch(new Scout::$removeFromSearchJob($models))
            ->onQueue($models->first()->syncWithSearchUsingQueue())
            ->onConnection($models->first()->syncWithSearchUsing());
    }

    /**
     * Determine if the model should be searchable.
     *
     * @return bool
     */
    public function shouldBeSearchable()
    {
        return true;
    }

    /**
     * When updating a model, this method determines if we should update the search index.
     *
     * @return bool
     */
    public function searchIndexShouldBeUpdated()
    {
        return true;
    }

    /**
     * Perform a search against the model's indexed data.
     *
     * @param  string  $query
     * @param  \Closure  $callback
     * @return Builder
     */
    public static function search($query = '', $callback = null)
    {
        return app(Builder::class, [
            'model' => new static,
            'query' => $query,
            'callback' => $callback,
            'softDelete'=> static::usesSoftDelete() && config('scout.soft_delete', false)
        ]);
    }

    /**
     * Make all instances of the model searchable.
     *
     * @param  int  $chunk
     * @return void
     */
    public static function makeAllSearchable($chunk = null)
    {
        $self = new static;

        $softDelete = static::usesSoftDelete() && config('scout.soft_delete', false);

        $self->newQuery()
            ->when(true, function ($query) use ($self) {
                $self->makeAllSearchableUsing($query);
            })
            ->when($softDelete, function ($query) {
                $query->withTrashed();
            })
            ->orderBy(
                $self->getScoutKeyName()
            )
            ->searchable($chunk);
    }

    /**
     * Modify the collection of models being made searchable.
     *
     * @param  \Illuminate\Support\Collection  $models
     * @return \Illuminate\Support\Collection
     */
    public function makeSearchableUsing(BaseCollection $models)
    {
        return $models;
    }

    /**
     * Modify the query used to retrieve models when making all of the models searchable.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    protected function makeAllSearchableUsing(EloquentBuilder $query)
    {
        return $query;
    }

    /**
     * Make the given model instance searchable.
     *
     * @return void
     */
    public function searchable()
    {
        $this->newCollection([$this])->searchable();
    }

    /**
     * Remove all instances of the model from the search index.
     *
     * @return void
     */
    public static function removeAllFromSearch()
    {
        $self = new static;

        $self->searchableUsing()->flush($self);
    }

    /**
     * Remove the given model instance from the search index.
     *
     * @return void
     */
    public function unsearchable()
    {
        $this->newCollection([$this])->unsearchable();
    }

    /**
     * Determine if the model existed in the search index prior to an update.
     *
     * @return bool
     */
    public function wasSearchableBeforeUpdate()
    {
        return true;
    }

    /**
     * Determine if the model existed in the search index prior to deletion.
     *
     * @return bool
     */
    public function wasSearchableBeforeDelete()
    {
        return true;
    }

    /**
     * Get the requested models from an array of object IDs.
     *
     * @param Builder $builder
     * @param  array  $ids
     * @return mixed
     */
    public function getScoutModelsByIds(Builder $builder, array $ids)
    {
        return $this->queryScoutModelsByIds($builder, $ids)->get();
    }

    /**
     * Get a query builder for retrieving the requested models from an array of object IDs.
     *
     * @param Builder $builder
     * @param  array  $ids
     * @return mixed
     */
    public function queryScoutModelsByIds(Builder $builder, array $ids)
    {
        $query = static::usesSoftDelete()
            ? $this->withTrashed() : $this->newQuery();

        if ($builder->queryCallback) {
            call_user_func($builder->queryCallback, $query);
        }

        $whereIn = in_array($this->getScoutKeyType(), ['int', 'integer']) ?
            'whereIntegerInRaw' :
            'whereIn';

        return $query->{$whereIn}(
            $this->getScoutKeyName(), $ids
        );
    }

    /**
     * Enable search syncing for this model.
     *
     * @return void
     */
    public static function enableSearchSyncing()
    {
        ModelObserver::enableSyncingFor(get_called_class());
    }

    /**
     * Disable search syncing for this model.
     *
     * @return void
     */
    public static function disableSearchSyncing()
    {
        ModelObserver::disableSyncingFor(get_called_class());
    }

    /**
     * Temporarily disable search syncing for the given callback.
     *
     * @param  callable  $callback
     * @return mixed
     */
    public static function withoutSyncingToSearch($callback)
    {
        static::disableSearchSyncing();

        try {
            return $callback();
        } finally {
            static::enableSearchSyncing();
        }
    }

    /**
     * Get the index name for the model.
     *
     * @return string
     */
    public function searchableAs()
    {
        return config('scout.prefix').$this->getTable();
    }

    /**
     * Get the indexable data array for the model.
     *
     * @return array
     */
    public function toSearchableArray()
    {
        return $this->toArray();
    }

    /**
     * Get the Scout engine for the model.
     *
     * @return mixed
     */
    public function searchableUsing()
    {
        return app(EngineManager::class)->engine();
    }

    /**
     * Get the queue connection that should be used when syncing.
     *
     * @return string
     */
    public function syncWithSearchUsing()
    {
        return config('scout.queue.connection') ?: config('queue.default');
    }

    /**
     * Get the queue that should be used with syncing.
     *
     * @return string
     */
    public function syncWithSearchUsingQueue()
    {
        return config('scout.queue.queue');
    }

    /**
     * Sync the soft deleted status for this model into the metadata.
     *
     * @return $this
     */
    public function pushSoftDeleteMetadata()
    {
        return $this->withScoutMetadata('__soft_deleted', $this->trashed() ? 1 : 0);
    }

    /**
     * Get all Scout related metadata.
     *
     * @return array
     */
    public function scoutMetadata()
    {
        return $this->scoutMetadata;
    }

    /**
     * Set a Scout related metadata.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return $this
     */
    public function withScoutMetadata($key, $value)
    {
        $this->scoutMetadata[$key] = $value;

        return $this;
    }

    /**
     * Get the value used to index the model.
     *
     * @return mixed
     */
    public function getScoutKey()
    {
        return $this->getKey();
    }

    /**
     * Get the auto-incrementing key type for querying models.
     *
     * @return string
     */
    public function getScoutKeyType()
    {
        return $this->getKeyType();
    }

    /**
     * Get the key name used to index the model.
     *
     * @return mixed
     */
    public function getScoutKeyName()
    {
        return $this->getKeyName();
    }

    /**
     * Determine if the current class should use soft deletes with searching.
     *
     * @return bool
     */
    protected static function usesSoftDelete()
    {
        return in_array(SoftDeletes::class, class_uses_recursive(get_called_class()));
    }
}

Burada "$self->qualifyColumn($self->getScoutKeyName())"  olan satırı "$self->getScoutKeyName()" bu satır ile, "$this->qualifyColumn($this->getScoutKeyName()), $ids" olan satırı ise "$this->getScoutKeyName(), $ids" bu satır ile değitşiriyoruz. Farklı bir dizine aldığımız için namespace olarak "namespace App\Traits;"  tanımlıyoruz. Namespace ve dizin değiştiği için çalışmayan bir çok fonksiyon var bunları da use ile dosyaya çağırarak çalışmasını sağlıyoruz.

use Illuminate\Database\Eloquent\Collection;
use Laravel\Scout\Builder;
use Laravel\Scout\EngineManager;
use Laravel\Scout\ModelObserver;
use Laravel\Scout\Scout;
use Laravel\Scout\SearchableScope;

Aslında en temelde yaptığımız şey qualifyColumn fonksiyonlarını devredışı bırakmak. Çünkü table_name.column_name şeklinde sorguyu oluşturan fonksiyon bu.

Searchable olarak tanımlamak istediğimiz modele app/traits altında oluşturduğumuz dosyayı çağırıyoruz, örnek olarak aşağıdaki modeli inceleyebilirsiniz.

<?php

namespace App\Models;

use App\Traits\Searchable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use MongoDB\Laravel\Eloquent\Model;

class Blog extends Model
{
    use HasFactory;
    use Searchable;
    
    protected $collection = 'blogs';
    
    protected $fillable = [
        'title',
        'slug',
        'body',
        'image',
        'user_id',
    ];
}
Bu yazının farklı bir dilde versiyonu bulunmaktadır.
English: https://niyazi.net/en/laravel-meilisearch-and-mongodb-integration

Bunları da okumak isteyebilirsiniz

Hiç yorum yok

Yorum Bırakın

E-posta adresiniz yayınlanmayacaktır. Zorunlu alanlar * ile işaretlenmiştir