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 ชนะ

ขยายการค้นหาด้วย AI
เพิ่มการผสาน AI เมื่อคุณต้องการการค้นหาภายในที่ฉลาดขึ้นหรือเวิร์กโฟลว์การจัดประเภทตั๋ว
สร้างด้วย AI

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

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

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

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

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

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

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

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

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

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

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

กล่อง “ค้นหาทั่วระบบ” มักมีค่าเริ่มต้นที่ซ่อนอยู่ ผู้ใช้ค้นหาใน 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 ตั๋ว)

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

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

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

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

ข้อผิดพลาดอีกอย่างคือใช้ 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 ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

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