Hướng dẫn tích hợp Meilisearch vào Node.js: Full-text Search dưới 50ms và Chịu lỗi chính tả thay thế Elasticsearch

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Bài toán tìm kiếm: Khi SQL LIKE và Elasticsearch khiến bạn đau đầu

Khi mới làm tính năng tìm kiếm cho website e-commerce hay blog, phản xạ đầu tiên của nhiều anh em là dùng LIKE '%keyword%' trong PostgreSQL hoặc MySQL. Cách này chạy ổn với vài nghìn dòng. Nhưng khi bảng chạm mốc 500.000 bản ghi, câu query lập tức ngốn cả giây. Full table scan làm nghẽn luôn connection pool của database.

Phương án nâng cấp tiếp theo thường là Elasticsearch. Giải pháp này mạnh, nhưng vận hành lại là một cực hình. Bạn phải vật lộn với cấu hình JVM, mapping phức tạp và cụm cluster ngốn tối thiểu 4GB – 8GB RAM chỉ ở trạng thái chờ. Với các dự án vừa và nhỏ, chi phí hạ tầng lẫn công sức bảo trì này thực sự quá tải.

Meilisearch ra đời để giải quyết đúng khoảng trống đó. Được viết bằng Rust, engine này chỉ ngốn vài trăm MB RAM, phản hồi truy vấn dưới 50ms và tự động xử lý lỗi gõ sai (typo-tolerance) ngay khi vừa cài đặt mà không bắt bạn cấu hình analyzer phức tạp.

Khái niệm cốt lõi trong Meilisearch

Trước khi gõ code, bạn chỉ cần nắm 5 thuật ngữ then chốt sau:

  • Index: Tương đương table trong SQL hoặc collection trong MongoDB. Đây là nơi chứa các documents cùng loại.
  • Document: Một bản ghi JSON. Mỗi document bắt buộc phải có một khóa chính duy nhất (primary key, mặc định là id).
  • Typo-tolerance: Cơ chế tự sửa lỗi chính tả khi người dùng gõ sai. Ví dụ: gõ “iphoen” vẫn ra “iPhone”. Engine tính toán khoảng cách Levenshtein ngay lúc query mà không làm chậm tốc độ.
  • Searchable Attributes: Danh sách các trường văn bản mà engine sẽ quét từ khóa qua.
  • Filterable & Sortable Attributes: Các trường cho phép lọc (như category, status) hoặc sắp xếp (theo price, createdAt).

Thực hành: Tích hợp Meilisearch vào Node.js

Bước 1: Khởi chạy Meilisearch bằng Docker

Chạy Docker là cách nhanh nhất để dựng môi trường local. Bạn mở terminal và gõ lệnh sau:

docker run -d --name meilisearch \
  -p 7700:7700 \
  -e MEILI_MASTER_KEY=my_secure_master_key_123 \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch:v1.7

Sau khi container start, bạn kiểm tra trạng thái bằng cURL: curl http://localhost:7700/health. Nếu nhận về {"status":"available"} là sẵn sàng.

Bước 2: Cài đặt SDK và khởi tạo kết nối

Tạo thư mục project và cài SDK chính thức:

mkdir node-meilisearch-demo && cd node-meilisearch-demo
npm init -y
npm install meilisearch dotenv

Tạo file meiliClient.js để khởi tạo client kết nối:

// meiliClient.js
const { MeiliSearch } = require('meilisearch');

const client = new MeiliSearch({
  host: 'http://localhost:7700',
  apiKey: 'my_secure_master_key_123',
});

module.exports = client;

Bước 3: Tạo Index và cấu hình Typo-tolerance

Tiếp theo, tạo file setupIndex.js để cấu hình index products:

// setupIndex.js
const client = require('./meiliClient');

async function configureIndex() {
  const index = client.index('products');

  // Chỉ định các trường tìm kiếm theo thứ tự ưu tiên
  await index.updateSearchableAttributes([
    'name',
    'description',
    'category'
  ]);

  // Chỉ định các trường dùng để lọc và sắp xếp
  await index.updateFilterableAttributes(['category', 'inStock']);
  await index.updateSortableAttributes(['price']);

  // Tinh chỉnh khoảng cách gõ sai
  await index.updateTypoTolerance({
    enabled: true,
    minWordSizeForTypos: {
      oneTypo: 4,  // Từ từ 4 ký tự cho phép sai 1 ký tự
      twoTypos: 8  // Từ từ 8 ký tự cho phép sai 2 ký tự
    },
    disableOnWords: [],
    disableOnAttributes: []
  });

  console.log('Cấu hình index thành công!');
}

