Tìm kiếm toàn văn PostgreSQL có thể đáp ứng nhiều ứng dụng. Dùng quy tắc quyết định đơn giản, truy vấn khởi tạo và checklist lập chỉ mục để biết khi nào cần thêm engine tìm kiếm.

Hầu hết mọi người không yêu cầu “full-text search” theo thuật ngữ kỹ thuật. Họ muốn một hộp tìm kiếm cảm thấy nhanh và tìm ra ý họ ngay ở trang đầu. Nếu kết quả chậm, nhiều rác, hoặc sắp xếp kỳ lạ, người dùng không quan tâm bạn dùng PostgreSQL full-text search hay engine khác. Họ chỉ ngừng tin tưởng tìm kiếm.
Đây là một quyết định: giữ tìm kiếm trong Postgres hay thêm engine tìm kiếm chuyên dụng. Mục tiêu không phải là độ liên quan hoàn hảo. Mà là một chuẩn cơ bản vững chắc, triển khai nhanh, dễ vận hành và đủ tốt cho cách người dùng thực sự dùng app của bạn.
Với nhiều ứng dụng, PostgreSQL full-text search đủ dùng trong thời gian dài. Nếu bạn có vài trường văn bản (title, description, notes), xếp hạng cơ bản và vài bộ lọc (status, category, tenant), Postgres có thể xử lý mà không cần hạ tầng thêm. Bạn có ít thành phần hơn để duy trì, backup đơn giản hơn và ít gặp câu hỏi “tại sao tìm kiếm sập mà app vẫn chạy?” hơn.
“Đủ” thường có nghĩa bạn có thể đạt ba mục tiêu cùng lúc:
Một ví dụ cụ thể: dashboard SaaS nơi người dùng tìm dự án theo tên và ghi chú. Nếu truy vấn như “onboarding checklist” trả về dự án đúng trong top 5, dưới 1 giây, và bạn không liên tục tinh chỉnh analyzer hay reindex, đó là “đủ.” Khi bạn không đạt được những mục tiêu đó mà không làm phức tạp hệ thống, đó là lúc so sánh “tìm kiếm tích hợp so với engine tìm kiếm” thực sự có ý nghĩa.
Các nhóm thường mô tả tìm kiếm bằng tính năng, không phải kết quả. Việc hữu ích là chuyển từng tính năng thành chi phí xây dựng, tinh chỉnh và giữ đáng tin cậy.
Những yêu cầu ban đầu thường nghe như: chịu lỗi gõ sai, facets và bộ lọc, highlight, xếp hạng “thông minh” và autocomplete. Ở phiên bản đầu, tách ra thứ cần thiết và thứ tùy chọn. Hộp tìm kiếm cơ bản thường chỉ cần tìm mục liên quan, xử lý các dạng từ thông dụng (số nhiều, thì), tôn trọng bộ lọc đơn giản và nhanh khi bảng lớn lên. Đó chính là nơi PostgreSQL full-text search thường phù hợp.
Postgres nổi bật khi nội dung của bạn nằm trong các trường văn bản bình thường và bạn muốn tìm kiếm gần dữ liệu: bài trợ giúp, bài blog, ticket hỗ trợ, tài liệu nội bộ, tiêu đề và mô tả sản phẩm, hoặc ghi chú trên hồ sơ khách hàng. Đây chủ yếu là các bài toán “tìm bản ghi đúng”, không phải “xây dựng sản phẩm tìm kiếm”.
Các thứ hay muốn thêm là nơi phức tạp len lỏi. Chịu lỗi gõ sai và autocomplete phong phú thường đẩy bạn tới công cụ ngoài. Facets có thể làm được trong Postgres, nhưng nếu bạn muốn nhiều facets, phân tích sâu và đếm nhanh trên dataset khổng lồ, engine chuyên dụng trở nên hấp dẫn hơn.
Chi phí ẩn hiếm khi là phí license. Là hệ thống thứ hai. Khi thêm engine tìm kiếm, bạn còn phải thêm đồng bộ dữ liệu và backfills (và lỗi kèm theo), giám sát và nâng cấp, hỗ trợ “tại sao tìm kiếm hiển thị dữ liệu cũ?”, và hai bộ nút điều chỉnh độ liên quan.
Nếu không chắc, bắt đầu với Postgres, triển khai thứ đơn giản, và chỉ thêm engine khi có yêu cầu rõ ràng không thể đáp ứng.
Dùng quy tắc ba kiểm tra. Nếu vượt cả ba, ở lại với PostgreSQL full-text search. Nếu thất bại nặng một trong ba, cân nhắc engine chuyên dụng.
Bắt đầu an toàn: triển khai baseline trong Postgres, log truy vấn chậm và các truy vấn “không kết quả”, rồi mới quyết định. Nhiều ứng dụng không bao giờ lớn tới mức phải chuyển, và bạn tránh được việc chạy và đồng bộ một hệ thống thứ hai quá sớm.
Dấu hiệu đỏ thường cho thấy cần engine chuyên dụng:
Dấu hiệu xanh cho việc ở lại Postgres:
PostgreSQL full-text search là cách tích hợp để chuyển văn bản thành dạng mà DB có thể tìm nhanh, mà không quét mọi hàng. Nó hoạt động tốt nhất khi nội dung đã ở trong Postgres và bạn muốn tìm nhanh, độ ổn định vận hành cao.
Có ba phần đáng biết:
ts_rank (hoặc ts_rank_cd) để ưu tiên hàng phù hợp hơn.Cấu hình ngôn ngữ quan trọng vì nó thay đổi cách Postgres xử lý từ. Với cấu hình đúng, “running” và “run” có thể khớp nhau (stemming), và từ dừng phổ biến có thể bị bỏ qua (stop words). Với cấu hình sai, tìm kiếm có thể cảm thấy hỏng vì cách người dùng viết bình thường không khớp với những gì đã được lập chỉ mục.
Prefix matching là tính năng người ta hay dùng khi muốn hành vi “typeahead-ish”, như khớp “dev” với “developer.” Trong Postgres FTS, điều này thường làm bằng toán tử tiền tố (ví dụ term:*). Nó có thể cải thiện cảm nhận chất lượng, nhưng thường làm tăng công việc cho mỗi truy vấn, nên coi đây là nâng cấp tuỳ chọn, không mặc định.
Postgres không cố gắng trở thành nền tảng tìm kiếm hoàn chỉnh với mọi tính năng. Nếu bạn cần sửa lỗi chính tả mơ hồ (fuzzy), autocomplete nâng cao, learning-to-rank, analyzer phức tạp cho từng trường, hoặc lập chỉ mục phân tán trên nhiều node, bạn đã ra khỏi vùng an toàn của built-in. Tuy nhiên với nhiều app, PostgreSQL full-text search cho phần lớn những gì người dùng mong đợi với ít thành phần hơn.
Dưới đây là cấu trúc nhỏ, thực tế cho nội dung bạn muốn tìm:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
Một baseline tốt cho PostgreSQL full-text search là: xây truy vấn từ input người dùng, lọc hàng trước (khi có thể), rồi xếp hạng các khớp còn lại.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at >= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Một vài chi tiết giúp tiết kiệm thời gian sau này:
WHERE trước khi xếp hạng (status, tenant_id, khoảng thời gian). Bạn sẽ xếp hạng ít hàng hơn, nên giữ nhanh.ORDER BY (như updated_at, rồi id). Điều này giữ phân trang ổn định khi nhiều kết quả có cùng rank.websearch_to_tsquery cho input người dùng. Nó xử lý dấu ngoặc kép và toán tử đơn giản theo cách người ta mong đợi.Khi baseline này hoạt động, chuyển biểu thức to_tsvector(...) vào cột lưu sẵn. Điều đó tránh tính toán lại trên mỗi truy vấn và giúp lập chỉ mục đơn giản hơn.
Hầu hết câu chuyện “PostgreSQL full-text search chậm” đều về một điều: DB đang xây tài liệu tìm kiếm mỗi truy vấn. Sửa việc đó trước bằng cách lưu tsvector đã sinh và lập chỉ mục nó.
tsvector: cột sinh hay trigger?Cột sinh (generated column) là lựa chọn đơn giản nhất khi tài liệu tìm kiếm được xây từ các cột cùng hàng. Nó luôn đúng và khó quên khi cập nhật.
Dùng tsvector duy trì bằng trigger khi tài liệu phụ thuộc bảng liên quan (ví dụ ghép product với tên category), hoặc khi bạn cần logic tuỳ chỉnh khó diễn đạt bằng một biểu thức sinh. Trigger thêm thành phần vận hành, nên giữ chúng nhỏ và test kỹ.
Tạo một GIN index trên cột tsvector. Đó là baseline khiến PostgreSQL full-text search cảm giác tức thì cho tìm kiếm ứng dụng điển hình.
Thiết lập phù hợp nhiều app:
tsvector trong cùng bảng với các hàng bạn tìm thường xuyên nhất.tsvector đó.@@ trên tsvector đã lưu, không phải to_tsvector(...) tính lúc truy vấn.VACUUM (ANALYZE) sau backfill lớn để planner hiểu chỉ mục mới.Giữ vector trong cùng bảng thường nhanh và đơn giản hơn. Bảng tìm kiếm riêng có thể hợp lý nếu bảng gốc ghi nhiều hoặc bạn lập chỉ mục một tài liệu ghép từ nhiều bảng và muốn cập nhật theo lịch riêng.
Partial index hữu ích khi bạn chỉ tìm một phần hàng, như status = 'active', một tenant cụ thể trong app đa tenant, hoặc một ngôn ngữ cụ thể. Chúng giảm kích thước chỉ mục và có thể tăng tốc, nhưng chỉ khi truy vấn luôn bao gồm cùng bộ lọc đó.
Bạn có thể đạt kết quả tốt bất ngờ với PostgreSQL full-text search nếu giữ quy tắc độ liên quan đơn giản và dễ dự đoán.
Chiến thắng dễ nhất là cân trọng số trường: khớp ở title nên có trọng lượng cao hơn trong body. Xây tsvector kết hợp với title được gán weight cao hơn mô tả, rồi xếp hạng bằng ts_rank hoặc ts_rank_cd.
Nếu bạn muốn ưu tiên “mới” hoặc “phổ biến”, làm cẩn thận. Một boost nhỏ ổn, nhưng đừng để nó lấn át độ liên quan văn bản. Mẫu thực tế: xếp hạng theo văn bản trước, rồi phá hoà bằng độ mới, hoặc thêm bonus có giới hạn để một mục mới không phù hợp hoàn toàn không thắng một mục cũ phù hợp hoàn hảo.
Synonym và phrase matching là nơi kỳ vọng thường khác nhau. Synonym không tự động; bạn chỉ có nếu thêm thesaurus hoặc dictionary tuỳ chỉnh, hoặc mở rộng truy vấn thủ công (ví dụ coi “auth” là “authentication”). Phrase matching cũng không mặc định: truy vấn thường khớp từ ở bất cứ đâu, không phải “chính xác cụm từ này.” Nếu người dùng gõ câu có dấu ngoặc kép hoặc câu dài, cân nhắc phraseto_tsquery hoặc websearch_to_tsquery để khớp hành vi tìm kiếm hơn.
Nội dung đa ngôn ngữ cần quyết định. Nếu bạn biết ngôn ngữ từng tài liệu, lưu nó và sinh tsvector với cấu hình thích hợp (English, Russian, v.v.). Nếu không biết, fallback an toàn là index với cấu hình simple (không stemming), hoặc giữ hai vector: một theo ngôn ngữ khi biết, một simple cho tất cả.
Để xác thực độ liên quan, giữ nhỏ và cụ thể:
Điều này thông thường đủ cho hộp tìm kiếm ứng dụng như “templates”, “docs”, hoặc “projects.”
Hầu hết câu chuyện “PostgreSQL full-text search chậm hoặc không liên quan” đến từ vài sai lầm có thể tránh. Sửa chúng thường đơn giản hơn là thêm hệ thống mới.
Một bẫy phổ biến là coi tsvector như giá trị tính toán luôn đúng. Nếu bạn lưu tsvector trong cột mà không cập nhật nó trên mọi insert/update, kết quả sẽ thấy ngẫu nhiên vì chỉ mục không còn khớp văn bản. Nếu bạn tính to_tsvector(...) tại thời điểm truy vấn, kết quả có thể đúng nhưng chậm, và bạn bỏ lỡ lợi ích của chỉ mục.
Một cách dễ làm giảm hiệu năng là xếp hạng trước khi thu hẹp tập ứng viên. ts_rank có ích, nhưng thường nên chạy sau khi Postgres đã dùng chỉ mục để tìm hàng khớp. Nếu bạn tính rank cho phần lớn bảng (hoặc join trước), bạn có thể biến tìm kiếm nhanh thành quét bảng.
Người ta cũng mong “contains” giống LIKE '%term%'. Wildcard dẫn không phù hợp với FTS vì FTS dựa trên từ (lexemes), không phải substring bất kỳ. Nếu bạn cần substring cho mã sản phẩm hay ID, dùng công cụ khác cho trường hợp đó (ví dụ chỉ mục trigram) thay vì đổ lỗi cho FTS.
Vấn đề hiệu năng thường đến từ xử lý kết quả, không phải việc khớp. Hai mẫu cần chú ý:
OFFSET lớn, khiến Postgres bỏ qua ngày càng nhiều hàng khi bạn lật trang.Vấn đề vận hành cũng quan trọng. Bloating chỉ mục có thể tích tụ sau nhiều cập nhật, và reindex có thể tốn kém nếu để tới khi đã đau. Đo thời gian truy vấn thực (và check EXPLAIN ANALYZE) trước và sau thay đổi. Không có số liệu, dễ “sửa” PostgreSQL full-text search bằng cách làm tệ hơn theo cách khác.
Trước khi đổ lỗi cho PostgreSQL full-text search, chạy các kiểm tra này. Hầu hết bug “tìm kiếm Postgres chậm hoặc không liên quan” đến basics thiếu, không phải tính năng.
Xây tsvector thực sự: lưu nó trong generated hoặc cột duy trì (không tính trên mỗi truy vấn), dùng cấu hình ngôn ngữ đúng (english, simple, v.v.), và áp trọng số nếu trộn trường (title > subtitle > body).
Chuẩn hoá những gì bạn lập chỉ mục: giữ các trường ồn (IDs, boilerplate, navigation text) ra khỏi tsvector, và cắt bớt blob lớn nếu người dùng không tìm chúng.
Tạo chỉ mục đúng: thêm GIN index trên cột tsvector và xác nhận nó được dùng trong EXPLAIN. Nếu chỉ một phần là searchable (ví dụ status = 'published'), partial index có thể giảm kích thước và tăng tốc.
Giữ bảng khỏe: dead tuples làm chậm index scan. Vacuum định kỳ quan trọng, đặc biệt với nội dung cập nhật thường xuyên.
Có kế hoạch reindex: migration lớn hoặc chỉ mục bloat đôi khi cần cửa sổ reindex có kiểm soát.
Khi dữ liệu và chỉ mục đúng, tập trung vào hình dạng truy vấn. Postgres FTS nhanh khi nó thu hẹp tập ứng viên sớm.
Lọc trước, rồi xếp hạng: áp các bộ lọc chặt (tenant, language, published, category) trước khi xếp hạng. Xếp hạng hàng nghìn dòng bạn sẽ loại sau là công việc lãng phí.
Dùng ordering ổn định: order by rank rồi tie-breaker như updated_at hoặc id để kết quả không nhảy giữa các lần làm mới.
Tránh “truy vấn làm mọi thứ”: nếu cần fuzzy hay chịu lỗi gõ, làm có chủ ý (và đo lường). Đừng vô tình ép quét tuần tự.
Test truy vấn thực: thu 20 truy vấn hàng đầu, kiểm tra độ liên quan bằng tay và giữ một danh sách kết quả mong đợi nhỏ để phát hiện hồi quy.
Theo dõi đường chậm: log truy vấn chậm, xem EXPLAIN (ANALYZE, BUFFERS), và monitor kích thước chỉ mục cùng tỷ lệ cache hit để phát hiện khi tăng trưởng thay đổi hành vi.
Trung tâm trợ giúp SaaS là nơi tốt để bắt đầu vì mục tiêu đơn giản: giúp người ta tìm bài viết giải đáp câu hỏi. Bạn có vài nghìn bài, mỗi bài có title, tóm tắt ngắn và nội dung. Hầu hết khách truy cập gõ 2–5 từ như “reset password” hay “billing invoice.”
Với PostgreSQL full-text search, việc này có thể xong khá nhanh. Bạn lưu tsvector cho các trường kết hợp, thêm GIN index và xếp hạng theo độ liên quan. Thành công trông như: kết quả dưới 100 ms, top 3 thường đúng, và bạn không cần thân thiện hệ thống.
Rồi sản phẩm lớn lên. Support muốn lọc theo area sản phẩm, nền tảng (web, iOS, Android) và gói (free, pro, business). Người viết tài liệu muốn synonym, “did you mean” và xử lý lỗi gõ tốt hơn. Marketing cần analytics như “từ khoá hàng đầu không có kết quả.” Lưu lượng tăng và tìm kiếm trở thành một trong những endpoint bận rộn nhất.
Đó là các tín hiệu cho thấy engine chuyên dụng có thể đáng chi phí:
Lộ trình di chuyển thực tế là giữ Postgres làm nguồn chân thực, ngay cả sau khi thêm engine. Bắt đầu bằng log truy vấn và các trường hợp không có kết quả, rồi chạy job sync async chỉ copy các trường có thể tìm sang index mới. Chạy cả hai song song một thời gian và chuyển dần, thay vì đặt cược tất cả vào ngày đầu.
Nếu tìm kiếm của bạn chủ yếu là “tìm tài liệu chứa các từ này” và dataset không quá khổng lồ, PostgreSQL full-text search thường đủ. Bắt đầu từ đó, làm cho chạy được, và chỉ thêm engine khi bạn có thể nêu rõ tính năng thiếu hoặc vấn đề về scale.
Tóm tắt tiện mang theo:
tsvector, thêm GIN index, và nhu cầu xếp hạng cơ bản.Bước thực tế tiếp theo: triển khai truy vấn khởi tạo và chỉ mục từ các phần trước, rồi log vài metric đơn giản trong một tuần. Theo dõi p95 query time, truy vấn chậm, và tín hiệu thành công thô như “search -> click -> không bounce ngay” (dù chỉ là bộ đếm sự kiện cơ bản cũng hữu ích). Bạn sẽ nhanh thấy cần xếp hạng tốt hơn hay chỉ cần UX tốt hơn (bộ lọc, highlight, snippet tốt hơn).
Bắt đầu lên kế hoạch engine chuyên dụng khi một trong các yêu cầu này trở thành bắt buộc (không phải tùy chọn): autocomplete mạnh trên mỗi phím ở quy mô, sửa lỗi chính tả/đề xuất chính xác, facets và aggregation với đếm nhanh trên nhiều trường, công cụ độ liên quan nâng cao (synonym sets, learning-to-rank, boost per-query), hoặc tải kéo dài và chỉ mục lớn khó giữ nhanh.
Nếu muốn di chuyển nhanh phía app, Koder.ai có thể là cách tiện để prototype UI và API tìm kiếm qua chat, rồi lặp an toàn dùng snapshot và rollback trong khi đo hành vi người dùng thực.
PostgreSQL full-text search là “đủ” khi bạn có thể đạt được ba điều cùng lúc:
Nếu bạn đạt được các điều trên với một tsvector được lưu sẵn + chỉ mục GIN, thường là bạn đang ở vị trí tốt.
Mặc định hãy chọn PostgreSQL full-text search trước. Nó triển khai nhanh hơn, giữ dữ liệu và tìm kiếm cùng nơi, và tránh phải xây dựng/duy trì pipeline đồng bộ riêng.
Chuyển sang engine tìm kiếm chuyên dụng khi bạn có một yêu cầu rõ ràng mà Postgres không đáp ứng tốt (độ khoan dung lỗi chính tả cao, autocomplete phong phú, faceting nặng, hoặc tải tìm kiếm cạnh tranh với công việc chính của DB).
Quy tắc đơn giản: ở lại Postgres nếu bạn vượt qua ba kiểm tra sau:
Nếu bạn thất bại nghiêm trọng ở một mục (đặc biệt là tính năng như gõ sai/autocomplete, hoặc lưu lượng tìm kiếm cao), cân nhắc engine chuyên dụng.
Dùng Postgres FTS khi tìm kiếm chủ yếu là “tìm bản ghi đúng” trên vài trường như title/body/notes, với bộ lọc đơn giản (tenant, status, category).
Phù hợp tốt cho trung tâm trợ giúp, tài liệu nội bộ, ticket hỗ trợ, tìm kiếm bài blog/bài viết, và dashboard SaaS nơi người dùng tìm bằng tên dự án và ghi chú.
Một truy vấn cơ bản thường:
websearch_to_tsquery.Lưu một tsvector được sinh trước và thêm một chỉ mục GIN. Điều đó tránh tính toán to_tsvector(...) trên mỗi yêu cầu.
Thiết lập thực tế:
Dùng generated column khi tài liệu tìm kiếm được xây từ các cột cùng hàng (đơn giản và khó sai).
Dùng cột được duy trì bằng trigger khi văn bản tìm kiếm phụ thuộc bảng liên quan hoặc logic tuỳ chỉnh.
Lựa chọn mặc định: generated column trước, chỉ dùng trigger khi thật sự cần ghép dữ liệu từ nhiều bảng.
Bắt đầu với độ liên quan dễ dự đoán:
Rồi kiểm tra bằng một danh sách nhỏ các truy vấn thật của người dùng và kết quả mong đợi.
Postgres FTS dựa trên từ (lexeme), không phải chuỗi con. Vì vậy nó không hoạt động như LIKE '%term%' cho chuỗi bất kỳ.
Nếu cần tìm chuỗi con (mã sản phẩm, ID, mảnh), xử lý riêng (thường với chỉ mục trigram) thay vì ép FTS làm việc không phù hợp.
Dấu hiệu bạn đã vượt quá Postgres FTS:
Lộ trình thực tế: giữ Postgres làm nguồn chân thực và thêm đồng bộ bất đồng bộ khi yêu cầu rõ ràng.
@@ trên tsvector đã lưu.ts_rank/ts_rank_cd cộng với tie-breaker ổn định như updated_at, id.Cách này giữ kết quả liên quan, nhanh và ổn định cho phân trang.
tsvectortsvector_column @@ tsquery.Đây là sửa chữa phổ biến nhất khi tìm kiếm chậm.