33 คะแนน โดย GN⁺ 2025-04-10 | 3 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้หลายคนจะมองว่า Full-Text Search (FTS) พื้นฐานของ PostgreSQL ช้า แต่ ถ้าปรับแต่งอย่างเหมาะสมก็ทำงานได้เร็วมาก
  • บล็อกของ Neon เปรียบเทียบส่วนขยาย pg_search ที่พัฒนาด้วย Rust กับ FTS พื้นฐาน และอ้างว่าแบบหลังช้ากว่า
  • แต่การเปรียบเทียบนี้มีความเป็นไปได้สูงว่าเกิดขึ้นในสภาพที่ ขาดการปรับแต่งพื้นฐานที่จำเป็น สำหรับ PostgreSQL FTS
  • บทความนี้พิสูจน์ด้วยตัวเลขว่า เพียงแค่ใช้การปรับแต่งง่าย ๆ กับการตั้งค่า FTS พื้นฐาน ก็เพิ่มประสิทธิภาพได้ถึง 50 เท่า

ภาพรวมการตั้งค่าเบนช์มาร์ก

  • ทดสอบบนตารางที่มีข้อมูลล็อก 10 ล้านรายการ
    CREATE TABLE benchmark_logs (  
        id SERIAL PRIMARY KEY,  
        message TEXT,  
        country VARCHAR(255),  
        severity INTEGER,  
        timestamp TIMESTAMP,  
        metadata JSONB  
    );  
    
  • โครงสร้างคิวรีที่เป็นปัญหา:
    SELECT country, COUNT(*)  
    FROM benchmark_logs  
    WHERE to_tsvector('english', message) @@ to_tsquery('english', 'research')  
    GROUP BY country  
    ORDER BY country;  
    
    • รัน to_tsvector() ภายในคิวรี → ไม่มีประสิทธิภาพอย่างมาก
    • แม้จะมี GIN index ก็ใช้งานได้ไม่เต็มประสิทธิภาพ

สภาพแวดล้อมการทดสอบ (จำลองค่าตั้งต้นเดิม)

  • ใช้ EC2 instance รุ่น i7ie.xlarge พร้อม local NVMe SSD
  • ใช้ 4 vCPUs และ PostgreSQL 16 (Docker)
  • การตั้งค่า PostgreSQL หลัก:
    -c shared_buffers=8GB  
    -c maintenance_work_mem=8GB  
    -c max_parallel_workers=4  
    -c max_worker_processes=4  
    
  • ข้อจำกัดด้านการประมวลผลแบบขนาน: max_parallel_workers_per_gather = 2 (Neon ใช้ 8)

ปัจจัยที่ทำให้ประสิทธิภาพลดลง 1: การคำนวณ tsvector แบบเรียลไทม์

  • เมื่อรัน to_tsvector() ภายในคิวรี:
  • ต้อง parse ข้อความและวิเคราะห์รูปคำใหม่ทุกครั้ง
  • ไม่สามารถใช้ index ได้เลย
  • วิธีแก้: สร้างคอลัมน์ tsvector ล่วงหน้าและทำดัชนี

    • 1. เพิ่มคอลัมน์ tsvector
    ALTER TABLE benchmark_logs ADD COLUMN message_tsvector tsvector;  
    
    • 2. เติมข้อมูล
      UPDATE benchmark_logs SET message_tsvector = to_tsvector('english', message);  
      
    • 3. สร้างดัชนี (ปิด fastupdate)
      CREATE INDEX idx_gin_logs_message_tsvector  
      ON benchmark_logs USING GIN (message_tsvector)  
      WITH (fastupdate = off);  
      
    • 4. แก้ไขคิวรี
      SELECT country, COUNT(*)  
      FROM benchmark_logs  
      WHERE message_tsvector @@ to_tsquery('english', 'research')  
      GROUP BY country  
      ORDER BY country;  
      

ปัจจัยที่ทำให้ประสิทธิภาพลดลง 2: การตั้งค่า fastupdate=on ของ GIN index

  • fastupdate=on ช่วยด้านประสิทธิภาพการเขียน แต่ส่งผลเสียต่อประสิทธิภาพการค้นหา
  • สำหรับชุดข้อมูลแบบอ่านอย่างเดียวหรือเน้นการค้นหา fastupdate=off เป็นสิ่งจำเป็น
  • ดัชนีจะเล็กและเร็วกว่า และไม่ต้องจัดการ pending list
  • วิธีสร้าง GIN index ที่ปรับแต่งแล้ว

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

ตัวเลขประสิทธิภาพที่ดีขึ้น: มากกว่า 50 เท่า

  • ก่อนปรับแต่ง: ประมาณ 41.3 วินาที (41,301 ms)
  • หลังปรับแต่ง: ประมาณ 0.88 วินาที (877 ms)
  • แสดงให้เห็นว่าประสิทธิภาพดีขึ้นราว 50 เท่า
  • ทำได้แม้ในสภาพแวดล้อมที่มีจำนวน worker สำหรับการประมวลผลแบบขนานน้อยกว่า

ประสิทธิภาพของ ts_rank อาจช้าจริงในทางปฏิบัติ

  • ts_rank หรือ ts_rank_cd ต้องประเมินผลลัพธ์ทั้งหมดก่อนแล้วจึงจัดเรียง จึงอาจค่อนข้างช้า
  • โดยเฉพาะเมื่อจัดการกับผลลัพธ์จำนวนมาก จะใช้ CPU/IO สูง

ฟีเจอร์จัดอันดับขั้นสูง: ส่วนขยาย VectorChord-BM25

  • หากความแม่นยำและความเร็วของการจัดเรียงสำคัญมาก การใช้ส่วนขยายเฉพาะทางจะมีประสิทธิภาพกว่า
  • VectorChord-BM25 เป็นส่วนขยายสำหรับ PostgreSQL ที่มีฟังก์ชันจัดอันดับบนอัลกอริทึม BM25
  • มีรายงานว่าเร็วกว่า Elasticsearch ถึง 3 เท่า

ข้อดีของ VectorChord-BM25

  • อัลกอริทึม BM25: อัลกอริทึมจัดอันดับการค้นหาที่พัฒนาต่อจาก TF-IDF
  • รูปแบบดัชนีเฉพาะทาง: ปรับให้เหมาะกับการค้นหาความเร็วสูง เช่น Block WeakAnd
  • มี type bm25vector สำหรับเก็บ representation ที่ผ่านการ tokenize แล้ว
  • เพิ่มทั้งความแม่นยำและความเร็วในการค้นหา

สรุป: FTS พื้นฐานของ PostgreSQL ก็เร็วได้มากพอ

  • หากใช้คอลัมน์ tsvector และ GIN index ที่เหมาะสม (fastupdate=off) ก็สามารถค้นหาได้เร็วมากแม้ใช้ FTS พื้นฐาน
  • การเปรียบเทียบด้านประสิทธิภาพควรทำบนพื้นฐานที่ผ่านการปรับแต่งแล้ว
  • หากต้องการฟีเจอร์จัดอันดับขั้นสูง อาจพิจารณาใช้ส่วนขยายอย่าง VectorChord-BM25
  • ข้อความสำคัญคือ ไม่ใช่เครื่องมือที่ช้า แต่อาจเป็นการตั้งค่าที่มีปัญหา

3 ความคิดเห็น

 
stadia 2025-06-03

ขอบคุณ ทำให้ผมได้ปรับแต่งคิวรีครับ

 
pcj9024 2025-04-10

ความเห็นใน Hacker News น่ากลัวจัง... "สิบล้านรายการ? ล้อเล่นหรือเปล่า?"

 
GN⁺ 2025-04-10
ความคิดเห็นจาก Hacker News
  • ในฐานะผู้ดูแล pg_search ตามเอกสารของ Postgres ทั้งแนวทางในบทความของ Neon/ParadeDB และกลยุทธ์ที่ใช้ที่นี่ต่างก็ถูกเสนอเป็นทางเลือกที่ใช้ได้

    • ปัญหาของ Postgres FTS ไม่ใช่การปรับแต่งคิวรีเดียวให้ดีที่สุด แต่คือการให้ประสิทธิภาพระดับ Elastic กับคิวรีจริงที่หลากหลาย
    • pg_search ถูกออกแบบมาเพื่อแก้ปัญหาอย่างหลัง และเบนช์มาร์กก็สะท้อนเรื่องนั้น
    • เบนช์มาร์กของ Neon/ParadeDB มีคิวรีทั้งหมด 12 แบบ ซึ่งไม่สมจริงสำหรับการใช้งานจริง
    • pg_search ทำงานกับคิวรีแบบ "สไตล์ Elastic" ที่หลากหลายและชนิดข้อมูลของ Postgres ได้ด้วยการกำหนดดัชนีอย่างง่ายเท่านั้น
  • การคำนวณ tsvector แบบเรียลไทม์เป็นความผิดพลาดครั้งใหญ่

    • ตอนนำ Postgres FTS ไปใช้ในโปรเจ็กต์ส่วนตัว ได้อ่านเอกสารและทำตามแนวทาง
    • เอกสารอธิบายอย่างชัดเจนถึงการสร้างเคสพื้นฐานที่ยังไม่ปรับแต่งและขั้นตอนการปรับแต่งให้เหมาะสม
    • คนที่ทำพลาดแบบนี้น่าจะไม่ได้อ่านเอกสาร หรือมีเจตนาจะนำเสนอ Postgres FTS แบบผิด ๆ
  • ไม่เข้าใจแนวโน้มที่พยายามยัดทุกอย่างลงใน Postgres

  • ดีใจที่ได้เห็นการทำ full-text search แบบ native ของ Postgres มากขึ้น

    • โซลูชันทางเลือกอย่าง lucene/tantivy ถูกออกแบบให้เข้ากับ immutable segment ดังนั้นเมื่อเอามาผูกกับ heap table ของ Postgres อาจกลายเป็นทางออกที่แย่กว่า
  • ไม่มี execution plan เลย ทำให้เข้าใจได้ยากว่าเกิดอะไรขึ้น

    • ถ้าคิวรีใช้ดัชนี การตรวจซ้ำ tsvector แบบเรียลไทม์จะใช้เฉพาะกับรายการที่ตรงกันเท่านั้น และคิวรีเบนช์มาร์กมี LIMIT 10 จึงมีการตรวจซ้ำไม่มาก
    • เงื่อนไขของคิวรีมีเงื่อนไขอยู่บนดัชนี gin 2 ตัว ทำให้ดูเหมือนว่า planner จะตรวจซ้ำรายการที่ตรงกันทั้งหมดก่อน
  • เมื่อหลายปีก่อนเคยอยากใช้ native FTS แต่ไม่สำเร็จ

    • บนตารางที่มีการแทรกข้อมูลหลายพันรายการต่อวินาที การอัปเดตทั้งหมดช้าจนทรานแซกชันหมดเวลา
    • เพิ่มดัชนีแล้ว แต่พอดัชนีตัวที่สองสร้างเสร็จ ระบบก็เกิด timeout
    • ต้องลบดัชนีทิ้งอีกครั้ง และไม่เคยมีโอกาสทดสอบประสิทธิภาพ FTS จริง ๆ
  • ได้แพ็กเกจส่วนขยาย pg_search และ vchord_bm25 แบบ RPM/DEB

    • ให้ลิงก์ไว้สำหรับคนที่อยากลองทำเบนช์มาร์กด้วยตัวเอง
  • เห็นหลายทีมข้ามไปใช้ Elasticsearch หรือ Meilisearch กันตรง ๆ

    • ถ้าใช้อย่างเหมาะสม ก็รีดประสิทธิภาพจาก native PG FTS ได้มาก
    • สงสัยว่าจะได้ประสิทธิภาพใกล้เคียงกันในเบราว์เซอร์ด้วย SQLite + FTS5 + Wasm หรือไม่
  • เรคอร์ด 10 ล้านรายการเป็นชุดข้อมูลระดับของเล่น

    • ชุดข้อมูลข้อความขนาดใหญ่อย่าง Wikipedia ทั้งชุด หรือคอมเมนต์ Reddit ก่อนปี 2022 น่าจะเหมาะกับการทำเบนช์มาร์กมากกว่า
  • เริ่มใช้ pg full-text ครั้งแรกตั้งแต่ราวปี 2008

    • ปัญหาของการค้นหาข้อความเต็มใน Postgres ไม่ใช่ว่าช้าเกินไป แต่คือไม่ยืดหยุ่นพอ
    • มันเหมาะกับการเพิ่มการค้นหาแบบง่าย ๆ แต่ไม่พอสำหรับการปรับแต่งการค้นหา
    • Solr และ Elasticsearch สามารถตั้งค่าดัชนีและกระบวนการค้นหาที่ซับซ้อนได้
    • Postgres อาจรับแนวคิดเหล่านี้มาใช้ได้ แต่ตอนนี้ยังไม่ได้มีอะไรให้เลย
    • Postgres แบ่งข้อความตามช่องว่าง และสามารถใช้ stop word กับ stemming แบบกำหนดเองได้
    • ไม่สามารถให้คะแนนผลการค้นหาตามน้ำหนักของฟิลด์ได้
    • เมื่อเทียบกับทางเลือกอื่นแล้ว มันเป็นระบบระดับของเล่น