Full-text search tiếng Việt — tsvector, unaccent, GIN FTS trong PostgreSQL
WHERE body LIKE '%deploy%' scan 1M row mất 2.5 giây và không match 'deployment'. Search 'khẩn cấp' không match khi user gõ không dấu. Full-text search với tsvector + unaccent dictionary giải quyết cả hai — không cần Elasticsearch.
TaskFlow vừa ra mắt search box trong dashboard. Product manager demo trước khách hàng: gõ "deploy" vào ô tìm kiếm — kết quả trả về sau 2.8 giây, và task có title "deployment pipeline" không xuất hiện. Gõ "khẩn cấp" — không có kết quả dù có chục task urgent. User thử gõ "khan cap" (không dấu) — vẫn trắng. Demo thất bại.
Query hiện tại là WHERE body LIKE '%deploy%' — leading wildcard chặn mọi B-tree index, buộc Seq Scan toàn bảng. "deployment" không match "deploy" vì LIKE là substring exact. "khẩn cấp" không match "khan cap" vì LIKE case-sensitive và diacritic-sensitive.
Full-text search (FTS) trong PostgreSQL là giải pháp cho cả hai vấn đề: tokenize + stem để "deployment" match "deploy", normalize dấu tiếng Việt để "khẩn cấp" match "khan cap". Bài này xây dựng search production-ready cho TaskFlow hoàn toàn trong PostgreSQL — không cần external service.
1. Analogy — Thư viện sách Việt và bảng tra từ khoá
Hình dung một thư viện lớn với hàng nghìn cuốn sách tiếng Việt về kỹ thuật:
| Cách tìm | Tương đương trong SQL | Đặc điểm |
|---|---|---|
| Đọc từng cuốn từ trang đầu tới cuối, tìm chuỗi "deploy" | LIKE '%deploy%' Seq Scan | Chính xác tuyệt đối nhưng chậm tuyến tính với số sách |
| Tra bảng tra từ khoá ở cuối sách ("deploy" → trang 45, 112) | FTS với GIN index | Index từng token → lookup O(log n), không đọc toàn bộ |
| Gộp "deploy", "deployed", "deployment" cùng một mục index | Stemming | Nhóm các form của từ về root |
| Gộp "khẩn" và "khan", "cấp" và "cap" cùng mục index | Unaccent normalize | Dấu và không dấu về cùng entry |
| Tìm cuốn nào chứa chuỗi con "dpl" (gõ sai) | pg_trgm similarity | Fuzzy substring, typo-tolerant, không phải FTS |
tsvector là bảng tra từ khoá của một document — preprocessed một lần, lookup nhiều lần. tsquery là câu hỏi bạn tra vào bảng đó. Operator @@ là hành động "tra bảng" — trả về true/false.
2. Vấn đề LIKE với 1M row
TaskFlow có bảng tasks 1 triệu row. Query search hiện tại:
EXPLAIN ANALYZE
SELECT id, title, body
FROM tasks
WHERE body LIKE '%deploy%';
Seq Scan on tasks (cost=0.00..24532.00 rows=1000 width=312)
(actual time=0.051..2487.312 rows=3241 loops=1)
Filter: (body ~~ '%deploy%'::text)
Rows Removed by Filter: 996759
Planning Time: 0.082 ms
Execution Time: 2531.447 ms
Leading wildcard %deploy% vô hiệu hoá mọi B-tree index — PostgreSQL phải đọc toàn bộ 1M row. Bài 4 Module 5 của khoá này đã giới thiệu pg_trgm GIN cho prefix-pattern search. Nhưng LIKE vẫn có hai vấn đề FTS giải được tốt hơn:
- Không tokenize: "deployment" không match "deploy" — LIKE cần exact substring.
- Không normalize: "khẩn cấp" không match "khan cap" — LIKE phân biệt dấu.
3. tsvector + tsquery — anatomy của FTS
tsvector là document đã được tiền xử lý: text gốc bị parse thành array các (token, position). tsquery là biểu thức boolean của tokens.
-- tsvector: preprocessed document
SELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog');
-- 'brown':3 'dog':9 'fox':4 'jump':5 'lazi':8 'quick':2
-- Stopwords (the, over) bị strip
-- Stemming: jumps -> jump, lazy -> lazi
-- Position number giữ lại cho ranking
-- tsquery: biểu thức tìm kiếm
SELECT to_tsquery('english', 'quick & brown');
-- 'quick' & 'brown'
SELECT to_tsquery('english', 'deploy | deployment');
-- 'deploy' | 'deploy' -- cả hai stem về 'deploy'
-- Operator @@ : tsvector match tsquery
SELECT to_tsvector('english', 'The quick brown fox') @@ to_tsquery('english', 'quick & fox');
-- true
SELECT to_tsvector('english', 'deploy production') @@ to_tsquery('english', 'deployment');
-- true -- vì 'deployment' stem về 'deploy'
So sánh với LIKE:
| Aspect | LIKE '%x%' | FTS tsvector @@ tsquery |
|---|---|---|
| "deployment" match "deploy" | Không | Có (stemming) |
| Index support | Không (leading %) | GIN index |
| Ranking by relevance | Không | ts_rank |
| Highlight match | Không | ts_headline |
| Diacritics normalize | Không | Có (với unaccent) |
4. plainto_tsquery vs websearch_to_tsquery
PostgreSQL cung cấp nhiều hàm convert text thành tsquery, mỗi hàm cho use case khác nhau:
-- plainto_tsquery: AND của tất cả token, không parse operator
SELECT plainto_tsquery('english', 'quick brown');
-- 'quick' & 'brown'
SELECT plainto_tsquery('english', 'deploy production');
-- 'deploy' & 'product' -- "production" stem về "product"
-- websearch_to_tsquery (PG 11+): Google-style syntax
SELECT websearch_to_tsquery('english', '"deploy production" -staging OR test');
-- 'deploy' <-> 'product' & !'stage' | 'test'
-- "phrase" = phrase search, - = NOT, OR = alternate
-- to_tsquery: operator parsing đầy đủ (& | ! <->)
-- Dùng khi bạn build query programmatically
SELECT to_tsquery('english', 'deploy & prod & !staging');
-- 'deploy' & 'prod' & !'stage'
Với TaskFlow search box từ user input, websearch_to_tsquery là lựa chọn tốt nhất — handle Google-style syntax mà hầu hết user đã quen, và an toàn với input không có operator (fallback về AND).
5. GIN index cho tsvector — FTS workflow cơ bản
Setup FTS cho bảng tasks với English config trước, sau đó mở rộng sang tiếng Việt:
-- Them generated column tsvector (tinh toan khi INSERT/UPDATE, luu vao disk)
ALTER TABLE tasks
ADD COLUMN body_tsv tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(body, ''))
) STORED;
-- GIN index tren tsvector column
CREATE INDEX idx_tasks_body_fts ON tasks USING gin(body_tsv);
-- Search: dung query variable de planner optimize
SELECT id, title, ts_rank(body_tsv, query) AS rank
FROM tasks,
plainto_tsquery('english', 'deploy production') query
WHERE body_tsv @@ query
ORDER BY rank DESC
LIMIT 10;
Bitmap Heap Scan on tasks (cost=32.45..1234.12 rows=312 width=124)
(actual time=1.203..4.892 rows=312 loops=1)
Recheck Cond: (body_tsv @@ plainto_tsquery('english', 'deploy production'))
-> Bitmap Index Scan on idx_tasks_body_fts
(actual time=1.101..1.101 rows=312 loops=1)
Planning Time: 0.421 ms
Execution Time: 5.234 ms
Từ 2531ms xuống 5ms — 500x nhanh hơn.
6. Tiếng Việt — vấn đề lớn với FTS mặc định
PostgreSQL không có Vietnamese dictionary built-in. Config 'english' chạy Snowball stemmer cho tiếng Anh — không hiểu tiếng Việt:
-- English config: "triển khai" không stem đúng
SELECT to_tsvector('english', 'Khẩn cấp triển khai sản phẩm');
-- 'cấp':2 'khẩn':1 'khai':4 'sản':5 'triển':3
-- Tokens giữ dấu -> "khan cap" không match "khẩn cấp"
-- User gõ không dấu -> không match document có dấu
SELECT to_tsvector('english', 'Khẩn cấp') @@ to_tsquery('english', 'khan & cap');
-- false -- vì English config giữ nguyên dấu
Cần hai thứ cho tiếng Việt:
- Strip diacritics: normalize "khẩn" và "khan" về cùng token.
- No stemming: tiếng Việt là ngôn ngữ đơn lập (isolating language) — không chia conjugation, không cần stem.
7. Unaccent + simple dictionary — Vietnamese FTS config
Extension unaccent strip dấu tiếng Việt. Kết hợp với simple dictionary (không stem, chỉ lowercase), tạo thành config đủ tốt cho production:
-- Bat unaccent extension
CREATE EXTENSION IF NOT EXISTS unaccent;
-- Test unaccent
SELECT unaccent('Khẩn cấp triển khai sản phẩm');
-- Khan cap trien khai san pham
-- Tao text search configuration riêng cho Vietnamese
CREATE TEXT SEARCH CONFIGURATION vietnamese (COPY = simple);
ALTER TEXT SEARCH CONFIGURATION vietnamese
ALTER MAPPING FOR hword, hword_part, word
WITH unaccent, simple;
-- Test config vietnamese
SELECT to_tsvector('vietnamese', 'Khẩn cấp triển khai sản phẩm');
-- 'cap':2 'khai':4 'khan':1 'pham':6 'san':5 'trien':3
-- User gõ không dấu
SELECT to_tsvector('vietnamese', 'Khan cap trien khai san pham');
-- 'cap':2 'khai':4 'khan':1 'pham':6 'san':5 'trien':3
-- Giống hệt nhau sau unaccent!
-- Match: document có dấu, query không dấu
SELECT to_tsvector('vietnamese', 'Khẩn cấp') @@ to_tsquery('vietnamese', 'khan & cap');
-- true
Cơ chế hoạt động: unaccent chạy trước simple trong pipeline — cả document và query đều bị strip dấu trước khi lưu vào tsvector / tsquery, nên dù user gõ có dấu hay không dấu đều map về cùng token.
-- Xác nhận: query có dấu cũng match document không dấu
SELECT to_tsvector('vietnamese', 'Khan cap') @@ to_tsquery('vietnamese', 'khẩn & cấp');
-- true
-- Và ngược lại: cả hai đều về cùng token sau unaccent
SELECT to_tsquery('vietnamese', 'khẩn & cấp');
-- 'khan' & 'cap' -- dấu bị strip trong query cũng
Tradeoff cần biết:
simpleconfig chia token theo whitespace. Word-boundary issue: "trienkhai" (không space) không match "triển khai" — phải có space giữa các từ trong document và query.- Tiếng Việt không có built-in tokenizer phân tích word boundary như tiếng Nhật/Trung. Với production cần Vietnamese segmentation chuyên nghiệp, có thể dùng extension
pgroonga(có MeCab backend) hoặc Elasticsearch vớianalysis-icu. simplekhông strip stopwords tiếng Việt ("và", "là", "của", "cho", "với") — những từ này được index và có thể gây noise. Tạo stopwords file riêng để cải thiện nếu cần.
8. ts_rank + ts_headline — UX cho search results
Sau khi có match, hai hàm giúp cải thiện UX của search results:
-- ts_rank: TF-IDF-like relevance score (0.0 đến 1.0)
-- Token xuất hiện nhiều lần + ít document chứa token -> rank cao hơn
SELECT
id,
title,
ts_rank(body_tsv, query) AS rank
FROM tasks,
plainto_tsquery('vietnamese', 'khan cap deploy') query
WHERE body_tsv @@ query
ORDER BY rank DESC
LIMIT 20;
-- ts_headline: extract và highlight đoạn văn chứa match
SELECT
id,
ts_headline(
'vietnamese',
body,
query,
'StartSel=<mark>, StopSel=</mark>, MaxWords=20, MinWords=10'
) AS snippet
FROM tasks,
plainto_tsquery('vietnamese', 'khan cap') query
WHERE body_tsv @@ query;
-- snippet: "...task <mark>khẩn</mark> <mark>cấp</mark> cần deploy ngay..."
-- (ts_headline re-highlight trên text gốc có dấu -- đúng behaviour)
Lưu ý: ts_headline gọi to_tsvector lại trên body để locate vị trí match — expensive nếu body dài. Với body lớn hơn 10KB, giới hạn MaxFragments=3 để tránh process toàn bộ text.
9. pg_trgm — fuzzy search và typo tolerance
FTS xử lý tốt stemming và diacritics, nhưng không phải fuzzy search. User gõ "dploy" hay "trienkha" sẽ không match. Extension pg_trgm giải quyết bài toán này:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- GIN trigram index cho title (substring + similarity)
CREATE INDEX idx_tasks_title_trgm ON tasks USING gin(title gin_trgm_ops);
-- Similarity search: typo tolerance
SELECT title, similarity(title, 'dploy') AS sim
FROM tasks
WHERE title % 'dploy' -- % là similarity operator, threshold mặc định 0.3
ORDER BY sim DESC
LIMIT 10;
-- "deploy production" có similarity khoảng 0.5 -> match dù typo
-- Kết hợp: FTS cho relevance ranking + trigram cho typo fallback
-- Query 1: FTS (nhanh, chính xác)
SELECT id, title, ts_rank(body_tsv, q) AS rank, 'fts' AS source
FROM tasks, plainto_tsquery('vietnamese', 'deploy') q
WHERE body_tsv @@ q
UNION ALL
-- Query 2: trigram fallback (khi FTS không có kết quả)
SELECT id, title, similarity(title, 'deploy')::float AS rank, 'trgm' AS source
FROM tasks
WHERE title % 'deploy'
AND NOT (body_tsv @@ plainto_tsquery('vietnamese', 'deploy'))
ORDER BY rank DESC
LIMIT 20;
Khi nào dùng FTS vs trigram:
| Nhu cầu | FTS (tsvector) | pg_trgm |
|---|---|---|
| Search nội dung dài, ranking | Tốt | Kém |
| Stemming / diacritic normalize | Tốt | Không hỗ trợ |
| Typo tolerance | Không | Tốt |
| Search-as-you-type (prefix) | Kém | Tốt |
| Substring không-anchored | Không | Tốt |
10. Pitfall tổng hợp
Pitfall 1 — to_tsvector tính toán lại mỗi INSERT:
-- Sai: tính lại to_tsvector mỗi lần query (không cache, CPU waste)
SELECT * FROM tasks WHERE to_tsvector('vietnamese', body) @@ query;
-- Seq Scan -- GIN trên body không dùng được
-- Sai: tính lại mỗi INSERT qua trigger thủ công (dễ miss)
-- Đúng: generated column -- PG tự cập nhật khi body thay đổi
ALTER TABLE tasks
ADD COLUMN body_tsv tsvector
GENERATED ALWAYS AS (to_tsvector('vietnamese', coalesce(body, ''))) STORED;
Pitfall 2 — FTS không phải substring search:
-- FTS tokenize "apple" thành token 'apple'
-- Search "apl" không match -- GIN lookup 'apl' không có entry
SELECT to_tsvector('vietnamese', 'apple watch') @@ to_tsquery('vietnamese', 'apl');
-- false
-- Fix: dùng pg_trgm cho substring need
-- Hoặc kết hợp: FTS cho relevance + trigram cho prefix autocomplete
Pitfall 3 — Stopwords tiếng Việt không có mặc định:
simple config không strip "và", "là", "của", "với", "cho" — các token này được index và chiếm inverted list lớn (xuất hiện trong gần mọi document). Nếu user tìm "task và deploy" thì "và" được include trong query → FTS tìm document có cả "va" VÀ "deploy". Giải pháp: tạo stopwords file riêng cho simple config hoặc pre-process query để strip stopwords trước khi gọi plainto_tsquery.
Pitfall 4 — GIN index size:
GIN trên tsvector thường chiếm 30–50% size của tổng tsvector data. Với 10GB body text, tsvector khoảng 2GB, GIN khoảng 600MB–1GB. Factor vào storage + backup planning.
Pitfall 5 — ts_headline expensive trên body dài:
ts_headline re-parse toàn bộ body để locate highlight. Với body vượt 10KB, set MaxFragments=3, FragmentDelimiter=... và cân nhắc store pre-computed headline hoặc giới hạn body tối đa 32KB.
11. Applied — TaskFlow search box production-ready
Setup hoàn chỉnh từ migration đến API query cho TaskFlow dashboard:
-- Step 1: enable extensions
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Step 2: tạo Vietnamese text search config
CREATE TEXT SEARCH CONFIGURATION vietnamese (COPY = simple);
ALTER TEXT SEARCH CONFIGURATION vietnamese
ALTER MAPPING FOR hword, hword_part, word
WITH unaccent, simple;
-- Step 3: thêm generated column kết hợp title + body
-- Title có weight cao hơn: setweight(tsvector, 'A') cho title weight A
-- Body weight B -- ts_rank sẽ ưu tiên title match
ALTER TABLE tasks
ADD COLUMN search_tsv tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('vietnamese', coalesce(title, '')), 'A') ||
setweight(to_tsvector('vietnamese', coalesce(body, '')), 'B')
) STORED;
-- Step 4: GIN index trên generated column
CREATE INDEX idx_tasks_search ON tasks USING gin(search_tsv);
-- Step 5: API endpoint /api/search?q=khan+cap+deploy
-- $1 = user input từ search box
SELECT
id,
title,
ts_headline(
'vietnamese',
coalesce(body, ''),
query,
'StartSel=<mark>, StopSel=</mark>, MaxWords=20, MaxFragments=2'
) AS snippet,
ts_rank(search_tsv, query) AS rank
FROM tasks,
websearch_to_tsquery('vietnamese', $1) query
WHERE search_tsv @@ query
AND project_id = $2 -- scoped to project, dùng được composite index
ORDER BY rank DESC, created_at DESC
LIMIT 20;
Pattern này tương tự cách Discourse (Ruby on Rails + PostgreSQL) implement full-text search cho forum threads — dùng tsvector generated column + GIN + ts_rank + ts_headline trước khi add Elasticsearch trong các giai đoạn scale sau này.
12. Deep Dive
- PostgreSQL Documentation Ch.12 "Full Text Search" — reference chính thức: tsvector/tsquery anatomy, text search configuration, GIN/GiST index cho FTS,
ts_rankweight,ts_headlineoptions. Section 12.6 "Dictionaries" giải thích cáchunaccentkết hợp vớisimpletrong pipeline. - PostgreSQL Documentation — unaccent extension — cách tạo custom
unaccent.rulesfile để handle ký tự đặc biệt ngôn ngữ khác ngoài Latin nếu cần mở rộng. - PostgreSQL Documentation — pg_trgm extension — trigram operators,
similarity(),word_similarity(), GIN vs GiST index cho trigram,pg_trgm.similarity_thresholdconfig. - PostgreSQL Documentation — Text Search Types —
tsvectorvàtsquerydata type specification,setweight()cho multi-column FTS, phrase search với<->operator.
13. Tóm tắt
tsvectorlà document đã tiền xử lý: token + position, strip stopwords, stem. Compute một lần, lookup nhiều lần qua GIN.tsquerylà biểu thức boolean token.plainto_tsquerycho user input thông thường;websearch_to_tsquerycho Google-style search box.- Operator
@@kiểm tra tsvector match tsquery — GIN index giúp lookup O(log n) thay vì Seq Scan. - PostgreSQL không có Vietnamese dictionary built-in — giải pháp thực dụng:
unaccentextension +simpledictionary tạo thành configvietnameseđủ tốt cho production. - Unaccent normalize cả document lẫn query — user gõ "khan cap" hay "khẩn cấp" đều về cùng token, match nhau.
- Generated column
STOREDlà cách đúng để lưu tsvector — PG tự tính lại khi row thay đổi, không cần trigger. setweightcho title weight A và body weight B —ts_rankưu tiên title match trong ranking.pg_trgmbổ sung typo tolerance và substring search — kết hợp FTS + trigram cho search box hoàn chỉnh.- GIN index size thường 30–50% tổng tsvector data — factor vào storage planning.
14. Tự kiểm tra
Q1Tại sao `WHERE body LIKE '%deploy%'` không dùng được B-tree index dù có index trên column `body`? FTS giải quyết bài toán này khác LIKE như thế nào?▸
B-tree index sort data theo giá trị key để enable binary search. Leading wildcard %deploy% yêu cầu tìm substring bất kỳ vị trí trong chuỗi — không có cách nào dùng sorted order để shortcut, buộc phải check từng row. PostgreSQL planner nhận ra điều này và chọn Seq Scan.
FTS giải quyết khác hoàn toàn: thay vì tìm substring trong text gốc, FTS decompose text thành token ngay khi INSERT (lưu vào tsvector), sau đó GIN index lưu mapping ngược: token → danh sách row chứa token. Query lookup token trong GIN → nhận list row trực tiếp, không scan toàn bảng.
Thêm nữa, FTS stem token: "deployment" và "deploy" cùng stem về "deploy" nên match nhau — LIKE không làm được điều này.
Q2Giải thích cơ chế của Vietnamese FTS config dùng `unaccent + simple`. Tại sao search 'khan cap' có thể match document chứa 'Khẩn cấp'?▸
Text search config định nghĩa pipeline xử lý token: text → tokenize → qua các dictionary filter → token cuối cùng lưu vào tsvector. Config vietnamese cấu hình pipeline: unaccent chạy trước (strip dấu), rồi simple (lowercase, không stem).
Pipeline này chạy giống nhau cho cả document lẫn query. Khi INSERT document "Khẩn cấp": unaccent biến "Khẩn" → "Khan", "cấp" → "cap"; simple lowercase → token cuối là "khan" và "cap". Khi query "khan cap": cũng qua pipeline → token "khan" và "cap". Hai tập token giống nhau → GIN match thành công.
Đây là lý do cả document có dấu lẫn query không dấu đều về cùng token — normalize tại điểm index, không phải tại điểm query.
Q3Vì sao nên dùng `GENERATED ALWAYS AS (...) STORED` cho `tsvector` column thay vì tính `to_tsvector(body)` trực tiếp trong WHERE clause?▸
Có hai lý do chính:
- Index usability: GIN index được tạo trên column
search_tsv. Khi WHERE clause tínhto_tsvector('vietnamese', body)on-the-fly, PostgreSQL không biết kết quả tương đương với column đã index → không dùng GIN, fallback Seq Scan. - Performance:
to_tsvectorlà operation tốn CPU (tokenize + dictionary lookup). Tính một lần khi INSERT/UPDATE, lưu vào disk → query chỉ đọc giá trị đã compute, không tính lại.
GENERATED ALWAYS AS ... STORED đảm bảo PG tự cập nhật column khi body thay đổi, không cần trigger thủ công (dễ miss hoặc race condition). Column này cũng không thể bị UPDATE trực tiếp — bảo đảm luôn in sync với body.
Q4TaskFlow thêm full-text search cho 2 trường: `title` và `body`. Làm thế nào để kết quả match ở `title` được rank cao hơn match ở `body`? Cơ chế `setweight` hoạt động như thế nào?▸
setweight(tsvector, label) gán weight label (A, B, C, D — từ cao đến thấp) cho tất cả lexeme trong tsvector. Khi || (concatenate) hai tsvector có weight khác nhau, position của mỗi token trong tsvector cuối giữ nguyên weight.
ts_rank tính score có xét weight: token weight A đóng góp vào score cao hơn weight B. Cụ thể: rank mặc định dùng weight vector [0.1, 0.2, 0.4, 1.0] cho D/C/B/A. Token "deploy" trong title (weight A) đóng góp 1.0, trong body (weight B) đóng góp 0.4 — title match rank cao hơn 2.5x.
Pattern chuẩn: setweight(to_tsvector(config, title), 'A') || setweight(to_tsvector(config, body), 'B') — concatenate trong generated column expression.
Q5FTS và `pg_trgm` đều có GIN index nhưng giải quyết bài toán khác nhau. Khi nào chọn FTS, khi nào chọn trigram, và khi nào cần cả hai?▸
FTS: tokenize + stem + rank theo relevance. Phù hợp khi search nội dung dài (body, description), cần ranking, hỗ trợ diacritic normalize. Không làm được: substring search ("apl" không match "apple"), typo tolerance.
Trigram: chia mọi chuỗi thành trigram (3-char n-gram), so sánh overlap. Phù hợp khi cần: typo tolerance ("dploy" match "deploy"), search-as-you-type với prefix ngắn, substring tùy ý. Không làm được: stemming, relevance ranking theo term frequency.
Cần cả hai khi search box cần đồng thời: (1) search nội dung dài với ranking — FTS; (2) autocomplete title khi user gõ dở — trigram; (3) typo fallback khi FTS không có kết quả — trigram. TaskFlow production pattern: FTS làm primary path, trigram làm fallback khi FTS result count bằng 0.
Q6GIN index cho tsvector có 'write tax' giống GIN cho JSONB không? Sau khi add `search_tsv` generated column + GIN index, INSERT throughput của bảng tasks thay đổi như thế nào và làm sao kiểm tra?▸
Có — GIN FTS index có write tax tương tự GIN JSONB. Mỗi INSERT phải: (1) tính to_tsvector cho generated column (CPU), (2) update GIN inverted list cho mỗi token trong tsvector (I/O). Task body có 50 token trung bình → 50 inverted list update per INSERT.
Mức độ impact phụ thuộc body length và token count. Với tasks body 200–500 words, GIN FTS thường giảm INSERT throughput 20–40% so với không có index. Kết hợp với GIN JSONB index (bài 3 module này) trên cùng bảng, tổng overhead có thể lên 50–60%.
Kiểm tra bằng:
SELECT indexname, idx_scan, idx_tup_read, pg_size_pretty(pg_relation_size(indexrelid)) AS size FROM pg_stat_user_indexes WHERE relname = 'tasks' ORDER BY pg_relation_size(indexrelid) DESC;
Nếu idx_scan của FTS index thấp sau 7 ngày production nhưng bảng write-heavy, cân nhắc drop và đánh giá lại xem search feature có thực sự được dùng không. Bulk load pattern: drop GIN trước, bulk INSERT, rebuild với maintenance_work_mem = '1GB'.
Bài tiếp theo: pgvector và semantic search — embedding, cosine similarity, Text-to-SQL teaser
Bài này có giúp bạn hiểu bản chất không?