04 ส.ค. 2568·อ่าน 2 นาที

PostgreSQL ค้นหาทั่วระบบ: full-text, trigram, partial indexes

เรียนรู้การออกแบบการค้นหาแบบ "ค้นหาทั่วระบบ" ใน PostgreSQL สำหรับหน้าจอภายในโดยเลือก full-text, ดัชนี trigram และ partial index เพื่อผลลัพธ์ที่เร็ว

PostgreSQL ค้นหาทั่วระบบ: full-text, trigram, partial indexes

ความหมายที่แท้จริงของ “ค้นหาทั่วระบบ” สำหรับเครื่องมือภายใน

บนหน้าจอภายใน “ค้นหาทั่วระบบ” มักหมายถึง: “ช่วยฉันหาบันทึกที่ฉันคิดไว้ ให้เร็ว แม้ฉันจะจำไม่ชัด” ผู้ใช้ไม่ได้เรียกดู พวกเขาพยายามข้ามตรงไปยังลูกค้าหนึ่งคน ตั๋ว ใบแจ้งหนี้ หรืออุปกรณ์

นั่นคือเหตุผลที่การค้นหาช้าที่ให้ความรู้สึกแย่กว่าหน้าโหลดช้า หน้าจะโหลดครั้งเดียว แต่การค้นหาเกิดขึ้นซ้ำหลายครั้ง ขณะคนกำลังคุยโทรศัพท์หรือทำการไล่ตรวจ หากผลลัพธ์ใช้เวลา 2–3 วินาที ผู้ใช้จะเปลี่ยนคำค้น กด backspace ลองคำอื่น และท้ายที่สุดก็ทำให้ระบบมีโหลดมากขึ้นและผู้ใช้หงุดหงิดมากขึ้น

จากกล่องค้นหาเดียว ผู้ใช้คาดหวังพฤติกรรมหลายอย่าง: การจับตรงส่วน ("alex" หา "Alexander"), การทนต่อการพิมพ์ผิดเล็กน้อย ("microsfot" ยังคงหา "Microsoft" ได้), การเรียงลำดับ “ผลลัพธ์ที่ดีที่สุด” อย่างสมเหตุสมผล (ID หรืออีเมลที่ตรงขึ้นลอยขึ้นด้านบน), น้ำหนักเล็กน้อยต่อความสดใหม่ และตัวกรองที่ใช้เป็นค่าตั้งต้น (ตั๋วเปิด ลูกค้าที่ใช้งานอยู่)

ความยากคืออินพุตเดียวมักซ่อนความตั้งใจหลายอย่าง เจ้าหน้าที่อาจวางหมายเลขตั๋ว พิมพ์ชื่อตัดตอน ค้นหาอีเมล หรือใส่เบอร์โทร แต่ละความตั้งใจต้องการกลยุทธ์ต่างกัน ดัชนีต่างกัน และบางครั้งกฎการจัดอันดับต่างกัน

ดังนั้นอย่าเริ่มจากดัชนี เริ่มจากการระบุความตั้งใจการค้นหาที่ผู้ใช้มีจริง ๆ แยกฟิลด์ระบุตัวตน (ID, อีเมล) ออกจากฟิลด์คลุมเครือ (ชื่อ หัวเรื่อง) และข้อความยาว (บันทึก)

เริ่มจากการตั้งชื่อข้อมูลและพฤติกรรมการค้นหา

ก่อนเลือกดัชนี จดสิ่งที่คนพิมพ์จริง “PostgreSQL search everywhere” ฟังดูเหมือนฟีเจอร์เดียว แต่จริง ๆ มักเป็นการผสมของการค้นหาที่แตกต่างกันมาก

เครื่องมือภายในผสมผสานตัวระบุ “แข็ง” (order ID, หมายเลขตั๋ว, รหัสใบแจ้งหนี้) กับข้อความ “อ่อน” (ชื่อลูกค้า, อีเมล, บันทึก, แท็ก) กลุ่มเหล่านี้ทำงานต่างกันใน PostgreSQL จึงไม่ควรปฏิบัติเหมือนกัน

แยกพฤติกรรมต่อไปนี้:

  • การค้นหาตรง: ใครสักคนค้นหา TCK-104883 คาดหวังผลลัพธ์ที่ชัดเจนเพียงรายการเดียว
  • การค้นหาแบบคลุมเครือ: ใครสักคนพิมพ์ john smth ต้องการการจับที่ยืดหยุ่นข้ามชื่อ (และอาจรวมอีเมล) และจะสแกนรายการสั้น ๆ
  • การค้นหาที่ขับเคลื่อนด้วยตัวกรอง: ใครสักคนเลือก “สถานะ = เปิด” และ “มอบหมายให้ = ฉัน” เป็นการกรอง; กล่องข้อความเป็นรอง

ตัดสินใจแต่ต้นว่าผลลัพธ์ต้องมีการจัดลำดับ (ผลลัพธ์ที่ดีที่สุดก่อน) หรือเพียงกรองเท่านั้น การจัดลำดับสำคัญสำหรับบันทึกและคำอธิบายยาว ๆ สำหรับ ID และอีเมล การจัดลำดับมักให้ความรู้สึกสุ่มและเพิ่มต้นทุน

รายการตรวจสอบสั้น ๆ มักเพียงพอ:

  • ฟิลด์ใดถูกค้นหาทุกวัน?
  • อินพุตใดเป็นตรง (ID, โค้ด), คลุมเครือ (ชื่อ), หรือข้อความยาว (บันทึก)?
  • ตัวกรองใดใช้เกือบทุกการค้นหา?
  • คุณต้องการการเรียงลำดับ “ผลลัพธ์ที่ดีที่สุด” หรือพอแค่เจอเท่านั้น?
  • ตารางจะโตเร็วแค่ไหน: พัน รายแสน หรือเป็นล้าน?

ถ้าคุณตั้งคำตอบเหล่านี้ไว้ล่วงหน้า ตัวเลือกดัชนีจะไม่รู้สึกเหมือนการเดาอีกต่อไป

พื้นฐาน: การจับตรงและทำไม ILIKE มักทำร้ายประสิทธิภาพ

ล็อกชัยชนะง่าย ๆ ก่อน สำหรับหลายหน้าภายใน ดัชนี B-tree ธรรมดาให้ผลลัพธ์ทันทีสำหรับการจับตรงเช่น ID หมายเลขคำสั่ง อีเมล และการอ้างอิงภายนอก

ถ้าผู้ใช้วางค่าตรง ๆ ให้แน่ใจว่า query ของคุณเป็นการเปรียบเทียบตรงจริง ๆ WHERE id = ... หรือ WHERE email = ... สามารถเร็วมากกับดัชนีปกติ ดัชนี unique บน email มักคุ้มค่าเพราะทั้งความเร็วและคุณภาพข้อมูล

ปัญหาเริ่มขึ้นเมื่อ “ค้นหาทั่วระบบ” ค่อย ๆ กลายเป็น ILIKE คำสั่งอย่าง name ILIKE '%ann%' มีไวด์การ์ดนำหน้า PostgreSQL จึงไม่สามารถใช้ดัชนี B-tree ปกติได้ มันต้องตรวจสอบแถวจำนวนมากและช้าลงเมื่อฐานข้อมูลโตขึ้น

การค้นหาพร็อกซิฟิกซ์ (prefix search) ใช้งานได้ แต่เฉพาะเมื่อลวดลายผูกกับจุดเริ่มต้น: name ILIKE 'ann%' แม้กระนั้นรายละเอียดก็สำคัญ (collation, การจัดการตัวพิมพ์ และว่าคุณสร้างดัชนีจากนิพจน์เดียวกันที่ใช้ใน query หรือไม่) หาก UI ต้องไม่สนใจตัวพิมพ์ วิธีปฏิบัติทั่วไปคือใช้ lower(name) และสร้างดัชนีที่ตรงกันบน lower(name)

นอกจากนี้ ช่วยได้ถ้าตกลงกันว่า “snappy” หมายถึงอะไร:

  • ประมาณ 200 ms หรือน้อยกว่าสำหรับงานฐานข้อมูลเมื่อร้อนใน cache
  • ต่ำกว่า 1 วินาทีทั้งระบบ รวมเครือข่ายและการเรนเดอร์
  • ไม่มีสถานะกำลังโหลดที่มองเห็นได้สำหรับการค้นหาทั่วไป

ด้วยเป้าหมายแบบนี้ จะง่ายกว่าที่จะตัดสินใจว่าควรยึดกับการจับตรงและ prefix หรือถึงเวลาที่ต้องใช้ full-text search หรือตัวดัชนี trigram

เมื่อ full-text search เป็นเครื่องมือที่เหมาะสม

Full-text search เหมาะที่สุดเมื่อผู้ใช้พิมพ์ภาษาธรรมชาติและคาดหวังว่าระบบจะหาสิ่งที่เกี่ยวข้อง ไม่ใช่แค่การจับตรง คิดถึงข้อความตั๋ว บันทึกภายใน คำอธิบายยาว บทความความรู้ และบันทึกการโทร

ข้อดีใหญ่คือการจัดลำดับความเกี่ยวข้อง แทนที่จะคืนรายการยาว ๆ ที่ผลลัพธ์ที่ดีที่สุดถูกฝังอยู่ Full-text search สามารถเรียงตามความเหมาะสมได้ ในเครื่องมือภายใน นั่นสำคัญ: ผู้ใช้ต้องการคำตอบภายในไม่กี่วินาที ไม่ใช่หลังจากสแกน 50 แถว

โดยรวมแล้ว full-text search มีส่วนประกอบสามอย่าง:

  • tsvector (ข้อความที่ค้นหาได้ เก็บหรือสร้างแบบ on-the-fly)
  • tsquery (สิ่งที่ผู้ใช้พิมพ์ แปลงเป็นคำค้น)
  • การตั้งค่าภาษา (วิธีทำให้คำปกติลงรูป)

การตั้งค่าภาษาคือจุดที่พฤติกรรมปรากฏชัด PostgreSQL จะตัด stop words ทั่วไป (เช่น “the” หรือ “and”) และทำ stemming ดังนั้น “pay”, “paid”, และ “payment” อาจจับกันได้ นั่นดีสำหรับบันทึกและข้อความ แต่บางครั้งอาจทำให้ผู้ใช้ประหลาดใจเมื่อค้นหาคำสั้น ๆ ที่พบบ่อยแล้วไม่พบผล

คำพ้องความหมาย (synonyms) เป็นอีกจุดที่ต้องตัดสินใจ ช่วยเมื่อบริษัทใช้คำต่างกันสำหรับสิ่งเดียวกัน (เช่น “refund” กับ “chargeback”) แต่ต้องดูแลรักษาให้สั้นและอิงจากคำที่ทีมซัพพอร์ตหรือปฏิบัติการใช้จริง

ตัวอย่างเชิงปฏิบัติ: การค้นหา "can't login after reset" ควรดึงตั๋วที่มีข้อความว่า "cannot log in after password reset" ถึงแม้ถ้อยคำจะแตกต่างกัน พฤติกรรม “ค้นหาให้เจอที่เกี่ยวข้อง” นี้คือสิ่งที่ full-text search ถูกออกแบบมาให้ทำ และมักเป็นตัวเลือกที่ดีกว่าพยายามทำให้ ILIKE ทำงานเหมือนเครื่องมือค้นหา

เมื่อดัชนี trigram ชนะ

ปรับใช้ตรงที่คุณต้องการ
ปรับใช้เครื่องมือภายในของคุณบน AppMaster Cloud หรือบน AWS, Azure, หรือ Google Cloud ของคุณเอง
ปรับใช้ตอนนี้

ดัชนี trigram เหมาะเมื่ ผู้ใช้พิมพ์ชิ้นส่วน พิมพ์ผิด หรือจำได้เพียงบางส่วน มันโดดเด่นบนฟิลด์สั้น ๆ ที่ full-text เข้มงวดเกินไป: ชื่อบุคคล ชื่อบริษัท หัวเรื่องตั๋ว SKU หมายเลขคำสั่ง และรหัสสินค้า

trigram คือชิ้นส่วนข้อความ 3 ตัวอักษร PostgreSQL เปรียบเทียบสตริงสองอันจากจำนวน trigram ที่แชร์กัน นั่นคือเหตุผลที่มันจับ "Jon Smth" กับ "John Smith" ได้ หรือ "ACM" กับ "ACME" และหาผลลัพธ์เมื่อคำค้นเป็นส่วนกลางของคำ

นี่มักเป็นทางที่เร็วที่สุดสู่กล่องค้นหาที่ยอมรับความผิดพลาดของมนุษย์เมื่อหน้าที่คือ “หาแถวที่ถูกต้อง” ไม่ใช่ “หาเอกสารเกี่ยวกับหัวข้อ”

จุดที่มันชนะ full-text

Full-text ดีสำหรับข้อความยาวและการจัดลำดับตามความหมาย แต่ไม่จัดการสตริงบางส่วนและการพิมพ์ผิดเล็กน้อยบนฟิลด์สั้น ๆ ได้โดยตรง Trigram ถูกสร้างมาเพื่อลักษณะความไม่แม่นยำแบบนั้น

คงต้นทุนการเขียนให้สมเหตุสมผล

ดัชนี trigram ใหญ่กว่าและเพิ่มภาระเมื่อเขียน จึงต้องเลือกเฉพาะคอลัมน์จริง ๆ ที่ผู้ใช้ใช้:

  • ชื่อ อีเมล บริษัท ชื่อผู้ใช้
  • ตัวระบุสั้น ๆ (SKU โค้ด อ้างอิง)
  • ฟิลด์ชื่อสั้น ๆ (ไม่ใช่บันทึกขนาดใหญ่)

ถ้าคุณตั้งชื่อฟิลด์ที่ทีมพิมพ์ลงในกล่องค้นหาได้ชัดเจน คุณมักจะรักษาดัชนี trigram ให้เล็กและเร็วได้

ดัชนีบางส่วนสำหรับตัวกรองที่ผู้ใช้ใช้จริง

สร้างเครื่องมือภายในแบบเน้นการค้นหา
สร้างหน้าค้นหาภายในที่เน้นการค้นหา ด้วยโมเดล PostgreSQL ตัวกรอง และผลลัพธ์ที่คาดเดาได้
ลอง AppMaster

กล่อง “ค้นหาทั่วระบบ” มักมีค่าเริ่มต้นที่ซ่อนอยู่ ผู้ใช้ค้นหาใน workspace, เฉพาะรายการที่ใช้งาน และยกเว้นรายการที่ลบแล้ว หากตัวกรองเหล่านี้ปรากฏเกือบทุกคำขอ ให้ทำให้กรณีทั่วไปเร็วขึ้นโดยการจัดทำดัชนีเฉพาะแถวที่ตรงกัน

