ใช้ Postgres เป็นเสิร์ชเอนจิน
(anyblockers.com)- สามารถสร้างเสิร์ชเอนจินแบบไฮบริดภายใน Postgres ที่รองรับทั้ง semantic search, full-text search และ fuzzy search ได้
- การค้นหาเป็นส่วนสำคัญของแอปจำนวนมาก แต่การทำให้ดีจริงนั้นไม่ง่าย โดยเฉพาะใน RAG pipeline ที่คุณภาพของการค้นหาอาจเป็นตัวตัดสินความสำเร็จหรือความล้มเหลวของทั้งกระบวนการ
- แม้ semantic search จะเป็นเทรนด์ แต่การค้นหาแบบอิงคำศัพท์ดั้งเดิมก็ยังคงเป็นแกนหลักของระบบค้นหา
- เทคนิคเชิงความหมายช่วยปรับปรุงผลลัพธ์ได้ แต่จะทำงานได้ดีที่สุดเมื่ออยู่บนพื้นฐานของ text search ที่แข็งแรง
การสร้างเสิร์ชเอนจินด้วย Postgres
- ผสาน 3 เทคนิคเข้าด้วยกัน:
- full-text search ด้วย
tsvector - semantic search ด้วย
pgvector - fuzzy matching ด้วย
pg_trgm
- full-text search ด้วย
- แนวทางนี้อาจไม่ได้ดีที่สุดแบบไร้ข้อกังขาในทุกกรณี แต่เป็นทางเลือกที่ยอดเยี่ยมแทนการสร้าง search service แยกต่างหาก
- เป็นจุดเริ่มต้นที่แข็งแรงซึ่งสามารถนำไปใช้งานและขยายต่อได้ภายในฐานข้อมูล Postgres เดิม
- เหตุผลที่ควรใช้ Postgres กับทุกอย่าง: Just use Postgres for everything, PostgreSQL is enough, Just use Postgres
การทำ FTS และ semantic search
- Supabase มี เอกสารเรื่องการทำ hybrid search ที่ยอดเยี่ยม จึงจะใช้เป็นจุดเริ่มต้น
- ทำ FTS ตามไกด์ด้วย GIN index และทำ semantic search ด้วย pgvector (หรือที่เรียกว่า bi-encoder dense retrieval)
- จากประสบการณ์ส่วนตัว การเลือก embedding ขนาด 1536 มิติให้ผลลัพธ์ที่ดีกว่ามาก
- แทนที่ฟังก์ชันของ Supabase ด้วย CTE และ query แล้วเติม
$นำหน้าพารามิเตอร์ - ที่นี่ใช้ RRF(Reciprocal Ranked Fusion) เพื่อรวมผลลัพธ์
- วิธีนี้ช่วยให้รายการที่อยู่อันดับสูงในหลายลิสต์ได้รับอันดับสูงในลิสต์สุดท้าย
- และยังช่วยไม่ให้รายการที่อยู่อันดับสูงในบางลิสต์แต่ต่ำในลิสต์อื่น ได้อันดับสูงเกินไปในผลลัพธ์สุดท้าย
- การคำนวณคะแนนโดยใช้ลำดับอันดับเป็นตัวหารอาจทำให้เรคอร์ดที่อยู่อันดับต่ำเสียเปรียบ
- จุดที่น่าสังเกต
$rrf_k: เพื่อป้องกันไม่ให้รายการอันดับหนึ่งได้คะแนนสูงเกินไปมาก (เพราะใช้การหารด้วยอันดับ) จึงมักเพิ่มค่าคงที่ k ในตัวส่วนเพื่อทำให้คะแนนเรียบขึ้น$_weight: สามารถกำหนดน้ำหนักให้แต่ละวิธีได้ ซึ่งมีประโยชน์มากในการปรับจูนผลลัพธ์
การทำ fuzzy search
- แม้ว่าวิธีก่อนหน้านี้จะแก้ปัญหาได้มาก แต่จะมีปัญหาทันทีเมื่อ named entity มีการสะกดผิด
- semantic search ช่วยลดปัญหาบางส่วนได้ด้วยการจับความคล้ายคลึงกัน แต่ยังลำบากกับชื่อ ตัวย่อ และข้อความอื่นที่ไม่ได้คล้ายกันในเชิงความหมาย
- เพื่อบรรเทาปัญหานี้ จึงเพิ่มส่วนขยาย
pg_trgmเพื่อให้รองรับ fuzzy search- มันทำงานด้วย trigram โดย trigram จะแยกคำออกเป็นลำดับอักขระ 3 ตัว จึงเหมาะกับ fuzzy search
- ทำให้สามารถจับคู่คำที่คล้ายกันได้แม้จะมีการพิมพ์ผิดหรือเปลี่ยนแปลงเล็กน้อย
- ตัวอย่างเช่น "hello" กับ "helo" มี trigram ร่วมกันหลายชุด จึงจับคู่กันได้ง่ายขึ้นในการค้นหาแบบ fuzzy
- สร้าง index ใหม่สำหรับคอลัมน์ที่ต้องการ แล้วเพิ่มเข้าไปใน query การค้นหาโดยรวม
- ส่วนขยาย pg_trgm เปิดเผยตัวดำเนินการ % เพื่อกรองข้อความที่มีความคล้ายมากกว่า
pg_trgm.similarity_threshold(ค่าเริ่มต้นคือ 0.3) - ยังมีตัวดำเนินการอื่นที่มีประโยชน์อีกหลายตัว
การปรับจูน full-text search
- ปรับน้ำหนักของ tsvector: เอกสารจริงไม่ได้มีแค่ชื่อเรื่อง แต่ยังมีเนื้อหาด้วย
- ถึงจะมีหลายคอลัมน์ ก็ยังคงเก็บ embedding ไว้เพียงคอลัมน์เดียว
- โดยส่วนตัวพบว่าการเก็บ
titleและbodyไว้ใน embedding เดียวกันไม่ได้ต่างด้านประสิทธิภาพมากนักเมื่อเทียบกับการเก็บหลาย embedding - ท้ายที่สุดแล้ว
titleควรเป็นการสรุปเนื้อหาแบบสั้น ๆ อยู่แล้ว จึงควรทดลองตามความเหมาะสม titleมักสั้นและอุดมด้วยคีย์เวิร์ด ขณะที่bodyยาวกว่าและมีรายละเอียดมากกว่า- ดังนั้นจึงควรปรับว่าน้ำหนักของคอลัมน์ full-text search ต่าง ๆ จะสัมพันธ์กันอย่างไร
- สามารถกำหนดลำดับความสำคัญตามตำแหน่งหรือความสำคัญของคำในเอกสารได้
- A-weight: สำคัญที่สุด (เช่น ชื่อเรื่อง, หัวข้อ) ค่าเริ่มต้น
1.0 - B-weight: สำคัญ (เช่น ช่วงต้นเอกสาร, บทสรุป) ค่าเริ่มต้น
0.4 - C-weight: ความสำคัญมาตรฐาน (เช่น เนื้อความหลัก) ค่าเริ่มต้น
0.2 - D-weight: สำคัญน้อยที่สุด (เช่น เชิงอรรถ, หมายเหตุ) ค่าเริ่มต้น
0.1
- A-weight: สำคัญที่สุด (เช่น ชื่อเรื่อง, หัวข้อ) ค่าเริ่มต้น
- ปรับน้ำหนักตามโครงสร้างเอกสารและความต้องการของแอปเพื่อจูน relevance ให้ละเอียดขึ้น
- เหตุผลที่ให้น้ำหนักกับชื่อเรื่องมากกว่า
- เพราะชื่อเรื่องมักสื่อหัวข้อหลักของเอกสารได้อย่างกระชับ
- ผู้ใช้มักกวาดตาดูชื่อเรื่องก่อนตอนค้นหา ดังนั้นการตรงกับคีย์เวิร์ดในชื่อเรื่องจึงมักสัมพันธ์กับเจตนาของผู้ใช้มากกว่าการตรงกันในเนื้อหา
การปรับตามความยาว
- หากอ่านเอกสารของ
ts_rank_cdจะพบว่ามีพารามิเตอร์ normalization-
ฟังก์ชันจัดอันดับทั้งสองใช้ตัวเลือกจำนวนเต็ม
normalizationเพื่อระบุว่าความยาวของเอกสารควรมีผลต่ออันดับอย่างไร ตัวเลือกจำนวนเต็มนี้เป็น bit mask ที่ควบคุมพฤติกรรมได้หลายแบบ: สามารถใช้|เพื่อระบุมากกว่าหนึ่งพฤติกรรมได้ (เช่น2|4)
-
- ตัวเลือกต่าง ๆ เหล่านี้สามารถใช้เพื่อ
- ปรับอคติจากความยาวเอกสาร
- ปรับสมดุล relevance ในชุดเอกสารที่หลากหลาย
- ปรับผลการจัดอันดับให้มีความสม่ำเสมอในการแสดงผล
- การตั้งค่า
0(ไม่ normalize) ให้กับชื่อเรื่อง และ1(log document length) ให้กับเนื้อหา ให้ผลลัพธ์ที่ดี - แต่ก็ควรทดลองหลายตัวเลือกเพื่อหาค่าที่เหมาะกับ use case ของตัวเองที่สุด
การ re-rank ด้วย cross-encoder
- ระบบค้นหาหลายระบบทำงานเป็นสองขั้นตอน
- กล่าวคือ ใช้ bi-encoder เพื่อดึงผลลัพธ์เริ่มต้น N รายการ แล้วใช้ cross-encoder เปรียบเทียบผลลัพธ์เหล่านี้กับคำค้นเพื่อจัดอันดับใหม่
- bi-encoder: เร็ว จึงเหมาะกับการค้นหาจากเอกสารจำนวนมาก
- cross-encoder
- ช้ากว่าแต่ให้ประสิทธิภาพดีกว่า จึงเหมาะกับการจัดอันดับผลลัพธ์ที่ดึงมาแล้วใหม่
- ประมวลผล query และ document ร่วมกัน ทำให้เข้าใจความสัมพันธ์ระหว่างทั้งสองได้ละเอียดกว่า
- แลกกับเวลาในการคำนวณและความสามารถในการขยายระบบ เพื่อให้ได้ความแม่นยำในการจัดอันดับที่ดีกว่า
- มีเครื่องมือหลายแบบสำหรับทำสิ่งนี้
- หนึ่งในตัวเลือกที่ดีที่สุดคือ Cohere's Rerank
- อีกวิธีคือ สร้างเองด้วย GPT ของ OpenAI
- cross-encoder ช่วยเพิ่มความแม่นยำของผลการค้นหาได้ เพราะเข้าใจความสัมพันธ์ระหว่าง query กับ document ได้ดีกว่า
- แต่เนื่องจากมีต้นทุนการคำนวณสูง จึงมีข้อจำกัดด้าน scalability
- ดังนั้นแนวทางแบบสองขั้นตอนจึงได้ผลดี คือใช้ bi-encoder สำหรับการค้นหารอบแรก และใช้ cross-encoder เฉพาะกับเอกสารจำนวนน้อยที่ดึงมาแล้ว
เมื่อไรควรมองหาโซลูชันทางเลือก
- PostgreSQL เป็นตัวเลือกที่เหมาะกับงานค้นหาหลายแบบ แต่ก็ไม่ได้ไร้ข้อจำกัด
- การไม่มีอัลกอริทึมขั้นสูงอย่าง BM25 อาจเริ่มรู้สึกได้เมื่อต้องจัดการกับเอกสารที่มีความยาวหลากหลาย
- full-text search ของ PostgreSQL พึ่งพา TF-IDF จึงอาจลำบากกับเอกสารที่ยาวมากและคำหายากในคอลเลกชันขนาดใหญ่
- ก่อนจะมองหาโซลูชันทางเลือก ควรวัดผลก่อนเสมอ เพราะอาจไม่คุ้มที่จะเปลี่ยนก็ได้
บทสรุป
- บทความนี้ครอบคลุมหลายเรื่อง ตั้งแต่ full-text search พื้นฐาน ไปจนถึงเทคนิคขั้นสูงอย่าง fuzzy matching, semantic search และการ boost ผลลัพธ์
- สามารถใช้ความสามารถอันทรงพลังของ Postgres เพื่อสร้างเสิร์ชเอนจินที่ทั้งยืดหยุ่นและทรงพลังตามความต้องการเฉพาะได้
- Postgres อาจไม่ใช่เครื่องมือแรกที่คนนึกถึงสำหรับงานค้นหา แต่จริง ๆ แล้วไปได้ไกลมาก
- หัวใจสำคัญของประสบการณ์การค้นหาที่ดี
- การทำซ้ำอย่างต่อเนื่องและการปรับจูนอย่างละเอียด
- ควรใช้เทคนิคดีบักที่กล่าวถึงเพื่อทำความเข้าใจประสิทธิภาพการค้นหา และอย่ากลัวที่จะปรับน้ำหนักกับพารามิเตอร์ตามฟีดแบ็กและพฤติกรรมของผู้ใช้
- แม้ PostgreSQL อาจขาดฟีเจอร์ค้นหาขั้นสูงบางอย่าง แต่ในกรณีส่วนใหญ่ก็ยังสร้างเสิร์ชเอนจินที่ทรงพลังได้เพียงพอ
- ก่อนจะมองหาโซลูชันอื่น ควรลองใช้ความสามารถของ Postgres ให้เต็มที่และวัดประสิทธิภาพก่อน ถ้ายังไม่พอค่อยพิจารณาทางเลือกอื่น
2 ความคิดเห็น
สงสัยเหมือนกันว่ารองรับการค้นหาภาษาเกาหลีได้ดีไหม
หัวข้อ Weekly วันนี้ก็เป็นเรื่อง Postgres เหมือนกัน แล้วนี่ก็ยังเป็น Postgres อีกเหมือนกันนะครับ ดูท่าทางว่ายิ่งฮิตก็ยิ่งมีบทความออกมาเยอะจริง ๆ
เรื่อง BM25 ดูได้จากด้านล่าง
pg_bm25 - ส่วนขยายค้นหาแบบ Full-Text สำหรับ Postgres ที่ให้คุณภาพระดับ Elastic
ParadeDB - PostgreSQL for Search