การค้นหาข้อความเต็มใน PostgreSQL สามารถรองรับแอปหลายประเภทได้ ใช้กฎตัดสินใจง่าย ๆ คำสืบค้นเริ่มต้น และเช็คลิสต์การทำดัชนีเพื่อรู้ว่าเมื่อใดควรเพิ่มเครื่องมือค้นหา

ts_rank (หรือ ts_rank_cd) เพื่อให้แถวที่เกี่ยวข้องมากกว่าขึ้นก่อน\n\nการตั้งค่าภาษาเป็นเรื่องสำคัญเพราะมันเปลี่ยนวิธีที่ Postgres จัดการคำ ด้วยการตั้งค่าที่เหมาะสม “running” และ “run” สามารถจับคู่กันได้ (stemming) และคำฟังก์ชันทั่วไปจะถูกละเว้น (stop words) หากตั้งค่าไม่ถูกต้อง การค้นหาอาจรู้สึกเสียเพราะคำที่ผู้ใช้คุ้นเคยไม่ตรงกับที่ถูกดัชนี\n\nการจับคำนำหน้า (prefix matching) คือฟีเจอร์ที่คนมักต้องการเมื่อต้องการพฤติกรรมแบบ “typeahead” เช่น แมตช์ “dev” กับ “developer” ใน Postgres FTS มักทำด้วยตัวดำเนินการพรีฟิกซ์ (เช่น term:*). มันช่วยเพิ่มความรู้สึกคุณภาพ แต่บ่อยครั้งเพิ่มงานต่อคำค้น ดังนั้นให้ถือเป็นการอัปเกรด ไม่ใช่ค่าเริ่มต้น\n\nสิ่งที่ Postgres ไม่พยายามเป็น: แพลตฟอร์มการค้นหาเต็มรูปแบบที่มีทุกฟีเจอร์ หากคุณต้องการการแก้สะกดแบบ fuzzy, autocomplete ขั้นสูง, learning-to-rank, analyzer ที่ซับซ้อนตามฟิลด์ หรือการทำดัชนีแบบกระจายข้ามหลายโหนด คุณอยู่นอกเขตสบายของ built-in สำหรับหลายแอป PostgreSQL full-text search ให้สิ่งที่ผู้ใช้คาดหวังส่วนใหญ่ด้วยชิ้นส่วนน้อยกว่ามาก\n\n## คำสั่งเริ่มต้นที่คุณคัดลอกแล้วปรับได้\n\nนี่เป็นรูปร่างเล็ก ๆ ที่เป็นจริงสำหรับเนื้อหาที่คุณต้องการค้นหา:\n\nsql\n-- Minimal example table\nCREATE TABLE articles (\n id bigserial PRIMARY KEY,\n title text NOT NULL,\n body text NOT NULL,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\n\nเกณฑ์พื้นฐานที่ดีสำหรับ PostgreSQL full-text search คือ: สร้างคำสืบค้นจากสิ่งที่ผู้ใช้พิมพ์, กรองแถวก่อน (เมื่อทำได้), แล้วจัดลำดับผลลัพธ์ที่เหลือ\n\nsql\n-- $1 = user search text, $2 = limit, $3 = offset\nWITH q AS (\n SELECT websearch_to_tsquery('english', $1) AS query\n)\nSELECT\n a.id,\n a.title,\n a.updated_at,\n ts_rank_cd(\n setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||\n setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),\n q.query\n ) AS rank\nFROM articles a\nCROSS JOIN q\nWHERE\n a.updated_at \u003e= now() - interval '2 years' -- example safe filter\n AND (\n setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||\n setweight(to_tsvector('english', coalesce(a.body, '')), 'B')\n ) @@ q.query\nORDER BY rank DESC, a.updated_at DESC, a.id DESC\nLIMIT $2 OFFSET $3;\n\n\nรายละเอียดที่ช่วยประหยัดเวลาในภายหลัง:\n\n- ใส่ตัวกรองราคาถูกใน WHERE ก่อนการจัดลำดับ (status, tenant_id, date ranges). คุณจะจัดลำดับแถวให้น้อยลง ทำให้เร็วขึ้น\n- เพิ่ม tie-breaker ใน ORDER BY เสมอ (เช่น updated_at, แล้ว id). นี่ช่วยให้ pagination เสถียรเมื่อผลลัพธ์หลายรายการมี rank เท่ากัน\n- ใช้ websearch_to_tsquery สำหรับอินพุตผู้ใช้ มันจัดการอัญประกาศและตัวดำเนินการง่าย ๆ ตามที่ผู้คนคาดหวัง\n\nเมื่อ baseline นี้ทำงานได้ ให้ย้ายนิพจน์ to_tsvector(...) ลงในคอลัมน์ที่เก็บไว้ จะหลีกเลี่ยงการคำนวณซ้ำในทุกคำขอและทำให้การทำดัชนีง่ายขึ้น\n\n## การตั้งค่าดัชนีที่มักคุ้มค่า\n\nเรื่องราว “PostgreSQL full-text search ช้า” ส่วนใหญ่เกิดจากสิ่งเดียว: ฐานข้อมูลสร้างเอกสารการค้นหาในทุกคำขอ แก้ตรงนั้นก่อนโดยเก็บ tsvector ที่เตรียมไว้และทำดัชนีมัน\n\n### เก็บ tsvector: generated column หรือ trigger?\n\ngenerated column เป็นตัวเลือกที่ง่ายที่สุดเมื่อเอกสารการค้นหาของคุณสร้างจากคอลัมน์ในแถวเดียวกัน มันถูกต้องโดยอัตโนมัติและยากที่จะลืมตอนอัปเดต\n\nใช้ tsvector ที่ดูแลด้วย trigger เมื่อเอกสารขึ้นกับตารางที่เกี่ยวข้อง (เช่น รวมแถวสินค้าเข้ากับชื่อหมวดหมู่) หรือเมื่อต้องการตรรกะพิเศษที่ยากจะเขียนเป็นนิพจน์เดียว Trigger เพิ่มชิ้นส่วนเคลื่อนไหว ดังนั้นเก็บให้เล็กและทดสอบให้ดี\n\n### ดัชนีที่คุณแทบจะต้องการเสมอ\n\nสร้าง GIN index บนคอลัมน์ tsvector นั่นคือตั้งต้นที่ทำให้ PostgreSQL full-text search รู้สึกทันใจสำหรับการค้นหาแอปทั่วไป\n\nการตั้งค่าที่ใช้ได้หลายแอป:\n\n- เก็บ tsvector ในตารางเดียวกับแถวที่คุณค้นหาบ่อยที่สุด\n- เพิ่ม GIN index บน tsvector นั้น\n- แน่ใจว่า query ของคุณใช้ @@ กับ tsvector ที่เก็บไว้ ไม่ใช่ to_tsvector(...) คำนวณบน fly\n- พิจารณา VACUUM (ANALYZE) หลังจาก backfill จำนวนมากเพื่อให้ planner เข้าใจดัชนีใหม่\n\nการเก็บ vector ในตารางเดียวมักเร็วกว่าง่ายกว่า ตารางแยกอาจมีเหตุผลถ้าตารางฐานเขียนหนักมาก หรือถ้าคุณดัชนีเอกสารรวมที่ข้ามหลายตารางและต้องการอัปเดตตามตารางเวลาของคุณเอง\n\nPartial indexes ช่วยเมื่อคุณค้นหาเฉพาะส่วนของแถว เช่น status = 'active', tenant เดียวในแอป multi-tenant, หรือภาษาหนึ่ง ๆ มันลดขนาดดัชนีและอาจทำให้การค้นหาเร็วขึ้น แต่เฉพาะเมื่อคำค้นของคุณรวมตัวกรองเดิมไว้เสมอ\n\n## ได้ความเกี่ยวข้องที่ยอมรับได้โดยไม่โอเวอร์เอนจิเนียร์\n\nคุณจะได้ผลลัพธ์ดีเกินคาดกับ PostgreSQL full-text search ถ้าคุณเก็บกฎความเกี่ยวข้องให้เรียบง่ายและคาดเดาได้\n\nชัยชนะที่ง่ายที่สุดคือน้ำหนักฟิลด์: การแมตช์ใน title ควรนับมากกว่าการแมตช์ใน body สร้าง tsvector ผสมที่ title ได้น้ำหนักมากกว่าคำอธิบาย แล้วจัดลำดับด้วย ts_rank หรือ ts_rank_cd\n\nถ้าคุณต้องการให้รายการ “ใหม่” หรือ “ยอดนิยม” ลอยขึ้นมา ให้ทำอย่างระมัดระวัง บูสต์เล็ก ๆ ใช้ได้ แต่อย่าให้มันกลบความเกี่ยวข้องข้อความ รูปแบบปฏิบัติได้คือ: เรียงตามข้อความก่อน แล้วค่อยใช้ความสดใหม่เป็นตัวทำลายเสมอ, หรือเพิ่มโบนัสแบบ capped เพื่อไม่ให้รายการใหม่ที่ไม่เกี่ยวข้องชนะรายการเก่าที่ตรงเป้าหมาย\n\nSynonyms และการแมตช์วลีเป็นจุดที่ความคาดหวังมักแตกต่าง Synonyms ไม่ได้มาอัตโนมัติ; คุณต้องเพิ่ม thesaurus หรือพจนานุกรมแบบกำหนดเอง หรือตาขยายคำสืบค้นเอง (เช่น แปลง “auth” เป็น “authentication”). การแมตช์วลีก็ไม่ใช่ค่าเริ่มต้น: คำค้นธรรมดาจับคำได้ทุกที่ ไม่ได้จับ "วลีนี้เป๊ะ ๆ" หากผู้ใช้พิมพ์คำที่มีอัญประกาศหรือคำถามยาว ๆ ให้พิจารณาใช้ phraseto_tsquery หรือ websearch_to_tsquery เพื่อแมตช์พฤติกรรมการค้นหาที่ผู้ใช้คาดหวัง\n\nเนื้อหาหลายภาษาเป็นการตัดสินใจ ถ้าคุณรู้ภาษาของแต่ละเอกสาร ให้เก็บมันและสร้าง tsvector ด้วยการตั้งค่าภาษาที่ถูกต้อง (English, Russian ฯลฯ). ถ้าไม่รู้ ทางปลอดภัยคือ index ด้วยการตั้งค่า simple (ไม่มี stemming) หรือเก็บสองเวกเตอร์: หนึ่งแบบตามภาษาเมื่อรู้ และหนึ่งแบบ simple สำหรับทุกอย่าง\n\nเพื่อยืนยันความเกี่ยวข้อง ให้เก็บแบบเล็กและชัดเจน:\n\n- เก็บ 10–20 คำค้นจริงจากผู้ใช้ (หรือแชทซัพพอร์ต)\n- เขียน 1–3 ผลลัพธ์ที่ควรอยู่ด้านบน\n- รันพวกมันหลังการปรับจูนแต่ละครั้งและจดว่าดีขึ้นหรือแย่ลงอย่างไร\n- หยุดเมื่อรู้สึกว่าเพียงพอสำหรับแอปของคุณ\n\nมักพอแล้วสำหรับกล่องค้นหาแอปเช่น “templates”, “docs”, หรือ “projects”.\n\n## ความผิดพลาดทั่วไปที่ทำให้การค้นหา Postgres ดูแย่\n\nเรื่องราว “PostgreSQL full-text search ช้าหรือไม่เกี่ยวข้อง” ส่วนใหญ่เกิดจากข้อผิดพลาดเล็ก ๆ น้อย ๆ ที่แก้ได้ การแก้เหล่านี้มักง่ายกว่าการเพิ่มระบบค้นหาใหม่\n\nกับดักทั่วไปคือคิดว่า tsvector เป็นค่าที่คำนวณแล้วอยู่ถูกต้องเอง หากคุณเก็บ tsvector ในคอลัมน์แต่ไม่อัปเดตมันทุกครั้งเมื่อ insert/update ผลลัพธ์จะดูสุ่มเพราะดัชนีไม่ตรงกับข้อความ หากคุณคำนวณ to_tsvector(...) แบบ on-the-fly ใน query ผลลัพธ์อาจถูกต้องแต่ช้าลง และคุณจะพลาดประโยชน์ของดัชนีที่จัดไว้แล้ว\n\nวิธีทำให้ประสิทธิภาพแย่คือจัดลำดับก่อนแคบชุดผู้สมัคร ts_rank มีประโยชน์ แต่ควรทำหลังจาก Postgres ใช้ดัชนีหาแถวที่ตรง หากคุณคำนวณ rank สำหรับส่วนใหญ่ของตาราง (หรือ join กับตารางอื่นก่อน) คุณจะเปลี่ยนการค้นหาเร็วให้กลายเป็น table scan\n\nหลายคนคาดหวังว่า “contains” จะเหมือน LIKE '%term%' นั่นไม่ตรงกับ FTS เพราะ FTS อิงจากคำ ไม่ใช่สตริงย่อย ถ้าคุณต้องการ substring search สำหรับรหัสสินค้าให้ใช้เครื่องมืออื่น (เช่น trigram indexing) แยกต่างหาก\n\nปัญหาด้านประสิทธิภาพมักมาจากการจัดการผลลัพธ์ ไม่ใช่การแมตช์ สองรูปแบบที่ควรระวัง:\n\n- การแบ่งหน้าแบบ OFFSET ขนาดใหญ่ ทำให้ Postgres ข้ามแถวมากขึ้นเมื่อคุณเลื่อนหน้า\n- ชุดผลลัพธ์ไม่จำกัด ที่สามารถคืนหลายหมื่นแถว\n\nเรื่องปฏิบัติการก็สำคัญ Index bloat อาจเกิดขึ้นหลังการอัปเดตมาก ๆ และการ reindex อาจแพงหากรอจนสถานการณ์เลวร้าย วัดเวลา query จริง (และดู EXPLAIN ANALYZE) ก่อนและหลังการเปลี่ยนแปลง หากไม่มีตัวเลข จะง่ายต่อการ “แก้” PostgreSQL full-text search แล้วทำให้มันแย่ในแบบอื่น\n\n## เช็คลิสต์ด่วน: ตรวจสอบรูปร่างคำสืบค้นและดัชนี\n\nก่อนโทษ PostgreSQL full-text search ให้รันการเช็คเหล่านี้ เรื่องส่วนใหญ่เกิดจากพื้นฐานขาดหาย ไม่ใช่ตัวฟีเจอร์เอง\n\n### การตรวจสอบข้อมูล + ดัชนี\n\nสร้าง tsvector จริง: เก็บมันใน generated หรือคอลัมน์ที่ดูแล (ไม่คำนวณในทุกคำขอ), ใช้การตั้งค่าภาษาให้ถูก (english, simple, ฯลฯ), และใส่น้ำหนักถ้าผสมฟิลด์ (title \u003e subtitle \u003e body).\n\nทำให้สิ่งที่คุณดัชนีเป็นระเบียบ: แยกฟิลด์รบกวน (IDs, boilerplate, navigation text) ออกจาก tsvector และตัด blobs ขนาดใหญ่ถ้าผู้ใช้ไม่ค้นหา\n\nสร้างดัชนีที่ถูกต้อง: เพิ่ม GIN index บนคอลัมน์ tsvector และยืนยันว่าใช้ใน EXPLAIN. ถ้ามีเพียงส่วนหนึ่งที่ค้นหาได้ (เช่น status = 'published'), partial index จะลดขนาดและทำให้การอ่านเร็วขึ้น\n\nเก็บตารางให้มีสุขภาพดี: dead tuples ทำให้ index scans ช้าลง การ vacuum เป็นประจำสำคัญ โดยเฉพาะเนื้อหาที่อัปเดตบ่อย\n\nมีแผน reindex: การย้ายข้อมูลใหญ่หรือดัชนีบวมอาจต้องหน้าต่าง reindex ที่ควบคุมได้\n\nเมื่อข้อมูลและดัชนีถูกต้องแล้ว ให้มุ่งที่รูปร่างคำสืบค้น PostgreSQL full-text search เร็วเมื่อมันสามารถแคบชุดผู้สมัครได้เร็ว\n\n### การตรวจสอบคำสืบค้น + runtime\n\nกรองก่อนแล้วค่อยจัดลำดับ: ใช้ตัวกรองเข้มงวด (tenant, language, published, category) ก่อนการจัดลำดับ การจัดลำดับหลายพันแถวที่คุณจะทิ้งทีหลังคือการทำงานที่เสียเปล่า\n\nใช้การเรียงลำดับเสถียร: เรียงตาม rank แล้ว tie-breaker เช่น updated_at หรือ id เพื่อไม่ให้ผลลัพธ์เปลี่ยนระหว่างการรีเฟรช\n\nหลีกเลี่ยง “คำค้นทำทุกสิ่ง”: หากต้องการ fuzzy matching หรือการทนต่อการพิมพ์ผิด ให้ทำอย่างตั้งใจ (และวัดผล) อย่าเผลอบังคับ sequential scans\n\nทดสอบคำค้นจริง: รวบรวม 20 คำค้นยอดนิยม ตรวจสอบความเกี่ยวข้องด้วยตนเอง และเก็บรายการผลลัพธ์ที่คาดหวังเล็ก ๆ เพื่อจับ regression\n\nดูเส้นทางช้า: บันทึกคำค้นช้า, ตรวจสอบ EXPLAIN (ANALYZE, BUFFERS), และมอนิเตอร์ขนาดดัชนีและอัตราการเข้าถึงแคช เพื่อให้เห็นเมื่อการเติบโตเปลี่ยนพฤติกรรม\n\n## สถานการณ์ตัวอย่าง: จากการค้นหาไซต์พื้นฐานสู่ความต้องการที่เติบโต\n\nศูนย์ช่วยเหลือ SaaS เป็นจุดเริ่มที่ดีเพราะเป้าหมายเรียบง่าย: ช่วยคนหา บทความที่ตอบคำถาม พวกคุณมีบทความเป็นพัน ๆ แต่ละบทความมี title, short summary, และ body ผู้เข้าชมมักพิมพ์ 2–5 คำเช่น “reset password” หรือ “billing invoice”\n\nกับ PostgreSQL full-text search นี่อาจสำเร็จอย่างรวดเร็ว คุณเก็บ tsvector ของฟิลด์รวม, เพิ่ม GIN index, และจัดลำดับตามความเกี่ยวข้อง ความสำเร็จดูเหมือน: ผลลัพธ์ขึ้นภายใน 100 ms, ท็อป 3 มักถูกต้อง, และไม่ต้องคอยดูแลมาก\n\nแล้วผลิตภัณฑ์เติบโต ฝ่ายซัพพอร์ตอยากกรองตามพื้นที่ผลิตภัณฑ์, แพลตฟอร์ม (web, iOS, Android), และแผน (free, pro, business). นักเขียนเอกสารอยาก synonyms, “did you mean”, และการจัดการการพิมพ์ผิดดีกว่า การตลาดอยากได้ analytics เช่น “คำค้นยอดนิยมที่ไม่มีผลลัพธ์” ทราฟฟิกขึ้นและการค้นหากลายเป็นหนึ่งใน endpoints ที่ใช้มากที่สุด\n\nสัญญาณเหล่านั้นคือเหตุผลที่เครื่องมือค้นหาเฉพาะอาจคุ้มค่า:\n\n- คุณต้อง facets และตัวกรองมากบนหน้าเดียวกับการค้นหาข้อความเต็ม\n- คุณต้อง fuzzy matching, การทนต่อการพิมพ์ผิด, หรือ autocomplete เป็นฟีเจอร์ชั้นหนึ่ง\n- คุณต้อง analytics การค้นหาและวงจร feedback สำหรับความเกี่ยวข้อง\n- ทราฟฟิกการค้นหาสูงจนแยกมันออกจากฐานข้อมูลหลักสำคัญ\n\nเส้นทางย้ายจริงคือเก็บ Postgres เป็น source of truth แม้หลังเพิ่ม search engine เริ่มจากบันทึกคำค้นและกรณีไม่พบผล จากนั้นรันงานซิงค์อะซิงค์ที่คัดเฉพาะฟิลด์ที่ค้นหาไปยังดัชนีใหม่ รันทั้งคู่คู่ขนานสักระยะแล้วค่อยสลับ แทนการเดิมพันทั้งหมดในวันเดียว\n\n## ขั้นตอนถัดไป: ส่ง baseline วัดผล แล้วตัดสินใจ\n\nถ้าการค้นหาของคุณเป็นส่วนใหญ่ “หาบทความที่มีคำเหล่านี้” และชุดข้อมูลไม่ใหญ่มาก PostgreSQL full-text search มักเพียงพอ เริ่มจากตรงนั้น ทำให้มันทำงาน แล้วเพิ่มเครื่องมือเฉพาะเมื่อคุณบอกได้ชัดเจนว่าขาดอะไร\n\nสรุปที่ควรเก็บไว้:\n\n- ใช้ Postgres FTS เมื่อคุณเก็บ tsvector, เพิ่ม GIN index, และความต้องการการจัดลำดับพื้นฐาน\n- ส่งคำสืบค้นเริ่มต้นหนึ่งชุดและการตั้งค่าดัชนีหนึ่งชุด แล้ววัดเวลาแฝงจริงและ "ผู้ใช้เจอหรือไม่"\n- ปรับความเกี่ยวข้องด้วยการเปลี่ยนเล็ก ๆ ที่ชัดเจน (น้ำหนัก, การตั้งค่าภาษา, การแยกวิเคราะห์คำสืบค้น) ไม่ใช่การเขียนใหม่ครั้งใหญ่\n- วางแผนใช้ search engine เมื่อคุณเจอช่องว่างชัดเจน (autocomplete, การทนต่อการพิมพ์ผิด, facets) หรือสัญญาณการเติบโต (ขนาด, โหลด)\n\nขั้นตอนปฏิบัติ: นำคำสืบค้นและดัชนีจากส่วนก่อนหน้านี้ไปใช้งาน แล้วบันทึกเมตริกง่าย ๆ ประมาณสัปดาห์หนึ่ง ติดตาม p95 query time, query ช้า, และสัญญาณความสำเร็จคร่าว ๆ เช่น “search -> click -> no immediate bounce” (แม้เป็นตัวนับเหตุการณ์พื้นฐานก็ช่วยได้) คุณจะเห็นเร็วว่าต้องการการจัดลำดับที่ดีขึ้นหรือแค่ UX ที่ดีกว่า (ตัวกรอง, ไฮไลต์, snippets ที่ดีกว่า)\n\nเริ่มวางแผน search engine เมื่อมีความต้องการจริงหนึ่งในนี้ (ไม่ใช่แค่สิ่งที่น่าจะมี): autocomplete ที่รวดเร็วบนทุกคีย์สโตรกในระดับสเกล, การทนต่อการพิมพ์ผิดและการแก้สะกดอย่างแข็งแรง, facets และการจัดกลุ่มข้ามหลายฟิลด์ที่ต้องการการนับเร็ว, เครื่องมือความเกี่ยวข้องขั้นสูง (synonym sets, learning-to-rank, การบูสต์ต่อคำค้น), หรือโหลดคงที่สูงและดัชนีใหญ่ที่ยากจะทำให้เร็ว\n\nหากคุณต้องการเคลื่อนที่เร็วด้านแอป Koder.ai (koder.ai) อาจเป็นวิธีสะดวกในการสร้างต้นแบบ UI และ API การค้นหาผ่านแชท แล้วปรับซ้ำอย่างปลอดภัยโดยใช้ snapshots และ rollback ขณะวัดพฤติกรรมผู้ใช้จริง.PostgreSQL full-text search ถือว่า “เพียงพอ” เมื่อคุณสามารถทำได้พร้อมกันสามอย่าง:\n\n- ผลลัพธ์ที่เกี่ยวข้อง: รายการที่ตรงควรขึ้นมาใกล้ด้านบน\n- ตอบสนองเร็ว: ผลลัพธ์ยังคงเร็วภายใต้โหลดปกติ\n- ค่าใช้จ่ายด้านการปฏิบัติการต่ำ: คุณไม่ต้องรันระบบที่สองหรือสายงานซิงก์เพิ่มเติม\n\nถ้าคุณทำได้ด้วย tsvector ที่เก็บไว้พร้อมดัชนี GIN โดยทั่วไปคุณอยู่ในจุดที่ดีแล้ว.
เริ่มจาก PostgreSQL full-text search โดยค่าเริ่มต้น มันส่งมอบได้เร็วกว่า เก็บข้อมูลและการค้นหาให้อยู่ในที่เดียว และหลีกเลี่ยงการสร้างและดูแลท่อการทำดัชนีแยกต่างหาก\n\nให้ย้ายไปยังเครื่องมือค้นหาเฉพาะเมื่อคุณมีความต้องการชัดเจนที่ Postgres ทำได้ไม่ดี (การทนต่อการพิมพ์ผิดคุณภาพสูง, autocomplete แบบสมบูรณ์, การแบ่งหน้าเชิงซับซ้อน หรือโหลดการค้นหาที่แข่งขันกับงานฐานข้อมูลหลัก).
กฎง่ายๆ คือ: อยู่กับ Postgres หากคุณผ่านสามเช็คลิสต์นี้:\n\n1) ความต้องการความเกี่ยวข้อง: ยอมรับการจัดลำดับแบบ "ดีพอ" ได้หรือไม่\n2) โหลด + เวลาแฝง: การค้นหาไม่ได้ทำให้ฐานข้อมูลหลักหนักเกินไป\n3) ความซับซ้อน: คุณค้นหาในไม่กี่ฟิลด์ข้อความพร้อมตัวกรองไม่กี่ตัว\n\nถ้าคุณล้มเหลวอย่างหนักในข้อหนึ่ง (โดยเฉพาะคุณสมบัติความเกี่ยวข้องอย่างการพิมพ์ผิด/autocomplete หรือล้านการจราจรค้นหา) ให้พิจารณาเครื่องมือค้นหาเฉพาะ.
ใช้ Postgres FTS เมื่อการค้นหาของคุณเป็นการ “ค้นหาบันทึกที่ถูกต้อง” ข้ามไม่กี่ฟิลด์ เช่น title/body/notes พร้อมตัวกรองง่ายๆ (tenant, status, category).\n\nมันเหมาะกับศูนย์ช่วยเหลือ, เอกสารภายใน, ตั๋วสนับสนุน, การค้นหาบทความ/บล็อก และแดชบอร์ด SaaS ที่ผู้ใช้ค้นหาชื่อโปรเจ็กต์หรือโน้ต.
รูปแบบคำสืบค้นพื้นฐานที่ดีมักจะ:\n\n- แปลงอินพุตผู้ใช้ด้วย websearch_to_tsquery.\n- กรองเงื่อนไขถูก ๆ ก่อน (tenant/status/date).\n- แมตช์ด้วย @@ กับ tsvector ที่เก็บไว้.\n- เรียงลำดับด้วย ts_rank/ts_rank_cd บวก tie-breaker เสถียร เช่น updated_at, id.\n\nรูปแบบนี้ช่วยให้ผลลัพธ์เกี่ยวข้อง เร็ว และไม่สั่นเมื่อแบ่งหน้า.
เก็บ tsvector ที่เตรียมไว้ล่วงหน้าและเพิ่มดัชนี GIN นี่จะหลีกเลี่ยงการคำนวณ to_tsvector(...) ในทุกคำขอ\n\nการตั้งค่าที่ใช้ได้จริง:\n\n- ใส่ tsvector ไว้ในตารางเดียวกับที่คุณค้นหา\n- สร้างดัชนี GIN บนคอลัมน์นั้น\n- ให้แน่ใจว่า query ใช้ tsvector_column @@ tsquery\n\nนี่เป็นการแก้ปัญหาทั่วไปเมื่อการค้นหารู้สึกช้า.
ใช้ generated column เมื่อเอกสารการค้นหาสร้างจากคอลัมน์ในแถวเดียวกัน (เรียบง่ายและแก้ไขยาก)\n\nใช้ trigger-maintained เมื่อข้อความการค้นหาขึ้นกับตารางที่เกี่ยวข้องหรือมีตรรกะพิเศษ\n\nค่าดีฟอลต์: เลือก generated column ก่อน และใช้ trigger เมื่อจำเป็นจริง ๆ.
เริ่มจากกฎการจัดลำดับที่คาดเดาได้:\n\n- ถ่วงน้ำหนักฟิลด์: การแมตช์ใน title ควรมีค่าน้ำหนักมากกว่า body\n- บูสต์เล็ก ๆ: ถา้เพิ่มความสดใหม่/ความนิยม ให้บูสต์เล็ก ๆ อย่าให้ล้นจนชนะความเกี่ยวข้องข้อความ\n- ใช้การตั้งค่าภาษาให้ถูกต้อง: การตัดคำ/stop words สำคัญสำหรับคำเช่น run/running\n\nตรวจสอบด้วยชุดคำค้นจริงจากผู้ใช้และผลลัพธ์ที่คาดหวังเล็ก ๆ ก่อนจะประกาศว่าโอเค.
Postgres FTS ทำงานแบบคำ (lexeme) ไม่ใช่การค้นหาย่อยสตริง ดังนั้นมันจะไม่เทียบเท่า LIKE '%term%' สำหรับสตริงบางส่วนแบบอิสระ\n\nถ้าต้องการการค้นหาย่อยสตริง (เช่น รหัสสินค้า หรือชิ้นส่วนของ ID) ให้ใช้เครื่องมืออื่น (เช่น trigram index) แยกต่างหาก แทนที่จะบังคับให้ FTS ทำงานที่มันไม่ออกแบบมา.
สัญญาณที่บอกว่าควรเพิ่มเครื่องมือค้นหาเฉพาะ:\n\n- ต้องการการทนต่อการพิมพ์ผิดคุณภาพสูง, synonyms ในระดับใหญ่, หรือต้องการ autocomplete ที่สมบูรณ์\n- ต้อง facets/aggregations จำนวนมากพร้อมการนับที่เร็วบนชุดข้อมูลใหญ่\n- การจราจรค้นหากดดันฐานข้อมูลหลักจนทำงานหนัก\n- ต้องการเครื่องมือปรับความเกี่ยวข้องขั้นสูง (feedback, learning-to-rank)\n\nเส้นทางปฏิบัติ: เก็บ Postgres เป็น source of truth แล้วเพิ่มการซิงก์แบบอะซิงค์เมื่อความต้องการชัดเจน.