configureIndex();

Chạy script cấu hình: node setupIndex.js.

Bước 4: Nạp dữ liệu mẫu (Indexing Documents)

Tạo file seedData.js để đẩy dữ liệu sản phẩm vào engine:

// seedData.js
const client = require('./meiliClient');

const sampleProducts = [
  {
    id: 'prod_1',
    name: 'Bàn phím cơ không dây Keychron K2',
    description: 'Bàn phím cơ layout 75%, kết nối Bluetooth hoặc Type-C, switch Gateron.',
    category: 'Phụ kiện máy tính',
    price: 1850000,
    inStock: true
  },
  {
    id: 'prod_2',
    name: 'Chuột không dây Logitech MX Master 3S',
    description: 'Chuột công thái học cao cấp, cảm biến 8000 DPI, cuộn vô cực MagSpeed.',
    category: 'Phụ kiện máy tính',
    price: 2450000,
    inStock: true
  },
  {
    id: 'prod_3',
    name: 'Màn hình Dell UltraSharp U2723QE 4K',
    description: 'Màn hình đồ họa 27 inch IPS Black, độ phân giải 4K, hỗ trợ Type-C 90W.',
    category: 'Màn hình',
    price: 12900000,
    inStock: false
  }
];

async function seed() {
  const index = client.index('products');
  const response = await index.addDocuments(sampleProducts);
  console.log('Enqueued task UID:', response.taskUid);
}

seed();

Cơ chế nạp dữ liệu của Meilisearch là bất đồng bộ. Lệnh addDocuments trả về taskUid ngay lập tức để ứng dụng của bạn không bị block, trong khi worker ngầm bên dưới tiếp tục đánh index.

Bước 5: Viết hàm Search với Filter và Highlight từ khóa

Tạo file search.js để kiểm tra khả năng tìm kiếm thực tế:

// search.js
const client = require('./meiliClient');

async function searchProducts(keyword, categoryFilter = null) {
  const index = client.index('products');

  const searchParams = {
    limit: 10,
    attributesToHighlight: ['name', 'description'],
    highlightPreTag: '<mark>',
    highlightPostTag: '</mark>',
  };

  if (categoryFilter) {
    searchParams.filter = `category = "${categoryFilter}"`;
  }

  const results = await index.search(keyword, searchParams);
  
  console.log(`Tìm thấy ${results.estimatedTotalHits} kết quả trong ${results.processingTimeMs}ms:\n`);
  results.hits.forEach((item, idx) => {
    console.log(`${idx + 1}. [${item.name}] - Giá: ${item.price.toLocaleString('vi-VN')} VND`);
    console.log(`   Khớp nội dung: ${item._formatted.name}`);
  });
}

// Cố tình gõ sai chính tả: "Logitehc" thay vì "Logitech"
searchProducts('Logitehc');

Chạy thử nghiệm: node search.js. Meilisearch trả về ngay chuột Logitech MX Master 3S trong vỏn vẹn 2ms đến 4ms, kèm sẵn cặp thẻ <mark> để frontend render highlight lên giao diện.

Trong một dự án e-commerce gần đây của team mình với hơn 120.000 SKU sản phẩm, việc chuyển từ PostgreSQL ILIKE sang Meilisearch đã giảm search latency từ 850ms xuống chỉ còn 12ms. Quan trọng hơn, team chỉ mất đúng 2 ngày để hoàn thiện toàn bộ tính năng tìm kiếm thay vì mất hàng tuần config Elasticsearch cluster.

Tổng kết

Meilisearch lấp đầy khoảng cách giữa sự chậm chạp của SQL LIKE và sự phức tạp của Elasticsearch. Với cú pháp SDK gọn nhẹ, khả năng chịu lỗi gõ sai nhạy bén và mức ngốn RAM cực thấp (chỉ ~150MB cho vài chục nghìn docs), đây là lựa chọn hàng đầu cho backend Node.js. Hãy thử spin-up một container và tích hợp vào dự án của bạn ngay hôm nay.

Share: