33 điểm bởi GN⁺ 2025-04-10 | 3 bình luận | Chia sẻ qua WhatsApp
  • Full-Text Search (FTS) mặc định của PostgreSQL thường bị cho là chậm, nhưng nếu tối ưu đúng cách thì có thể chạy rất nhanh
  • Blog của Neon so sánh extension pg_search viết bằng Rust với FTS mặc định và cho rằng giải pháp sau chậm hơn
  • Tuy nhiên, phép so sánh này nhiều khả năng được thực hiện trong trạng thái bỏ qua các bước tối ưu cơ bản nhưng thiết yếu cho PostgreSQL FTS
  • Bài viết này chứng minh bằng số liệu rằng chỉ cần áp dụng các tối ưu đơn giản cho cấu hình FTS mặc định cũng có thể tăng hiệu năng gấp 50 lần

Tổng quan thiết lập benchmark

  • Thử nghiệm được thực hiện dựa trên một bảng chứa 10 triệu bản ghi log
    CREATE TABLE benchmark_logs (  
        id SERIAL PRIMARY KEY,  
        message TEXT,  
        country VARCHAR(255),  
        severity INTEGER,  
        timestamp TIMESTAMP,  
        metadata JSONB  
    );  
    
  • Cấu trúc truy vấn gây vấn đề:
    SELECT country, COUNT(*)  
    FROM benchmark_logs  
    WHERE to_tsvector('english', message) @@ to_tsquery('english', 'research')  
    GROUP BY country  
    ORDER BY country;  
    
    • Chạy to_tsvector() ngay trong truy vấn → rất kém hiệu quả
    • Dù có GIN index cũng không được tận dụng đúng cách

Môi trường kiểm thử (sao chép cấu hình mặc định)

  • Sử dụng instance EC2 i7ie.xlarge, SSD NVMe cục bộ
  • 4 vCPU, PostgreSQL 16 (Docker)
  • Các thiết lập PostgreSQL chính:
    -c shared_buffers=8GB  
    -c maintenance_work_mem=8GB  
    -c max_parallel_workers=4  
    -c max_worker_processes=4  
    
  • Giới hạn xử lý song song: max_parallel_workers_per_gather = 2 (Neon dùng 8)

Nguyên nhân suy giảm hiệu năng 1: tính toán tsvector theo thời gian thực

  • Khi chạy to_tsvector() ngay trong truy vấn:
  • Mỗi lần đều phải phân tích văn bản, phân tích hình thái...
  • Hoàn toàn không thể tận dụng index
  • Cách khắc phục: tạo sẵn cột tsvector và đánh index

    • 1. Thêm cột tsvector
    ALTER TABLE benchmark_logs ADD COLUMN message_tsvector tsvector;  
    
    • 2. Điền dữ liệu
      UPDATE benchmark_logs SET message_tsvector = to_tsvector('english', message);  
      
    • 3. Tạo index (tắt fastupdate)
      CREATE INDEX idx_gin_logs_message_tsvector  
      ON benchmark_logs USING GIN (message_tsvector)  
      WITH (fastupdate = off);  
      
    • 4. Sửa lại truy vấn
      SELECT country, COUNT(*)  
      FROM benchmark_logs  
      WHERE message_tsvector @@ to_tsquery('english', 'research')  
      GROUP BY country  
      ORDER BY country;  
      

Nguyên nhân suy giảm hiệu năng 2: thiết lập fastupdate=on của GIN index

  • fastupdate=on có lợi cho hiệu năng ghi, nhưng gây bất lợi cho hiệu năng tìm kiếm
  • Với tập dữ liệu chỉ đọc hoặc thiên về tìm kiếm, fastupdate=off là bắt buộc
  • Index nhỏ hơn, nhanh hơn và không cần xử lý pending list
  • Cách tạo GIN index đã tối ưu

    CREATE INDEX idx_gin_logs_message_tsvector  
    ON benchmark_logs USING GIN (message_tsvector)  
    WITH (fastupdate = off);  
    

Mức cải thiện hiệu năng: hơn 50 lần

  • Trước khi tối ưu: khoảng 41,3 giây (41.301 ms)
  • Sau khi tối ưu: khoảng 0,88 giây (877 ms)
  • Cho thấy mức cải thiện hiệu năng khoảng 50 lần
  • Vẫn có thể đạt được hiệu năng này ngay cả trong môi trường có ít xử lý song song hơn

Hiệu năng của ts_rank thực sự có thể chậm

  • ts_rank hoặc ts_rank_cd có thể tương đối chậm vì phải đánh giá rồi sắp xếp toàn bộ kết quả
  • Đặc biệt khi xử lý lượng kết quả lớn, gánh nặng CPU/IO sẽ cao

Tính năng xếp hạng nâng cao: extension VectorChord-BM25

  • Nếu độ chính xác và tốc độ sắp xếp là yếu tố quan trọng, dùng extension chuyên dụng sẽ hiệu quả hơn
  • VectorChord-BM25 là extension cho PostgreSQL, cung cấp khả năng xếp hạng dựa trên thuật toán BM25
  • Thậm chí có báo cáo cho rằng nhanh hơn Elasticsearch 3 lần

Ưu điểm của VectorChord-BM25

  • Thuật toán BM25: thuật toán xếp hạng tìm kiếm tiên tiến hơn TF-IDF
  • Định dạng index chuyên dụng: tối ưu cho tìm kiếm tốc độ cao như Block WeakAnd
  • Cung cấp kiểu bm25vector: lưu biểu diễn đã được tokenize
  • Cải thiện cả độ chính xác lẫn tốc độ tìm kiếm

