Arama
DataTable ve liste sayfalarının arama kutusu Türkçe harflere ve büyük/küçük harfe takılmadan arar; büyük tablolarda da hızlı kalır. Bu sayfa nasıl çalıştığını ve nasıl ayarlanacağını anlatır.
Size sağladıkları:
- Kullanıcı "ayse" yazınca "Ayşe", "ismail isik" yazınca "İsmail Işık" bulunur.
- Varsayılan arama indeks kullanır; milyonlarca satırda da tabloyu baştan sona okumaz.
- DataTable ve
WithListingaynı altyapıyı kullanır: aynı alan tanımı (SearchField), aynı sürücü, aynı terim için aynı satırlar. - Yazım hatası toleransı gerekirse kendi sürücünüzle bir arama motoruna (Meilisearch, Typesense) geçersiniz; tablo kodu değişmez.
Not
Arama altyapısı PRO paketindedir (
acunsoft/acun-ui-pro); Acun temalarında kurulu gelir.
En kısa örnek
Bir migration'da arama sütununu ekleyin:
Schema::table('customers', function (Blueprint $table) {
$table->searchColumn('email'); // email_search sütunu ve indeksi
});
Sonra alanı aranabilir yapın. DataTable'da sütunu işaretleyin, liste sayfasında alanı verin:
// DataTable
Column::make('E-posta', 'email')->searchable(),
// WithListing
protected function searchFields(): array
{
return [SearchField::make('email')];
}
Arama, veritabanının email sütunundan ürettiği email_search sütununda çalışır. Bu bir üretilen sütundur (generated column): değerini veritabanı hesaplar ve günceller. Varsayılan arama baştan aramadır ve indeksi kullanır.
Neden üretilen bir arama sütunu?
Arama, verinin aranmaya hazır bir kopyasında yapılır. Kaynak sütun olduğu gibi kalır:
name name_search
Ayşe Yılmaz ayse yilmaz
İsmail IŞIK ismail isik
Her katman tek bir iş yapar:
- Kaynak sütun saklar: Veriyi olduğu gibi UTF-8 olarak tutar ("Ayşe Yılmaz"). Görüntüleme ve sıralama onu kullanır.
- Arama sütunu sadeleştirir: Türkçe harfleri ve büyük/küçük harfi tek biçime indirir ("ayse yilmaz").
- İkili karşılaştırma kuralı (binary collation) karşılaştırır: Değer zaten sadeleştiği için bayt bayt karşılaştırılır; dil kuralı gerekmez.
- İndeks hızlandırır: Baştan arama indekste gezinir; tabloyu baştan sona okumaz.
Sütunu veritabanı ürettiği için insert, ham update, içe aktarma ve Model::query()->update() da onu kendiliğinden günceller. PHP'de doldurma, model olayı ya da geriye dönük doldurma yoktur. Sorgu anında LOWER(REPLACE(sütun…)) LIKE '%…%' yazmak ise her aramada bütün satırları okur ve indeks kullanamaz.
Sadeleştirme kuralı PHP'de Acun\Ui\Search\Normalizer::fold(), SQL'de Normalizer::expression() ile uygulanır. İkisi aynı sonucu verir:
- İ, I, ı → i · Ş, ş → s · Ç, ç → c · Ğ, ğ → g · Ö, ö → o · Ü, ü → u. Bu adım küçük harfe çevirmeden önce yapılır.
- Küçük harfe çevrilir. É, Ä gibi diğer harfleri MySQL, MariaDB, PostgreSQL ve SQL Server küçültür; SQLite yalnızca A–Z'yi küçültür.
- Sekme ve satır sonları boşluk olur, art arda boşluklar teke iner, baştaki ve sondaki boşluk silinir. SQL tarafı 32 boşluğa kadar olan dizileri teke indirir.
Böylece "ayse" → "Ayşe", "ali" → "ALİ" ve "ALI", "ismail isik" → "İsmail Işık", "ÇAĞRI" → "çağrı" bulunur. Türkçe arama ayrı bir mod değildir; her mod bu sadeleşmiş değerlerde çalışır.
Yazım hataları
Sadeleştirme yalnızca harfleri ve büyük/küçük harfi tek biçime indirir: "aysse", "Ayşe"yi bulmaz. Veritabanı sürücüsünde bulanık arama, benzerlik ya da ses benzerliği araması yoktur. Yazım hatası toleransı ve alaka sıralaması gerekiyorsa bir arama motoru için kendi sürücünüzü yazın (bkz. Sürücüler); tablo kodu değişmez.
Arama sütunu eklemek
searchColumn(), bir migration'da arama sütununu ve indeksini ekler:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->searchColumn('number'); // number_search + indeks
$table->searchColumn('customer_email'); // customer_email_search + indeks
$table->searchColumn('customer_name', index: false);
// Birden çok kaynak: tek boşlukla birleşir, NULL boş sayılır; ad zorunludur.
$table->searchColumn(['customer_name', 'customer_email'], 'customer_search');
});
}
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropSearchColumn('number_search');
$table->dropSearchColumn('customer_email_search');
$table->dropSearchColumn('customer_name_search', index: false);
$table->dropSearchColumn('customer_search');
});
}
};
| Parametre | Varsayılan | Ne yapar |
|---|---|---|
$source |
— | Kaynak sütun ya da sütun listesi |
$name |
{kaynak}_search |
Sütun adı; birden çok kaynakta zorunlu |
index |
true |
Baştan ve tam arama için indeks; yalnızca içinde aranan sütunda gereksiz |
fullText |
false |
Tam metin indeksi ekler; veritabanına göre aşağıya bakın |
length |
255 |
varchar uzunluğu; 0 sınırsız text demektir |
_search eki acun-ui.search.suffix, varsayılan uzunluk acun-ui.search.length ile değişir. Uzunluğu aşan değerin baştaki karakterleri tutulur.
dropSearchColumn() sütunu indeksiyle birlikte kaldırır. Sütunu oluştururken verdiğiniz index ve fullText değerlerini ona da verin.
Sütun adları sizin kuralınıza uyabilir. Örneğin Türkçe adlandırılan bir projede $table->searchColumn('name', 'ad_arama').
Veritabanına göre ne oluşur
- SQLite: VIRTUAL sütun, varsayılan BINARY karşılaştırma kuralı ve normal indeks.
fullTextetkisizdir;FullTextmodu içinde aramaya döner. - MySQL / MariaDB: VIRTUAL sütun;
fullText: trueile STORED. Karşılaştırma kuralıutf8mb4_bin. Normal indeks;length: 0ise ilk 191 karakter indekslenir.fullText: truebir FULLTEXT indeks ekler; bu indeks STORED sütun ister. - PostgreSQL: STORED sütun,
"C"karşılaştırma kuralı ve normal btree indeks.fullText: truebir pg_trgm GIN indeksi (gin_trgm_ops) ekler. pg_trgm uzantısı önceki bir migration'da kurulmuş olmalıdır; yoksa migration açıklayıcı bir hata verir. - SQL Server: PERSISTED hesaplanan sütun (computed column),
Latin1_General_100_BIN2karşılaştırma kuralı ve normal indeks.fullTextetkisizdir.
MySQL'de üretilen SQL komutu (kısaltılmış):
alter table `orders` add `customer_email_search` varchar(255) collate 'utf8mb4_bin'
as (SUBSTR(TRIM(REPLACE(… LOWER(REPLACE(REPLACE(`customer_email`, 'İ', 'i'), 'I', 'i') …) …)), 1, 255));
alter table `orders` add index `orders_customer_email_search_index`(`customer_email_search`);
PostgreSQL'de:
alter table "orders" add column "customer_email_search" varchar(255) collate "C" null
generated always as (SUBSTR(TRIM(REPLACE(… LOWER(REPLACE("customer_email", 'İ', 'i') …) …)), 1, 255)) stored;
create index "orders_customer_email_search_index" on "orders" ("customer_email_search");
İfade yalnızca her seferinde aynı sonucu veren işlevler kullanır. PostgreSQL'de bunlar IMMUTABLE işlevlerdir: replace, lower, trim, substr, coalesce, ||. concat_ws STABLE olduğu için kullanılmaz.
Büyük mevcut tablolar
Mevcut büyük bir tabloya sütun eklemenin maliyeti veritabanına göre değişir:
- MySQL / MariaDB: VIRTUAL sütun eklemek anlık bir ALTER'dır; indeks çevrimiçi kurulur.
fullText: trueise STORED sütun ve FULLTEXT indeks tabloyu yeniden yazar. Milyonlarca satırda bunu yoğun olmayan bir saate ya da pt-online-schema-change, gh-ost gibi bir çevrimiçi şema aracına planlayın. - PostgreSQL: STORED sütun eklemek tabloyu yeniden yazar ve bu sırada tabloyu kilitler. İndeksi ayrı bir migration'da
CREATE INDEX CONCURRENTLYile kurmak içinindex: falseverip komutu kendiniz yazabilirsiniz. - SQLite: VIRTUAL sütun anlıktır.
Dikkat edilecekler
- SQLite'ta tabloyu yeniden kuran değişiklikler.
->change()ya da yabancı anahtar ekleyip silmek, Laravel'in SQLite'ta tabloyu yeniden kurmasına yol açar. Bu sırada üretilen sütun düz bir sütuna dönüşür ve kaynağını izlemeyi bırakır. Yerel ve test ortamında paket bunu hata ile bildirir. Çözüm: yeni bir migration'da iki ayrıSchema::table()çağrısıyla öncedropSearchColumn(), sonrasearchColumn().dropColumn()verenameColumn()güvenlidir. - PostgreSQL'de kaynak sütunun tipi. Üretilen bir sütunun kullandığı sütunun tipi değiştirilemez. Önce
dropSearchColumn(), sonra tip değişikliği, en son yenidensearchColumn(). select *. Arama sütunları modele de gelir. Model JSON'a çevriliyorsa sütunları$hiddenlistesine ekleyin.replicate()kullanıyorsanız bu sütunları hariç tutun; üretilen sütuna yazılamaz.
Arama modları
Mod, terimin değerin neresinde aranacağını belirler:
use Acun\Ui\Search\SearchField;
use Acun\Ui\Search\SearchMode;
SearchField::make('number'); // Prefix: değerin başı (varsayılan)
SearchField::make('national_id', SearchMode::Exact); // değerin tamamı
SearchField::make('customer_name', SearchMode::Contains); // değerin herhangi bir yeri
SearchField::make('customer_name', 'fulltext'); // her sözcük, sıra önemsiz
| Mod | Nasıl eşleşir | Ne zaman |
|---|---|---|
Prefix (varsayılan) |
Değer terimle başlar; indeks kullanır | Kod, sipariş no, e-posta: baştan yazılan değerler |
Exact |
Değer terime eşittir; indeks kullanır | T.C. kimlik no, tam kod |
Contains |
Terim değerin herhangi bir yerindedir | Küçük tablolar; adın içindeki bir sözcük |
FullText |
Terimdeki her sözcük değerde geçer; sıra önemsiz | Büyük tablolarda ad gibi alanlar |
Modu adıyla da yazabilirsiniz: 'prefix', 'exact', 'contains', 'fulltext'. Her mod sadeleşmiş değerlerde çalışır.
Her modun ürettiği koşul:
- Prefix:
LIKE 'terim%'; SQLite'taGLOB 'terim*'. İndeks kullanır. - Exact:
= 'terim'. İndeks kullanır. - Contains:
LIKE '%terim%'. İndeks kullanmaz; PostgreSQL'de pg_trgm indeksiyle kullanır. - FullText: MySQL ve MariaDB'de
MATCH … AGAINST('+ayse* +yil*' IN BOOLEAN MODE). Burada her sözcük değerdeki bir sözcüğün başı olmalıdır. Diğer veritabanlarında her sözcük içinLIKE '%…%'kullanılır.
Bilmeniz gerekenler:
Prefix, "ayse yilmaz" değerini "ayse" ile bulur, "yilmaz" ile bulmaz. Ad alanlarında ikinci sözcük de aranmalıysa küçük tablolardaContains, büyük tablolardaFullTextkullanın.FullTextMySQL ve MariaDB'defullText: trueile oluşan FULLTEXT indeksi kullanır. Sonuç, sunucunun tam metin ayarlarına bağlıdır: en kısa sözcük uzunluğu, durdurma sözcükleri. E-postadaki@ve.sözcük ayırıcıdır. Terimdeki+,-,*,"gibi işleç karakterleri de sözcük ayırıcı sayılır.FullTextPostgreSQL'de pg_trgm GIN indeksiyle hızlanır. SQLite'ta aynı koşul indekssiz çalışır; küçük veride yeterlidir.- Alanlar tek bir gruplanmış
where()içindeORile bağlanır. Sorgunun kendi koşulları (kapsamlar, yetki, filtreler) korunur. Gruptaki tek bir indekssiz alan (Contains) bütün aramayı taramaya çevirir; büyük tablolarda EXPLAIN ile kontrol edin. - Terimdeki
%ve_düz metindir; SQLite'ta*,?ve[de öyle. LIKE koşulları açık bir kaçış karakteri (ESCAPE '!') taşır; davranış her veritabanında aynıdır. - Boş ya da yalnızca boşluktan oluşan terim sorguyu değiştirmez.
- Terimin en çok ilk 200 karakteri ve ilk 10 sözcüğü kullanılır (
max_length,max_words).
İndeks notları
Baştan aramanın her veritabanında indeksi nasıl kullandığı:
-
SQLite, GLOB: SQLite'ın
LIKEişleci ASCII'de büyük/küçük harfe duyarsızdır. İndeksi yalnızcaNOCASEsütunlarda ya da bağlantı genelindekicase_sensitive_likeayarıyla kullanır.GLOBbüyük/küçük harfe duyarlıdır ve olağan BINARY indeksi kullanır. Değer zaten sadeleştiği için doğru seçim budur:SEARCH orders USING INDEX orders_number_search_index (number_search>? AND number_search<?). -
MySQL:
utf8mb4_binsütundaLIKE 'terim%'indekste aralık taraması yapar (type: range,key: …_search_index). -
PostgreSQL:
"C"karşılaştırma kurallı sütunda düz bir btree indeksiLIKE 'terim%'için kullanılır (Index Scan using …_search_index);text_pattern_opsgerekmez.ContainsveFullTextiçin pg_trgm uzantısı ve GIN indeksi gerekir:DB::statement('CREATE EXTENSION IF NOT EXISTS pg_trgm'); // önceki bir migration'da Schema::table('customers', fn (Blueprint $table) => $table->searchColumn('name', fullText: true)); // create index "customers_name_search_trigram" on "customers" using gin ("name_search" gin_trgm_ops)Küçük tablolarda sorgu planlayıcısı taramayı daha ucuz bulabilir. Üç harften kısa sözcükler üç harflik parça (trigram) üretmez.
DataTable'da arama
Arama kutusu, ->searchable() ile işaretlenen sütunlarda arar. Arama {alan}_search sütununu okur; column: ile paylaşılan bir sütuna yönlendirebilirsiniz:
use Acun\Ui\DataTable\Column;
use Acun\Ui\Search\SearchField;
use Acun\Ui\Search\SearchMode;
protected function columns(): array
{
return [
Column::make('Sipariş no', 'number')->searchable()->sortable(), // number_search, baştan
Column::make('Müşteri', 'customer_name')->searchable(SearchMode::Contains), // customer_name_search
Column::make('Kişi', 'customer_name')->searchable('contains', column: 'customer_search'),
];
}
Sütun olarak gösterilmeyen bir alanda da aramak için kendi searchFields() metodunuzu yazın. Gizli sütun eklemek gerekmez:
// E-posta, müşteri hücresinin içinde gösteriliyor; arama yine de bulur.
protected function searchFields(): array
{
return [...parent::searchFields(), SearchField::make('customer_email')];
}
Arama kutusu yalnızca aranacak bir alan varsa görünür. SearchField iki biçimde kurulur:
| Çağrı | Okunan sütun |
|---|---|
SearchField::make('email') |
email_search, baştan arama |
SearchField::make('users.email', SearchMode::Prefix) |
users.email_search: birleştirilmiş (join) tablo |
SearchField::make('users.name', SearchMode::Contains, 'ad_arama') |
users.ad_arama |
SearchField::column('customer_search', SearchMode::Contains) |
Birden çok kaynaktan üretilmiş sütun |
Alan listesinde düz bir metin, SearchField::make() ile aynıdır: 'email' yazmak SearchField::make('email') demektir.
Liste sayfasında arama
WithListing bileşeni alanları searchFields() ile verir ve sorguda applySearch() çağırır. applySearch() varsayılan olarak $this->search metnini ve searchFields() değerini kullanır:
use Acun\Ui\Search\SearchField;
use Acun\Ui\Search\SearchMode;
protected function searchFields(): array
{
return [
SearchField::make('name', SearchMode::Contains),
SearchField::make('email'),
];
}
private function query(): Builder
{
return $this->applySearch(Customer::query())
->when($this->status !== '', fn (Builder $q) => $q->where('status', $this->status))
->orderBy($this->sortField, $this->sortDirection);
}
İkinci bir arama kutusu (ör. bir "müşteri" filtresi) terimi ve alanları kendisi verir:
->when($this->customer !== '', fn (Builder $q) => $this->applySearch($q, $this->customer, [SearchField::make('users.email')]))
searchFields() tanımlamadan ve alan vermeden applySearch() çağıran bir bileşen açıklayıcı bir hata verir. Livewire dışında (rapor, controller) aynı arama için: app(SearchManager::class)->driver()->apply($query, $term, $fields). Liste sayfasının tamamı için Liste sayfası kılavuzuna bakın.
Sürücüler
Sürücü, aramanın nerede yapılacağını belirler. DataTable ve WithListing yalnızca Acun\Ui\Search\Contracts\SearchDriver sözleşmesini tanır:
public function apply(Builder $query, string $term, array $fields): Builder;
database(varsayılan): Yukarıda anlatılan arama sütunları.- Kendi sürücünüz: Bir arama motoru (Meilisearch, Typesense, Elasticsearch…) kullanacaksanız
SearchDriverarayüzünü uygulayan bir sınıf yazın. Tablo kodu değişmez.
Bir arama motoru için en kolay yol şudur: motordan eşleşen kayıtların anahtarlarını alın, sonra aynı sorguyu whereKey() ile daraltın. Böylece kapsamlar, filtreler, yetki kuralları, sıralama ve sayfalama geçerli kalır:
namespace App\Search;
use Acun\Ui\Search\Contracts\SearchDriver;
use Illuminate\Database\Eloquent\Builder;
final class ElasticSearchDriver implements SearchDriver
{
public function __construct(private readonly ProductSearchEngine $engine) {}
public function apply(Builder $query, string $term, array $fields): Builder
{
if (trim($term) === '') {
return $query;
}
return $query->whereKey($this->engine->keys($term, limit: 1000));
}
}
Sürücü yazarken:
- Terim yazıldığı gibi gelir; sadeleştirmeyi sürücü yapar.
- Boş ya da yalnızca boşluktan oluşan terimde sorguya dokunmayın.
- Sorgunun kendi koşullarını silmeyin; yalnızca koşul ekleyin.
Sürücüye bir ad verin ve varsayılan yapın. Ad yerine doğrudan sınıf adı da kullanılabilir:
// config/acun-ui.php
'search' => [
'driver' => 'elastic',
'drivers' => ['elastic' => App\Search\ElasticSearchDriver::class],
],
Varsayılan sürücüyü ortam değişkeniyle de seçebilirsiniz: ACUN_UI_SEARCH_DRIVER=elastic. Tek bir tablo ya da liste kendi sürücüsünü seçebilir:
use Acun\Ui\Search\Contracts\SearchDriver;
use Acun\Ui\Search\SearchManager;
protected function searchDriver(): SearchDriver
{
return app(SearchManager::class)->driver('elastic');
}
Geliştirme ortamında denetim
Eksik bir arama sütunu, geliştirirken sessizce boş sonuç vermek yerine ne yapmanız gerektiğini söyleyen bir hata verir:
Acun UI search: the table "orders" has no "customer_email_search" column.
…
Schema::table('orders', function (Blueprint $table) {
$table->searchColumn('customer_email');
});
Yerel (local) ve test (testing) ortamında veritabanı sürücüsü aramadan önce her arama sütununu denetler: sütun var mı ve hâlâ üretilen bir sütun mu. Birleştirilen (join) tablolar ve takma adlar da denetlenir. Denetim her tablonun şemasını istek başına bir kez okur. Üretim ortamında şema hiç okunmaz. acun-ui.search.verify_columns ile açıp kapatabilirsiniz: null yalnızca local ve testing, true ya da false her ortamda.
Yapılandırma
Arama ayarları config/acun-ui.php dosyasının search bölümündedir (bütün anahtarlar: Yapılandırma). Dosyayı php artisan vendor:publish --tag=acun-ui-config ile yayımlayın:
'search' => [
'driver' => env('ACUN_UI_SEARCH_DRIVER', 'database'),
'drivers' => [], // 'ad' => SearchDriver sınıfı
'suffix' => '_search', // searchColumn('email') → email_search
'length' => 255, // varchar uzunluğu (0: text)
'max_length' => 200, // terimin en çok kaç karakteri kullanılır
'max_words' => 10, // terimin en çok kaç sözcüğü kullanılır
'verify_columns' => null, // null: local ve testing
],
Büyük tablo kontrol listesi
Büyük bir tabloda aramayı yayına almadan önce şunları kontrol edin:
- Aranan her alanın bir
searchColumn()sütunu var; baştan ya da tam aranan sütunlar indeksli. - Varsayılan mod
Prefix.Containsyalnızca küçük tablolarda ya da PostgreSQL'de pg_trgm ile kullanılıyor. - Aynı aramada tek bir
Containsalanı bütün grubu taramaya çevirir. Ad gibi alanlarda MySQL'defullText: trueileSearchMode::FullText, PostgreSQL'de pg_trgm kullanılıyor. - EXPLAIN planı indeksi gösteriyor: MySQL
type: rangevekey: …_search_index, tam metindetype: fulltext; PostgreSQLIndex Scanya daBitmap Index Scan; SQLiteSEARCH … USING INDEX. - MySQL'de STORED/FULLTEXT ve PostgreSQL'de STORED sütun ekleyen migration tabloyu yeniden yazar. Yoğun olmayan bir saate ya da çevrimiçi bir şema aracına planlandı; PostgreSQL indeksi
CONCURRENTLYile kuruluyor. - Çok büyük tablolarda toplam sayım da maliyetlidir:
acun-ui.datatable.paginationiçinsimpleya dacursordeğerini düşünün. - Arama sütunları
select *ile geliyor: JSON'a çevrilen modellerde$hidden. - Yazım hatası toleransı ya da alaka sıralaması gerekiyorsa bir arama motoru için kendi sürücünüzü yazın; tablo kodu değişmez.
Daha fazlası
- Liste sayfası:
WithListingile arama, filtre ve sayfalama. - DataTable ve DataTable: tüm seçenekler:
->searchable()ve diğer sütun ayarları. - Dil ve çeviri: Türkçe harf kuralları.