Partial index คือดัชนีปกติตัวหนึ่งที่มี WHERE clause PostgreSQL เก็บมันให้เล็กเพราะบันทึกเฉพาะแถวที่คุณสนใจมากที่สุด นั่นมักหมายถึงหน้าหน้าน้อยลงให้อ่านและอัตราการถูกเก็บในแคชที่ดีขึ้น

เป้าหมาย partial-index ทั่วไปได้แก่ แถวที่ใช้งาน (status = 'active'), การลบแบบนุ่ม (deleted_at IS NULL), การแยก tenant, และหน้าต่าง "ล่าสุด" (เช่น 90 วันล่าสุด)

กุญแจคือให้ตรงกับ UI หากหน้าจอซ่อนแถวที่ลบเสมอ ควรให้ query รวม deleted_at IS NULL เสมอ และ partial index ควรใช้เงื่อนไขเดียวกัน ความไม่ตรงกันเล็กน้อย เช่น ใช้ is_deleted = false ในที่หนึ่งและ deleted_at IS NULL ในอีกที่ อาจทำให้ planner ไม่ใช้ดัชนี

Partial index ทำงานร่วมกับ full-text และ trigram ได้ด้วย ตัวอย่างเช่น ดัชนีการค้นหาข้อความเฉพาะแถวที่ไม่ถูกลบจะช่วยควบคุมขนาดดัชนี

ข้อเสีย: partial index น้อยประโยชน์สำหรับคำค้นหาที่เกิดขึ้นไม่บ่อย หากบางครั้งมีการค้นหาข้ามแถวที่ลบ PostgreSQL อาจย้อนกลับไปใช้แผนช้า จัดการด้วยเส้นทางสำหรับผู้ดูแล หรือเพิ่มดัชนีที่สองเฉพาะเมื่อคำค้นหาหายากนั้นกลายเป็นเรื่องปกติ

ผสมวิธีโดยไม่ทำให้การค้นหาเป็นปริศนา

ทีมส่วนใหญ่จะผสมเทคนิคเพราะกล่องค้นหาเดียวต้องรองรับความตั้งใจต่างกัน เป้าหมายคือทำให้ลำดับการปฏิบัติชัดเจนเพื่อให้ผลลัพธ์คาดเดาได้

ลำดับความสำคัญเรียบง่ายช่วยได้ ไม่ว่าจะทำเป็นหลาย query หรือเป็น query เดียวที่มี CASE ชัดเจน

บันไดลำดับความสำคัญที่คาดเดาได้

เริ่มเข้มงวด แล้วค่อยผ่อนคลายเมื่อจำเป็น:

  • Exact match ก่อน (ID, อีเมล, หมายเลขตั๋ว, SKU) โดยใช้ดัชนี B-tree
  • Prefix match ถัดมาเมื่อเหมาะสม
  • Trigram match หลังจากนั้นสำหรับการพิมพ์ผิดและเศษชื่อ/หัวเรื่อง
  • Full-text search สุดท้ายสำหรับบันทึกยาว คำอธิบาย และเนื้อหาอิสระ

เมื่อคุณยึดตามบันไดเดียวกัน ผู้ใช้จะเรียนรู้ว่ากล่องหมายถึงอะไร พวกเขาจะไม่คิดว่าระบบพังเมื่อ “12345” เจอตั๋วทันทีในขณะที่ “refund policy” ต้องค้นหาไฟล์ข้อความยาว

กรองก่อน แล้วค่อยคลุมเครือ

การค้นหาแบบคลุมเครือแพงเมื่อมันต้องพิจารณาทั้งตาราง จำกัดชุดผู้สมัครด้วยตัวกรองที่ผู้ใช้ใช้จริง (สถานะ ทีมที่มอบหมาย ช่วงวันที่ บัญชี) แล้วค่อยรัน trigram หรือ full-text บนที่เหลือ แม้ดัชนี trigram ที่เร็วก็จะรู้สึกช้าเมื่อคุณให้มันสกอร์หลายล้านแถว

คุ้มค่าที่จะเขียนกฎหนึ่งย่อหน้าที่เพื่อนร่วมทีมที่ไม่ใช่เทคนิคอ่านเข้าใจได้ เช่น: “เราจับหมายเลขตั๋วแบบตรงก่อน แล้วชื่อผู้ใช้ที่ยอมรับการพิมพ์ผิด แล้วค่อยค้นหาบันทึก” คำจำกัดความร่วมนี้ป้องกันข้อถกเถียงภายหลังว่าทำไมแถวหนึ่งจึงโผล่ขึ้น

ขั้นตอนทีละขั้น: เลือกแนวทางและนำไปใช้อย่างปลอดภัย

เป็นเจ้าของสแต็กของคุณ
ควบคุมเต็มที่ด้วยการส่งออกรหัสต้นฉบับสำหรับการโฮสต์เองและปรับแต่งลึกกว่า
ส่งออกรหัส

กล่องค้นหาที่เร็วคือชุดของการตัดสินใจเล็ก ๆ เขียนมันก่อน แล้วงานฐานข้อมูลจะง่ายขึ้น

  1. กำหนดอินพุต: มันเป็นกล่องเดียวเท่านั้น หรือกล่องเดียวพร้อมตัวกรอง (สถานะ เจ้าของ ช่วงวันที่)?
  2. เลือกประเภทการจับต่อฟิลด์: ID/โค้ดต้องการจับตรง ชื่อและอีเมลมักต้อง prefix หรือการจับแบบคลุมเครือ ข้อความยาวควรใช้การค้นหาภาษาธรรมชาติ
  3. เพิ่มดัชนีที่ถูกต้องและยืนยันว่าใช้: สร้างดัชนี แล้วตรวจสอบ query จริงด้วย EXPLAIN (ANALYZE, BUFFERS)
  4. เพิ่มการจัดลำดับหรือการเรียงที่ตรงกับความตั้งใจ: ถ้าผู้ใช้พิมพ์ “invoice 1042” ผลลัพธ์ตรงควรขึ้นก่อน ถ้าพิมพ์ชื่อสะกดผิด การจัดลำดับตามความใกล้เคียงควรชนะ
  5. ทดสอบด้วยคำค้นจริง: ลองพิมพ์ผิด คำสั้นมาก (เช่น “al”), ข้อความยาวที่วางมา, อินพุตว่าง, และโหมด “เฉพาะตัวกรอง”

เพื่อปล่อยอย่างปลอดภัย เปลี่ยนทีละอย่างและเตรียมย้อนกลับง่าย สำหรับดัชนีใหม่บนตารางใหญ่ ให้ใช้ CREATE INDEX CONCURRENTLY เพื่อไม่บล็อกการเขียน หากเป็นไปได้ ปล่อยหลัง feature flag และเปรียบเทียบความหน่วงก่อน-หลัง

รูปแบบปฏิบัติที่ใช้ได้จริงสำหรับ “PostgreSQL search everywhere” คือ: exact match ก่อน (เร็วและแม่นยำ), trigram สำหรับฟิลด์ที่คนพิมพ์ผิด, และ full-text สำหรับข้อความยาวที่ได้ประโยชน์จากการจัดลำดับ

ตัวอย่างสมจริง: กล่องค้นหาเดียวในแผงแอดมินซัพพอร์ต

จินตนาการแผงแอดมินซัพพอร์ตที่ทีมมีแค่กล่องค้นหาเดียว แต่คาดหวังให้มันหาได้ทั้งลูกค้า ตั๋ว และบันทึก นี่คือปัญหา "อินพุตเดียว หลายความหมาย"

ชัยชนะแรกคือทำให้ความตั้งใจปรากฏโดยไม่เพิ่มความฝืด หากคำค้นดูเหมือนอีเมลหรือเบอร์โทร ให้จัดเส้นทางเป็นการค้นหาผู้ใช้ หากดูเหมือนหมายเลขตั๋ว (เช่น "TKT-10482") ส่งไปที่ตั๋วโดยตรง ทุกอย่างที่เหลือตกไปเป็นการค้นหาข้อความในหัวเรื่องและบันทึกตั๋ว

สำหรับการค้นหาลูกค้า ดัชนี trigram มักให้ความรู้สึกดีที่สุด ชื่อกับบริษัทมีความยุ่งเหยิงและผู้ใช้พิมพ์ชิ้นส่วน ดัชนี trigram ทำให้การค้นหาอย่าง “jon smi” หรือ “acm” เร็วและยืดหยุ่น

สำหรับบันทึกตั๋ว ให้ใช้ full-text search บันทึกเป็นประโยคจริงและคุณมักต้องการการจับที่เกี่ยวข้อง ไม่ใช่แค่ "มีสตริงย่อยนี้" การจัดลำดับช่วยเมื่อหลายตั๋วพูดถึงคำเดียวกัน

ตัวกรองสำคัญกว่าที่ทีมส่วนใหญ่นึก หากเจ้าหน้าที่อยู่ใน “ตั๋วเปิด” เสมอ ให้เพิ่ม partial index ที่ครอบคลุมเฉพาะแถวเปิด ทำแบบเดียวกันสำหรับ “ลูกค้าที่ใช้งาน” มันทำให้ดัชนีเล็กลงและทำให้เส้นทางปกติเร็ว

คำค้นสั้น ๆ สมควรได้กฎ มิฉะนั้นฐานข้อมูลจะทำงานหนักเกินความจำเป็น:

  • 1–2 ตัวอักษร: แสดงตั๋วเปิดล่าสุดและลูกค้าที่อัปเดตล่าสุด
  • 3+ ตัวอักษร: รัน trigram สำหรับฟิลด์ลูกค้า และ full-text สำหรับข้อความตั๋ว
  • ไม่มีความตั้งใจชัดเจน: แสดงรายการผสม แต่จำกัดแต่ละกลุ่ม (เช่น 10 ลูกค้า และ 10 ตั๋ว)