Kết luận: FTS mặc định của PostgreSQL cũng đủ nhanh

  • Khi dùng cột tsvector và GIN index phù hợp (fastupdate=off), FTS mặc định cũng có thể tìm kiếm rất nhanh
  • Việc so sánh hiệu năng nên được thực hiện trên cùng một mặt bằng tối ưu
  • Nếu cần tính năng xếp hạng nâng cao, có thể cân nhắc các công cụ mở rộng như VectorChord-BM25
  • Thông điệp cốt lõi: không phải công cụ chậm, mà có thể vấn đề nằm ở cấu hình

3 bình luận

 
stadia 2025-06-03

Nhờ đó tôi đã tinh chỉnh truy vấn.

 
pcj9024 2025-04-10

Ý kiến trên Hacker News đáng sợ thật... "Mười triệu? Đùa à?"

 
GN⁺ 2025-04-10
Ý kiến trên Hacker News
  • Với tư cách là người bảo trì của pg_search, theo tài liệu Postgres thì cả chiến lược trong bài của Neon/ParadeDB lẫn chiến lược dùng ở đây đều được nêu là những phương án thay thế hợp lệ

    • Vấn đề của Postgres FTS không phải là tối ưu một truy vấn đơn lẻ, mà là cung cấp hiệu năng ở mức Elastic cho nhiều truy vấn thực tế khác nhau
    • pg_search được thiết kế để giải quyết vấn đề thứ hai, và benchmark cũng phản ánh điều đó
    • Benchmark của Neon/ParadeDB chỉ gồm tổng cộng 12 truy vấn, điều này không thực tế trong các trường hợp sử dụng ngoài đời thật
    • pg_search hoạt động với nhiều truy vấn kiểu "Elastic-style" và các kiểu dữ liệu của Postgres chỉ với định nghĩa chỉ mục đơn giản
  • Tính toán tsvector theo thời gian thực là một sai lầm lớn

    • Khi triển khai Postgres FTS cho một dự án cá nhân, tôi đã đọc tài liệu và làm theo hướng dẫn
    • Tài liệu giải thích rất rõ cách tạo một trường hợp cơ bản chưa tối ưu rồi tối ưu hóa nó
    • Người mắc lỗi này có vẻ либо không đọc tài liệu, либо có ý định mô tả sai về Postgres FTS
  • Tôi không hiểu xu hướng muốn nhét mọi thứ vào Postgres

  • Tôi vui khi thấy nhiều triển khai tìm kiếm toàn văn thuần Postgres hơn

    • Các giải pháp thay thế (lucene/tantivy) được thiết kế cho các immutable segment, nên khi ghép với bảng heap của Postgres có thể trở thành giải pháp tệ hơn
  • Không có execution plan nên rất khó hiểu chuyện gì đang xảy ra

    • Nếu truy vấn dùng chỉ mục, việc recheck tsvector theo thời gian thực chỉ áp dụng cho các kết quả khớp, và vì truy vấn benchmark là LIMIT 10 nên số lần recheck là ít
    • Điều kiện truy vấn có ràng buộc trên 2 chỉ mục GIN, nên có vẻ planner đang recheck toàn bộ các kết quả khớp trước
  • Vài năm trước, tôi muốn dùng FTS native nhưng đã thất bại

    • Trên một bảng có hàng nghìn insert/giây, việc cập nhật toàn bộ trở nên chậm đến mức transaction bị timeout
    • Tôi đã thêm chỉ mục, nhưng khi chỉ mục thứ hai hoàn tất thì hệ thống bắt đầu bị timeout
    • Tôi phải xóa lại chỉ mục và chưa từng có cơ hội kiểm thử hiệu năng FTS thực sự
  • Tôi đã đóng gói RPM/DEB cho các extension pg_search và vchord_bm25

    • Tôi để lại liên kết cho những ai muốn tự benchmark
  • Tôi đã thấy nhiều đội ngũ chuyển thẳng sang Elasticsearch hoặc Meilisearch

    • Nếu dùng đúng cách, có thể khai thác được khá nhiều hiệu năng từ PG FTS native
    • Tôi tự hỏi liệu có thể đạt hiệu năng tương tự trong trình duyệt với SQLite + FTS5 + Wasm hay không
  • 10 triệu bản ghi là một bộ dữ liệu đồ chơi

    • Những bộ dữ liệu văn bản lớn như toàn bộ Wikipedia hoặc bình luận Reddit trước năm 2022 sẽ phù hợp hơn cho benchmark
  • Tôi lần đầu dùng full-text của pg vào khoảng năm 2008

    • Vấn đề của tìm kiếm toàn văn Postgres không phải là quá chậm, mà là không đủ linh hoạt
    • Nó phù hợp để thêm tìm kiếm đơn giản, nhưng không đủ nếu muốn tinh chỉnh tìm kiếm
    • Solr và Elasticsearch cho phép thiết lập xử lý chỉ mục và tìm kiếm phức tạp
    • Postgres có thể tiếp nhận các tính năng này, nhưng hiện tại chưa cung cấp gì cả
    • Postgres tách theo khoảng trắng, và có thể dùng stop word cùng stemming theo cách thủ công
    • Không thể chấm điểm kết quả tìm kiếm dựa trên trọng số của các trường
    • So với các lựa chọn thay thế, đây là một hệ thống đồ chơi