Ô tìm kiếm là thứ ai cũng dùng, nhưng đằng sau nó là một bài toán khó ở quy mô lớn: tìm một từ trong hàng triệu tài liệu, tức thì. Cách ngây thơ — LIKE '%từ%' — phải đọc từng tài liệu để kiểm, tức O(n) và không index được. Elasticsearch (và thư viện Lucene bên dưới) giải bằng inverted index. Bài này mổ xẻ nguyên lý đó và dựng thật trong PostgreSQL (tsvector + GIN) để đo trực tiếp — vì Postgres FTS dùng chính cấu trúc inverted index.

Bài toán: LIKE phải quét cả bảng

LIKE '%unicornrare%' có % ở đầu nên B-tree index vô dụng (index chỉ giúp khi biết tiền tố). Postgres buộc phải seq scan — đọc và kiểm từng dòng:

SELECT * FROM docs WHERE noi_dung LIKE '%unicornrare%';   -- quét cả 500.000 dòng

Với 500 nghìn tài liệu còn tạm; với hàng trăm triệu thì mỗi tìm kiếm là một cơn ác mộng I/O.

Cách giải: inverted index (từ → danh sách tài liệu)

Ý tưởng cốt lõi của Lucene/Elasticsearch: thay vì lưu "tài liệu → các từ", lật ngược thành "từ → danh sách tài liệu chứa nó" (posting list). Tìm một từ giờ là tra thẳng danh sách, không quét gì. PostgreSQL có sẵn cấu trúc này: kiểu tsvector (tách từ, chuẩn hoá) + index GIN (chính là một inverted index):

ALTER TABLE docs ADD COLUMN tsv tsvector;
UPDATE docs SET tsv = to_tsvector('simple', noi_dung);   -- tách từ, chuẩn hoá
CREATE INDEX docs_tsv_gin ON docs USING GIN (tsv);        -- GIN = inverted index

Truy vấn full-text dùng toán tử @@, đi qua GIN:

SELECT * FROM docs WHERE tsv @@ to_tsquery('simple', 'unicornrare');

Ảnh chụp đoạn mã nền tối minh hoạ tìm kiếm toàn văn như Elasticsearch inverted index đánh bại LIKE PostgreSQL tsvector cộng GIN đo bằng EXPLAIN ANALYZE thật, một LIKE phần trăm tu phần trăm phải quét từng bản ghi seq scan O của n không index được SELECT từ docs WHERE noi_dung LIKE phần trăm unicornrare phần trăm phần trăm đứng đầu nên B-tree index vô dụng quét cả 500000 dòng, hai inverted index ánh xạ từ tới danh sách tài liệu chứa nó ý tưởng cốt lõi Elasticsearch Lucene thay vì tài liệu tới từ lật ngược thành từ tới danh sách tài liệu 3 17 88 tìm từ bằng tra thẳng danh sách ALTER TABLE docs ADD COLUMN tsv tsvector UPDATE docs SET tsv to_tsvector simple noi_dung tách từ chuẩn hoá CREATE INDEX docs_tsv_gin ON docs USING GIN tsv GIN là inverted index, ba truy vấn full-text @@ dùng GIN tra thẳng danh sách tài liệu SELECT từ docs WHERE tsv @@ to_tsquery simple unicornrare GIN tra unicornrare ra ngay 5 tài liệu không quét cả bảng tìm nhiều từ AND to_tsquery redis và kafka GIN giao 2 danh sách

Hình 1: LIKE %từ% buộc seq scan (không index được); inverted index (GIN của Postgres) ánh xạ từ → danh sách tài liệu, to_tsvector tách từ như analyzer của Elasticsearch, @@ tra thẳng qua GIN.

Đo THẬT trong PostgreSQL

Ta nạp 500.000 tài liệu (mỗi cái ~12 từ ngẫu nhiên), thêm một từ hiếm unicornrare vào đúng 5 tài liệu, rồi so LIKE vs GIN bằng EXPLAIN ANALYZE:

LIKE '%unicornrare%'             -> Parallel Seq Scan (quét 500k)
   Execution Time: 39,319 ms
tsv @@ to_tsquery('unicornrare') -> Bitmap Index Scan (GIN)
   Execution Time: 0,036 ms
>> GIN nhanh hơn ~1.092× (39,319 ms / 0,036 ms), CÙNG kết quả (5 = 5)

Cùng câu trả lời (cả hai tìm đúng 5 tài liệu), nhưng inverted index nhanh hơn ~1.092 lần — vì nó tra thẳng thay vì quét. Quan trọng: GIN không bỏ sót (LIKE count = GIN count = 5); index chỉ nhanh hơn, không đổi kết quả. Tìm nhiều từ (AND) cũng nhanh vì GIN giao các posting list:

to_tsquery('redis & kafka & postgres') -> 18.743 tài liệu trong 20,566 ms

