Canlı Arama ve Otomatik Tamamlama
MySQL FULLTEXT boolean mod ile canlı arama: önek eşleşmesi, alaka skoru, kategori facet sayıları, debounce ve yarış koruması, Türkçe harf katlaması ve güvenli vurgulama.
Ekran Görüntüleri 4 görsel
Bu Örnek Ne Yapıyor?
- MySQL FULLTEXT indeksi ile arama; LIKE %kelime% gibi tam tablo taraması yapmaz
- Boolean mod ve önek eşleşmesi: "perfor" yazınca "performans" bulunur
- Türkçe eklemeli bir dil olduğu için önek eşleşmesi "indeksin" ve "indeksleri" biçimlerini de yakalar
- Ham girdi asla AGAINST() içine konmaz; boolean modda artı, eksi ve yıldız karakterleri operatördür
- Üç harften kısa terimlerde bilinçli LIKE geri dönüşü ve modun ekranda gösterilmesi
- Alaka skoru sonuç kartında görünür; sıralama alaka, en yeni ve en eski olarak seçilebilir
- Kategori facet sayıları, kategori filtresi uygulanmadan hesaplanır
- Sonuç bulunamazsa terim düşürerek sonuç veren daha kısa bir sorgu önerilir
- Debounce ile 10 tuş vuruşu tek isteğe iner
- Sıra numarasıyla yarış koruması: yavaş dönen eski yanıt yeni sonucu ezemez
- Debounce yarış durumunu çözmez; ikisi farklı sorunlardır ve ayrı çözüm ister
- Türkçe harf katlaması birebirdir; mb_strtolower İ harfinde iki kod noktası döndürüp vurgulama konumlarını kaydırıyordu
- Vurgulama sırası: ham metinde işaretle, sonra kaçışla, en son mark etiketine çevir
- Otomatik tamamlama yalnızca başlıklarda arar; bunun için ayrı bir FULLTEXT indeksi vardır
- Klavye gezinmesi, URL senkronu, paylaşılabilir derin bağlantı ve mobilde sıfır yatay kaydırma
Nerede İşe Yarar?
- Blog, haber ve dokümantasyon sitelerinde site içi arama
- Yönetim panellerinde kayıt arama ve hızlı erişim kutusu
- E-ticarette ürün arama ve otomatik tamamlama
- Bilgi bankası ve destek merkezi araması
- Elasticsearch kurmadan önce MySQL ile ne kadar yol alınabileceğini ölçmek
- Türkçe metinlerde harf katlaması ve ek sorununu çözen bir arama katmanı
Gereksinimler
- PHP 8.0+ (pdo_mysql, mbstring) · MySQL 5.6+ veya MariaDB 10.0.5+ (InnoDB FULLTEXT) · Apache/mod_rewrite
Nasıl Kurulur?
- MySQL sürümünüzü kontrol edin: InnoDB FULLTEXT için 5.6+ (MariaDB 10.0.5+) gerekir.
- Depoyu indirin ya da ZIP olarak açın.
- Veritabanını kurun: mysql -u root -p < cy_search.sql (dosya veritabanını kendisi oluşturur).
- Projeyi bir web sunucusu altına koyun ya da php -S 127.0.0.1:8000 ile çalıştırın.
- Tarayıcıda açın; 38 yazı ve 6 kategori dolu bir arama ekranı gelir.
- Örnek arama düğmesiyle akışı deneyin, sonra iki harflik bir terim yazıp LIKE moduna düşüşü görün.
- Canlıya alırken system/config.local.php.example dosyasını config.local.php olarak kopyalayıp künyeyi oraya yazın.
Veritabanı şeması projedeki cy_search.sql dosyasında.
Nasıl Çalışıyor?
Canlı Arama ve Otomatik Tamamlama
PHP PDO · MySQL FULLTEXT · AJAX · Bootstrap 5 · Çılgın Yazılım Tasarım Kalıbı
Yazarken arayan, ne bulduğunu ve nasıl bulduğunu söyleyen bir arama kutusu.
🇹🇷 Türkçe · 🇬🇧 English
Canlı Demo
Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.
▲ Görsele tıklayarak demoyu açabilirsinizDemoda 60 saniyede neleri deneyebilirsiniz?
| # | Şunu deneyin | Perde arkasında ne oluyor? |
|---|---|---|
| 1 | ▶ Örnek arama düğmesine basın | performans aratılır: 8 sonuç, 6 kategorinin 5'ine dağılmış. Eşleşen sözcükler hem başlıkta hem özette vurgulanır |
| 2 | Ölçüm satırına bakın: FULLTEXT boolean · 48 ms | Hangi modun çalıştığı ve ne kadar sürdüğü ekranda yazar. Bu satır eğitseldir — arama motorunun kararını görünür kılar |
| 3 | Kutuya perf yazın | Öneri listesi açılır ve yalnızca başlıklarda arar. Gövdeye de bakan bir öneri listesi, başlığında o kelime hiç geçmeyen yazıları önerirdi |
| 4 | ↓ ↓ Enter ile bir öneri seçin | Fare olmadan çalışır. Esc listeyi kapatır, ok tuşları listede döner (sondan sonra başa) |
| 5 | guvenlik yazın — şapkasız, düz harflerle | Yine de güvenlik bulunur ve vurgulanır. Türkçe harf katlaması hem MySQL karşılaştırmasında hem vurgulama kodunda ayrı ayrı çözülmüştür |
| 6 | ci yazın (iki harf) | Mod LIKE'a düşer ve bunu söyler. MySQL'in FULLTEXT eşiği 3 harftir; kısa terimde sessizce sıfır sonuç döndürmek yerine yavaş ama doğru yolu seçiyoruz |
| 7 | mysql kuyruk react aratın | Sonuç yok — ama ekran boş kalmaz: sunucu terim düşürerek sonuç veren daha kısa bir sorgu önerir ve kaç sonuç vereceğini söyler |
| 8 | Bir kategori çipine tıklayın | Facet sayıları filtre uygulanmadan hesaplanır; "Web 2"ye tıkladığınızda diğer kategoriler 0'a düşmez, geri dönebilirsiniz |
| 9 | Adres çubuğuna bakın | ?q=performans&sayfa=2 yazar. Bağlantıyı gönderdiğinizde karşı taraf aynı sonuçları görür. Bir sonuca tıklayınca #makale-7 eklenir |
| 10 | Telefonunuzdan açın | Arama kutusu küçültülmez — dokunmatikte asıl hedef odur. Skor rozeti gizlenir, yatay kaydırma yoktur |
İpucu: Demoyu açıkken F12 → Network sekmesini açın. Yazarkenautocompletevesearchisteklerinin nasıl debounce edildiğini, her isteğe verilenseqnumarasını ve HTTP durum kodlarını (200 / 403 / 405 / 429) canlı görebilirsiniz.
Demo alanı hakkında bilinmesi gerekenler
| Konu | Durum |
|---|---|
| Veriler | cy_search.sql içindeki 38 yazı, 6 kategori. Metinler bilinçli olarak birbirine değer: "performans" beş kategoride geçer, böylece facet sayaçları anlamlı olur. |
| Sıfırlama | Demo veritabanı düzenli aralıklarla başlangıç haline döner. |
| MySQL sürümü | FULLTEXT indeksi InnoDB'de MySQL 5.6+ / MariaDB 10.0.5+ ile gelir. |
| Arama kaydı | Tutulmuyor. Önerilen aramalar bir geçmiş tablosundan değil, yazıların etiketlerinden üretilir. |
APP_DEBUG | Canlıda otomatik false — sunucu adından türetilir, yerelde true kalır. |
| Bağımlılık | Sıfır. Composer yok, npm yok, CDN yok, arama kütüphanesi yok — ne Elasticsearch ne de bir JS autocomplete paketi. |
Demo geçici olarak kapalıysa endişelenmeyin: depoyu klonlayıp cy_search.sql'i içe aktarmanız aynı ekranı kendi bilgisayarınızda 2 dakikada ayağa kaldırır → Kurulum
Bu Proje Nedir?
Arama kutusu, bir panelin en çok kullanılan ve en az düşünülen parçasıdır. Tipik uygulaması tek satırdır:
SELECT * FROM articles WHERE title LIKE '%$q%'Bu satırın dört ayrı sorunu vardır ve hiçbiri hemen görünmez:
- İndeks kullanamaz. Baştaki
%yüzünden MySQL her satırı okur. Bin satırda fark edilmez, yüz binde sayfa donar. - Sıralama yoktur. Hangi sonucun daha alakalı olduğu bilinmez; tablo sırası neyse o gelir.
- Her tuş vuruşunda çalışır. "performans" yazmak 10 istek demektir.
- Yanıtlar sırasız döner.
peristeği yavaş,performansisteği hızlı dönerse ekranda yanlış sonuç kalır — ve bu sorunu debounce çözmez.
Bu proje dördünü de ele alıyor: FULLTEXT indeksi ile alaka skoruna göre arıyor, boolean mod önek eşleşmesiyle kullanıcı henüz yazarken sonuç veriyor, debounce ile istek sayısını düşürüyor ve sıra numarasıyla yarış durumunu kapatıyor.
Bir de beşinci bir şey yapıyor: ne yaptığını söylüyor. Hangi modun çalıştığı (FULLTEXT mi LIKE mı), kaç sonuç bulunduğu ve sorgunun kaç milisaniye sürdüğü ekranda yazar. Bu satır gerçek bir projede kaldırılabilir — ama bir örneğin işi, kararlarını görünür kılmaktır.
Kimler için uygun?
- Kendi paneline arama kutusu ekleyecek ve
LIKE '%...%'ile başlamak istemeyenler - Elasticsearch kurmadan önce MySQL'in ne kadarını yapabileceğini merak edenler
- Türkçe metinde arama yapan herkes (harf katlaması, ekler, önek eşleşmesi)
- Otomatik tamamlamayı kütüphanesiz ve klavye erişilebilir yazmak isteyenler
- Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir tasarım kalıbı arayanlar
Klonla, cy_search.sql'i içe aktar, çalıştır. Başka hiçbir kurulum adımı yok. Composer yok, npm yok, internet bağlantısı bile gerekmiyor — tüm kütüphaneler proje içinde.
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
İçindekiler
- Canlı Demo
- Ekran Görüntüleri
- Beş Kritik Karar
- Neler Var?
- Güvenlik: Neyi, Nasıl Kapattık?
- Kurulum
- Yapılandırma
- Kendi Projenize Eklemek
- Çılgın Yazılım Tasarım Kalıbı
- Dosya Yapısı
- Nasıl Çalışıyor?
- AJAX API Referansı
- Veritabanı Şeması
- Sık Sorulanlar
- Canlı Ortama Alırken
- Sorun Giderme
- Yol Haritası
- Katkı
- Lisans
Ekran Görüntüleri
Canlı arama
Vurgulanmış sonuçlar, alaka skoru, kategori facet'leri ve ölçüm satırı. Sayaç şeridi hem veri kümesini (38 yazı, 6 kategori) hem o anki aramayı (8 sonuç, 48 ms, FULLTEXT boolean) gösterir.

