Başlangıç
arayuz-ve-tablo/php-pdo-ajax-crud
PHP PDO Ajax CRUD Örneği
- PHP 8
- PDO
- MySQL
- Ajax
- +2
292 inceleme 90 canlı açılış 2 beğeni 2 yorum
Arayüz ve Tablo Orta
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.
Veritabanı şeması projedeki cy_search.sql dosyasında.
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%'% yüzünden MySQL her satırı okur. Bin satırda fark edilmez, yüz binde sayfa donar.per isteği yavaş, performans isteğ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?
LIKE '%...%' ile başlamak istemeyenlerKlonla, 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.
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.
AGAINST() içine konmazBoolean 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üvenlik yazan 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);
}
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)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;IŞIK araması ışık kelimesini bulmalı ve vurgulamalıdır. Bunlar iki farklı sorundur:
utf8mb4_unicode_ci karşılaştırması bunu hallederiBirincisi 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!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');
}
İ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);& 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 "&"\x00 … \x01 gibi metinde asla bulunmayacak ayraçlar koy<mark> ve </mark> ile değiştir$out = str_replace(["\x00", "\x01"], ['<mark>', '</mark>'], e($prefix . $out . $suffix));Arama çekirdeği
tr_fold) — birebir, indis kaydırmazArayüz ve altyapı
role="combobox" + aria-activedescendant ile ekran okuyucu desteği?q=...&kategori=...&sirala=...&sayfa=...#makale-7, paylaşılabilir yazı detayı| 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 <mark>'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). |
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 tutulmazSunucudaki sabit index.php tarafından istemciye taşınır:
window.CY_MIN_LEN = <?= (int) MIN_QUERY_LEN ?>;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);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ı doldurunconfig.local.php → .env → ortam değişkeni → yerel varsayılan.
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çinMATCH(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.
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.
| 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.
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);
}
Kullanıcının işletim sistemi koyu temadaysa otomatik devreye girer. Zorlamak isterseniz:
<html data-cy-theme="dark"> <!-- veya "light" -->
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
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=… │
└──────────────────────────────────────────────────────────────────────┘
| 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 |
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
}
}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.
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.
| 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) |
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ı |
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).
APP_DEBUG zaten ortamdan türetiliyor — yine de canlıda false olduğunu doğrulayınsystem/config.local.php oluşturun; künyeyi config.php'ye yazmayınroot yerine sınırlı yetkili (yalnızca SELECT) bir kullanıcı oluşturun — bu uygulama hiçbir şey yazmazSHOW INDEX FROM articlesmod ve ms) göstermek istemiyorsanız renderResults() içinden kaldırınsession.cookie_secure = 1 ve session.cookie_httponly = 1 ayarlayınEXPLAIN ile sorgu planını kontrol edin; facet GROUP BY'ı ilk yavaşlayan yerdir.htaccess çalışmaz; .sql ve .md erişimini sunucu yapılandırmasından kapatın:```nginx
location ~* \.(sql|log|ini|bak)$ { deny all; }
```
| 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. |
arama ↔ search, veritabanı ↔ database)MATCH toplamı)boolean_query, tr_fold, highlight, should_use_like)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 |
AGAINST() içine koymayın. boolean_query() bu projenin güvenlik sınırıdır.style.css üzerinden yapın; cilginyazilim.css markaya aittir ve diğer projelerle ortaktır.MIT — ticari kullanım dahil serbesttir.
cilginyazilim.com tarafından ❤ ile geliştirildi
İncelemek istediğiniz dosyayı ağaçtan seçin.
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).
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.
Bu örneğin daha gelişmişi projenizde mi lazım?
İhtiyacınızı anlatın; bu yapıyı sizin sisteminize uyarlayalım ya da size özel geliştirelim.
Henüz yorum yok. Takıldığınız bir yer varsa ilk soruyu siz sorun.
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.
Başlangıç
arayuz-ve-tablo/php-pdo-ajax-crud
292 inceleme 90 canlı açılış 2 beğeni 2 yorum
Orta
arayuz-ve-tablo/surukle-birak-gorev-panosu
268 inceleme 77 canlı açılış 1 beğeni 2 yorum
İleri
arayuz-ve-tablo/datatables-performans-optimizasyonu
184 inceleme 73 canlı açılış 1 beğeni