Triển khai Chức năng Tìm kiếm: So sánh PostgreSQL Full-Text, Algolia và Meilisearch
Khi dữ liệu của bạn tăng lên, câu lệnh LIKE đơn giản không còn đáp ứng được nhu cầu về hiệu suất và độ chính xác. Bài viết này sẽ phân tích chi tiết ba giải pháp tìm kiếm hàng đầu là PostgreSQL Full-Text Search, Algolia và Meilisearch, cùng với hướng dẫn lựa chọn công cụ phù hợp nhất cho ứng dụng của bạn.

Tìm kiếm thường khó khăn hơn vẻ bề ngoài của nó. Câu lệnh LIKE '%query%' hoạt động tốt cho đến khi:
- Bảng dữ liệu của bạn đạt 100.000 dòng (dẫn đến quét tuần tự - Seq Scan, gây chậm).
- Người dùng gõ sai chính tả (không có tính năng khớp mờ - fuzzy matching).
- Người dùng tìm kiếm bằng nhiều ngôn ngữ khác nhau.
- Người dùng mong đợi kết quả được sắp xếp theo mức độ liên quan.
Đó là lúc bạn cần một giải pháp tìm kiếm thực thụ.
Tùy chọn 1: PostgreSQL Full-Text Search
Đây là lựa chọn đủ tốt cho hầu hết các ứng dụng mà không cần thêm hạ tầng phức tạp.
-- Thêm cột tsvector để tìm kiếm nhanh
ALTER TABLE posts ADD COLUMN search_vector tsvector;
-- Populate nó với dữ liệu
UPDATE posts SET search_vector =
to_tsvector('english',
coalesce(title, '') || ' ' || coalesce(content, '') || ' ' || coalesce(tags::text, '')
);
-- Tự động cập nhật khi dữ liệu thay đổi
CREATE FUNCTION update_search_vector() RETURNS trigger AS $$
BEGIN
NEW.search_vector := to_tsvector('english',
coalesce(NEW.title, '') || ' ' || coalesce(NEW.content, '')
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER posts_search_vector_update
BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
-- Tạo chỉ mục GIN để tìm kiếm nhanh
CREATE INDEX posts_search_idx ON posts USING GIN(search_vector);
// Tìm kiếm có tính điểm xếp hạng (ranking)
async function searchPosts(query: string) {
return prisma.$queryRaw`
SELECT
id,
title,
ts_rank(search_vector, plainto_tsquery('english', ${query})) AS rank,
ts_headline('english', content, plainto_tsquery('english', ${query}),
'MaxWords=50, MinWords=20'
) AS excerpt
FROM posts
WHERE search_vector @@ plainto_tsquery('english', ${query})
ORDER BY rank DESC
LIMIT 20
`;
}
Ưu điểm: Không cần hạ tầng bổ sung, tính nhất quán giao dịch (transactionally consistent) cao.
Hạn chế: Không chấp nhận sai chính tả, thiên về tiếng Anh, khả năng tùy chỉnh độ liên kết còn hạn chế.
Tùy chọn 2: Algolia
Dịch vụ tìm kiếm được quản lý (Managed search). Mang lại trải nghiệm người dụng (UX) tốt nhất trong ngành.
npm install algoliasearch
import algoliasearch from 'algoliasearch';
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_API_KEY!);
const index = client.initIndex('posts');
// Lưu tài liệu vào chỉ mục
await index.saveObject({
objectID: post.id,
title: post.title,
content: post.content.slice(0, 10000), // Algolia có giới hạn kích thước
author: post.author.name,
tags: post.tags,
publishedAt: post.publishedAt.getTime(),
});
// Thực hiện tìm kiếm
const { hits } = await index.search(query, {
attributesToRetrieve: ['title', 'author', 'tags'],
attributesToHighlight: ['title', 'content'],
hitsPerPage: 20,
typoTolerance: true, // Tìm được 'recat' -> React
});
Ưu điểm: Tốc độ cực nhanh (dưới 10ms), chấp nhận sai chính tả tốt, bảng điều khiển phân tích dữ liệu (dashboard) xuất sắc.
Chi phí: Gói miễn phí: 10.000 tìm kiếm/tháng, 10.000 bản ghi. Chi phí tăng rất nhanh sau khi vượt quá giới hạn.
Tùy chọn 3: Meilisearch
Lựa thay thế mã nguồn mở cho Algolia. Bạn có thể tự chủ server (self-host) hoặc sử dụng Meilisearch Cloud.
# Tự chủ với Docker
docker run -d -p 7700:7700 -v meilidata:/meili_data getmeili/meilisearch
npm install meilisearch
import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({ host: 'http://localhost:7700', apiKey: process.env.MEILI_API_KEY });
const index = client.index('posts');
// Cấu hình các thuộc tính tìm kiếm và xếp hạng
await index.updateSettings({
searchableAttributes: ['title', 'content', 'tags'],
rankingRules: [
'words',
'typo',
'proximity',
'attribute',
'sort',
'exactness'
],
typoTolerance: {
enabled: true,
minWordSizeForTypos: { oneTypo: 5, twoTypos: 9 },
},
});
// Thêm tài liệu
await index.addDocuments(posts.map(p => ({ id: p.id, ...p })));
// Tìm kiếm
const results = await index.search(query, {
limit: 20,
attributesToHighlight: ['title', 'content'],
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
});
Ưu điểm: Chấp nhận sai chính tả, tốc độ nhanh, mã nguồn mở, chi phí dễ dự đoán.
Hạn chế: Ít trưởng thành hơn Algolia, hệ sinh thái (ecosystem) còn nhỏ hơn.
Giữ cho Dữ liệu Tìm kiếm Đồng bộ
Khi sử dụng các giải pháp bên ngoài như Algolia hay Meilisearch, việc đồng bộ dữ liệu từ cơ sở dữ liệu chính sang chỉ mục tìm kiếm là rất quan trọng.
// Sau bất kỳ thao tác thay đổi bài viết nào, đồng bộ hóa sang chỉ mục tìm kiếm
async function createPost(data: PostInput) {
const post = await db.posts.create({ data });
// Cập nhật chỉ mục bất đồng bộ (không chặn phản hồi)
setImmediate(async () => {
await searchIndex.saveObject({
objectID: post.id,
title: post.title,
content: post.content,
});
});
return post;
}
// Hoặc sử dụng background job để độ tin cậy cao hơn
await queue.add('sync-search', { postId: post.id, operation: 'upsert' });
Hướng dẫn Lựa chọn
| Tình huống | Khuyến nghị |
|---|---|
| < 100k bản ghi, tìm kiếm cơ bản | PostgreSQL FTS |
| Cần chấp nhận sai chính tả | Meilisearch (tự chủ server) |
| Cần UX tốt nhất, có ngân sách | Algolia |
| Mã nguồn mở, chi phí dự đoán được | Meilisearch Cloud |
| Nội dung đa ngôn ngữ | Elasticsearch hoặc Meilisearch |
Tích hợp tìm kiếm với các adapter cho PostgreSQL FTS và Meilisearch có thể tìm thấy trong Whoff Agents AI SaaS Starter Kit.