ความผิดพลาดทั่วไปที่ทำให้การค้นาหรือช้าหรือสับสน

ปรับแบบจำลองข้อมูลให้ถูกต้อง
ระบุฟิลด์ระบุตัวตนกับฟิลด์ที่คลุมเครือตั้งแต่ต้นเพื่อให้แอปของคุณยังเร็วเมื่อข้อมูลโตขึ้น
ออกแบบข้อมูล

บั๊กส่วนใหญ่ของ “ทำไมการค้นหาช้าจัง?” เกิดจากการกระทำของทีมเอง เป้าหมายไม่ใช่การจัดทำดัชนีทุกอย่าง แต่จัดทำดัชนีสิ่งที่ผู้ใช้ทำจริง

กับดักทั่วไปคือเพิ่มดัชนีบนหลายคอลัมน์ “กันไว้ก่อน” การอ่านอาจดีขึ้น แต่การแทรกและอัปเดตแต่ละครั้งต้องทำงานมากขึ้น ในเครื่องมือภายในที่บันทึกเปลี่ยนตลอดวัน (ตั๋ว คำสั่ง ผู้ใช้) ความเร็วเขียนมีความสำคัญ

ข้อผิดพลาดอีกอย่างคือใช้ full-text เมื่อสิ่งที่ต้องการจริง ๆ คือการค้นหาทนต่อการพิมพ์ผิดบนชื่อหรืออีเมล Full-text ดีสำหรับเอกสารและคำอธิบาย แต่มันไม่ใช่การแก้ปัญหาทั่วไปสำหรับ “Jon” กับ “John” หรือ “gmail.con” กับ “gmail.com” นั่นมักเป็นกรณีของ trigram

ตัวกรองยังสามารถทำลายแผนได้เงียบ ๆ หากการค้นหาส่วนใหญ่เกิดขึ้นพร้อมตัวกรองคงที่ (เช่น status = 'open' หรือ org_id = 42) ดัชนีที่ดีที่สุดอาจเป็น partial index ที่ตรงกับเงื่อนไขนั้น ถ้าคุณลืม สิ่งที่คาดว่าจะใช้ดัชนีอาจกลายเป็นการสแกนแถวมากกว่าที่คิด

บางความผิดพลาดที่เห็นบ่อย:

  • เพิ่มดัชนีมากเกินไปโดยไม่วัดต้นทุนการเขียน
  • คาดหวังว่า full-text จะทำงานเหมือน autocomplete ที่ทนต่อการพิมพ์ผิด
  • มองข้ามว่าตัวกรองทั่วไปเปลี่ยนดัชนีที่ถูกใช้
  • ทดสอบบนข้อมูลเล็ก ๆ แทนที่จะเป็นความถี่คำจริง (คำที่พบบ่อยเทียบกับ ID หายาก)
  • การเรียงลำดับโดยคอลัมน์ที่ไม่มีดัชนีรองรับ บังคับให้เกิด sort ช้า

ตัวอย่าง: หน้าซัพพอร์ตค้นหาตั๋วโดยหัวเรื่อง ชื่อลูกค้า และหมายเลขตั๋ว แล้วเรียงโดยกิจกรรมล่าสุด หาก latest_activity_at ไม่มีดัชนีสำหรับชุดที่ถูกกรอง (เช่น ตั๋วเปิด) การ sort นั้นอาจลบความเร็วที่คุณได้จากดัชนีการค้นหา

เช็ครวดเร็วก่อนปล่อย

หลีกเลี่ยงหนี้เทคโนโลยีด้านการค้นหา
เตรียมซอร์สโค้ดพร้อมใช้งานโปรดักชันขณะที่ความต้องการค้นหาของคุณเปลี่ยน
สร้างโค้ด

ก่อนเรียกฟีเจอร์ “ค้นหาทั่วระบบ” เสร็จ ให้ระบุพฤติกรรมที่คุณสัญญาต่อผู้ใช้ชัดเจน

  • ผู้ใช้พยายามหาบันทึกโดยตัวระบุตรง (หมายเลขตั๋ว อีเมล)?
  • พวกเขาคาดหวังการแมทช์แบบคลุมเครือสำหรับการพิมพ์ผิด?
  • ต้องการผลลัพธ์มีการจัดลำดับจากบันทึกยาวหรือไม่?

หากคุณผสมโหมด หลักแหลมว่าฝ่ายใดชนะเมื่อขัดแย้ง

จากนั้นระบุ 2–3 ฟิลด์ที่ขับเคลื่อนการค้นหาส่วนใหญ่ หาก 80% ของการค้นหาคือโดยอีเมล ชื่อ และหมายเลขตั๋ว ปรับจูนสิ่งเหล่านั้นก่อน และพิจารณาที่เหลือเป็นรอง