Ảnh chụp bảng kết quả chạy thật trên PostgreSQL 16 500000 tài liệu output thật, tìm từ hiếm unicornrare có trong 5 tài liệu EXPLAIN ANALYZE LIKE phần trăm unicornrare phần trăm Parallel Seq Scan quét 500k Execution Time 39,319 ms tsv @@ to_tsquery unicornrare Bitmap Index Scan GIN Execution Time 0,036 ms GIN nhanh hơn 1092 lần 39,319 ms chia 0,036 ms cùng kết quả 5 bằng 5, đúng đắn GIN không bỏ sót LIKE count 5 GIN count 5 khớp inverted index cho đúng kết quả chỉ nhanh hơn, tìm nhiều từ AND redis và kafka và postgres GIN giao danh sách to_tsquery redis kafka postgres 18743 tài liệu trong 20,566 ms GIN giao 3 danh sách posting không phải quét rồi lọc từng dòng, Elasticsearch Lucene làm gì nguyên lý inverted index inverted index ánh xạ từ tới danh sách tài liệu posting list như GIN của Postgres analyzer tách từ chuẩn hoá lowercase stem bỏ stopword như to_tsvector truy vấn giao hợp các posting list xếp hạng theo TF-IDF BM25 khác biệt ES là engine phân tán chuyên tìm kiếm cộng xếp hạng cộng phân mảnh Postgres FTS đủ cho phần lớn nhu cầu

Hình 2: Chạy thật trên PostgreSQL — LIKE seq scan 39,319ms vs GIN inverted index 0,036ms (~1.092×), cùng 5 kết quả; AND ba từ giao posting list 20,566ms/18.743 tài liệu. Kèm nguyên lý inverted index của Elasticsearch/Lucene.

Elasticsearch làm gì hơn Postgres FTS

Nguyên lý giống nhau, nhưng Elasticsearch là engine tìm kiếm phân tán chuyên dụng:

  • Analyzer mạnh hơn: tách từ đa ngôn ngữ, stemming, đồng nghĩa, n-gram — to_tsvector của Postgres làm phần cốt lõi (lowercase, stem, bỏ stopword) nhưng ES linh hoạt hơn nhiều.
  • Xếp hạng liên quan (relevance) bằng BM25/TF-IDF là mặc định — ES sinh ra để trả "kết quả liên quan nhất", không chỉ "khớp hay không". Postgres có ts_rank nhưng đơn giản hơn.
  • Phân tán và phân mảnh sẵn có: ES chia index ra nhiều shard trên nhiều node, sao chép, và gộp kết quả — quy mô mà một Postgres đơn không với tới.

Bài học: cùng một cấu trúc dữ liệu (inverted index) là nền của cả hai; ES thêm lớp phân tán + xếp hạng chuyên biệt. Với phần lớn nhu cầu "tìm text trong app", Postgres FTS đủ và tránh phải vận hành thêm một hệ.

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

Inverted index tốn chỗ và tốn ghi. Phải lưu thêm cấu trúc (tsvector + GIN) và cập nhật nó mỗi lần dữ liệu đổi — ghi chậm hơn, tốn đĩa hơn. GIN đặc biệt chậm ghi (có thể hoãn bằng fastupdate). Đổi tốc độ đọc lấy chi phí ghi + lưu trữ.

Không giúp cho mọi kiểu tìm. Inverted index tra theo từ (token). Tìm chuỗi con giữa từ (LIKE '%ana%' khớp 'banana') thì FTS không làm — cần trigram (pg_trgm) hoặc công cụ khác. Chọn cấu trúc theo kiểu truy vấn.

FTS ≠ engine tìm kiếm đầy đủ. Nếu cần relevance tinh vi, gợi ý (autocomplete), highlight, phân tích log quy mô lớn, thì ES/OpenSearch đáng công vận hành. Đừng nhảy sang ES chỉ vì "cần tìm kiếm" khi Postgres FTS đủ — nhưng cũng đừng ép Postgres làm việc của một search engine phân tán.

Ba ý mang về

  1. LIKE '%từ%' không mở rộng được: % đầu chuỗi khiến B-tree vô dụng, buộc seq scan cả bảng — đo thật 39,319ms trên 500k tài liệu chỉ để tìm 5.
  2. Inverted index (từ → danh sách tài liệu) là lời giải: PostgreSQL tsvector + GIN cho cùng kết quả nhưng nhanh hơn ~1.092× (0,036ms), không bỏ sót, và xử lý AND nhiều từ bằng giao posting list — chính cấu trúc mà Elasticsearch/Lucene dùng.
  3. Cùng nguyên lý, khác quy mô: Postgres FTS đủ cho tìm text trong app; Elasticsearch thêm analyzer mạnh, xếp hạng BM25 và phân tán/phân mảnh — chọn theo nhu cầu, và nhớ inverted index đánh đổi chi phí ghi/lưu trữ lấy tốc độ đọc.

Nguồn

Phần sau ta xét cách Stack Overflow chia tải nhiều máy chủ bằng HAProxy — dựng thật với nhiều backend và xem health check tự loại máy chủ hỏng.