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

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