ติดตั้ง Meilisearch ทำ Full-text Search บน VPS

Meilisearch คือ Search Engine แบบ Open Source ที่ออกแบบมาเพื่อ Developer ตอบสนองต่อ Search Query ได้เร็วมาก รองรับการค้นหาแบบ Typo-tolerant และ Faceted Search ติดตั้งง่ายบน VPS และ Integrate กับ PHP, Node.js ได้ไม่ยาก เหมาะสำหรับเว็บไซต์ E-commerce, Blog หรือ App ที่ต้องการ Search ที่ดีกว่า SQL LIKE

ทำไมถึงใช้ Meilisearch แทน SQL LIKE

System Requirement: Meilisearch ต้องการ RAM อย่างน้อย 1GB (แนะนำ 2GB ขึ้นไปสำหรับ Production) เนื่องจาก Index ถูกโหลดขึ้น RAM ทั้งหมดเพื่อความเร็ว VPS แพ็กเกจ 2GB RAM ขึ้นไปเหมาะสมที่สุด

ติดตั้ง Meilisearch บน Ubuntu

ขั้นที่ 1: Download และติดตั้ง

# Download Meilisearch binary
curl -L https://install.meilisearch.com | sh

# ย้ายไปยัง PATH
sudo mv ./meilisearch /usr/local/bin/

# ตรวจสอบ version
meilisearch --version

ขั้นที่ 2: สร้าง User และ Directory

# สร้าง system user (no login)
sudo useradd -r -s /bin/false meilisearch

# สร้าง directory เก็บ data
sudo mkdir -p /var/lib/meilisearch/data
sudo chown -R meilisearch:meilisearch /var/lib/meilisearch

ขั้นที่ 3: สร้าง systemd Service

sudo nano /etc/systemd/system/meilisearch.service
[Unit]
Description=Meilisearch Search Engine
After=network.target

[Service]
User=meilisearch
Group=meilisearch
WorkingDirectory=/var/lib/meilisearch
ExecStart=/usr/local/bin/meilisearch \
  --db-path /var/lib/meilisearch/data \
  --http-addr 127.0.0.1:7700 \
  --master-key YOUR_MASTER_KEY_HERE \
  --env production
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

แทน YOUR_MASTER_KEY_HERE ด้วย Key ที่แข็งแรง (อย่างน้อย 16 ตัวอักษร) เช่น openssl rand -hex 32

ขั้นที่ 4: Start และ Enable Service

sudo systemctl daemon-reload
sudo systemctl enable meilisearch
sudo systemctl start meilisearch
sudo systemctl status meilisearch

# ทดสอบว่า API ตอบกลับ
curl http://127.0.0.1:7700/health

ตั้งค่า Nginx Reverse Proxy

Meilisearch รันบน Port 7700 เฉพาะ localhost ควรตั้ง Nginx เพื่อ Expose ผ่าน HTTPS:

server {
    listen 443 ssl;
    server_name search.yourdomain.com;

    # SSL certs
    ssl_certificate /etc/letsencrypt/live/search.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/search.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:7700;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

สร้าง Index และ Index Documents (Node.js)

npm install meilisearch
const { MeiliSearch } = require('meilisearch');

const client = new MeiliSearch({
  host: 'https://search.yourdomain.com',
  apiKey: 'YOUR_MASTER_KEY_HERE'
});

// สร้าง Index
const index = client.index('products');

// เพิ่มข้อมูล
await index.addDocuments([
  { id: 1, name: 'iPhone 15', category: 'smartphone', price: 32900 },
  { id: 2, name: 'Samsung Galaxy S24', category: 'smartphone', price: 28900 },
]);

// ค้นหา
const results = await index.search('iphone', {
  limit: 10,
  attributesToHighlight: ['name']
});
console.log(results.hits);

เชื่อมต่อจาก PHP

composer require meilisearch/meilisearch-php
<?php
require 'vendor/autoload.php';
use Meilisearch\Client;

$client = new Client('https://search.yourdomain.com', 'YOUR_MASTER_KEY_HERE');
$index  = $client->index('products');

// Index documents
$index->addDocuments([
  ['id' => 1, 'name' => 'สินค้าทดสอบ', 'price' => 999],
]);

// Search
$results = $index->search('สินค้า');
foreach ($results->getHits() as $hit) {
    echo $hit['name'] . PHP_EOL;
}

API Key Security: ใน Production ควรสร้าง Search-only API Key แยกต่างหากสำหรับ Client-side เพื่อไม่ให้ Frontend รู้ Master Key เช่น curl -X POST 'http://127.0.0.1:7700/keys' -H 'Authorization: Bearer MASTER_KEY' -d '{"actions":["search"],"indexes":["products"],"expiresAt":null}'

การปรับแต่ง Ranking Rules และ Filterable Attributes

Meilisearch มีระบบ Ranking ในตัวที่สามารถปรับแต่งได้ตาม Use Case ของแต่ละ Index โดยค่าเริ่มต้น Meilisearch จะเรียงผลลัพธ์ตามลำดับความเกี่ยวข้อง (Relevance) แต่ Developer สามารถกำหนด Custom Ranking Rules เพิ่มเติมได้

กำหนด Filterable Attributes

ก่อนจะใช้ Filter ใน Search Query จำเป็นต้องกำหนด Attribute ที่ต้องการ Filter ล่วงหน้าก่อน เพราะ Meilisearch ต้อง Re-index ข้อมูลสำหรับ Attribute นั้นๆ

const index = client.index('products');

// กำหนด Filterable Attributes
await index.updateFilterableAttributes([
  'category',
  'price',
  'brand',
  'in_stock'
]);

// กำหนด Sortable Attributes
await index.updateSortableAttributes([
  'price',
  'created_at',
  'rating'
]);

// ค้นหาพร้อม Filter และ Sort
const results = await index.search('laptop', {
  filter: 'category = "electronics" AND price < 30000 AND in_stock = true',
  sort: ['price:asc'],
  limit: 20
});

ปรับ Ranking Rules

// ดู Ranking Rules ปัจจุบัน
await index.getRankingRules();

// ปรับ Ranking Rules (เพิ่ม Custom Rule)
await index.updateRankingRules([
  'words',
  'typo',
  'proximity',
  'attribute',
  'sort',
  'exactness',
  'rating:desc'  // Custom: เรียงตาม rating ก่อน
]);

การ Monitor และ Backup Meilisearch บน VPS

ใน Production ควรตั้งค่า Monitoring และ Backup เพื่อป้องกันการสูญหายของ Search Index และรู้เมื่อ Server มีปัญหา

ตรวจสอบสถานะและสถิติ

# ตรวจสอบสุขภาพของ Meilisearch
curl -H 'Authorization: Bearer MASTER_KEY' \
  http://127.0.0.1:7700/health

# ดูสถิติของ Index ทั้งหมด
curl -H 'Authorization: Bearer MASTER_KEY' \
  http://127.0.0.1:7700/stats

# ดูสถิติ Index เฉพาะ
curl -H 'Authorization: Bearer MASTER_KEY' \
  http://127.0.0.1:7700/indexes/products/stats

Backup ข้อมูล Meilisearch

วิธีที่ง่ายที่สุดคือ Dump ข้อมูลผ่าน API ซึ่ง Meilisearch จะสร้างไฟล์ Snapshot ให้อัตโนมัติ

# สร้าง Dump (ใช้ API)
curl -X POST 'http://127.0.0.1:7700/dumps' \
  -H 'Authorization: Bearer MASTER_KEY'

# Dump จะถูกบันทึกไว้ที่ /var/lib/meilisearch/data/dumps/
ls /var/lib/meilisearch/data/dumps/

# หรือ Backup ทั้ง Directory ผ่าน rsync
sudo rsync -av /var/lib/meilisearch/data/ /backup/meilisearch/

ตั้ง Cron Job สำหรับ Backup อัตโนมัติ

# เพิ่มใน Crontab
sudo crontab -e

# Backup ทุกวันเวลา 02:00
0 2 * * * rsync -a /var/lib/meilisearch/data/ /backup/meilisearch/$(date +\%Y-\%m-\%d)/
คำสั่ง API Method ผลลัพธ์
/health GET สถานะ Meilisearch
/stats GET สถิติ Index ทั้งหมด
/dumps POST สร้าง Dump ข้อมูล
/indexes/{uid}/documents DELETE ลบ Documents ทั้งหมด

การตั้งค่า Typo Tolerance และ Stop Words

หนึ่งในจุดเด่นของ Meilisearch คือระบบ Typo Tolerance ที่ช่วยให้ผู้ใช้ค้นหาได้แม้พิมพ์ผิด นอกจากนี้ยังสามารถกำหนด Stop Words เพื่อบอก Meilisearch ว่าคำไหนไม่มีความหมายในการค้นหา เช่น "และ" "หรือ" "ของ" ซึ่งจะทำให้ผลลัพธ์การค้นหาแม่นยำขึ้น

ปรับการตั้งค่า Typo Tolerance

// ปรับ Typo Tolerance ต่อ Index
await index.updateTypoTolerance({
  enabled: true,
  minWordSizeForTypos: {
    oneTypo: 5,    // คำที่มีความยาว 5 ตัวขึ้นไปยอมรับ Typo 1 ตัว
    twoTypos: 9    // คำที่มีความยาว 9 ตัวขึ้นไปยอมรับ Typo 2 ตัว
  },
  disableOnWords: ['iphone', 'samsung', 'แบรนด์ที่ต้องพิมพ์ถูก'],
  disableOnAttributes: ['serial_number', 'sku'] // Field ที่ไม่ยอมรับ Typo
});

กำหนด Stop Words สำหรับภาษาไทย

// กำหนด Stop Words — คำที่ไม่มีผลต่อการค้นหา
await index.updateStopWords([
  'และ', 'หรือ', 'ของ', 'ใน', 'ที่', 'การ', 'มี', 'เป็น',
  'จาก', 'ด้วย', 'กับ', 'ให้', 'ไม่', 'นี้', 'นั้น', 'อยู่',
  'the', 'a', 'an', 'is', 'in', 'on', 'at', 'for', 'with'
]);

// ดู Stop Words ที่ตั้งไว้
await index.getStopWords();

การกำหนด Stop Words ช่วยลดขนาด Index และเพิ่มความแม่นยำของผลลัพธ์ เพราะ Meilisearch จะไม่นำคำเหล่านี้มา Match กับ Query ทำให้คำที่มีความหมายจริงๆ ได้รับ Weight มากขึ้น ผลลัพธ์ที่ได้จึงตรงกับเจตนาของผู้ใช้มากขึ้น

การทำ Search-as-you-type และ Instant Search บน Frontend

ฟีเจอร์ Search-as-you-type หรือ Instant Search คือการแสดงผลลัพธ์ทันทีที่ผู้ใช้พิมพ์แต่ละตัวอักษร โดยไม่ต้องกด Enter Meilisearch เหมาะกับการใช้งานนี้มากเพราะ Response Time ต่ำกว่า 50ms ทำให้ UI รู้สึกลื่นไหล

ตัวอย่าง Instant Search ด้วย JavaScript Vanilla

<input type="text" id="searchBox" placeholder="ค้นหาสินค้า...">
<div id="searchResults"></div>

<script>
const searchBox = document.getElementById('searchBox');
const resultsDiv = document.getElementById('searchResults');

let debounceTimer;
searchBox.addEventListener('input', function() {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(async () => {
    const query = this.value.trim();
    if (query.length < 2) { resultsDiv.innerHTML = ''; return; }

    const res = await fetch(
      `https://search.yourdomain.com/indexes/products/search`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer SEARCH_ONLY_API_KEY'
        },
        body: JSON.stringify({ q: query, limit: 8,
          attributesToHighlight: ['name', 'description'],
          highlightPreTag: '<mark>',
          highlightPostTag: '</mark>' })
      }
    );
    const data = await res.json();

    resultsDiv.innerHTML = data.hits.map(hit => `
      <div class="result-item">
        <strong>${hit._formatted?.name || hit.name}</strong>
        <span>฿${hit.price.toLocaleString()}</span>
      </div>
    `).join('');
  }, 200); // Debounce 200ms
});
</script>

