การค้นหาแบบ Full-Text ของ PostgreSQL: ถ้าทำถูกวิธีก็เร็วได้ (แก้ความเข้าใจผิดว่าเป็นของช้า)
(blog.vectorchord.ai)- แม้หลายคนจะมองว่า 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;
- 1. เพิ่มคอลัมน์
ปัจจัยที่ทำให้ประสิทธิภาพลดลง 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 ความคิดเห็น
ขอบคุณ ทำให้ผมได้ปรับแต่งคิวรีครับ
ความเห็นใน Hacker News น่ากลัวจัง... "สิบล้านรายการ? ล้อเล่นหรือเปล่า?"
ความคิดเห็นจาก Hacker News
ในฐานะผู้ดูแล
pg_searchตามเอกสารของ Postgres ทั้งแนวทางในบทความของ Neon/ParadeDB และกลยุทธ์ที่ใช้ที่นี่ต่างก็ถูกเสนอเป็นทางเลือกที่ใช้ได้pg_searchถูกออกแบบมาเพื่อแก้ปัญหาอย่างหลัง และเบนช์มาร์กก็สะท้อนเรื่องนั้นpg_searchทำงานกับคิวรีแบบ "สไตล์ Elastic" ที่หลากหลายและชนิดข้อมูลของ Postgres ได้ด้วยการกำหนดดัชนีอย่างง่ายเท่านั้นการคำนวณ
tsvectorแบบเรียลไทม์เป็นความผิดพลาดครั้งใหญ่ไม่เข้าใจแนวโน้มที่พยายามยัดทุกอย่างลงใน Postgres
ดีใจที่ได้เห็นการทำ full-text search แบบ native ของ Postgres มากขึ้น
ไม่มี execution plan เลย ทำให้เข้าใจได้ยากว่าเกิดอะไรขึ้น
tsvectorแบบเรียลไทม์จะใช้เฉพาะกับรายการที่ตรงกันเท่านั้น และคิวรีเบนช์มาร์กมีLIMIT 10จึงมีการตรวจซ้ำไม่มากเมื่อหลายปีก่อนเคยอยากใช้ native FTS แต่ไม่สำเร็จ
ได้แพ็กเกจส่วนขยาย
pg_searchและvchord_bm25แบบ RPM/DEBเห็นหลายทีมข้ามไปใช้ Elasticsearch หรือ Meilisearch กันตรง ๆ
เรคอร์ด 10 ล้านรายการเป็นชุดข้อมูลระดับของเล่น
เริ่มใช้ pg full-text ครั้งแรกตั้งแต่ราวปี 2008