รายการตรวจสอบก่อนปล่อยสั้น ๆ:

  • ยืนยันโหมดการจับหลักต่อฟิลด์ (จับตรง, แมทช์คลุมเครือ, หรือข้อความจัดลำดับ)
  • ระบุฟิลเตอร์ที่ผู้ใช้ใช้ประจำและตรวจสอบว่าดัชนีตรงกับการรวมกันเหล่านั้น
  • ตัดสินใจว่าจะจัดการการค้นหาแนวสั้นและว่างอย่างไร (เช่น กำหนดให้ fuzzy ต้องมี 2–3 ตัวอักษร; แสดง “ล่าสุด” เมื่อว่าง)
  • ทำให้การจัดลำดับอธิบายได้: ล่าสุดที่สุด, ผลการจับข้อความที่ดีที่สุด, หรือกฎผสมเรียบง่าย

สุดท้าย ทดสอบด้วยขนาดข้อมูลและเวลาเชิงสมจริง ไม่ใช่แค่ความถูกต้อง คิวรีที่รู้สึกทันทีกับ 1,000 แถวอาจลากเมื่อมี 1,000,000 แถว

ขั้นตอนต่อไป: เปลี่ยนแผนเป็นหน้าค้นหาภายในที่เร็ว

กล่องค้นหาจะยังเร็วเมื่อทีมตกลงกันว่ามันควรทำอะไร เขียนกฎเป็นภาษาปกติ: “การแมทช์” หมายถึงอะไร (ตรง, prefix, ทนต่อการพิมพ์ผิด), ฟิลด์ใดถูกค้นหา และตัวกรองเปลี่ยนผลอย่างไร

เก็บชุดทดสอบสั้น ๆ ของการค้นหาจริงและใช้เป็นชุด regression สิบถึงยี่สิบคำค้นมักพอ: ชื่อที่พบบ่อยสองสามคำ, อีเมลบางส่วน, การพิมพ์ผิด, ชิ้นข้อความยาวที่วางมา, และกรณี “ไม่มีผลลัพธ์” รันก่อนและหลังการเปลี่ยนแปลงเพื่อไม่ให้งานปรับปรุงประสิทธิภาพทำให้ความเกี่ยวข้องเปลี่ยนโดยไม่รู้ตัว

ถ้าคุณสร้างเครื่องมือภายในด้วย AppMaster (appmaster.io) จะช่วยให้กำหนดกฎการค้นหาไปพร้อมกับแบบจำลองข้อมูลและตรรกะธุรกิจ เพื่อพฤติกรรม UI และทางเลือกฐานข้อมูลจะไม่เบี่ยงเบนเมื่อความต้องการเปลี่ยน

คำถามที่พบบ่อย

“ค้นหาทั่วระบบ” ปกติหมายถึงอะไรในเครื่องมือภายใน?

มองเป็น “หาบันทึกที่ฉันคิดไว้ ให้เร็วที่สุด” มากกว่าการเรียกดูทั่วไป เริ่มจากเขียนความตั้งใจจริงของผู้ใช้ (ค้นหาด้วย ID, ค้นหาชื่อ/อีเมลที่ยอมรับการพิมพ์ผิดได้, ค้นหาข้อความยาว) และตัวกรองเริ่มต้นที่พวกเขาใช้บ่อย ตัดสินใจเหล่านี้จะบอกคุณว่าควรรันคำค้นแบบไหนและควรลงทุนกับดัชนีใด

ทำไม `ILIKE '%...%'` ถึงทำให้การค้นหาช้า?

ILIKE '%term%' มีไวด์การ์ดนำหน้า ดังนั้น PostgreSQL มักจะไม่ใช้ดัชนี B-tree ปกติและต้องสแกนหลายแถว มันอาจดูโอเคกับตารางเล็ก ๆ แต่จะช้าลงอย่างมากเมื่อข้อมูลโตขึ้น หากต้องการการจับสตริงย่อยหรือการทนต่อการพิมพ์ผิด ให้วางแผนใช้ trigram หรือ full-text แทนการคาดหวังว่า ILIKE จะขยายตัวได้

วิธีที่เร็วที่สุดในการจัดการการค้นหาแบบตรง เช่น ID หรืออีเมล คืออะไร?

ใช้การเปรียบเทียบแบบตรง เช่น WHERE id = $1 หรือ WHERE email = $1 และรองรับด้วยดัชนี B-tree (โดยปกติอีเมลมักเป็น unique) การค้นหาแบบตรงคือการค้นหาที่ถูกที่สุดและทำให้ผลลัพธ์น่าเชื่อถือ หากผู้ใช้วางหมายเลขตั๋วหรืออีเมลทั้งตัว ให้ส่งคำค้นไปที่เส้นทางนี้ก่อน

ฉันจะทำการค้นหา prefix แบบไม่สนใจตัวพิมพ์โดยไม่ทำให้ดัชนีเสียได้อย่างไร?

ชอบรูปแบบพ้องต้นแบบที่ผูกกับจุดเริ่มต้นเช่น name ILIKE 'ann%' และทำให้สอดคล้องกับดัชนีของคุณ สำหรับพฤติกรรมไม่สนใจตัวพิมพ์ ตัวอย่างที่ใช้กันบ่อยคือค้นหา lower(name) และสร้างดัชนีบนนิพจน์เดียวกันเพื่อให้ query planner ใช้งานได้ หากไม่ได้ปักลวดลายที่จุดเริ่มต้น prefix search จะไม่พอ

