PostgreSQL Full-Text Search: Tìm kiếm tiếng Việt “ngon” bổ rẻ không cần Elasticsearch

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Làm tìm kiếm mà không cần Elasticsearch? Hoàn toàn khả thi!

Câu chuyện thường gặp là khi cần làm tính năng search, anh em hay nghĩ ngay đến Elasticsearch (ES) hay Algolia. Phải thừa nhận chúng rất mạnh. Tuy nhiên, nếu bạn chỉ đang chạy một con VPS 2GB RAM, việc gánh thêm cụm máy chủ Java nặng nề của ES là một cực hình. Chưa kể, bạn còn phải đau đầu giải quyết bài toán đồng bộ dữ liệu (sync data) giữa database chính và bộ search engine.

Kinh nghiệm thực tế cho thấy PostgreSQL là “vũ khí bí mật” cực kỳ lợi hại. Với bộ công cụ tsvectortsquery tích hợp sẵn, bạn hoàn toàn có thể build một bộ máy tìm kiếm tiếng Việt chuẩn chỉ. Mọi thứ gói gọn trong một database duy nhất, không rườm rà, không tốn thêm RAM.

Quick Start: Chạy thử tìm kiếm trong 5 phút

Thay vì ngồi “nhai” lý thuyết, anh em hãy mở terminal hoặc pgAdmin lên. Hãy thử thực thi các câu lệnh dưới đây để thấy tốc độ phản hồi của nó.

-- 1. Tạo table demo
CREATE TABLE blog_posts (
    id SERIAL PRIMARY KEY,
    title TEXT,
    content TEXT
);

-- 2. Chèn dữ liệu mẫu
INSERT INTO blog_posts (title, content) VALUES 
('Học lập trình Python', 'Python là ngôn ngữ cực kỳ mạnh mẽ cho AI'),
('Cơ bản về PostgreSQL', 'PostgreSQL hỗ trợ tìm kiếm toàn văn rất tốt'),
('Lộ trình trở thành Dev', 'Bạn cần học SQL, cấu trúc dữ liệu và giải thuật');

-- 3. Query tìm kiếm
SELECT * FROM blog_posts 
WHERE to_tsvector('simple', title || ' ' || content) @@ to_tsquery('simple', 'lập & trình');

Phép toán @@ chính là chìa khóa. Nó so khớp từ khóa “lập” và “trình” trong khối dữ liệu đã được vector hóa. Kết quả trả về gần như tức thì với tập dữ liệu nhỏ.

Giải mã tsvector và tsquery

Để làm chủ Full-Text Search (FTS), bạn chỉ cần nắm vững hai khái niệm cốt lõi.

1. tsvector (Text Search Vector)

Hãy tưởng tượng tsvector là một bản danh sách các từ (lexemes) đã được làm sạch và chuẩn hóa. Nó loại bỏ các từ vô nghĩa và ghi nhớ chính xác vị trí của từng từ.

Ví dụ: SELECT to_tsvector('simple', 'Chào anh em itfromzero');

Kết quả trả về: 'anh':2 'chào':1 'em':3 'itfromzero':4. Nhờ danh sách định vị này, Postgres không cần quét qua từng ký tự khi bạn tìm kiếm nữa.

2. tsquery (Text Search Query)

Đây là ngôn ngữ truy vấn. Bạn có thể sử dụng các toán tử logic linh hoạt:

  • & (AND): Phải chứa cả hai từ.
  • | (OR): Chứa một trong hai là đủ.
  • ! (NOT): Loại trừ kết quả có chứa từ này.
  • <-> (FOLLOWED BY): Tìm cụm từ chính xác theo thứ tự (rất quan trọng cho từ ghép tiếng Việt).

Lưu ý về bộ từ điển ‘simple’

Tại sao lại dùng ‘simple’ cho tiếng Việt? Khác với tiếng Anh có biến thể từ (như run/running), tiếng Việt là ngôn ngữ đơn lập. Việc dùng bộ từ điển mặc định sẽ giúp giữ nguyên cấu trúc từ, tránh việc Postgres tự ý cắt đuôi làm sai lệch ý nghĩa.

Nâng cao: Tối ưu hiệu suất cho hàng trăm nghìn record

Vấn đề nảy sinh khi table của bạn lớn dần. Nếu cứ để Postgres quét toàn bộ table (Seq Scan), database sẽ sớm “quá tải”. Lúc này, Index là cứu cánh duy nhất.

1. GIN Index – Tăng tốc vượt trội

GIN (Generalized Inverted Index) là loại index chuyên biệt cho tìm kiếm toàn văn. Thay vì index theo dòng, nó index theo từng từ đơn lẻ.

CREATE INDEX idx_fts_post ON blog_posts 
USING GIN (to_tsvector('simple', title || ' ' || content));

Với table khoảng 100.000 dòng, GIN Index có thể kéo tốc độ query từ vài giây xuống còn dưới 10ms. Một con số cực kỳ ấn tượng.

2. Xử lý tìm kiếm không dấu

Người dùng Việt Nam thường có thói quen gõ không dấu. Để xử lý việc này, hãy sử dụng extension unaccent.

CREATE EXTENSION IF NOT EXISTS unaccent;

-- Cách làm chuyên nghiệp: Tạo function và index đồng bộ
CREATE OR REPLACE FUNCTION f_unaccent_vector(text) RETURNS tsvector AS $$
    SELECT to_tsvector('simple', unaccent($1));
$$ LANGUAGE SQL IMMUTABLE;

CREATE INDEX idx_fts_unaccent ON blog_posts USING GIN (f_unaccent_vector(title || ' ' || content));

3. Generated Columns – Tuyệt chiêu từ Postgres 12

Thay vì tính toán lại vector mỗi lần query, bạn nên lưu nó vào một cột riêng. Việc này giúp tiết kiệm CPU đáng kể.

ALTER TABLE blog_posts 
ADD COLUMN search_vector tsvector 
GENERATED ALWAYS AS (to_tsvector('simple', unaccent(title) || ' ' || unaccent(content))) STORED;

CREATE INDEX idx_search_vector ON blog_posts USING GIN (search_vector);

Lúc này, câu lệnh tìm kiếm chỉ đơn giản là: SELECT * FROM blog_posts WHERE search_vector @@ to_tsquery('simple', 'python');

Mẹo thực chiến để UI/UX xịn hơn

Sau nhiều dự án triển khai, mình rút ra 3 thủ thuật giúp tính năng search chuyên nghiệp hơn:

  • Trọng số (Weighting): Hãy ưu tiên kết quả tìm thấy ở Tiêu đề (Weight A) cao hơn ở Nội dung (Weight B). Người dùng sẽ tìm thấy thứ họ cần nhanh hơn.
  • Xếp hạng (Ranking): Sử dụng hàm ts_rank để đưa những bài viết có mật độ từ khóa cao lên đầu trang.
  • Làm nổi bật (Highlight): Dùng hàm ts_headline để tự động bôi đậm (tag <b>) từ khóa trong đoạn văn bản trả về.

Đừng vội vàng cài đặt những công cụ phức tạp khi chưa tận dụng hết sức mạnh của database hiện tại. Triết lý KISS (Keep It Simple, Stupid) luôn đúng trong kỹ thuật. Dùng tốt FTS của PostgreSQL giúp hệ thống gọn nhẹ, dễ bảo trì và cực kỳ ổn định.

Nếu bạn gặp khó khăn khi cấu hình hoặc muốn tối ưu sâu hơn cho tập dữ liệu lớn, hãy để lại comment bên dưới. Team itfromzero sẽ cùng bạn gỡ rối!

Share: