PostgreSQL Full-Text Search: Tối Ưu Tìm Kiếm Địa Chỉ Autocomplete Tiếng Việt
Hướng dẫn chi tiết cách xây dựng hệ thống gợi ý địa chỉ tiếng Việt nhanh, không dấu và tìm kiếm lỗi chính tả bằng unaccent, FTS và Trigram Indexing trong PostgreSQL.
Một trong những trải nghiệm người dùng khó chịu nhất trên các form thanh toán thương mại điện tử hoặc ứng dụng giao hàng là nhập địa chỉ nhưng không ra kết quả—hoặc tệ hơn là phải đợi vài giây để các gợi ý hiển thị.
Tại Việt Nam, vấn đề này còn phức tạp hơn do các đặc trưng ngôn ngữ: dấu tiếng Việt, các từ viết tắt (vd: "Q1" thay vì "Quận 1", "TP.HCM" thay vì "Thành phố Hồ Chí Minh"), gõ sai chính tả và việc người dùng nhập các thành phần địa chỉ theo thứ tự không cố định.
Mặc dù các công cụ tìm kiếm bên ngoài như Elasticsearch rất tốt, nhưng việc vận hành một cụm cluster riêng biệt thường là quá mức cần thiết cho các dự án vừa và nhỏ. Hướng dẫn này sẽ giúp bạn tối ưu hóa PostgreSQL để xây dựng tính năng gợi ý địa chỉ nhanh, tìm kiếm lỗi chính tả bằng cách sử dụng các extension và index có sẵn.
Vấn Đề: Tại Sao Tìm Kiếm LIKE Thông Thường Lại Thất Bại
Cách đơn giản nhất để tìm kiếm địa chỉ là sử dụng các toán tử LIKE hoặc ILIKE trong SQL:
SELECT id, full_address FROM addresses
WHERE full_address ILIKE '%nguyen trai%';Cách làm này chạy tốt trong môi trường thử nghiệm nhưng sẽ gặp lỗi ngay lập tức trong môi trường production vì hai lý do:
- Table Scans (
O(N)): Ký tự đại diện%ở phía trước ngăn PostgreSQL sử dụng index B-Tree tiêu chuẩn. Database buộc phải quét qua từng dòng trong bảng (Full Table Scan), dẫn đến sử dụng CPU cao và phản hồi chậm khi dữ liệu lớn dần. - Khớp Chính Xác: Tìm kiếm "nguyen trai" sẽ không khớp với "Nguyễn Trãi" (lệch dấu) hoặc "nguyn trai" (sai chính tả).
Để xây dựng một hệ thống gợi ý địa chỉ sẵn sàng cho production, chúng ta cần một giải pháp nhanh (dưới 50ms), không phân biệt dấu và chấp nhận lỗi chính tả.
Bước 1: Xử Lý Dấu Tiếng Việt Với unaccent
Người dùng Việt Nam thường tìm kiếm không dấu để tiết kiệm thời gian. Để xử lý việc này, chúng ta có thể sử dụng extension tích hợp sẵn unaccent của PostgreSQL để loại bỏ dấu khỏi văn bản (ví dụ: "Nguyễn Trãi" trở thành "Nguyen Trai").
1. Kích Hoạt Extension
Chạy lệnh này trên database của bạn:
CREATE EXTENSION IF NOT EXISTS unaccent;2. Truy Vấn Với unaccent
Bây giờ bạn có thể bọc cả cột dữ liệu và từ khóa tìm kiếm trong hàm unaccent():
SELECT id, full_address FROM addresses
WHERE unaccent(full_address) ILIKE unaccent('%Nguyễn Trãi%');3. Tối Ưu Hóa Với Functional Index
Việc áp dụng hàm lên một cột trong quá trình truy vấn vẫn sẽ bỏ qua các index tiêu chuẩn. Để giữ tốc độ nhanh, hãy tạo một index trực tiếp trên kết quả của hàm:
CREATE INDEX idx_addresses_unaccent_address
ON addresses (lower(unaccent(full_address)));Bây giờ, các truy vấn khớp chính xác biểu thức hàm sẽ sử dụng Index Scan. Tuy nhiên, phương pháp này vẫn chưa giải quyết được bài toán gợi ý khi gõ dở dang ("nguy...") hoặc sửa lỗi chính tả.
Bước 2: Tăng Tốc Với PostgreSQL Full-Text Search (FTS)
Với tập dữ liệu lớn hơn, chúng ta cần tách chuỗi thành các token (từ khóa). PostgreSQL cung cấp bộ máy tìm kiếm Full-Text Search mạnh mẽ sử dụng tsvector (biểu diễn tài liệu dưới dạng vector) và tsquery (truy vấn tìm kiếm).
Thay vì phân tích chuỗi trên mỗi truy vấn, chúng ta có thể tính toán trước một search vector kết hợp cả tên đường, phường, quận và tỉnh:
1. Tạo Cột Search Vector
ALTER TABLE addresses ADD COLUMN search_vector tsvector;2. Điền Dữ Liệu Dùng unaccent
Để tìm kiếm FTS không phân biệt dấu, chúng ta chuyển đổi các thành phần địa chỉ thành chuỗi không dấu trước khi tạo vector:
UPDATE addresses
SET search_vector = to_tsvector('simple', unaccent(
coalesce(street, '') || ' ' ||
coalesce(ward, '') || ' ' ||
coalesce(district, '') || ' ' ||
coalesce(province, '')
));(Lưu ý: Chúng ta dùng từ điển 'simple' để PostgreSQL không áp dụng các quy tắc rút gọn từ của tiếng Anh cho tên riêng Việt Nam.)
3. Tạo GIN Index
Index GIN (Generalized Inverted Index) được thiết kế riêng để tăng tốc độ tìm kiếm vector:
CREATE INDEX idx_addresses_search_vector
ON addresses USING gin(search_vector);4. Thực Hiện Truy Vấn
Để truy vấn, chúng ta phân tích chuỗi nhập vào và dùng toán tử khớp @@:
SELECT full_address FROM addresses
WHERE search_vector @@ to_tsquery('simple', 'nguyen & trai');Ưu điểm: FTS cực kỳ nhanh (O(log N)) và dễ dàng mở rộng lên hàng triệu dòng dữ liệu.
Nhược điểm: Yêu cầu khớp toàn bộ từ. Truy vấn tìm kiếm nguy sẽ không khớp với nguyen trừ khi bạn sử dụng ký tự gợi ý (nguy:*), và nó hoàn toàn không hỗ trợ sửa lỗi chính tả như ngyuen.
Bước 3: Trigram Indexing Cho Autocomplete & Tìm Kiếm Gần Đúng (Fuzzy)
Để mang lại trải nghiệm "gõ tới đâu gợi ý tới đó" thực sự, chúng ta cần Trigram Indexing thông qua extension pg_trgm. Một trigram là một nhóm gồm 3 ký tự liên tiếp được cắt ra từ một chuỗi. Ví dụ, các trigram của "HCM" là:
h,hc,hcm,cm,m
Bằng cách đánh index các trigram này, PostgreSQL có thể đo lường mức độ "tương đồng" giữa hai chuỗi dựa trên số lượng trigram chung, cho phép tìm kiếm chuỗi con và tìm kiếm gần đúng cực nhanh.
1. Kích Hoạt Extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;2. Tạo GIN Trigram Index
CREATE INDEX idx_addresses_trgm
ON addresses USING gin (lower(unaccent(full_address)) gin_trgm_ops);3. Truy Vấn Cho Tính Năng Autocomplete
Chúng ta sử dụng toán tử % để lọc các dòng đạt ngưỡng tương đồng (mặc định là 0.3) và hàm similarity() để sắp xếp kết quả:
-- Thiết lập ngưỡng tạm thời nếu cần (vd: 0.25 để tăng khả năng sửa lỗi)
SET pg_trgm.similarity_threshold = 0.25;
SELECT full_address, similarity(lower(unaccent(full_address)), 'nguyn trai') as score
FROM addresses
WHERE lower(unaccent(full_address)) % 'nguyn trai'
ORDER BY score DESC
LIMIT 5;Chỉ với một câu lệnh truy vấn duy nhất này, bạn đã giải quyết được 3 vấn đề:
- Không phân biệt dấu (nhờ có
unaccent). - Hỗ trợ gợi ý từ đang gõ dở (nhờ phân tách trigram).
- Hỗ trợ sửa lỗi chính tả (khớp từ "nguyn trai" với "Nguyễn Trãi" với độ tương đồng cao).
So Sánh Hiệu Năng: Benchmark Các Phương Pháp
Dưới đây là so sánh thực tế giữa 3 chiến lược trên khi truy vấn trên bảng chứa 1.500.000 địa chỉ Việt Nam chạy trên RDS tiêu chuẩn (2 vCPU, 4GB RAM):
| Chiến Lược | Độ Trễ Trung Bình | Dung Lượng Index | Không Phân Biệt Dấu? | Sửa Lỗi Chính Tả? |
| :--- | :--- | :--- | :--- | :--- |
| ILIKE (Không Index) | ~1.200ms (Chậm) | N/A | Không | Không |
| FTS GIN Index | ~8ms (Rất Nhanh) | ~140 MB | Có | Không |
| Trigram GIN Index | ~22ms (Nhanh) | ~310 MB (Cao) | Có | Có |
Mặc dù Trigram chạy chậm hơn một chút và tốn nhiều dung lượng ổ đĩa hơn Full-Text Search thông thường, nhưng nó mang lại sự cân bằng tốt nhất giữa tốc độ và trải nghiệm người dùng cho các ô tìm kiếm gợi ý.
Bài Toán Tự Xây Dựng: Khi Nào Nên Chuyển Sang Dùng API Ngoài
Xây dựng bộ máy tìm kiếm địa chỉ trong PostgreSQL là một cách tuyệt vời để giữ cho kiến trúc hệ thống của bạn đơn giản. Tuy nhiên, khi sản phẩm của bạn phát triển lớn hơn, bạn sẽ gặp phải những giới hạn về mặt logic của việc tìm kiếm văn bản trong database:
- Bản đồ viết tắt: Trigram không thể tự động hiểu "q.1" là "Quận 1", "đống đa" là "Đống Đa" hay "tp hcm" là "Thành phố Hồ Chí Minh" nếu không tự xây dựng bộ từ điển đồng nghĩa phức tạp.
- Thiếu Tọa Độ (Geocoding): Đánh index văn bản trong database chỉ giúp khớp chữ. Nó không biết rằng "123 Nguyễn Trãi, Quận 5" nằm ở tọa độ
10.7554, 106.6642. Để hiển thị vị trí trên bản đồ hoặc tính toán phí giao hàng, bạn vẫn cần dịch vụ Geocoding. - Hiệu Năng Ghi & RAM: Các hệ thống thanh toán có lượng truy cập cao sẽ làm cạn kiệt kết nối database bằng các truy vấn tìm kiếm địa chỉ, làm giảm tài nguyên dành cho các giao dịch quan trọng hơn (như thanh toán và tạo đơn hàng).
Nếu sản phẩm của bạn liên quan nhiều đến logistics, vận chuyển hoặc cần tối ưu tỷ lệ chuyển đổi khi thanh toán tại Việt Nam, hãy cân nhắc chuyển giao việc quản lý địa chỉ cho một nền tảng chuyên biệt như GoGoDuk.
GoGoDuk cung cấp các API Bản đồ Việt Nam chuyên cho lập trình viên để phục vụ geocoding, tìm kiếm địa chỉ và ranh giới hành chính. Bạn sẽ có ngay bộ gợi ý địa chỉ thông minh, sửa lỗi chính tả vượt trội:
// Để GoGoDuk xử lý việc tìm kiếm phức tạp và tối ưu kết nối DB
const response = await fetch("https://api.gogoduk.com/v1/suggest?input=nguyn trai q5", {
headers: { "X-API-Key": process.env.GOGODUK_API_KEY! }
});
const { suggestions } = await response.json();Sự kết hợp giữa cơ sở dữ liệu PostgreSQL nhẹ và các API chuyên dụng sẽ giúp hệ thống backend của bạn luôn nhanh, gọn gàng và dễ bảo trì.
Tìm hiểu thêm về cách cấu hình server cho dự án của bạn tại bài viết Dockerizing ứng dụng NestJS cho Production, hoặc tối ưu hóa trải nghiệm điền địa chỉ tại Tối ưu hóa Autocomplete Địa chỉ Việt Nam cho Trải nghiệm Thanh toán.
Hệ thống của bạn đang gặp vấn đề hiệu năng hay mở rộng tải?
Tôi chuyên xây dựng hạ tầng bản đồ độ trễ thấp, streaming pipeline thời gian thực (Kafka, ClickHouse) và các hệ thống backend tối ưu. Hãy cùng hợp tác để nâng cấp sản phẩm của bạn.
Bài viết liên quan
12 Jun 2026
Chuyển từ Google Maps API sang API Bản Đồ Việt Nam: Chi Phí & Code
So sánh chi phí và hạn chế của Google Maps Platform tại Việt Nam, rồi chuyển geocoding, reverse geocoding và gợi ý địa chỉ sang GoGoDuk kèm code thực tế.
8 Jun 2026
Redis 8.8 Trình Làng: Cấu Trúc Array Mới, Native Rate Limiter Và Bước Nhảy Vọt Hiệu Năng
Đánh giá kỹ thuật chi tiết các tính năng mới trong bản phát hành Redis 8.8 (02/06/2026). Tìm hiểu cơ chế Array O(1) mới của antirez, lệnh rate limiter INCREX bản địa và Subkey notifications cấp độ field của Hash.
8 Jun 2026
PostGIS Performance Tuning: Từ 2s Xuống 10ms Cho Truy Vấn Địa Lý Việt Nam (Gogoduk Case Study)
Khám phá các kỹ thuật tối ưu hóa cơ sở dữ liệu PostGIS thực tế từ dự án Gogoduk Map API. Tìm hiểu cách chuyển dịch từ Geometry sang Geography, thiết kế Partial GIST Index và rút gọn đa giác để đạt tốc độ truy vấn 10ms.
8 Jun 2026
Redis Lua Script & SETNX: Giải Pháp Rate Limiting & Quota Alerting Hiệu Năng Cao Cho API
Tìm hiểu cách Gogoduk xây dựng hệ thống Rate Limiting và Quota Alerting cho API bằng Redis và Go. Hướng dẫn sử dụng Lua Script đảm bảo tính nguyên tử, chống rò rỉ RAM và dùng SETNX chống trùng lặp thông báo.