- Tìm kiếm toàn văn của PostgreSQL là cách tiếp cận xây dựng chức năng tìm kiếm bằng cách kết hợp
tsvector,tsquery,@@,ts_rankvà chỉ mục GIN, mà không cần công cụ tìm kiếm riêng - Chuẩn hóa từ khóa tìm kiếm và tài liệu theo đơn vị lexeme, đồng thời biểu diễn truy vấn bằng các toán tử như AND, OR, NOT, FOLLOWED BY để có thể triển khai phần lớn cú pháp tìm kiếm phổ biến
- Trong môi trường ví dụ, chỉ mục GIN đã giảm thời gian tìm kiếm từ hơn 200ms xuống khoảng 4ms, nhưng khi có nhiều kết quả, chi phí xếp hạng và sắp xếp của
ts_rankcó thể trở thành nút thắt - Điều chỉnh độ liên quan được triển khai bằng cách thêm các tín hiệu như trọng số tiêu đề, số phiếu bầu, điểm đánh giá, thể loại, độ mới vào biểu thức sắp xếp, hoặc gán trọng số cột bằng
setweight - Có thể hỗ trợ chịu lỗi chính tả, tìm kiếm faceted, tự động hoàn thành, tìm kiếm cụm từ chính xác và tìm kiếm lai, nhưng với PostgreSQL cần tự ghép nối các thành phần và phải kiểm tra giới hạn hiệu năng trên tập dữ liệu lớn
Cách tiếp cận tìm kiếm toàn văn của PostgreSQL
- PostgreSQL cung cấp các thành phần cấp thấp cho tìm kiếm toàn văn, có thể kết hợp chúng để tạo chức năng của một công cụ tìm kiếm
- Cách này linh hoạt, nhưng đòi hỏi nhiều công sức triển khai hơn so với Elasticsearch, Typesense hay Meilisearch, vốn có tìm kiếm toàn văn là mục đích chính
- Các truy vấn ví dụ sử dụng bộ dữ liệu Wikipedia Movie Plots trên Kaggle
- Bao gồm 34.000 tiêu đề phim
- Kích thước ở định dạng CSV khoảng 81MB
Các thành phần cốt lõi
- Tìm kiếm toàn văn của PostgreSQL hoạt động xoay quanh các yếu tố sau
tsvector: lưu văn bản cần tìm kiếm dưới dạng danh sách lexeme đã chuẩn hóatsquery: biểu diễn truy vấn tìm kiếm đã chuẩn hóa@@: toán tử khớp dùng để kiểm tratsquerycó khớp vớitsvectorhay khôngts_rank,ts_rank_cd: tính điểm liên quan của kết quả tìm kiếm- Chỉ mục GIN: chỉ mục đảo để truy vấn
tsvectorhiệu quả
tsvector và cấu hình tìm kiếm
tsvectorlưu một danh sách lexeme đã sắp xếp- Lexeme tương tự token, nhưng là chuỗi đã được chuẩn hóa để nhiều dạng khác nhau của cùng một từ được quy về một dạng
- Với cấu hình tiếng Anh, chữ hoa được chuyển thành chữ thường và hậu tố được loại bỏ để chuẩn hóa
- Khi phân tích câu tiếng Anh bằng
to_tsvector, các từ dừng như “I”, “to”, “an” sẽ bị loại bỏ- “refuse” và “Refusing” đều được chuyển thành
refus - Dấu câu bị bỏ qua
- Vị trí từ trong văn bản gốc và trọng số cũng được ghi lại
- “refuse” và “Refusing” đều được chuyển thành
- Nếu dùng cấu hình
simplethay cho cấu hình tìm kiếmenglish, từ sẽ được đưa vào đúng theo dạng được tìm thấy trong văn bản- “refuse” và “refusing” vẫn là hai lexeme khác nhau
- Cấu hình
simpleđặc biệt hữu ích cho các cột chứa nhãn hoặc tag
- PostgreSQL cung cấp các cấu hình tìm kiếm tích hợp cho nhiều ngôn ngữ, nhưng không có cấu hình CJK (tiếng Trung, tiếng Nhật, tiếng Hàn)
- Với các ngôn ngữ không được hỗ trợ, cấu hình
simplecó thể hoạt động khá thực tế - Tuy nhiên chưa chắc nó có đủ tốt cho CJK hay không
- Với các ngôn ngữ không được hỗ trợ, cấu hình
tsquery và biểu diễn truy vấn
tsquerylà kiểu dữ liệu biểu diễn truy vấn tìm kiếm đã chuẩn hóa- Từ khóa tìm kiếm phải là lexeme đã được chuẩn hóa
- Nhiều từ khóa có thể được kết hợp bằng các toán tử AND, OR, NOT, FOLLOWED BY
to_tsquery,plainto_tsquery,websearch_to_tsquerygiúp chuyển văn bản người dùng nhập thànhtsqueryphù hợp- Vai trò chính là chuẩn hóa các từ có trong văn bản đầu vào
- Dùng
websearch_to_tsquerycó thể tạo truy vấn gần giống ô tìm kiếm thông thườngdarth vaderđược xử lý như phép AND logic, yêu cầu cả hai từ đều có trong tài liệu- Cũng có thể tìm kiếm OR và loại trừ từ
- Tìm kiếm cụm từ biểu diễn dạng các từ nối tiếp nhau theo đúng thứ tự
- Với cấu hình tiếng Anh, các từ dừng như “the” bị loại bỏ, nên trong một số tìm kiếm cụm từ, gần như toàn bộ cụm có thể biến mất
- Trong trường hợp này, dùng cấu hình
simplecó thể cho kết quả như kỳ vọng
- Trong trường hợp này, dùng cấu hình
- Toán tử
@@được dùng khi kiểm tratsquerycó khớp vớitsvectorhay không
Chỉ mục GIN và hiệu năng tìm kiếm
- GIN là viết tắt của Generalized Inverted Index, một loại chỉ mục được thiết kế cho các truy vấn tìm giá trị phần tử nằm bên trong giá trị phức hợp
- GIN có thể dùng không chỉ cho tìm kiếm văn bản mà còn cho truy vấn JSON
- Có thể tạo một cột
tsvectorkết hợp nhiều cột có thể tìm kiếm, rồi tạo chỉ mục GIN trên cột đó - Trong môi trường ví dụ, chỉ mục GIN đã giảm thời gian tìm kiếm từ hơn 200ms xuống khoảng 4ms
Xếp hạng và tính toán độ liên quan
- Để có trải nghiệm tìm kiếm tốt, cần sắp xếp kết quả theo độ liên quan
- PostgreSQL cung cấp hai hàm xếp hạng định nghĩa sẵn là
ts_rankvàts_rank_cd- Cả hai hàm đều xét đến tần suất xuất hiện của từ khóa tìm kiếm
ts_rank_cdcũng phản ánh độ gần nhau giữa các lexeme khớp
- Độ liên quan phụ thuộc rất nhiều vào tính chất của ứng dụng
- Các hàm xếp hạng mặc định chỉ giống điểm khởi đầu; có thể tự tạo hàm xếp hạng riêng hoặc kết hợp với các yếu tố khác khi cần
ts_rankphải truy cập cộtsearchcủa từng kết quả- Nếu điều kiện
WHEREkhớp với nhiều hàng, PostgreSQL phải duyệt tất cả các hàng đó để tính xếp hạng và sắp xếp - Trong môi trường ví dụ, một truy vấn trả về trong 5–7ms, nhưng truy vấn phải xếp hạng hơn 1.000 kết quả như
darth OR vadermất khoảng 80ms
- Nếu điều kiện
Tinh chỉnh độ liên quan
- Độ liên quan dựa trên tần suất từ là giá trị mặc định tốt, nhưng dữ liệu có thể có những tín hiệu quan trọng hơn tần suất
- Trong bộ dữ liệu phim, có thể phản ánh các tín hiệu sau vào độ liên quan
- Coi kết quả khớp ở tiêu đề quan trọng hơn kết quả khớp trong mô tả hoặc cốt truyện
- Đẩy các phim phổ biến hơn lên cao dựa trên điểm đánh giá hoặc số phiếu bầu
- Nếu người dùng thích hài, xếp phim hài cao hơn
- Xem các tiêu đề mới hơn là liên quan hơn so với tiêu đề cũ
- Các công cụ tìm kiếm chuyên dụng cung cấp tính năng cấu hình để các cột hoặc trường khác nhau ảnh hưởng đến xếp hạng
- Có thể xem các ví dụ tài liệu liên quan tại Elastic, Typesense, Meilisearch
Boost dựa trên số, ngày tháng và giá trị chính xác
- PostgreSQL không trực tiếp cung cấp boost dựa trên cột khác, nhưng xếp hạng rốt cuộc là một biểu thức sắp xếp, nên có thể thêm tín hiệu riêng vào
- Để phản ánh số phiếu bầu, có thể triển khai bằng cách cộng boost dựa trên số phiếu vào điểm xếp hạng
- Ví dụ dùng log để làm mức ảnh hưởng tăng chậm hơn
- Hệ số
0.01đưa booster về thang đo gần với điểm xếp hạng
- Cũng có thể tạo hàm phức tạp hơn, chỉ boost điểm đánh giá khi số phiếu bầu đạt một mức nhất định
- Nếu muốn đẩy một thể loại cụ thể lên, có thể dùng một hàm như
valueBooster, chỉ trả về hệ số khi giá trị khớp với một giá trị cột cụ thể
Trọng số cột
- Có thể gán trọng số cho lexeme trong
tsvector - PostgreSQL hỗ trợ bốn trọng số A, B, C, D
- A là trọng số cao nhất
- D là thấp nhất và là giá trị mặc định
- Có thể dùng hàm
setweightđể kiểm soát trọng số khi tạo cộttsvector - Nếu gán trọng số cao hơn cho cột tiêu đề, phim có từ khóa tìm kiếm trong tiêu đề sẽ được đưa lên đầu kết quả và điểm xếp hạng cũng tăng
- Việc chỉ có bốn lớp trọng số là một hạn chế, và trọng số phải được áp dụng khi tính
tsvector
Chịu lỗi chính tả và tìm kiếm mờ
- PostgreSQL không trực tiếp hỗ trợ tìm kiếm mờ hay chịu lỗi chính tả khi dùng
tsvectorvàtsquery - Có thể triển khai theo cách sau, giả định lỗi chính tả nằm ở phía truy vấn
- Lập chỉ mục tất cả lexeme trong nội dung vào một bảng riêng
- Với từng từ trong truy vấn, tìm các từ ứng viên bằng độ tương đồng hoặc Levenshtein distance
- Sửa truy vấn để bao gồm các từ đã tìm được
- Thực hiện tìm kiếm với truy vấn đã sửa
- Ví dụ dùng Levenshtein distance vì đây là cách các công cụ tìm kiếm sử dụng cho tìm kiếm mờ
- Sau khi có danh sách từ ứng viên, cần điều chỉnh truy vấn để bao gồm tất cả các từ đó
Tìm kiếm faceted
- Tìm kiếm faceted được dùng rộng rãi, đặc biệt trên các trang thương mại điện tử, để người dùng liên tục thu hẹp phạm vi tìm kiếm
- Trong PostgreSQL, có thể triển khai bằng cách định nghĩa thủ công các danh mục rồi thêm chúng vào điều kiện
WHEREcủa tìm kiếm - Cũng có thể tạo danh mục bằng thuật toán dựa trên dữ liệu hiện có
- Ví dụ tạo facet “Decade” dựa trên năm của phim
- Có thể tính kèm số lượng kết quả khớp theo từng thập niên và hiển thị trong ngoặc
- Để lấy nhiều facet trong một truy vấn, có thể kết hợp CTE
- Cách này có thể hoạt động tốt với tập dữ liệu nhỏ đến trung bình, nhưng có thể chậm với tập dữ liệu rất lớn
Phạm vi và giới hạn của công cụ tìm kiếm PostgreSQL
- Bằng cách kết hợp các thành phần tìm kiếm toàn văn của PostgreSQL, có thể tạo một công cụ tìm kiếm khá nâng cao
- Tìm kiếm dựa trên PostgreSQL cũng hỗ trợ join và giao dịch ACID
- Đây là những khả năng mà các công cụ tìm kiếm khác thường không có
- Các chủ đề tìm kiếm nâng cao khác đáng đề cập gồm
- Gợi ý từ khóa và tự động hoàn thành
- Khớp cụm từ chính xác
- Tìm kiếm lai kết hợp với
pg-vector
- Những tính năng này có thể làm được bằng PostgreSQL, nhưng cần tự kết hợp các thành phần
- Trong một số trường hợp, hiệu năng có thể suy giảm trên tập dữ liệu rất lớn
- Bài tiếp theo, part 2, so sánh khi triển khai tìm kiếm trong PostgreSQL với khi thêm Elasticsearch vào hạ tầng và đồng bộ dữ liệu
1 bình luận
Ý kiến trên Hacker News
Không nên cố làm việc này theo kiểu bắt chước các tính năng của Elasticsearch
Vào những năm 2000, tôi từng làm một công cụ tìm kiếm bằng MySQL 3.x để phân tích dữ liệu EXIF của ảnh và lập chỉ mục hệ phân loại 3 tầng cùng số lượng đếm; đây là việc mà ngay cả nhiều nhà cung cấp đắt đỏ cũng không làm nổi, và Autonomy thậm chí còn không phân loại đúng được cấp cao nhất
Tôi mất 6 tuần mới tạm thành công, phải viết loại SQL mong manh đến mức chỉ cần đổi thứ tự cột trong
SELECTlà hiệu năng đã khác, và tôi không bao giờ muốn làm lại nữa. Dù về bản chất cơ sở dữ liệu không khác nhau quá nhiều, công cụ tìm kiếm đã tiến hóa hơn rất nhiềuCó thể thử vì tò mò kỹ thuật, nhưng tìm kiếm không chỉ là token hóa rồi xong. Chẳng mấy chốc bạn sẽ cần duyệt theo phân loại, đa ngôn ngữ, đồng nghĩa tự động, gợi ý sửa chính tả kiểu "Did you mean", hiệu năng ở quy mô lớn, rồi tự đưa mình vào ngõ cụt. Vì sức khỏe tinh thần, tốt hơn là dùng hẳn công cụ tìm kiếm; đồng bộ giữa PG và ES cũng có các công cụ như ZomboDB hay PGSync
Ngược lại, nếu nhà phân tích dữ liệu hay lập trình viên chỉ muốn tìm trong các cột văn bản của một cơ sở dữ liệu lớn linh hoạt hơn
LIKE/ILIKE, thì tạo chỉ mục/bảng tìm kiếm toàn văn ngay trong cùng cơ sở dữ liệu có thể là cách dễ hơn và nhanh hơn để đạt 90% nhu cầuTôi rất mong chờ phần 2 so sánh Postgres với Elasticsearch. Một ứng dụng ở công ty tôi dùng PG cho CRUD đối tượng và Elastic cho tìm kiếm, nhưng chúng tôi đã đánh giá thấp hoàn toàn công sức đồng bộ hai kho dữ liệu này, đến mức giờ đang cân nhắc bỏ Elasticsearch
Cứ 5 phút một cron job sẽ tìm trong DB các đối tượng cần lập chỉ mục với điều kiện
last_modified_at > last_indexing_started_timestamp, lập chỉ mục chúng vào Elasticsearch, rồi cập nhậtlast_indexing_started_timestampbằng thời điểm bắt đầu đồng bộ. Như vậy, đối tượng nào bị sửa trong khoảng từ lúc job bắt đầu tới lúc kết thúc sẽ được bắt ở lần chạy sauNếu cần dựng lại Elasticsearch thì chỉ việc xóa thời điểm lập chỉ mục cuối cùng và đồng bộ lại từ đầu; hệ thống sẽ tự phục hồi và không bị lệch đồng bộ
Như mọi tác vụ bất đồng bộ khác, luôn cần giám sát và retry, nhưng ES ổn định và nhanh nên hiếm khi có vấn đề. Dù vậy, yêu cầu nhất quán của chúng tôi khá lỏng, chỉ cần trạng thái mới nhất của PG đến được ES trong một khoảng thời gian hợp lý là đủ; nếu yêu cầu khác thì có thể câu chuyện cũng khác
Sau khi chuyển sang dùng tìm kiếm toàn văn của Postgres với trọng số cho chỉ mục và truy vấn, chúng tôi đã xử lý được mọi thứ cần thiết hoàn toàn trong Postgres chỉ với trigger cập nhật và các truy vấn tìm kiếm rất nhanh
Nhìn chung vẫn là thành công, và gánh nặng vận hành giảm đáng kể nên thời gian kỹ thuật bỏ vào hoàn toàn xứng đáng, nhưng đây không phải thứ để bắt đầu một cách hời hợt
Tùy nhu cầu, materialized view, view thường, và trigger có thể là lựa chọn tốt hơn. Tìm kiếm văn bản tích hợp sẵn có thể không hợp với use case của bạn, và việc tự làm phương án thay thế cũng không nhất thiết là quá khó
Nếu không có thời gian truy vấn p50/p99 dưới tải thực tế thì khó mà coi là đã được chứng minh. Một công cụ tìm kiếm trả kết quả sau 1 phút không phải là "nâng cao", và với cơ sở dữ liệu quan hệ như Postgres thì trên lý thuyết dĩ nhiên cũng làm được
Tôi nhìn chung đồng ý; nếu ở quy mô lớn, chẳng hạn vài triệu bản ghi trở lên, thì có lẽ tôi sẽ không dùng cách này. Mối quan tâm chính của tôi là có thể tái tạo được bao nhiêu tính năng
Với các use case tìm kiếm nhỏ, cách này có lợi thế là ít hạ tầng phải duy trì hơn, có tính nhất quán mạnh, và hỗ trợ join. Ở Xata, chúng tôi cũng đang nghĩ tới một lộ trình chuyển mượt từ Postgres ở quy mô nhỏ sang Elasticsearch với mức thay đổi phá vỡ tối thiểu
Nhìn từ góc đó thì tốc độ trả kết quả nhanh không hẳn đáng ngạc nhiên. Bạn có thể liên tục cập nhật sẵn kết quả cache bằng các tác vụ nền, rồi khi có yêu cầu thì trả ra ngay. Kiểu cache và thời gian phản hồi này có vẻ là câu chuyện khác với tốc độ tính toán kết quả tìm kiếm thực sự
Có người dùng kết nối chậm, và cũng có thể những từ khóa như động đất chỉ tăng đột biến ở một vài khu vực nhất định
Nếu truy vấn quá ngẫu nhiên thì cache không có kết quả và hệ thống sẽ trông tệ hơn thực tế; còn nếu không đủ ngẫu nhiên thì lại trông tốt hơn thực tế
Vì vậy, ưu thế của ES và các công cụ tương tự nằm ở khả năng mở rộng ngang qua nhiều nút hoặc các tính năng bổ sung đặt trên chỉ mục chính
Chỉ dùng
ts_rankthì ổn, nhưng thường người ta sẽ muốn điều chỉnh thứ hạng bằng các chỉ số mức độ liên quan khác. Nếu giải quyết được việc không thể dùng các chỉ số đó làm tiêu chí xếp hạng chính, thì kết quả sẽ nhanh tương đương truy vấn trên bảng DB thông thường có chỉ mụcHồi tuổi teen, tôi đã thử tự xây từ đầu khi còn chưa hiểu rõ cả search engine lẫn database. Tôi muốn xem có thể đi xa đến đâu, và có thể trả về kết quả tìm kiếm liên quan ở mức nào với tốc độ ra sao
Việc làm nhanh một database và search engine cơ bản có lẽ khá dễ ngay cả với lập trình viên nghiệp dư. Nếu hiểu các thuật toán khoa học máy tính nền tảng và cách tận dụng hệ điều hành cùng phần cứng, bạn có thể làm được trong một hai tháng
Ngay cả với ngôn ngữ bậc cao, tốc độ cũng không tệ, vào khoảng 250 nghìn QPS trên một laptop năm 2003. Nếu sharding thì khả năng mở rộng cũng không phải vấn đề lớn. Lập chỉ mục, khóa và tính nhất quán phức tạp hơn lưu trữ và truy vấn nhiều
Vấn đề thật sự lớn là tính chủ quan của tìm kiếm. Khi xuất hiện những câu hỏi như người dùng thật sự muốn tìm gì, làm sao tìm được khi chính họ còn không biết mình đang tìm gì, làm sao ngăn người muốn lạm dụng hệ thống, và xử lý truy vấn cùng dataset phức tạp thế nào, thì độ khó tăng lên vài bậc lớn
Tôi tò mò không biết bạn có thực sự dùng database đa dụng hay không và liệu con số đó có chính xác không. Tôi muốn biết bạn đã dùng kỹ thuật gì để vượt qua các engine này
Khi vượt quá mức đó thì cần các lời giải sáng tạo, và từ lúc ấy nhiều trade-off bắt đầu xuất hiện
Một trong nhiều vấn đề khi tìm kiếm bên trong Postgres là đây là tác vụ ngốn CPU, và xu hướng ngày càng chuyển sang dùng cả GPU. Lý tưởng nhất là để CPU của database dành cho các cập nhật giao dịch của mô hình dữ liệu cốt lõi
Tôi đã thấy nhiều cụm ES và Solr chạy 100% CPU trên hơn 10 node khi reindex, hoặc ngay cả lúc bình thường cũng dùng 30–50% trên hơn 10 node. Database tương ứng có thể chỉ là, chẳng hạn, instance AWS cỡ L/XL với 50–100GB dữ liệu và mức dùng CPU khoảng 30%
Nếu chuyển toàn bộ phần CPU cho search sang DB chính thì giờ bạn sẽ phải shard cả DB. Dù vậy, với side project thì các extension PG cho search, recursive join, vector, v.v. rất thú vị và đơn giản
Tôi đã thật sự làm việc này trước đây và làm ra thứ gì đó khá nhanh
https://austingwalters.com/fast-full-text-search-in-postgres...
Website hiện tại là https://askhn.ai
[1]: https://en.wikipedia.org/wiki/Kerning
Kết hợp các kỹ thuật này với pgvector thì còn có thể dùng embedding để tìm nội dung liên quan nữa. Cảm giác khá như ma thuật
Nhân tiện, không phải "Dark" Vader mà là Darth Vader. Hồi nhỏ tôi cũng từng nghĩ là "Dark"
Bài rất hay và rõ ràng. SQLite cũng cung cấp tính năng lập chỉ mục nâng cao và stemming qua plugin tiêu chuẩn
Với tiếng Anh thì SQLite cũng hoạt động tốt
https://github.com/daitangio/knowledge
Bạn nên thử xem. Khá mạnh đấy
Bài hay, nhưng phần nói rằng PostgreSQL không hỗ trợ fuzzy search thì hơi sai. Extension
pg_trgmvà chỉ mục trigram GIN hỗ trợ các trường hợp dùng fuzzy search như ví dụ trong bàihttps://www.postgresonline.com/article_pfriendly/169.html
Truy vấn có thể nhanh hơn đáng kể, nhưng phải đánh đổi bằng việc dùng nhiều bộ nhớ hơn và thời gian xử lý khi cập nhật cũng tăng