Otomatik tamamlama ve klavye
Öneri listesi yalnızca başlıklarda arar ve eşleşen kısmı vurgular. Klavyeyle seçili öğe, fare ile üzerine gelinen öğeyle aynı görünür — kullanıcı girdi yöntemini değiştirdiğinde arayüzün dili değişmemelidir.

Yazı detayı
Sonuç kartı bir özet gösterir; tıklayınca yazının tamamı açılır ve arama sözcükleri burada da vurgulanır. Adres çubuğu #makale-7 olur; bağlantı paylaşılabilir. Etiketlere tıklamak yeni bir arama başlatır.

Mobil görünüm
Arama kutusu küçültülmez — dokunmatik ekranda asıl hedef odur. Skor rozeti gizlenir, facet çipleri sarar, yatay kaydırma yoktur.
Beş Kritik Karar
1) LIKE değil FULLTEXT — ve neden boolean mod
LIKE '%kelime%' indeks kullanamaz: baştaki joker yüzünden MySQL tabloyu baştan sona okur. FULLTEXT indeksi ise kelimeleri önceden ayrıştırıp saklar ve alaka skoru üretir.
FULLTEXT'in iki modu vardır ve bu proje boolean modu seçer:
| Mod | Sözdizimi | Ne yapar |
|---|---|---|
| Natural language | AGAINST('yapay zeka') | Kelimeleri VEYA'lar, skora göre sıralar |
| Boolean | AGAINST('+yapay* +zeka*' IN BOOLEAN MODE) | Operatör ve önek eşleşmesi destekler |
Boolean modun seçilme sebebi tek bir kelimede özetlenir: *****. Canlı aramada kullanıcı henüz yazmaktadır; perfor yazdığında performans bulunmalıdır. Önek eşleşmesi yalnızca boolean modda vardır.
Türkçe için ikinci bir sebep daha var: dil eklemeli olduğu için indeks, indeksin, indeksleri üç ayrı sözcüktür ve natural language modu bunları birbirine bağlamaz. indeks* üçünü de yakalar — kök bulmaya (stemming) yaklaşan ücretsiz bir yaklaşıklık.
2) Ham girdi asla AGAINST() içine konmaz
Boolean modda + - * " ( ) ~ < > @ karakterlerinin hepsi operatördür. Kullanıcının yazdığını olduğu gibi geçirmek iki farklı hataya yol açar:
C++araması sözdizimi hatası verir-güvenlikyazan kullanıcı, farkında olmadan o kelimeyi dışlar ve neden sonuç bulamadığını anlayamaz
Çözüm, harf ve rakam dışındaki her şeyi atmak; operatörleri biz eklemek:
function boolean_query(string $raw): string
{
$clean = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $raw) ?? '';
$words = preg_split('/\s+/u', trim($clean), -1, PREG_SPLIT_NO_EMPTY) ?: [];
$terms = [];
foreach ($words as $w) {
if (mb_strlen($w) < 2) { continue; }
$terms[] = '+' . $w . '*'; // +kelime* → zorunlu + önek
}
return implode(' ', $terms);
}
3) Debounce yarış durumunu ÇÖZMEZ
Canlı aramanın iki ayrı sorunu vardır ve çoğu örnek yalnızca birincisini çözer.
Sorun 1 — çok fazla istek. Çözüm debounce: yazma durduktan 200 ms sonra tek istek.
Sorun 2 — yanıtların sırasız dönmesi. Debounce bunu azaltır ama kapatmaz. Ağ gecikmesi eşit değildir:
gönderilen: 1(per) ────────────────────→ yavaş
2(perf) ──────→ hızlı
3(performans) ─→ hızlı
dönen: 2 ✔ → 3 ✔ → 1 ✗ (1 < 3 olduğu için ATILIR)Bu koruma olmasaydı ekranda
per sonuçları kalırdı — kullanıcı doğru yazmıştır ama yanlış sonucu görür. Çözüm, her isteğe artan bir numara vermek ve sunucunun onu aynen geri döndürmesi:
if (res.seq < renderedSeq) { return; } // eski yanıt — at
renderedSeq = res.seq;Kararın istemcide verilmesi zorunludur: "en son hangi isteği gönderdim" bilgisi yalnızca orada vardır.
4) Türkçe harf katlaması iki ayrı yerde çözülür
IŞIK araması ışık kelimesini bulmalı ve vurgulamalıdır. Bunlar iki farklı sorundur:
- Bulmak MySQL'in işidir ve
utf8mb4_unicode_cikarşılaştırması bunu hallederi - Vurgulamak PHP'nin işidir ve orada iki tuzak vardır
Birincisi bilinen tuzak: mb_strtolower('I') → i verir, ama Türkçede Inın küçüğü ıdır. İkincisi daha sinsi ve arayanı uzun süre uğraştırır:
mb_strlen('İ') // 1
mb_strlen(mb_strtolower('İ')) // 2 ← i + birleştirici nokta!Vurgulama konumları karakter indisine dayandığı için, katlanmış metnin uzunluğu orijinalden farklı olduğunda bütün işaretler kayar. Bu yüzden kendi eşlememizi yazıyoruz — ve tablo birebirdir:
function tr_fold(string $s): string
{
$map = ['İ'=>'i','I'=>'i','ı'=>'i','Ş'=>'s','ş'=>'s','Ğ'=>'g','ğ'=>'g',
'Ü'=>'u','ü'=>'u','Ö'=>'o','ö'=>'o','Ç'=>'c','ç'=>'c'];
return mb_strtolower(strtr($s, $map), 'UTF-8');
}
5) Vurgulama sırası: önce işaretle, sonra kaçışla
İlk akla gelen yol çalışıyor gibi görünür ama HTML varlıklarını bozar:
// YANLIŞ: önce kaçışla, sonra <mark> ekle
$safe = htmlspecialchars($text);
$safe = str_replace($word, '<mark>' . $word . '</mark>', $safe);Metinde
& geçtiğinde kaçışlanmış hâli & olur. Kullanıcı amp aratırsa vurgulama tam o varlığın ortasına girer:
& → &<mark>amp</mark>; ← bozuk HTML, ekranda ham "&"Doğru sıra üç adımdır:
- Ham metinde eşleşmeleri bul, yerlerine
\x00 … \x01gibi metinde asla bulunmayacak ayraçlar koy - Metnin tamamını kaçışla (ayraçlar da güvenle taşınır)
- Ayraçları `
ve` ile değiştir
$out = str_replace(["\x00", "\x01"], ['<mark>', '</mark>'], e($prefix . $out . $suffix));Bu, vurgulama özelliğinin XSS'e dönüşmeden çalışmasını sağlayan şeydir: çıktıdaki tek HTML etiketi bizim koyduğumuzdur.
Neler Var?
Arama çekirdeği
- MySQL FULLTEXT indeksi, boolean mod, önek eşleşmesi
- Ham girdi güvenli bir boolean ifadeye çevrilir (operatör enjeksiyonu yok)
- 3 harften kısa terimlerde bilinçli LIKE geri dönüşü — ve bunu söyler
- Alaka skoru ekranda; sıralama alaka / en yeni / en eski (beyaz liste)
- Kategori facet sayıları, filtre uygulanmadan hesaplanır
- Sonuç yoksa terim düşürerek sonuç veren bir sorgu önerir
- Türkçe harf katlaması (
tr_fold) — birebir, indis kaydırmaz - Özet penceresi ilk eşleşmenin çevresinden kesilir
- Öneri listesi yalnızca başlıklarda arar (ayrı FULLTEXT indeksi)
Arayüz ve altyapı
- Debounce (200 ms) + sıra numarasıyla yarış koruması
- Klavye gezinmesi: ↑ ↓ Enter Esc, döngüsel
role="combobox"+aria-activedescendantile ekran okuyucu desteği- Vurgulama XSS'e dönüşmeden: işaretle → kaçışla → etikete çevir
- URL senkronu:
?q=...&kategori=...&sirala=...&sayfa=... - Derin bağlantı
#makale-7, paylaşılabilir yazı detayı - Önerilen aramalar, arama kaydı tutmadan etiketlerden üretilir
- Sayaç şeridi + ölçüm satırı (mod ve süre)
- İki ayrı hız sınırı kovası (autocomplete / search)
- Mobil: 767 / 480px eşikleri, yatay kaydırma yok
Güvenlik: Neyi, Nasıl Kapattık?
| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| SQL Injection | "... LIKE '%".$_POST['q']."%'" | Tüm sorgular prepared statement, EMULATE_PREPARES = false. Sıralama SQL'e metin olarak girdiği için SORT_WHITELIST beyaz listesinden geçer. |
| FULLTEXT operatör enjeksiyonu | AGAINST($q IN BOOLEAN MODE) | Ham girdide + - " ( ) ~ operatördür. boolean_query() harf/rakam dışını atar; operatörleri yalnızca kod ekler. |
| XSS (vurgulanan metinde) | $('#r').html(row.title) | Vurgulama ham metinde ayraçla yapılır, sonra tamamı kaçışlanır, en son ayraçlar `'a çevrilir. Çıktıdaki tek etiket bizim koyduğumuzdur. |
| CSRF | (genelde hiç yok)* | Oturuma bağlı 32 baytlık token, her istekte, hash_equals() ile sabit zamanlı doğrulama. Token'sız istek → HTTP 403. |
| LIKE joker karakterleri | LIKE '%$q%' | %, _, \ kaçışlanır — % arayan kullanıcı tüm tabloyu değil, % içeren satırları bulur. |
| Kaynak tüketimi (DoS) | Her tuş vuruşunda sorgu | MIN_QUERY_LEN, SEARCH_PAGE_SIZE, AUTOCOMPLETE_LIMIT ve iki ayrı hız sınırı kovası. Otomatik tamamlama sık ama ucuz, arama seyrek ama pahalıdır; tek kova ikisine de haksızlık ederdi. |
| Bozuk kodlamanın yanıtı yutması | json_encode() → false → boş gövde | JSON_INVALID_UTF8_SUBSTITUTE: tek bozuk bayt yüzünden "sonuç yok" denmez. |
| Bilgi sızıntısı | Ekrana basılan MySQL hataları | APP_DEBUG sunucu adından türetilir; canlıda otomatik false, detay error_log()'a gider. |
| Kurulum dosyasının indirilmesi | /cy_search.sql → HTTP 200 | .htaccess: .sql, .md, .json, .log` … kapalı (README dosyaları bilinçli istisnadır). |
Kurulum
Sadece görmek istiyorsanız kurulum gerekmez → Canlı Demoyu açın. Aşağıdaki adımlar projeyi kendi bilgisayarınızda çalıştırmak içindir (~2 dakika).
Gereksinimler
- PHP 8.0+ (
pdo_mysqlvembstringeklentileri) - MySQL 5.6+ veya MariaDB 10.0.5+ — InnoDB FULLTEXT indeksi için
- Apache (XAMPP / WAMP / Laragon) — ya da PHP'nin yerleşik sunucusu
Adımlar
1 — Projeyi indirin
git clone https://github.com/CilginYazilim/search-autocomplete.git
cd search-autocomplete2 — Veritabanını oluşturun
cy_search.sql veritabanını da kendisi oluşturur; önceden cy_search adında bir veritabanı açmanıza gerek yok.
mysql -u root -p < cy_search.sql
İsteğe bağlı — kendi veritabanı bilgileriniz:
cp .env.example .env(Windows:copy .env.example .env) deyipDB_*
satırlarını doldurun. Bu dosya olmadan da çalışır; varsayılanlar yerel bir
XAMPP kurulumuna (root, boş parola) göredir..env.gitignore
içindedir — parolanız depoya gitmez.
phpMyAdmin ile: İçe Aktar → Dosya seç → cy_search.sql → Başlat
3 — Çalıştırın
php -S 127.0.0.1:8000XAMPP kullanıyorsanız projeyi
htdocs altına koyup şu adresi açın:http://localhost/search-autocomplete/
4 — Tarayıcıda açın → http://127.0.0.1:8000/
Karşınıza 38 yazı dolu, çalışır durumda bir arama ekranı gelecek. ▶ Örnek arama düğmesine basarak hemen deneyebilirsiniz.
Ortam değişkenleri
Depo kökündeki .env dosyasına yazın; system/config.php dosyasına
hiç dokunmayın:
cp .env.example .env # Windows: copy .env.example .env.env .gitignore içindedir: depoya gönderilmez ve dağıtım (deploy) onusilmez.
system/config.php ise depoda durur ve her dağıtımda depodakisürümle değiştirilir — parolayı oraya yazarsanız hem GitHub'a gider hem de
ilk deploy'da kaybolur.
Dosyayı hiç oluşturmasanız da uygulama çalışır; aşağıdaki varsayılanlar
yerel bir XAMPP kurulumuna göredir.
Değer arama sırası: .env → sunucunun gerçek ortam değişkeni
(Apache SetEnv, systemd…) → buradaki varsayılan.
| Değişken | Varsayılan | Ne işe yarar |
|---|---|---|
DB_HOST | 127.0.0.1 | Veritabanı sunucusu |
DB_NAME | cy_search | Veritabanı adı |
DB_USER | root | Kullanıcı |
DB_PASS | (boş) | Şifre — koda yazmayın |
APP_TIMEZONE | Europe/Istanbul | PHP'nin saat dilimi |
APP_DEBUG | ortamdan | Hataların ekrana basılıp basılmayacağı |
APP_TIMEZONE neden var? XAMPP'ın php.ini dosyasındaki
date.timezone, MySQL'in kullandığı sistem diliminden farklı olabilir.
Test makinesinde PHP Europe/Berlin, MySQL Europe/Istanbul
kullanıyordu; aynı anı anlatan iki satır bir saat farklı görünüyordu.
Zaman hesapları SQL tarafında yapıldığı için doğruydu, ama ekrana
basılan saat kayıyordu. Artık dilim açıkça sabitleniyor — sunucunuz başka
bir bölgedeyse bu değişkeni tanımlamanız yeterli, koda dokunmayın.
Yapılandırma
Tüm ayarlar system/config.php içindedir.
| Sabit | Varsayılan | Ne işe yarar |
|---|---|---|
MIN_QUERY_LEN | 2 | Bu uzunluğun altında arama yapılmaz (istemciye de taşınır) |
SEARCH_PAGE_SIZE | 6 | Sonuç sayfası boyutu |
AUTOCOMPLETE_LIMIT | 7 | Açılır öneri sayısı |
SNIPPET_LEN | 240 | Sonuç kartındaki özet uzunluğu (karakter) |
SORT_WHITELIST | alaka, yeni, eski | İzin verilen sıralamalar (beyaz liste) |
RATE_LIMIT_AUTOCOMPLETE | [180, 60] | Öneri isteği sınırı (sık ama ucuz) |
RATE_LIMIT_SEARCH | [90, 60] | Arama isteği sınırı (seyrek ama pahalı) |
MIN_QUERY_LEN iki yerde tutulmaz
Sunucudaki sabit index.php tarafından istemciye taşınır:
window.CY_MIN_LEN = <?= (int) MIN_QUERY_LEN ?>;Elle kopyalanmış bir sayı olsaydı, biri değişip diğeri unutulduğunda kullanıcı sunucunun "çok kısa" dediği bir sorgu için boş bir liste görür ve nedenini anlamazdı.
FULLTEXT sözcük eşiğini değiştirmek
InnoDB varsayılan olarak 3 harften kısa sözcükleri indekslemez. Bunu düşürmek MySQL yapılandırmasında yapılır ve indeksin yeniden oluşturulmasını gerektirir:
[mysqld]
innodb_ft_min_token_size = ••••••••-- MySQL yeniden başlatıldıktan sonra:
ALTER TABLE articles DROP INDEX ft_articles_title_body;
ALTER TABLE articles ADD FULLTEXT ft_articles_title_body (title, body);Bu proje eşiği değiştirmez; bunun yerine kısa terimlerde LIKE'a düşer ve hangi modda çalıştığını söyler. Sebebi basit: paylaşılan bir sunucuda MySQL yapılandırmasına erişiminiz olmayabilir.
Şifreyi koda yazmayın
Canlı veritabanı künyesi config.php'ye yazılmaz: o dosya depoda durur ve her dağıtımda depodaki sürümle değiştirilir. Bunun yerine yanına config.local.php koyun (.gitignore içindedir):
cp system/config.local.php.example system/config.local.php
# sonra dört satırı doldurunÖncelik sırası:
config.local.php → .env → ortam değişkeni → yerel varsayılan.
Kendi Projenize Eklemek
Arama katmanını başka bir tabloya (ürün, belge, kişi) taşımak için:
1 — FULLTEXT indeksini ekleyin
MATCH() içindeki sütun listesi bir indeksin sütun listesiyle birebir eşleşmelidir:
ALTER TABLE products
ADD FULLTEXT ft_products_name_desc (name, description),
ADD FULLTEXT ft_products_name (name); -- öneri listesi içinİkinci indeks olmadan
MATCH(name) yazamazsınız ve MySQL şunu der: Can't find FULLTEXT index matching the column list.
2 — Sorgulardaki tablo ve sütun adlarını değiştirin
handle_search() ve handle_autocomplete() içindeki üç sorgu. Yer tutucu adlandırma düzenini (:q_facet, :q_cnt, :q_sel, :q_whr) bozmayın — sebebi aşağıdaki tuzakta.
3 — Facet sütununu seçin
Bu projede category. Sizde brand, status ya da department olabilir. Facet sorgusuna kategori filtresi uygulanmaz; bu bilinçlidir.
4 — Vurgulanacak alanları belirleyin
highlight() alan bağımsızdır; hangi metni verirseniz onu işaretler. Detayda null uzunluk vererek tam metni alabilirsiniz.
Atlamayın:boolean_query()fonksiyonunu kopyalamadanAGAINST()kullanmayın. Ham girdiyi boolean moda vermek, kullanıcının yazdığı bir tire işaretinin aramayı sessizce bozmasına yeter.
Çılgın Yazılım Tasarım Kalıbı
assets/css/cilginyazilim.css dosyası bu projeye değil markaya aittir. Diğer örnek projelerde de aynı görsel dili kullanabilmek için ayrı bir dosya olarak tutulur; bu örneğe özgü her şey (arama kutusu, öneri listesi, sonuç kartları, sayfalama) assets/css/style.css içindedir.
Hazır bileşenler
| Sınıf | Ne işe yarar | ||
|---|---|---|---|
.cy-card / .cy-card__header / __body / __footer | Gradyan başlıklı ana kart | ||
.cy-brand / .cy-brand__mark / __title / __subtitle | Logo kutusu + başlık bloğu | ||
.cy-btn + --primary \ | --onbrand \ | --glass | Marka butonları |
.cy-badge + --glass \ | --soft | Rozetler | |
.cy-modal | Gradyan başlıklı modal | ||
.cy-toast + --success \ | --danger \ | --info | Bildirim balonları |
.cy-topbar / .cy-footer-note | Üst şerit ve alt bilgi bloğu |
Bu örneğe özgü (style.css): .cy-searchbox / __icon / __clear, .cy-ac / __cat, .cy-chip, .cy-meta / __mode / __time, .cy-result / __title / __score / __snippet, .cy-pager / .cy-page, .cy-empty-state, .cy-stats / .cy-stat, mark.
Renkleri değiştirmek
Tüm bileşenler CSS değişkenlerinden beslenir. Tek yeri değiştirmek yeter:
:root {
--cy-brand-900: #061321; /* Logodaki en koyu lacivert */
--cy-brand-600: #0b5cb5; /* Ana marka mavisi */
--cy-accent: #0ea5e9; /* Vurgu rengi */
--cy-gradient: linear-gradient(135deg, #061321, #0b5cb5 45%, #0284c7);
}
Koyu tema
Kullanıcının işletim sistemi koyu temadaysa otomatik devreye girer. Zorlamak isterseniz:
<html data-cy-theme="dark"> <!-- veya "light" -->
Dosya Yapısı
search-autocomplete/
├── index.php → Arayüz. Veritabanına DOKUNMAZ; tek kart + bir modal.
├── .env.example → Veritabanı bilgileri (isteğe bağlı) — .gitignore içinde
├── cy_search.sql → Şema + 38 yazı (NOW() - INTERVAL) + iki FULLTEXT indeksi
├── .htaccess → Dizin listeleme kapalı, .sql/.md kapalı (README istisna)
│
├── system/
│ ├── config.php → Ayarlar, PDO bağlantısı, APP_DEBUG türetimi
│ ├── config.local.php → (siz oluşturursunuz) canlı künye — .gitignore'da
│ ├── config.local.php.example
│ ├── function.php → CSRF, hız sınırı, Türkçe katlama, boolean sorgu, vurgulama
│ ├── ajax.php → 4 uç nokta
│ └── .htaccess → BEYAZ LİSTE: yalnızca ajax.php dışarıya açık
│
├── assets/
│ ├── css/
│ │ ├── cilginyazilim.css → MARKA KALIBI (projeler arası ortak — dokunmayın)
│ │ ├── style.css → Yalnızca bu örneğe özgü stiller
│ │ └── bootstrap.min.css
│ ├── js/
│ │ ├── search.js → Debounce, yarış koruması, klavye, URL senkronu (8 bölüm)
│ │ ├── jquery-3.7.0.js
│ │ └── bootstrap.bundle.js
│ └── images/logo.png
│
├── docs/screenshots/ → README'de kullanılan görseller
├── CHANGELOG.md
├── README.md · README.en.md
└── LICENSE → MIT
Nasıl Çalışıyor?
KULLANICI YAZIYOR: p → e → r → f → o → r
│
▼
┌─ DEBOUNCE (200 ms) ──────────────────────────────────────────────────┐
│ clearTimeout(timer) → setTimeout(…, 200) │
│ 10 tuş vuruşu → 1 istek │
└──────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─ autocomplete ───────────────┐ ┌─ search ─────────────────────────────┐
│ MATCH(title) AGAINST(…) │ │ 1) facet: GROUP BY category │
│ ft_articles_title indeksi │ │ (kategori filtresi UYGULANMAZ) │
│ yalnızca BAŞLIKLAR │ │ 2) count: filtre UYGULANMIŞ toplam │
│ LIMIT 7 │ │ 3) page: skor + LIMIT off, len │
│ │ │ 4) 0 sonuç → terim düşürme önerisi │
│ ↳ seq geri döner │ │ ↳ seq, mod, took_ms geri döner │
└──────────────────────────────┘ └──────────────────────────────────────┘
│ │
▼ ▼
┌─ YARIŞ KORUMASI ─────────────────────────────────────────────────────┐
│ if (res.seq < renderedSeq) return; ← eski yanıt ATILIR │
│ renderedSeq = res.seq; │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌─ ÇİZİM ──────────────────────────────────────────────────────────────┐
│ · vurgulanmış başlık ve özet (.html() — sunucuda kaçışlanmış) │
│ · kategori ve tarih (esc() — ham veri) │
│ · facet çipleri, sayfalama, ölçüm satırı │
│ · history.replaceState → ?q=…&kategori=…&sayfa=… │
└──────────────────────────────────────────────────────────────────────┘
Sorumluluk dağılımı
| Katman | Dosya | Sorumluluk |
|---|---|---|
| Sunum | index.php | Yalnızca HTML iskeleti. Veri yok, sorgu yok. |
| Arayüz mantığı | assets/js/search.js | Debounce, yarış koruması, klavye, URL, çizim |
| Uç noktalar | system/ajax.php | İstek doğrulama, sorgular, facet, öneri zinciri |
| Alan mantığı | system/function.php | Türkçe katlama, boolean sorgu, vurgulama |
| Ayar | system/config.php | Sabitler, PDO, APP_DEBUG türetimi |
AJAX API Referansı
Tüm istekler system/ajax.php adresine POST ile gider ve CSRF token taşır.
search — sonuç listesi + facet
İstek: q, seq, page, category, sort
{
"success": true,
"seq": 7,
"q": "performans",
"mode": "FULLTEXT boolean",
"sort": "alaka",
"total": 8,
"all_total": 8,
"page": 1,
"pages": 2,
"facets": [{"category": "Veritabanı", "count": 3}, {"category": "Web", "count": 2}],
"results": [
{"id": 37, "title": "Mobil <mark>performans</mark>: ilk boyama süresi",
"snippet": "… <mark>Performans</mark> bir özellik değil…",
"category": "Mobil", "published": "31.07.2026", "score": 0.92}
],
"did_you_mean": null,
"took_ms": 48.1
}all_total kategori filtresi uygulanmadan önceki toplamdır; "Tümü" çipinde o kullanılır. total ise filtre uygulanmış hâlidir. İkisini karıştırmak, bir kategoriye tıklandığında "Tümü (3)" gibi yanlış bir sayı gösterirdi.
Sonuç yoksa did_you_mean dolu gelir:
{"total": 0, "did_you_mean": {"q": "mysql", "total": 2}}
autocomplete — öneri listesi
İstek: q, seq
{
"success": true, "seq": 12, "q": "perf",
"suggestions": [
{"id": 37, "title": "Mobil performans: ilk boyama süresi",
"category": "Mobil",
"html": "Mobil <mark>perf</mark>ormans: ilk boyama süresi"}
]
}title ham metindir (kutuya yazılır), html vurgulanmış hâldir (listede gösterilir). İki ayrı alan olmasının sebebi budur: biri .val(), diğeri .html() ile kullanılır.
detail — tek yazı
İstek: id, q (vurgulama için)
{
"success": true,
"article": {
"id": 7, "title": "SQL enjeksiyonu ve prepared statement",
"body": "Kullanıcı verisi asla … en sık görülen <mark>güvenlik</mark> açığıdır.",
"category": "Güvenlik", "tags": ["sql", "injection", "pdo", "guvenlik"],
"published": "28.08.2026", "words": 34
}
}Gövde kırpılmaz:
highlight() fonksiyonuna null uzunluk verilir, yani pencere yoktur.
stats — sayaçlar ve önerilen aramalar
{"success": true, "total": 38, "categories": 6, "newest": "28.08.2026",
"min_len": 2, "suggested": ["performans", "guvenlik", "llm", "mysql", "arama"]}suggested bir arama geçmişi tablosundan değil, yazıların etiketlerinden üretilir; en sık geçen sekiz etiket döner.
Çok kısa sorgu bir hata değildir
q uzunluğu MIN_QUERY_LEN altındaysa sunucu 200 ile ve too_short: true ile döner. 400 dönmez — kullanıcı henüz yazmaktadır ve tarayıcı konsolunu kırmızı hatalarla doldurmanın bir anlamı yoktur.
HTTP durum kodları
| Kod | Anlamı |
|---|---|
200 | İşlem başarılı (too_short: true de 200'dür) |
400 | Geçersiz parametre ya da bilinmeyen işlem |
403 | CSRF token geçersiz veya oturum düşmüş |
404 | Yazı bulunamadı |
405 | POST dışı istek |
429 | Hız sınırı aşıldı (retry_after saniye döner) |
500 | Sunucu / veritabanı hatası (FULLTEXT indeksi eksik olabilir) |
Veritabanı Şeması
CREATE TABLE `articles` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(200) NOT NULL,
`body` TEXT NOT NULL,
`category` VARCHAR(60) NOT NULL,
`tags` VARCHAR(255) NOT NULL DEFAULT '',
`published_at` DATE NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_articles_category` (`category`),
KEY `idx_articles_published` (`published_at`),
FULLTEXT KEY `ft_articles_title_body` (`title`, `body`),
FULLTEXT KEY `ft_articles_title` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| Karar | Neden |
|---|---|
İki sütunlu TEK indeks (title, body) | MATCH() içindeki sütun listesi, bir FULLTEXT indeksinin sütun listesiyle birebir eşleşmelidir. Ayrı ayrı tanımlanmış iki indeks MATCH(title, body) sorgusuna hizmet edemez |
İkinci indeks (title) | Öneri listesi yalnızca başlıklarda arar ve bunun için MATCH(title) yazması gerekir. Gövdeye de bakan bir öneri listesi, perf yazan kullanıcıya başlığında o kelime hiç geçmeyen yazıları öneriyordu |
tags indekslenmez | Etiketler aranmaz, yalnızca öneri çipi üretmek için okunur. Etiketle arama istenseydi tags de MATCH listesine girer ve indeks yeniden oluşturulurdu |
idx_articles_published | "En yeni / en eski" sıralamasının dayandığı indeks |
idx_articles_category | Kategori filtresi ve facet GROUP BY'ı bu indeksi kullanır |
utf8mb4_unicode_ci | FULLTEXT eşleşmesi karşılaştırma kuralına göre çalışır; ü/u ve İ/i katlaması buradan gelir |
| InnoDB | FULLTEXT desteği InnoDB'ye MySQL 5.6 ile geldi; öncesinde yalnızca MyISAM'da vardı |
Sık Sorulanlar
Ne zaman Elasticsearch'e geçmeliyim?
MySQL FULLTEXT şunları yapmaz: kök bulma (stemming), eş anlamlı sözlüğü, yazım denetimi ("bunu mu demek istediniz?"), coğrafi arama, çok dilli analiz zincirleri ve gerçek anlamda ölçeklenen dağıtık indeks.
Bunlara ihtiyacınız yoksa —ki çoğu panel aramasında yoktur— MySQL yeter ve ayrı bir servis işletmek zorunda kalmazsınız. Yüz binlerce belgede ve kelime bazlı aramada FULLTEXT hâlâ makul çalışır.
Geçiş sinyalleri: eş anlamlıya ihtiyaç duymaya başladığınızda, sorgu süreleri kabul edilemez olduğunda, ya da aramanın kendisi ürünün ana özelliği hâline geldiğinde.
Neden natural language modu değil boolean mod?
Tek sebep yeter: önek eşleşmesi. Canlı aramada kullanıcı henüz yazmaktadır; perfor yazdığında performans bulunmalıdır. * operatörü yalnızca boolean modda vardır.
Türkçe için ikinci bir sebep daha var: dil eklemeli olduğu için indeks, indeksin, indeksleri üç ayrı sözcüktür. indeks* üçünü de yakalar; natural language modu yakalamaz.
Bedeli: boolean mod varsayılan olarak alaka skorunu natural language kadar iyi hesaplamaz ve %50 eşiği gibi kolaylıkları yoktur. Bu örnek için doğru ödünleşim.
"ci" gibi kısa terimler neden LIKE'a düşüyor?
InnoDB'de innodb_ft_min_token_size varsayılan olarak 3'tür: daha kısa sözcükler indekse hiç girmez. ci araması boolean modda sessizce sıfır sonuç döndürür — hata yok, sadece boşluk. Kullanıcı için bu, aramanın bozuk olduğu anlamına gelir.
Bu yüzden sorgudaki en uzun sözcük 3 harften kısaysa LIKE'a düşüyoruz. LIKE indeks kullanamaz ve yavaştır; ama kısa bir terimde doğru cevap vermek, hızlı biçimde hiçbir şey dönmemekten iyidir. Hangi modun çalıştığı ekranda da yazar.
Eşiği düşürmek mümkündür ama MySQL yapılandırmasına erişim ve indeksin yeniden oluşturulmasını gerektirir (bkz. Yapılandırma).
Debounce varken yarış koruması gerçekten gerekli mi?
Evet, çünkü ikisi farklı sorunları çözer. Debounce istek sayısını azaltır; ağ gecikmesinin eşit olmasını sağlamaz.
200 ms sonra perf isteği gider. Kullanıcı yazmaya devam eder, 200 ms sonra performans isteği gider. İkinci istek 40 ms'de, birinci istek 600 ms'de dönerse ekranda perf sonuçları kalır. Debounce bunu engellemez; yalnızca daha seyrek yaşanmasını sağlar.
Kaçınılması kolay, fark edilmesi zor bir hatadır: yerelde neredeyse hiç görülmez, gerçek ağda düzenli olarak görülür.
Vurgulama neden sunucuda yapılıyor, istemcide değil?
İki sebep. Birincisi, özet penceresi zaten sunucuda kesiliyor: 900 karakterlik bir gövdeden ilk eşleşmenin çevresini almak, hangi kelimelerin arandığını bilmeyi gerektirir. İstemcide yapmak, gövdenin tamamını ağdan geçirmek demektir.
İkincisi ve daha önemlisi güvenlik: kaçışlama ile vurgulamanın sırası kritiktir (bkz. 5. karar). Bunu tek bir yerde, sunucuda doğru yapmak; her çizim noktasında istemcide tekrar doğru yapmaya çalışmaktan güvenlidir.
Önerilen aramalar nereden geliyor? Arama kaydı tutuyor musunuz?
Hayır, bir arama geçmişi tablosu yok. Öneriler yazıların etiketlerinden üretilir: en sık geçen sekiz etiket alınır.
Bu bilinçli bir tercih. Arama kaydı tutmak kişisel veri toplamaktır ve ayrı bir sorumluluk getirir; ayrıca yeni kurulmuş bir sistemde geçmiş boştur ve öneri kutusu haftalarca boş kalır. İçerikten türeyen öneriler ilk günden anlamlıdır.
Facet sayıları neden kategori filtresi uygulanmadan hesaplanıyor?
Çünkü facet'in işi "buradan nereye gidebilirim?" sorusunu cevaplamaktır. "Web" kategorisine tıkladığınızda diğer kategorilerin sayısı 0 görünseydi, oradan başka bir kategoriye geçemezdiniz — tek çıkış yolu filtreyi tamamen temizlemek olurdu.
Bu yüzden iki ayrı sayı vardır: all_total (filtresiz toplam, "Tümü" çipinde) ve total (filtreli toplam, ölçüm satırında).
Canlı Ortama Alırken
- [ ]
APP_DEBUGzaten ortamdan türetiliyor — yine de canlıdafalseolduğunu doğrulayın - [ ]
system/config.local.phpoluşturun; künyeyiconfig.php'ye yazmayın - [ ] Veritabanı için
rootyerine sınırlı yetkili (yalnızcaSELECT) bir kullanıcı oluşturun — bu uygulama hiçbir şey yazmaz - [ ] FULLTEXT indekslerinin gerçekten oluştuğunu doğrulayın:
SHOW INDEX FROM articles - [ ] Ölçüm satırını (
modvems) göstermek istemiyorsanızrenderResults()içinden kaldırın - [ ] Hız sınırı kovalarını trafiğinize göre gözden geçirin; otomatik tamamlama beklediğinizden sık çağrılır
- [ ] HTTPS kullanın;
session.cookie_secure = 1vesession.cookie_httponly = 1ayarlayın - [ ] Tablo büyüdükçe
EXPLAINile sorgu planını kontrol edin; facetGROUP BY'ı ilk yavaşlayan yerdir - [ ] Nginx kullanıyorsanız
.htaccessçalışmaz;.sqlve.mderişimini sunucu yapılandırmasından kapatın:
```nginx
location ~* \.(sql|log|ini|bak)$ { deny all; }
```
Sorun Giderme
| Belirti | Çözüm |
|---|---|
| Hiçbir arama sonuç vermiyor | FULLTEXT indeksi oluşmamış olabilir: SHOW INDEX FROM articles WHERE Index_type = 'FULLTEXT'. cy_search.sql'i yeniden içe aktarın. |
| Türkçe kelimeler bulunamıyor | Veritabanı utf8mb4 değil ya da SQL dosyası SET NAMES utf8mb4 olmadan aktarılmış. Bu durumda indeks bozuk baytlar üzerine kurulur ve hata sessizdir. Dosyayı yeniden yükleyin. |
| 3 harften kısa terimler bulunamıyor | Beklenen davranış: innodb_ft_min_token_size varsayılan 3'tür. Uygulama bu durumda LIKE'a düşer ve ekranda modu yazar. |
| "Invalid parameter number" hatası | Bir FULLTEXT ifadesi aynı sorguda iki kez geçiyor ve aynı yer tutucu adını kullanıyor. EMULATE_PREPARES = false altında her yer tutucu benzersiz olmalıdır (bkz. handle_search() yorumları). |
| Öneri listesi açılmıyor | Yazdığınız terim MIN_QUERY_LEN altında olabilir. Ya da ?q= ile gelen sayfada liste bilinçli olarak açılmaz. |
| Öneriler alakasız görünüyor | Öneri listesi yalnızca başlıklarda arar. Aradığınız kelime yalnızca gövdede geçiyorsa sonuç listesinde çıkar, öneride çıkmaz. |
| Vurgulama kayıyor / bozuk karakter çıkıyor | tr_fold() değiştirilmiş olabilir. Eşleme birebir olmalıdır: bir karakter tam olarak bir karaktere dönüşmeli, yoksa indisler kayar. |
| HTTP 403 dönüyor | Oturum düşmüş — sayfayı yenileyin. Sunucuda session.save_path yazılabilir olmalıdır. |
| HTTP 429 dönüyor | Hız sınırı. Otomatik tamamlama kovası dakikada 180 istektir; otomatik bir test aracı bunu kolayca aşar. |
$ is not defined | JavaScript yükleme sırası bozulmuş. jQuery her zaman en başta gelmelidir. |
Yol Haritası
- [ ] Eş anlamlı sözlüğü (
arama↔search,veritabanı↔database) - [ ] Yazım denetimi ve "bunu mu demek istediniz?" (trigram benzerliği)
- [ ] Tarih aralığı filtresi ve çoklu kategori seçimi
- [ ] Sonsuz kaydırma seçeneği (anahtar tabanlı sayfalama ile)
- [ ] Arama sonuçlarının CSV / JSON dışa aktarımı
- [ ] Başlığı gövdeden ağırlıklı sayan bileşik skor (iki
MATCHtoplamı) - [ ] PHPUnit testleri (
boolean_query,tr_fold,highlight,should_use_like) - [ ] Koyu tema için elle açma/kapama düğmesi
Katkı
Bu proje herkese açıktır — dilediğiniz geliştirmeyle katkı sağlayabilirsiniz.
📦 Depo: github.com/CilginYazilim/search-autocomplete
| Nasıl katkı sağlarım? | Nereden |
|---|---|
| 🐛 Hata bildir | Issues |
| 💡 Özellik öner | Issues |
| 🔧 Kod gönder | Pull Requests |
| ❓ Soru sor | Discussions |
Katkı ölçütleri
- Kod açıklamalı olsun. Bu projenin temel amacı öğretmek; yorumsuz kod PR'ı geri döner.
- Ham girdiyi
AGAINST()içine koymayın.boolean_query()bu projenin güvenlik sınırıdır. - Vurgulama sırasını değiştirmeyin. Önce işaretle, sonra kaçışla, en son etikete çevir — ters sıra XSS'e ya da bozuk HTML'e açılır.
- Yarış korumasını kaldırmayın. Yerelde hiç görülmeyen, üretimde düzenli görülen bir hatayı kapatır.
- Tasarım değişikliklerini
style.cssüzerinden yapın;cilginyazilim.cssmarkaya aittir ve diğer projelerle ortaktır.
Lisans
MIT — ticari kullanım dahil serbesttir.
Kaynak Kod soldaki ağaçtan bir dosya seçin
-
assets
-
css
- bootstrap.min.css 227.5 KB
- cilginyazilim.css 22 KB
- style.css 15.6 KB
-
images
- logo.png 70.4 KB
-
js
- bootstrap.bundle.js 203.2 KB
- jquery-3.7.0.js 278.3 KB
- search.js 23.1 KB
-
-
docs
-
screenshots
- 01-canli-arama.png 200.3 KB
- 02-oneri-listesi.png 160.6 KB
- 03-yazi-detay.png 164.8 KB
- 04-mobil.png 163.7 KB
-
-
system
- .htaccess 1.7 KB
- ajax.php 17.7 KB
- config.local.php.example 1.5 KB
- config.php 11 KB
- function.php 12.4 KB
- .gitignore 1.1 KB
- .htaccess 3.6 KB
- CHANGELOG.md 11.6 KB
- cy_search.sql 19.4 KB
- index.php 11.7 KB
- LICENSE 1.1 KB
- README.en.md 44.7 KB
- README.md 46.6 KB
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli
değerler gösterilmeden önce maskelenir (••••••••).
Sık Sorulan Sorular
MySQL FULLTEXT kök bulma, eş anlamlı sözlüğü, yazım denetimi, coğrafi arama ve dağıtık indeks sunmaz. Bunlara ihtiyacınız yoksa — çoğu panel aramasında yoktur — MySQL yeter ve ayrı bir servis işletmek zorunda kalmazsınız. Yüz binlerce belgede ve kelime bazlı aramada FULLTEXT hâlâ makul çalışır. Geçiş sinyalleri: eş anlamlıya ihtiyaç duymaya başlamak, sorgu sürelerinin kabul edilemez olması, ya da aramanın ürünün ana özelliği hâline gelmesi.
Tek sebep yeter: önek eşleşmesi. Canlı aramada kullanıcı henüz yazmaktadır ve "perfor" yazdığında "performans" bulunmalıdır; yıldız operatörü yalnızca boolean modda vardır. Türkçe için ikinci bir sebep daha var: dil eklemeli olduğu için indeks, indeksin ve indeksleri üç ayrı sözcüktür ve natural language modu bunları bağlamaz. Bedeli, boolean modun alaka skorunu natural language kadar iyi hesaplamaması ve yüzde elli eşiği gibi kolaylıklara sahip olmamasıdır.
InnoDB'de innodb_ft_min_token_size varsayılan olarak üçtür: daha kısa sözcükler indekse hiç girmez ve boolean modda sessizce sıfır sonuç döner. Kullanıcı için bu, aramanın bozuk olduğu anlamına gelir. Bu yüzden uygulama sorgudaki en uzun sözcük üç harften kısaysa LIKE yoluna düşer; yavaştır ama doğru cevap verir ve hangi modda çalıştığını ekranda söyler. Eşiği düşürmek mümkündür ama MySQL yapılandırmasına erişim ve indeksin yeniden oluşturulmasını gerektirir.
Evet, çünkü ikisi farklı sorunları çözer. Debounce istek sayısını azaltır ama ağ gecikmesinin eşit olmasını sağlamaz. "perf" isteği gider, kullanıcı yazmaya devam eder, "performans" isteği gider; ikinci istek kırk milisaniyede, birinci istek altı yüz milisaniyede dönerse ekranda "perf" sonuçları kalır. Kullanıcı doğru yazmıştır ama yanlış sonucu görür. Çözüm her isteğe artan bir numara vermek ve sunucunun onu aynen geri döndürmesidir; karar istemcide verilir çünkü "en son hangi isteği gönderdim" bilgisi yalnızca orada vardır.
İki ayrı yerde sorun çıkar. Bulmak MySQL'in işidir ve utf8mb4_unicode_ci karşılaştırması ü ile u, İ ile i katlamasını yapar. Vurgulamak ise PHP'nin işidir ve orada iki tuzak vardır: birincisi mb_strtolower büyük I harfini i yapar ama Türkçede I'nın küçüğü ı'dır; ikincisi ve daha sinsisi, mb_strtolower büyük İ harfini iki kod noktasına çevirir (i artı birleştirici nokta). Vurgulama konumları karakter indisine dayandığı için katlanmış metnin uzunluğu değiştiğinde bütün işaretler kayar. Bu yüzden birebir kendi eşleme tablomuzu kullanıyoruz.
Özet penceresi zaten sunucuda kesiliyor: dokuz yüz karakterlik bir gövdeden ilk eşleşmenin çevresini almak, hangi kelimelerin arandığını bilmeyi gerektirir. Sıra ise güvenlik meselesidir. Önce kaçışlayıp sonra mark etiketi eklemek HTML varlıklarını bozar: metinde ve işareti geçtiğinde kaçışlanmış hâli ampersand-amp-noktalı virgül olur ve kullanıcı "amp" aratırsa vurgulama tam o varlığın ortasına girer. Doğru sıra üç adımdır: ham metinde ayraçla işaretle, tamamını kaçışla, en son ayraçları etikete çevir. Böylece çıktıdaki tek HTML etiketi kodun koyduğu olur.
Yorumlar 0 konuşma
Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.
Soru Sor veya Yorum Yaz
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.