สังเกตว่าใช้ Debounce 200ms เพื่อไม่ให้ส่ง Request ทุกตัวอักษร และใช้ SEARCH_ONLY_API_KEY แทน Master Key เสมอ การใช้ _formatted ใน Response ทำให้ได้ข้อความที่ Highlight คำค้นหาแล้วพร้อมแสดงผล

ตั้งค่า CORS บน Nginx สำหรับ Instant Search

# เพิ่มใน Nginx server block ของ Meilisearch
add_header 'Access-Control-Allow-Origin' 'https://yourdomain.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;

location / {
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    proxy_pass http://127.0.0.1:7700;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

Performance Tip: ถ้า Meilisearch และเว็บไซต์หลักอยู่บน VPS เดียวกัน ให้ Frontend เรียก API ผ่าน PHP Backend แทน (Server-side search) เพื่อซ่อน API Key และลด CORS issue ได้ทันที

การ Update และ Delete Documents ใน Meilisearch

การจัดการข้อมูลใน Meilisearch ทำได้ผ่าน REST API โดยมีคำสั่งหลักสำหรับการเพิ่ม แก้ไข และลบ Documents ซึ่งทุกคำสั่งทำงานแบบ Asynchronous หมายความว่าคำสั่งจะถูกส่งไปยัง Task Queue ก่อน แล้ว Meilisearch จะประมวลผลในพื้นหลัง

การแก้ไข Documents

// อัปเดต Document ด้วย Primary Key
await index.updateDocuments([
  { id: 1, name: 'iPhone 15 Pro', price: 42900 }
]);

// ลบ Document ตาม ID
await index.deleteDocument(1);

// ลบหลาย Document พร้อมกัน
await index.deleteDocuments({ filter: 'price < 1000' });

// ตรวจสอบสถานะ Task
const task = await index.updateDocuments([...]);
const status = await client.getTask(task.taskUid);

การตรวจสอบสถานะ Task

เนื่องจาก Meilisearch ทำงานแบบ Asynchronous การ Index ข้อมูลจำนวนมากอาจใช้เวลาสักครู่ ควรตรวจสอบสถานะ Task เพื่อให้มั่นใจว่า Indexing เสร็จสมบูรณ์ก่อน Search

# ดูรายการ Task ทั้งหมด
curl -H 'Authorization: Bearer MASTER_KEY' \
  'http://127.0.0.1:7700/tasks'

# ดู Task เฉพาะ (แทน TASK_ID ด้วยตัวเลขจริง)
curl -H 'Authorization: Bearer MASTER_KEY' \
  'http://127.0.0.1:7700/tasks/TASK_ID'

การทำ Multi-index Search และ Federation

Meilisearch รองรับการค้นหาข้ามหลาย Index พร้อมกันในคำขอเดียว (Multi-search) ซึ่งเป็น Feature ที่มีประโยชน์มากสำหรับ E-commerce ที่ต้องการค้นหาทั้งสินค้า บทความ และหมวดหมู่ในครั้งเดียว

// ค้นหาหลาย Index พร้อมกัน
const results = await client.multiSearch({
  queries: [
    { indexUid: 'products', q: 'iphone', limit: 5 },
    { indexUid: 'articles', q: 'iphone', limit: 3 },
    { indexUid: 'categories', q: 'smartphone', limit: 3 }
  ]
});

console.log(results.results[0].hits); // ผลลัพธ์จาก products
console.log(results.results[1].hits); // ผลลัพธ์จาก articles

Multi-search ลด Latency ได้มากเพราะส่ง Request เพียงครั้งเดียวแทนที่จะส่งทีละ Index ซึ่งสำคัญมากสำหรับ Search Box บน Frontend ที่ต้องการแสดงผลทันทีที่ผู้ใช้พิมพ์ (Type-as-you-search)

การ Integrate Meilisearch กับ Laravel Scout

ถ้าใช้ Laravel Framework อยู่แล้ว สามารถ Integrate กับ Meilisearch ได้ง่ายๆ ผ่าน Laravel Scout ซึ่งเป็น Package ที่ Laravel พัฒนาขึ้นเพื่อเชื่อมต่อกับ Search Engine ต่างๆ รองรับ Meilisearch ได้โดยตรง ไม่ต้องเขียน Code เชื่อมต่อเอง

ติดตั้ง Laravel Scout และ Meilisearch Driver

composer require laravel/scout
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

ตั้งค่า .env สำหรับ Meilisearch

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=YOUR_MASTER_KEY_HERE

เพิ่ม Searchable Trait ใน Model

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Product extends Model
{
    use Searchable;

    // กำหนด Field ที่จะ Index
    public function toSearchableArray(): array
    {
        return [
            'id'       => $this->id,
            'name'     => $this->name,
            'category' => $this->category,
            'price'    => $this->price,
        ];
    }
}

ค้นหาผ่าน Eloquent

// ค้นหาง่ายๆ ผ่าน Scout
$products = Product::search('iphone')->get();

// ค้นหาพร้อม Filter
$products = Product::search('laptop')
    ->where('category', 'electronics')
    ->paginate(20);

Sync ข้อมูลขึ้น Meilisearch

# Import ข้อมูลทั้งหมดขึ้น Meilisearch
php artisan scout:import "App\Models\Product"

# ลบ Index ทั้งหมด
php artisan scout:flush "App\Models\Product"

การใช้ Laravel Scout ทำให้การ Sync ข้อมูลระหว่าง MySQL กับ Meilisearch เป็นอัตโนมัติ เมื่อสร้างหรือแก้ไขหรือลบ Record ใน Database Scout จะอัปเดต Search Index ให้โดยอัตโนมัติผ่าน Observer ไม่ต้องเรียก API เอง

เปรียบเทียบ Meilisearch กับ Elasticsearch และ Typesense

เมื่อตัดสินใจเลือก Search Engine สำหรับ VPS มีตัวเลือกหลัก 3 ตัวที่นิยมในกลุ่ม Developer ไทยและต่างประเทศ แต่ละตัวมีจุดเด่นต่างกัน

คุณสมบัติ Meilisearch Elasticsearch Typesense
ความง่ายในการตั้งค่า สูง — Binary ไฟล์เดียว ต่ำ — ต้องการ Java, Config ซับซ้อน สูง — คล้าย Meilisearch
RAM ขั้นต่ำ 1 GB 2–4 GB 1 GB
Typo Tolerance ดีมาก (Built-in) ต้องตั้งค่าเอง ดี (Built-in)
ความเหมาะสม E-commerce, Blog, App ขนาดกลาง Log Analytics, Enterprise E-commerce, SaaS

สำหรับ Developer ที่เพิ่งเริ่มต้นหรือโปรเจกต์ขนาดกลาง Meilisearch เป็นตัวเลือกที่ดีที่สุด ตั้งค่าง่าย ใช้ RAM น้อยและให้ผลลัพธ์ที่ดีโดยไม่ต้องปรับแต่งมาก ในขณะที่ Elasticsearch เหมาะกับองค์กรที่ต้องการ Log Analytics และ Aggregation ที่ซับซ้อน

การแก้ไขปัญหาที่พบบ่อยเมื่อใช้ Meilisearch บน VPS

แม้ Meilisearch จะตั้งค่าง่าย แต่มีปัญหาที่พบบ่อยบางอย่างที่ Developer ควรรู้ไว้เพื่อแก้ไขได้รวดเร็วเมื่อเกิดขึ้น

Service ไม่ Start หลังรีบูต

ปัญหานี้มักเกิดจากการที่ไม่ได้ Enable Service ด้วย systemctl ตรวจสอบด้วยคำสั่งดังนี้

# ตรวจว่า Service ถูก Enable หรือเปล่า
sudo systemctl is-enabled meilisearch

# ถ้าตอบ disabled ให้ Enable
sudo systemctl enable meilisearch

# ตรวจดู Error Log ของ Service
sudo journalctl -u meilisearch -n 50 --no-pager

API ส่ง 401 Unauthorized

ปัญหานี้หมายความว่า API Key ที่ส่งไปไม่ถูกต้องหรือ Master Key ที่ตั้งไว้ใน Service File ไม่ตรงกับที่ใช้ในโค้ด ให้ตรวจสอบ Master Key ใน /etc/systemd/system/meilisearch.service และเทียบกับที่ใช้ใน Application

Index ใช้ RAM มากเกินไป

Meilisearch โหลด Index ทั้งหมดขึ้น RAM ถ้า Index มีขนาดใหญ่มาก VPS อาจ OOM (Out Of Memory) ได้ วิธีแก้คือลด Field ที่ Index โดยกำหนด Searchable Attributes เฉพาะที่จำเป็น หรืออัปเกรด RAM ของ VPS

// กำหนดเฉพาะ Field ที่ต้องการ Search
await index.updateSearchableAttributes([
  'name',
  'description',
  'brand'
]);
// Field ที่ไม่ได้ระบุจะยังอยู่ใน Document แต่ไม่ถูก Index
// ทำให้ประหยัด RAM ได้มาก

ผลลัพธ์การค้นหาไม่ตรงความต้องการ

ถ้าผลลัพธ์ที่ได้ไม่ตรงกับที่คาดหวัง ให้ลองปรับ Ranking Rules หรือตั้งค่า Synonyms เพื่อให้ Meilisearch เข้าใจว่าคำบางคำมีความหมายเหมือนกัน เช่น "มือถือ" และ "โทรศัพท์" หรือ "notebook" และ "laptop"

// กำหนด Synonyms
await index.updateSynonyms({
  'มือถือ': ['โทรศัพท์', 'smartphone', 'phone'],
  'laptop': ['notebook', 'โน้ตบุ๊ก'],
  'tv': ['television', 'ทีวี']
});

การอัปเกรด Meilisearch เป็น Version ใหม่

Meilisearch ออก Version ใหม่อย่างสม่ำเสมอ การอัปเกรดทำได้ง่ายๆ โดยการดาวน์โหลด Binary ใหม่มาแทนอันเก่า อย่างไรก็ตามควรอ่าน Release Notes ก่อนทุกครั้งเพราะบาง Version อาจมี Breaking Change ที่กระทบ API หรือโครงสร้าง Index

# หยุด Service ก่อน
sudo systemctl stop meilisearch

# ดาวน์โหลด Binary ใหม่
curl -L https://install.meilisearch.com | sh
sudo mv ./meilisearch /usr/local/bin/meilisearch

# เริ่ม Service ใหม่
sudo systemctl start meilisearch
meilisearch --version

ถ้า Version ใหม่มีการเปลี่ยน Database Format อาจต้อง Re-index ข้อมูลใหม่ทั้งหมด ดังนั้นจึงควรทำ Backup Data ก่อนอัปเกรดทุกครั้ง และทดสอบใน Server ทดสอบก่อน Production เสมอ

สรุป: Meilisearch เป็น Search Engine ที่ตั้งค่าง่าย รวดเร็ว และ Integrate กับ PHP และ Laravel ได้ดีเยี่ยม เหมาะมากสำหรับ E-commerce, Blog และ Web Application ทั่วไปที่ต้องการ Full-text Search ที่ดีกว่า SQL LIKE โดยใช้ VPS สเปคไม่สูง เพียง RAM 2 GB ก็เพียงพอสำหรับการใช้งานระดับกลาง

ต้องการ VPS สำหรับ Search Engine?

VPS Linux Ubuntu เริ่มต้น 500 บาท/เดือน พร้อม Full Root Access และ SSD รัน Meilisearch, Elasticsearch หรือ Search Stack ใดก็ได้

ดูแพ็กเกจ VPS