เมื่อไหร่ที่ควรใช้ดัชนี trigram สำหรับกล่องค้นหา?

ใช้ดัชนี trigram เมื่อผู้ใช้พิมพ์เศษข้อความ พิมพ์ผิดเล็กน้อย หรือจำได้เพียง "ประมาณนั้น" โดยเฉพาะบนฟิลด์สั้น ๆ เช่น ชื่อ บริษัท หัวเรื่อง SKU โค้ด และชื่อผู้ใช้ มันจับชิ้นส่วน 3 ตัวอักษรและเปรียบเทียบความคล้ายคลึงกัน คัดเลือกคอลัมน์ที่ผู้ใช้จริง ๆ ใช้ เพราะดัชนี trigram ใหญ่กว่าและเพิ่มค่าใช้จ่ายเมื่อเขียน

เมื่อไหร่ที่ PostgreSQL full-text search เป็นตัวเลือกที่ดีกว่า?

ใช้ full-text search เมื่อผู้ใช้ค้นหาประโยคหรือคีย์เวิร์ดในเนื้อหาที่ยาวกว่า เช่น บันทึก ข้อความ คำอธิบาย หรือเอกสารความรู้ ข้อได้เปรียบหลักคือการคัดอันดับความเกี่ยวข้อง เพื่อให้ผลลัพธ์ที่ดีที่สุดอยู่บนสุด แต่อย่าลืมพฤติกรรมของภาษา เช่น สเตมมิงและการตัด stop-word ซึ่งเป็นประโยชน์กับเนื้อหาเชิงประโยคแต่บางครั้งอาจทำให้คำสั้น ๆ ไม่คืนค่าอะไร

ดัชนีแบบบางส่วน (partial indexes) ช่วยหน้าจอ “ค้นหาทั่วระบบ” อย่างไร?

เพิ่ม partial index เมื่อการค้นหาส่วนใหญ่มีตัวกรองเดียวกัน เช่น deleted_at IS NULL, status = 'open' หรือข้อจำกัด tenant/workspace ดัชนีจะครอบคลุมเฉพาะชุดที่ใช้งานบ่อย ทำให้ขนาดเล็กลงและเร็วขึ้น ตรวจสอบให้คำค้นของคุณใช้เงื่อนไขเดียวกับ partial index เป๊ะ ๆ มิฉะนั้น PostgreSQL อาจไม่ใช้มัน

ฉันจะรวม exact, trigram และ full-text โดยไม่ทำให้ผู้ใช้สับสนได้อย่างไร?

ใช้บันไดลำดับความสำคัญที่สม่ำเสมอเพื่อให้ผลลัพธ์คาดเดาได้: exact match ก่อนสำหรับ ID/อีเมล, ตามด้วย prefix ถ้าเหมาะ, แล้ว trigram สำหรับชื่อ/หัวเรื่องที่ยอมรับการพิมพ์ผิด, และ full-text สำหรับบันทึกหรือคำอธิบายยาว ๆ ใช้ตัวกรองเริ่มต้นก่อนเพื่อลดจำนวนแถวที่การค้นหาแบบคลุมเครือต้องพิจารณา

ฉันควรทำอย่างไรกับการค้นหา 1–2 ตัวอักษรหรือการป้อนว่าง?

ตั้งกฎง่าย ๆ เช่น ให้ต้องมี 3+ ตัวอักษรก่อนจะรัน fuzzy search และใช้การค้นหาสั้น ๆ เพื่อแสดงรายการล่าสุดหรือรายการที่เข้าถึงบ่อย ๆ การป้อน 1–2 ตัวอักษรสร้างสัญญาณรบกวนมากและอาจทำงานหนักโดยไม่คุ้มค่า กำหนดด้วยว่าจะแสดงอะไรเมื่อช่องว่างว่าง

ฉันจะตรวจสอบประสิทธิภาพและปล่อยการเปลี่ยนแปลงการค้นหาอย่างปลอดภัยได้อย่างไร?

สร้างดัชนีแล้วตรวจสอบ query จริงด้วย EXPLAIN (ANALYZE, BUFFERS) บนขนาดข้อมูลที่สมจริง มีกระบวนการปล่อยทีละอย่างเพื่อให้ย้อนกลับง่าย บนตารางใหญ่ ให้สร้างดัชนีด้วย CREATE INDEX CONCURRENTLY เพื่อไม่บล็อกการเขียน และถ้าเป็นไปได้ ทำงานนี้หลัง feature flag เพื่อตรวจวัดเวลาเฉลี่ยก่อน-หลัง หากคุณสร้างหน้าจอใน AppMaster (appmaster.io) ให้กำหนดกฎการค้นหาควบคู่กับแบบจำลองข้อมูลและตรรกะธุรกิจเพื่อป้องกันความคลาดเคลื่อนเมื่อความต้องการเปลี่ยน

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม