ช่องค้นหาในหลังบ้านมักต้องค้นข้ามหลายตาราง
ในหลังบ้านของเว็บขายของออนไลน์ แอดมินพิมพ์อะไรก็ได้ลงช่องค้นหาของหน้าคำสั่งซื้อ บางทีเป็นเลขคำสั่งซื้อ บางทีเป็นชื่อลูกค้า เบอร์โทร หรือชื่อสินค้า ข้อมูลพวกนี้อยู่คนละตาราง ที่ผ่านมาเราก็มักเขียน orWhereHas() ต่อกันไปทีละความสัมพันธ์ ซึ่งยาวขึ้นทุกครั้งที่มีคนขอค้นเพิ่มอีกช่อง
ยิ่งถ้ามีความสัมพันธ์แบบ polymorphic เช่น ตาราง comments ที่ผูกได้ทั้งกับสินค้าและบทความ เราต้องใช้ whereHasMorph() แยกตามชนิดอีกชั้น
Laravel Searchable ของ Mozex เก็บรายการพวกนี้ไว้ใน model ที่เดียว ไม่ใช่ตัวเดียวกับ Laravel Searchable ของ Spatie ตัวของ Spatie รวมผลจากหลาย model มาแสดงด้วยกัน ส่วนตัวนี้กรอง Eloquent query ตัวเดิม เราจึงต่อ where(), with() หรือ paginate() ได้ตามปกติ
ติดตั้งและประกาศคอลัมน์ที่ค้นได้
ต้องใช้ PHP 8.2 ขึ้นไป กับ Laravel 12.69 หรือ 13.30 ขึ้นไป ส่วนที่ใช้กับ Filament รองรับ Filament 4 และ 5 package ไม่มี migration ของตัวเอง
composer require mozex/laravel-searchable
php artisan searchable:install
คำสั่งที่สองสร้าง config/searchable.php ไว้ปรับค่าเริ่มต้น เช่น จำนวนคำค้นสูงสุด จากนั้นใส่ trait Searchable ใน model แล้วบอกว่าค้นคอลัมน์ไหนได้
use Mozex\Searchable\Searchable;
class Order extends Model
{
use Searchable;
public function searchableColumns(): array
{
return [
'number',
'customer.name',
'customer.phone',
'items.product.name',
];
}
}
$orders = Order::search('สมชาย 0812')->paginate(20);
คอลัมน์ในความสัมพันธ์เขียนด้วยจุด ซ้อนได้หลายชั้นอย่าง items.product.name ลำดับใน array คือความสำคัญ ถ้าเจอใน number จะได้คะแนนสูงกว่าเจอแค่ในชื่อสินค้า
กฎการค้นที่ควรรู้ไว้ตอนอธิบายให้คนใช้หลังบ้านฟัง
- พิมพ์หลายคำ ทุกคำต้องเจอในคอลัมน์ใดคอลัมน์หนึ่ง อย่าง
สมชาย 0812ด้านบน ชื่อกับเบอร์อยู่คนละคอลัมน์ก็ยังเจอ - ใส่
"..."ครอบคำที่ต้องการให้ค้นติดกันทั้งวลี - ใช้แค่ 10 คำแรก คำที่เกินมา package จะตัดทิ้ง (ปรับได้ใน config)
- ถ้าผู้ใช้พิมพ์
%หรือ_package ค้นเป็นตัวอักษรตรง ๆ ไม่ใช่ wildcard ของ SQL
เงื่อนไขค้นหาทั้งหมดอยู่ในวงเล็บเดียว จึงไม่ไปกวน where() ตัวอื่นที่ต่อไว้ เช่น Order::query()->where('status', 'pending')->search($term) ยังกรองเฉพาะคำสั่งซื้อที่รอดำเนินการอยู่
ค้นในความสัมพันธ์แบบ polymorphic
ต้องลงทะเบียน morph map ก่อน เพื่อให้ package รู้ว่าชื่อชนิดไหนคือ model อะไร
// ใน boot() ของ AppServiceProvider
Relation::morphMap([
'product' => Product::class,
'article' => Article::class,
]);
แล้วเขียน ความสัมพันธ์:ชนิด.คอลัมน์ เครื่องหมาย : บอกชนิดของ model ปลายทาง
class Comment extends Model
{
use Searchable;
public function searchableColumns(): array
{
return [
'body',
'commentable:product.name',
'commentable:article.title',
];
}
}
ไล่ต่อเข้าไปใน model ปลายทางได้อีก เช่น commentable:article.author.name ถ้าเขียนเองด้วย whereHasMorph() ก็ทำได้ แต่ต้องเขียนซ้ำทุกที่ที่ค้น ส่วนตัวนี้ประกาศครั้งเดียวแล้วใช้ได้ทุกที่
ความสัมพันธ์ที่อยู่คนละฐานข้อมูล
ถ้า customer เป็น BelongsTo ไปหา model ที่ใช้ connection อื่น เช่น ข้อมูลลูกค้าอยู่ในฐานข้อมูลของอีกระบบ SQL ปกติ join ข้ามกันไม่ได้ package จะค้นในฐานข้อมูลนั้นก่อน แล้วเอา id ที่เจอกลับมากรองฝั่งเรา
ค่าเริ่มต้นเอา id มาไม่เกิน 50 ตัวต่อคอลัมน์ต่อคำค้น ถ้าคำค้นกว้างจนเจอเกินนั้น ผลบางส่วนจะหายไปเงียบ ๆ ปรับได้ทีละ query ด้วย Order::search('สมชาย', externalLimit: 200) แต่ยิ่งมากก็ยิ่งส่ง id เข้า query ฝั่งเรามากขึ้น
ส่วนนี้ใช้ได้แค่ BelongsTo ตรง ๆ กับ morph ที่รองรับ ไล่ซ้อนหลายชั้นข้ามฐานข้อมูลไม่ได้ และในเวอร์ชัน 1.3.0 ยังใช้ primary key ของ model ปลายทางเสมอ ไม่สน where() ที่เขียนไว้ใน method ของความสัมพันธ์ ถ้าความสัมพันธ์ไหนพึ่งเงื่อนไขพวกนั้น ต้องเขียน query เอง
ช่องค้นหาของตาราง Filament เรียงผลตามความใกล้เคียง
ส่วนที่น่าจะได้ใช้บ่อยที่สุดสำหรับหลังบ้าน Filament คือ macro advancedSearchable()
TextColumn::make('number')
->advancedSearchable()
->sortable(),
ใส่ที่คอลัมน์เดียว ช่องค้นหาของตารางจะค้นทุกคอลัมน์ที่ประกาศไว้ใน searchableColumns() รวม polymorphic ด้วย แล้วเรียงผลตามความใกล้เคียงให้เอง
- ลำดับใน
searchableColumns()คือความสำคัญ - ในคอลัมน์ของตารางหลัก คำที่ตรงทั้งคำมาก่อน ตามด้วยคำที่ขึ้นต้นด้วยคำค้น แล้วค่อยเป็นคำที่มีคำค้นอยู่ตรงกลาง
- ความสัมพันธ์ชั้นเดียวก็ได้คะแนนไล่ระดับแบบนี้ ส่วนที่ซ้อนลึกกว่านั้นหรืออยู่คนละฐานข้อมูลได้แค่ว่าเจอหรือไม่เจอ
ผลจะเรียงตามความใกล้เคียงก่อน ถ้าเท่ากันค่อยใช้การเรียงเริ่มต้นของตาราง และถ้าผู้ใช้กดเรียงตามคอลัมน์เอง การเรียงของผู้ใช้ชนะ ปิดการเรียงตามความใกล้เคียงได้ที่ searchable.filament.relevance_sort
ช่องค้นหารวมของ panel (global search) ก็ใช้ได้ผ่าน SearchableGlobalSearchProvider โดยแต่ละ resource ยังเลือกคอลัมน์ด้วย getGloballySearchableAttributes() ตามปกติ และยังเคารพสิทธิ์การเข้าถึงของ Filament เหมือนเดิม
LIKE ค้นภาษาไทยได้ แต่ช้าลงเมื่อข้อมูลเยอะ
package ค้นด้วย LIKE '%คำค้น%' ซึ่งเป็นข้อดีสำหรับข้อความภาษาไทย full-text search ของ MySQL แบบปกติแยกคำด้วยช่องว่างและเครื่องหมาย ประโยคภาษาไทยที่เขียนติดกันจึงกลายเป็นคำยาวคำเดียว ค้นคำที่อยู่กลางประโยคไม่เจอ ส่วน LIKE หาตัวอักษรที่ตรงกันตรงไหนก็ได้
ข้อแลกคือ % ด้านหน้าทำให้ index ปกติช่วยไม่ได้ ฐานข้อมูลต้องไล่ดูทุกแถว ยิ่งค้นข้ามความสัมพันธ์และเรียงตามความใกล้เคียงก็ยิ่งมี subquery เพิ่ม ต้นฉบับแนะนำให้ลองกับข้อมูลจริงก่อนเอาไปใช้กับหน้าที่คนเข้าเยอะ ลองดูเวลาที่ query ค้นหาใช้จริงกับจำนวนแถวที่เรามี เช่น จาก card Slow Queries ของ Laravel Pulse
ถ้าต้องการให้ค้นเจอแม้พิมพ์ผิด หรือข้อมูลใหญ่จนต้องมี index สำหรับค้นโดยเฉพาะ ต้นฉบับแนะนำ Laravel Scout คู่กับ Meilisearch และยังมีตัวเลือกอื่นอีก
- Scout แบบ database engine ไม่ต้องตั้ง service แยก
- Cross Eloquent Search ของ Protone Media ค้นความสัมพันธ์ซ้อนได้ ใช้ full-text ของ MySQL และ PostgreSQL ได้ และแบ่งหน้าผลที่รวมจากหลาย model ได้ แต่การเรียงตามความใกล้เคียงไม่ครอบคลุมการค้นในความสัมพันธ์
Laravel Searchable เหมาะที่สุดกับหน้าค้นหาในหลังบ้านที่ต้องค้นผ่านความสัมพันธ์เยอะ โดยเฉพาะ model ที่แสดงอยู่ในตาราง Filament ด้วย รายละเอียดเพิ่มเติมอ่านต่อได้ที่บทความต้นฉบับบน Laravel Newsครับ






