กลับไปหน้าบทความ
#PostgreSQL#Full-text Search#GIN Index#Elasticsearch#Node.js

เริ่มทำ Full-text Search ใน PostgreSQL ก่อนเพิ่ม Elasticsearch

PostgreSQL มี Full-text Search ที่ค้นหาและจัดอันดับผลลัพธ์ได้โดยไม่ต้องเพิ่มบริการใหม่ในระบบ บทความนี้อธิบาย tsvector, tsquery, GIN index และเกณฑ์ตัดสินใจว่าควรขยับไปใช้ Elasticsearch เมื่อใด

9 สิงหาคม 2569อ่านประมาณ 2 นาที

แชร์บทความ

เริ่มทำ Full-text Search ใน PostgreSQL ก่อนเพิ่ม Elasticsearch

เริ่มทำ Full-text Search ใน PostgreSQL ก่อนเพิ่ม Elasticsearch

หลายทีมเริ่มออกแบบฟีเจอร์ค้นหาด้วยคำถามว่า “ต้องตั้ง Elasticsearch เลยไหม” แต่ถ้าข้อมูลอย่างบทความ สินค้า support ticket หรือข้อความต่าง ๆ อยู่ใน PostgreSQL อยู่แล้ว คำตอบคือยังไม่จำเป็นเสมอไป PostgreSQL มี Full-text Search ในตัวซึ่งทำงานได้จริงจังกว่าการค้นด้วย LIKE และช่วยให้ทีมเริ่มส่งมอบฟีเจอร์ได้โดยไม่ต้องเพิ่มบริการใหม่ใน production

เริ่มจากความต้องการของระบบ

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

ข้อดีของการเริ่มจากฐานข้อมูลเดิมคือโครงสร้างระบบไม่ซับซ้อนขึ้น ทีมไม่ต้องดูแล search cluster หรือสร้างกระบวนการซิงก์ข้อมูลข้ามระบบตั้งแต่วันแรก การสำรองข้อมูล การตรวจสอบปัญหา และการ deploy จึงยังอยู่ในกระบวนการที่ทีมคุ้นเคย

ทำความเข้าใจ tsvector และ tsquery

หัวใจของ PostgreSQL Full-text Search มีสองส่วน ได้แก่ tsvector และ tsquery โดย tsvector คือเอกสารที่ถูกตัดคำและปรับให้อยู่ในรูป token ที่ฐานข้อมูลใช้ค้นหาได้ ส่วน tsquery คือคำค้นที่ถูกแปลงให้อยู่ในรูปแบบเดียวกันเพื่อใช้จับคู่กับเอกสาร

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

ตัวดำเนินการพื้นฐานที่ควรรู้มีดังนี้:

  • web & backend หมายถึงเอกสารต้องพบทั้งคำว่า web และ backend
  • web | backend หมายถึงเอกสารพบคำใดคำหนึ่งก็ถือว่าตรงกับการค้นหา

การเลือก text search configuration ต้องเหมาะกับภาษาของข้อมูลจริง เพราะกระบวนการตัดคำและ normalize มีผลโดยตรงต่อคุณภาพของผลลัพธ์ หากระบบมีเนื้อหาหลายภาษา ควรทดสอบ token ที่ PostgreSQL สร้างจากข้อมูลตัวอย่างก่อนนำโครงสร้างไปใช้ทั้งหมด

สร้าง search vector และกำหนดน้ำหนัก

ในงานจริงมักสร้างคอลัมน์ tsvector แบบ generated column โดยรวมข้อมูลจากหลายฟิลด์ เช่น title และ content ไว้ด้วยกัน วิธีนี้ทำให้ search vector ถูกปรับปรุงตามข้อมูลต้นทางโดยอัตโนมัติ และไม่ต้องเขียนโค้ดฝั่งแอปพลิเคชันเพื่อซิงก์ค่าด้วยตนเอง

ฟิลด์แต่ละส่วนไม่จำเป็นต้องมีความสำคัญเท่ากัน คำที่พบในชื่อบทความควรมีน้ำหนักมากกว่าคำเดียวกันที่ซ่อนอยู่ลึกในเนื้อหา จึงสามารถใช้ setweight กำหนดให้ชื่อเป็นระดับ A และเนื้อหาเป็นระดับ B ได้

ALTER TABLE articles
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
  setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
  setweight(to_tsvector('english', coalesce(content, '')), 'B')
) STORED;

CREATE INDEX articles_search_vector_gin
ON articles
USING GIN (search_vector);

ตัวอย่างนี้ใช้ configuration ชื่อ english จึงควรเปลี่ยนให้เหมาะกับภาษาของข้อมูลจริง การใช้ coalesce ยังช่วยให้การสร้าง vector ทำงานได้แม้บางฟิลด์มีค่าเป็น NULL

ทำไมต้องมี GIN index

GIN index เหมาะกับข้อมูลที่หนึ่งแถวประกอบด้วย token จำนวนมาก โดยทำหน้าที่ช่วยค้นหาว่า token แต่ละคำปรากฏอยู่ในแถวใดบ้าง PostgreSQL จึงไม่ต้องตรวจสอบทุกแถวใหม่ทั้งหมดเมื่อมีคำค้นเข้ามา

การเขียนเงื่อนไข Full-text Search ถูกต้องแต่ลืมสร้าง index อาจยังดูเร็วเมื่อมีข้อมูลเพียงเล็กน้อย ปัญหาจะเริ่มเห็นชัดเมื่อจำนวนแถวเพิ่มขึ้น เพราะฐานข้อมูลต้องใช้เวลาค้นหามากขึ้นอย่างต่อเนื่อง ดังนั้น GIN index ควรเป็นส่วนหนึ่งของการออกแบบตั้งแต่เริ่มทดสอบกับข้อมูลที่มีขนาดใกล้เคียงของจริง

จัดอันดับผลลัพธ์ด้วย ts_rank

Full-text Search มีประโยชน์มากกว่าการตรวจว่าเอกสารมีคำค้นหรือไม่ เพราะ PostgreSQL มี ts_rank และ ts_rank_cd สำหรับคำนวณความเกี่ยวข้องของผลลัพธ์ หากผู้ใช้ค้นคำว่า “node postgres search” บทความที่มีหลายคำเหล่านี้อยู่ในชื่อควรถูกดันขึ้นมาก่อนบทความที่กล่าวถึงเพียงบางคำในช่วงท้ายของเนื้อหา

ตัวอย่างต่อไปนี้แปลงข้อความธรรมดาจากผู้ใช้เป็น query ค้นหา จากนั้นเลือกเฉพาะแถวที่ตรงกันและเรียงตามคะแนนความเกี่ยวข้อง:

WITH q AS (
  SELECT plainto_tsquery('english', $1) AS query
)
SELECT
  articles.id,
  articles.title,
  ts_rank_cd(articles.search_vector, q.query) AS rank
FROM articles
CROSS JOIN q
WHERE articles.search_vector @@ q.query
ORDER BY rank DESC;

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

เชื่อมต่อจาก Node.js อย่างปลอดภัย

Backend ที่เขียนด้วย Node.js สามารถส่งคำสั่งเหล่านี้ผ่าน pg, Prisma raw query, Kysely หรือ Drizzle ได้โดยไม่ต้องติดตั้ง SDK สำหรับ search engine โดยเฉพาะ สิ่งสำคัญคือคำค้นจากผู้ใช้ต้องถูกส่งผ่าน parameterized query เช่น $1 เสมอ และไม่ควรนำ keyword มาต่อเป็น SQL string ด้วยตนเอง

การห่อ logic การค้นหาไว้ในฟังก์ชันหรือ repository กลางจะช่วยให้ควบคุม query, ranking และ pagination ได้ง่ายขึ้น เมื่อภายหลังต้องเปลี่ยนวิธีสร้าง tsquery หรือย้ายไปใช้ search engine แยก ทีมก็สามารถแก้ไขผ่านจุดเชื่อมต่อเดียวได้

งานที่เหมาะกับ PostgreSQL Full-text Search

PostgreSQL Full-text Search เหมาะกับระบบที่ข้อมูลต้นทางอยู่ใน PostgreSQL และความต้องการยังเน้นการค้นหาข้อความพร้อมจัดอันดับแบบตรงไปตรงมา ตัวอย่างงานที่นำไปใช้ได้ดี ได้แก่:

  • ค้นหาบทความหรือ documentation ภายในระบบ
  • ค้นหา product catalog ขนาดเล็กถึงกลาง
  • ค้นหา support ticket
  • ค้นหา note หรือ message ภายใน dashboard
  • ค้นหาข้อมูลสำหรับหน้า admin หรือเครื่องมือภายในองค์กร

ระบบลักษณะนี้มักได้ประโยชน์จากการลดภาระด้าน operations มากกว่าการเพิ่ม search service ตั้งแต่ต้น ทีมสามารถพัฒนาฟีเจอร์ได้เร็วขึ้นและยังไม่ต้องแก้ปัญหาความสอดคล้องของข้อมูลระหว่างฐานข้อมูลหลักกับดัชนีภายนอก

เมื่อใดควรเลือก Elasticsearch หรือ OpenSearch

PostgreSQL Full-text Search ไม่ใช่ Elasticsearch ฉบับย่อและไม่ได้ถูกสร้างมาเพื่อมอบประสบการณ์แบบ search engine ครบทุกด้าน หากข้อกำหนดเริ่มซับซ้อนเกินขอบเขตของฐานข้อมูล การแยกระบบค้นหาออกมาอาจเป็นการลงทุนที่เหมาะสมกว่า

ความต้องการที่ควรพิจารณา Elasticsearch หรือ OpenSearch ได้แก่:

  • ต้องรองรับการพิมพ์ผิดหรือ typo tolerance อย่างจริงจัง
  • ต้องมี autocomplete ที่ซับซ้อน
  • ต้องจัดการ synonym จำนวนมากหรือหลายภาษา
  • ต้องมี faceted search บนข้อมูลขนาดใหญ่
  • ต้องวิเคราะห์ search logs และพฤติกรรมการค้นหาอย่างละเอียด
  • ต้องขยายระบบค้นหาแยกจากฐานข้อมูลหลัก

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

แนวทางนำไปใช้แบบเป็นขั้นตอน

วิธีที่เหมาะสมคือเริ่มจากโครงสร้างเล็กที่สุดที่ตอบโจทย์ แล้วใช้ข้อมูลจริงตรวจสอบว่าควรพัฒนาต่อในทิศทางใด กระบวนการนี้ช่วยให้ทีมตัดสินใจจาก latency และคุณภาพผลลัพธ์แทนการคาดเดาล่วงหน้า

  1. ระบุฟิลด์ที่ต้องค้นหาและกำหนดว่าฟิลด์ใดสำคัญกว่า
  2. สร้าง generated tsvector จากข้อมูลเหล่านั้น
  3. กำหนดน้ำหนักให้ชื่อ เนื้อหา และ metadata ตามความเหมาะสม
  4. สร้าง GIN index บน search vector
  5. เพิ่ม ranking ด้วย ts_rank หรือ ts_rank_cd
  6. ตรวจ query plan ด้วย EXPLAIN ANALYZE
  7. วัด latency และคุณภาพผลลัพธ์ด้วยข้อมูลขนาดใกล้เคียง production
  8. พิจารณา search engine แยกเมื่อพบข้อจำกัดที่ PostgreSQL แก้ไม่ได้อย่างคุ้มค่า

สรุป

หากเป้าหมายของระบบคือค้นหา record ที่เกี่ยวข้องให้รวดเร็วและเรียงอันดับได้ PostgreSQL Full-text Search อาจพาโปรเจกต์ไปได้ไกลกว่าที่คิด การเริ่มด้วย tsvector, GIN index และ ranking ช่วยให้ได้ฟีเจอร์เร็วพร้อมรักษาความเรียบง่ายของระบบไว้ เมื่อความต้องการเติบโตจนต้องใช้ความสามารถเฉพาะทาง จึงค่อยย้ายไป Elasticsearch หรือ OpenSearch ด้วยเหตุผลที่พิสูจน์ได้จากการใช้งานจริง