LIKE và trigram tìm theo chuỗi ký tự thô — chúng không biết queries và query là cùng một từ, cũng không biết the là từ vô nghĩa để bỏ qua. Full-text search (FTS) của PostgreSQL làm điều khác hẳn: nó tìm theo từ đã chuẩn hóa về gốc, hiểu ngữ pháp cơ bản của ngôn ngữ. Bài này đo thật FTS so với LIKE, chỉ ra vì sao FTS không index còn chậm hơn LIKE, và cách một cột tsvector cộng GIN index biến nó thành công cụ tìm kiếm nghiêm túc.

tsvector: văn bản thành danh sách từ chuẩn hóa

Nền tảng của FTS là kiểu tsvector — kết quả của việc "xử lý" một đoạn văn bản: tách từ, đưa mỗi từ về gốc (stemming), và loại các stop word (từ quá phổ biến, vô nghĩa cho tìm kiếm):

SELECT to_tsvector('english', 'Optimizing slow queries using EXPLAIN');
-- 'explain':5 'optim':1 'queri':3 'slow':2 'use':4

Chú ý: queries thành queri, optimizing thành optim, using thành use — tất cả về gốc. Và:

SELECT to_tsvector('english', 'The quick brown fox');
-- 'brown':3 'fox':4 'quick':2      ← 'the' bị loại (stop word)

the biến mất hoàn toàn. Đây là thứ LIKE không bao giờ làm được.

Ảnh chụp đoạn mã SQL nền tối minh hoạ full-text search tìm theo từ và gốc từ không phải chuỗi thô, tsvector văn bản thành danh sách từ đã chuẩn hóa SELECT to_tsvector english Optimizing slow queries using EXPLAIN kết quả explain 5 optim 1 queri 3 slow 2 use 4 mỗi từ về gốc queries thành queri optimizing thành optim kèm vị trí, to_tsvector english The quick brown fox brown 3 fox 4 quick 2 the stop word bị loại, tsquery và toán tử at at khớp theo từ SELECT sao FROM bai WHERE to_tsvector english noi_dung at at to_tsquery english query, query khớp cả queries querying cùng gốc queri LIKE phần trăm query phần trăm thì không khớp queries FTS hiểu ngôn ngữ, tăng tốc cột tsvector sẵn cộng GIN index ALTER TABLE bai ADD COLUMN tsv tsvector GENERATED ALWAYS AS to_tsvector english noi_dung STORED CREATE INDEX idx_tsv ON bai USING gin tsv SELECT sao FROM bai WHERE tsv at at to_tsquery english query, toán tử truy vấn và xếp hạng liên quan slow và query AND slow hoặc query OR không query NOT slow đứng liền query tìm cụm ORDER BY ts_rank tsv q DESC xếp theo độ liên quan

Hình 1: to_tsvector đưa văn bản về danh sách từ gốc (stemming) và bỏ stop word. to_tsquery cùng @@ khớp theo từ, nên query tìm được queries. Tăng tốc bằng cột tsvector STORED + GIN.

tsquery và @@: khớp theo từ, hiểu ngôn ngữ

Tìm kiếm dùng toán tử @@ giữa tsvector và tsquery:

SELECT * FROM bai
WHERE to_tsvector('english', noi_dung) @@ to_tsquery('english', 'query');

Vì cả hai vế đều được stem về gốc queri, truy vấn query khớp mọi biến thể: queries, querying, queried. So sánh trực tiếp:

  • Tìm query bằng FTS: khớp Optimizing slow queries — TRUE.
  • 'Optimizing slow queries' LIKE '%query%': FALSE — LIKE chỉ so ký tự, query không phải chuỗi con của queries.

Đây là khác biệt bản chất: FTS hiểu rằng người dùng gõ query là muốn tìm mọi dạng của từ đó, còn LIKE mù về ngữ nghĩa.

Đo thật: FTS không index còn chậm hơn LIKE

Điều quan trọng nhất về hiệu năng, đo trên bảng bai 2 triệu tài liệu:

Ảnh chụp bảng kết quả đo thật nền tối full-text search bai 2 triệu tài liệu PostgreSQL 16 tìm query khớp 800 nghìn dòng EXPLAIN ANALYZE shared_buffers 128MB, ILIKE phần trăm queries phần trăm chuỗi thô Parallel Seq Scan 404 mili giây, to_tsvector noi_dung at at to_tsquery không index Parallel Seq Scan tính tsvector mỗi dòng 2490 mili giây, cột tsv GENERATED cộng GIN index Bitmap Index Scan Bitmap Heap khoảng 90 mili giây, không index FTS còn chậm hơn LIKE phải tính to_tsvector cho từng dòng 2,49 giây FTS chỉ đáng khi có cột tsvector lưu sẵn cộng GIN index 2490 mili giây xuống 90 mili giây khoảng 28 lần nhanh hơn, điều LIKE không làm được hiểu ngôn ngữ to_tsvector english Optimizing slow queries using EXPLAIN bằng explain 5 optim 1 queri 3 slow 2 use 4 mỗi từ về gốc to_tsvector english The quick brown fox bằng brown 3 fox 4 quick 2 the bị loại, tìm query tsvector khớp cùng gốc queri t TRUE queries LIKE phần trăm query phần trăm f FALSE LIKE mù nghĩa, AND nhiều từ cộng xếp hạng ts_rank WHERE tsv at at to_tsquery english slow và query ORDER BY ts_rank tsv q DESC LIMIT 5 Bitmap Index Scan top-N heapsort khoảng 91 mili giây xếp theo độ liên quan, cốt lõi FTS tìm theo từ đã chuẩn hóa gốc stemming và bỏ stop word điều LIKE trigram không làm được nhưng nó chỉ nhanh khi có cột tsvector STORED cộng GIN index tính tsvector lúc chạy còn chậm hơn cả LIKE kèm ts_rank để xếp hạng

