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?
- Metin bir AI modeli (embedding model) ile sayisal vektore cevriliyor
- Bu vektorler veritabaninda saklaniyor
- Arama yaptiginda, arama metni de vektore cevriliyor
- 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.