· Admin

Laravel'de Arama: Full-Text, Semantic ve Vector Search

Bir proje gelistiriyorsun ve arama ozelligi eklemenin zamani geldi. Ama hangi yaklasimi secmelisin? Basit LIKE sorgusu mu, full-text search mu, yoksa fancy bir vector search mi? Bu yazida Laravel'de arama yontemlerini basitten karmasiga dogru inceliyorum.

Seviye 0: WHERE LIKE (Baslangic)

Her seyin basladigi yer. Kucuk projeler icin yeterli ama sinirlarini bilmen gerek.

// Basit LIKE sorgusu
$posts = Post::where('title', 'like', "%{$search}%")
    ->orWhere('body', 'like', "%{$search}%")
    ->get();

Sorunlari

  • Performans: %kelime% pattern'i index kullanamaz. 100K kayitta hissedilir.
  • Relevance yok: Sonuclari siralayamazsin. Basliktaki eslesme ile icerideki eslesme ayni oncelikte.
  • Typo toleransi yok: "laravle" yazarsan "laravel" bulamaz.
  • Turkce karakter sorunu: Buyuk/kucuk harf donusumleri sorunlu olabilir.

Ne zaman kullanilir?

  • Kucuk veri setleri (< 10K kayit)
  • Admin panelinde icerik filtreleme
  • Hizli prototipleme

Biraz Daha Akilli LIKE

// Birden fazla kelimeyi ayri ayri ara
$terms = explode(' ', $search);

$posts = Post::query()
    ->where(function ($query) use ($terms) {
        foreach ($terms as $term) {
            $query->where(function ($q) use ($term) {
                $q->where('title', 'like', "%{$term}%")
                  ->orWhere('body', 'like', "%{$term}%");
            });
        }
    })
    ->orderByRaw("
        CASE
            WHEN title LIKE ? THEN 1
            WHEN title LIKE ? THEN 2
            ELSE 3
        END
    ", ["%{$search}%", "%{$search}%"])
    ->get();

Seviye 1: MySQL Full-Text Search (Paket Gerektirmez)

MySQL ve PostgreSQL'in yerlesik full-text arama ozelligini Laravel ile dogrudan kullanabilirsin.

Migration

// MySQL icin FULLTEXT index
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->timestamps();

    $table->fullText(['title', 'body']);
});

Kullanim

// Laravel 10+ ile gelen whereFullText
$posts = Post::whereFullText(['title', 'body'], $search)->get();

// Boolean mode ile
$posts = Post::whereFullText(['title', 'body'], '+laravel -wordpress', [
    'mode' => 'boolean',
])->get();

// Relevance skoruna gore siralama (raw query ile)
$posts = Post::query()
    ->selectRaw("*, MATCH(title, body) AGAINST(? IN BOOLEAN MODE) as relevance", [$search])
    ->whereFullText(['title', 'body'], $search)
    ->orderByDesc('relevance')
    ->get();

Boolean Mode Operatorleri

Operator Anlam Ornek
+ Kelimenin olmasi zorunlu +laravel +api
- Kelimeyi haric tut +laravel -wordpress
* Wildcard larav*
"" Tam eslesen ifade "rest api"
> Relevance artir >laravel api
< Relevance azalt <laravel api

Avantajlari

  • Ekstra paket gerektirmez
  • Relevance puanlama var
  • Kelime koku eslestirme (stemming) var (Ingilizce icin iyi, Turkce icin zayif)
  • LIKE'dan cok daha hizli

Dezavantajlari

  • Turkce icin stemming yetersiz
  • Typo toleransi yok
  • Soneclerin kalitesi sinirli
  • Minimum kelime uzunlugu var (varsayilan 4 karakter)

Seviye 2: Laravel Scout (Database Driver)

Laravel Scout, modellere arama ozelligi ekleyen resmi paket. Database driver'i ile ekstra servis gerektirmeden kullanabilirsin.

composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
SCOUT_DRIVER=database

Model'e Searchable trait'i ekle:

use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;

    public function toSearchableArray(): array
    {
        return [
            'title' => $this->title,
            'body' => $this->body,
            'author_name' => $this->author->name,
            'tags' => $this->tags->pluck('name')->implode(' '),
        ];
    }
}

Kullanim:

// Basit arama
$posts = Post::search('laravel api')->get();

// Ek filtrelerle
$posts = Post::search('laravel')
    ->where('is_published', true)
    ->where('author_id', 5)
    ->paginate(15);

// Sadece ID'leri al (performans icin)
$postIds = Post::search('laravel')->keys();

Database driver LIKE sorgusu kullanir ama Scout'un API'si ile standart bir arayuz saglar. Ileride Meilisearch'e gectiginde kodun degismez.

Seviye 3: Laravel Scout + Meilisearch

Iste gercek arama deneyimi burada basliyor. Meilisearch, typo toleransli, hizli, relevance puanlamali bir arama motoru.

Kurulum

# Meilisearch'u Docker ile calistir
docker run -d --name meilisearch \
    -p 7700:7700 \
    -e MEILI_MASTER_KEY='masterKey123' \
    -v meilisearch_data:/meili_data \
    getmeili/meilisearch:latest
composer require meilisearch/meilisearch-php
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=masterKey123

Index Ayarlari

// config/scout.php
'meilisearch' => [
    'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
    'key' => env('MEILISEARCH_KEY'),
    'index-settings' => [
        Post::class => [
            'filterableAttributes' => ['is_published', 'author_id', 'category_id'],
            'sortableAttributes' => ['created_at', 'title'],
            'searchableAttributes' => ['title', 'body', 'author_name', 'tags'],
            'rankingRules' => [
                'words',
                'typo',
                'proximity',
                'attribute',
                'sort',
                'exactness',
            ],
        ],
    ],
],

Indexleme

# Mevcut verileri indexle
php artisan scout:import "App\Models\Post"

# Index ayarlarini senkronize et
php artisan scout:sync-index-settings

Kullanim

// Typo toleransli arama ("laravle" yazsan bile "laravel" bulur)
$posts = Post::search('laravle api')
    ->where('is_published', true)
    ->paginate(15);

// Siralama
$posts = Post::search('laravel')
    ->orderBy('created_at', 'desc')
    ->get();

Neden Meilisearch?

  • Typo toleransi: Yanlis yazimi otomatik duzeltir
  • Anlik sonuclar: ~50ms response time
  • Turkce destegi: Tokenization Turkce icin iyi calisiyor
  • Faceted search: Filtreleme + sayma (e-ticaret icin ideal)
  • Highlight: Eslesen kelimeleri vurgular
  • Self-hosted: Verilerin sende kalir
  • Ucretsiz: Acik kaynak

Seviye 4: Algolia (Hosted Cozum)

Meilisearch'un hosted alternatifi. Kendi sunucunu yonetmek istemiyorsan Algolia iyi bir secenek.

composer require algolia/algoliasearch-client-php
SCOUT_DRIVER=algolia
ALGOLIA_APP_ID=your-app-id
ALGOLIA_SECRET=your-secret-key

Scout API'si ayni oldugu icin kodun degismez. Sadece driver degisir.

Algolia'nin artilari:

  • Yonetilmesi gereken sunucu yok
  • Global CDN ile dusuk latency
  • Analytics dashboard'u var
  • A/B testing ozelligi

Eksileri:

  • Ucretli (free tier sinirli)
  • Veriler disarida
  • Buyuk veri setlerinde pahali

Seviye 5: Vector Search (Semantic Arama)

Iste en heyecan verici kisim. Vector search, kelimeleri degil anlami arar. "araba tamiri" aradiginda "oto mekanik" sonuclarini da bulur.

Nasil Calisiyor?

  1. Metin bir AI modeli (embedding model) ile sayisal vektore cevriliyor
  2. Bu vektorler veritabaninda saklaniyor
  3. Arama yaptiginda, arama metni de vektore cevriliyor
  4. En yakin vektorler (benzer anlamlar) bulunuyor

pgvector ile PostgreSQL

PostgreSQL kullaniyorsan pgvector eklentisi ile vector search yapabilirsin.

-- pgvector eklentisini aktiflestir
CREATE EXTENSION vector;

Migration:

// pgvector icin migration
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->timestamps();
});

// pgvector sutunu ekle
DB::statement('ALTER TABLE posts ADD COLUMN embedding vector(1536)');
DB::statement('CREATE INDEX posts_embedding_idx ON posts USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)');

Embedding Olusturma

use Illuminate\Support\Facades\Http;

class EmbeddingService
{
    public function generate(string $text): array
    {
        $response = Http::withToken(config('services.openai.key'))
            ->post('https://api.openai.com/v1/embeddings', [
                'model' => 'text-embedding-3-small',
                'input' => $text,
            ]);

        return $response->json('data.0.embedding');
    }
}