Hình 2: ILIKE '%queries%' seq scan 404 ms; FTS không index (to_tsvector(...) @@ ...) còn chậm hơn — 2.490 ms vì tính tsvector cho mỗi dòng; cột tsvector STORED + GIN index đưa về ~90 ms (~28 lần nhanh hơn).

  • ILIKE '%queries%': Seq Scan, 404 ms.
  • FTS không index (to_tsvector(noi_dung) @@ to_tsquery('query')): Seq Scan, 2.490 ms — chậm hơn cả LIKE. Vì PostgreSQL phải tính to_tsvector cho từng dòng ngay lúc chạy, đắt hơn nhiều so với so chuỗi.
  • Cột tsvector STORED + GIN index: Bitmap Index Scan, ~90 ms.

Bài học then chốt: FTS chỉ đáng dùng khi có cột tsvector lưu sẵn và GIN index. Viết WHERE to_tsvector(cột) @@ ... trực tiếp là tệ nhất trong ba lựa chọn. Cách đúng là dựng cột sinh tự động và index nó:

ALTER TABLE bai ADD COLUMN tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('english', noi_dung)) STORED;
CREATE INDEX idx_bai_tsv ON bai USING gin(tsv);
SELECT * FROM bai WHERE tsv @@ to_tsquery('english', 'query');

Cột GENERATED ... STORED tự cập nhật mỗi khi noi_dung đổi, nên bạn không phải lo đồng bộ tay.

Toán tử truy vấn và xếp hạng

tsquery hỗ trợ ghép điều kiện phong phú, và ts_rank xếp kết quả theo độ liên quan:

WHERE tsv @@ to_tsquery('english', 'slow & query')   -- AND
ORDER BY ts_rank(tsv, to_tsquery('english','slow & query')) DESC LIMIT 5;

Các toán tử: & (AND), | (OR), ! (NOT), <-> (hai từ đứng liền nhau — tìm cụm). Đo thật truy vấn AND + xếp hạng: Bitmap Index Scan → top-N heapsort, ~91 ms. ts_rank cho điểm dựa trên tần suất và vị trí từ khớp, để bạn trả về kết quả liên quan nhất trước — thứ mà LIKE hoàn toàn không có.

Đánh đổi cần cân nhắc

FTS mạnh về từ, yếu về khớp gần đúng và lỗi chính tả. Nó tìm theo từ chuẩn hóa nên tuyệt cho tìm tài liệu, nhưng không tìm được PostgreSQL khi người dùng gõ postgre (thiếu ký tự) hay gõ sai chính tả. Cho tìm gần đúng, tự động hoàn thành, chịu lỗi gõ, trigram (pg_trgm) mới là công cụ — hai thứ bổ sung nhau, không thay thế.

Chọn đúng cấu hình ngôn ngữ. to_tsvector('english', ...) dùng luật stemming và stop word của tiếng Anh. Với tiếng Việt, tiếng không có bộ stemmer tích hợp, thường phải dùng cấu hình simple (chỉ tách từ, không stem, không bỏ stop word) hoặc một extension riêng. Dùng sai cấu hình cho ngôn ngữ khác sẽ stem bậy hoặc bỏ nhầm từ.

Cột STORED tốn dung lượng và chi phí ghi. Cột tsvector sinh tự động chiếm chỗ và phải tính lại mỗi lần cột nguồn đổi. Với bảng ghi rất nhiều mà tìm kiếm ít, cân nhắc. Nhưng trong hầu hết ca (tìm nhiều hơn ghi), đánh đổi này rất đáng.

Ba ý mang về

  1. FTS tìm theo từ đã chuẩn hóa gốc và bỏ stop word — to_tsvector đưa queries về queri và loại the, nên query khớp cả queries, querying; điều LIKE không làm được (đo thật LIKE '%query%' không khớp queries).
  2. FTS chỉ nhanh khi có cột tsvector STORED + GIN index: đo thật, to_tsvector(cột) @@ ... không index còn chậm hơn LIKE (2.490 ms so với 404 ms) vì tính lại mỗi dòng; cột sinh tự động + GIN đưa về ~90 ms.
  3. Dùng ts_rank để xếp hạng liên quan và & | ! <-> để ghép truy vấn — FTS mạnh cho tìm tài liệu theo từ, nhưng cho khớp gần đúng và chịu lỗi chính tả thì trigram mới là công cụ đúng, hai thứ bổ sung nhau.

Phần sau ta quay lại công cụ vừa nhắc, đào sâu hơn: Phần sau mổ xẻ trigram pg_trgm cho tìm gần đúng — cách nó đo độ tương tự chuỗi, tìm được cả khi gõ sai chính tả, và index GiST/GIN cho nó.