Model Observer ile Otomatik Embedding

class PostObserver
{
    public function __construct(
        protected EmbeddingService $embeddingService
    ) {}

    public function saved(Post $post)
    {
        $text = "{$post->title} {$post->body}";
        $embedding = $this->embeddingService->generate($text);

        // pgvector formatinda kaydet
        $vector = '[' . implode(',', $embedding) . ']';
        DB::statement("UPDATE posts SET embedding = ? WHERE id = ?", [$vector, $post->id]);
    }
}

Vector Search Sorgusu

class PostSearchService
{
    public function __construct(
        protected EmbeddingService $embeddingService
    ) {}

    public function semanticSearch(string $query, int $limit = 10): Collection
    {
        $embedding = $this->embeddingService->generate($query);
        $vector = '[' . implode(',', $embedding) . ']';

        return Post::query()
            ->selectRaw("*, embedding <=> ? as distance", [$vector])
            ->orderByRaw("embedding <=> ?", [$vector])
            ->limit($limit)
            ->get();
    }
}

<=> operatoru cosine distance hesaplar. Deger ne kadar kucukse, o kadar benzer.

Hybrid Search (Full-Text + Vector)

En iyi sonuclar icin ikisini birlestir:

public function hybridSearch(string $query, int $limit = 10): Collection
{
    $embedding = $this->embeddingService->generate($query);
    $vector = '[' . implode(',', $embedding) . ']';

    return Post::query()
        ->selectRaw("
            *,
            ts_rank(to_tsvector('turkish', title || ' ' || body), plainto_tsquery('turkish', ?)) as text_score,
            1 - (embedding <=> ?) as vector_score
        ", [$query, $vector])
        ->selectRaw("
            (0.4 * ts_rank(to_tsvector('turkish', title || ' ' || body), plainto_tsquery('turkish', ?))
            + 0.6 * (1 - (embedding <=> ?))) as combined_score
        ", [$query, $vector])
        ->orderByDesc('combined_score')
        ->limit($limit)
        ->get();
}

Karar Rehberi

Hangi arama yontemini secmelisin? Istek ve karar agaci:

Senaryo Cozum
< 10K kayit, basit filtre WHERE LIKE
10K-100K, keyword arama, ek paket istemiyorum MySQL Full-Text
Standart arama API'si, ileride degistirebilmeliyim Scout (database driver)
Typo tolerans, hizli, self-hosted Scout + Meilisearch
Hosted, yonetim istemiyorum Scout + Algolia
Anlamsal arama, "benzer icerik" Vector Search (pgvector)
En iyi sonuc kalitesi Hybrid (Full-Text + Vector)

Maliyet Karsilastirmasi

Cozum Sunucu Maliyeti API Maliyeti
WHERE LIKE $0 $0
Full-Text (MySQL) $0 $0
Scout (database) $0 $0
Meilisearch +$5-20/ay (RAM) $0
Algolia $0 $0-300/ay
pgvector + OpenAI +$5/ay (DB) ~$0.02/1M token

Pratik Ipuclari

1. Arama Debounce

Frontend'de her tuslama icin istek gonderme:

// 300ms debounce
let timeout;
searchInput.addEventListener('input', (e) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
        fetchResults(e.target.value);
    }, 300);
});

2. Arama Gecmisi

// Populer aramalari kaydet
class SearchLog extends Model
{
    protected $fillable = ['query', 'results_count', 'user_id'];
}

// Middleware veya controller'da
SearchLog::create([
    'query' => $search,
    'results_count' => $results->total(),
    'user_id' => auth()->id(),
]);

3. Sonuclari Cache'le

$cacheKey = 'search:' . md5($query . $page);

$results = Cache::remember($cacheKey, 300, function () use ($query, $page) {
    return Post::search($query)->paginate(15, 'page', $page);
});

Sonuc

Cogu proje icin baslangicta WHERE LIKE veya Full-Text yeterli. Kullanici deneyimi onemliyse Meilisearch'e gec. Anlamsal arama ihtiyacin varsa (ornegin "benzer makaleler" ozelligi) pgvector ekle.

Benim tavsiyem: Scout ile basla, driver olarak database sec. Ihtiyac buyudukce Meilisearch'e gecis tek satirlik env degisikligi. Yani mimariyi dogru kurarsan, arama motorunu degistirmek agrisiz olur.