วิธีที่ผมเพิ่มระบบค้นหาที่รองรับการพิมพ์ผิด (Typo-Tolerant Search) ด้วย OpenSearch และ CJK

การค้นหาที่ไม่พบผลลัพธ์เลย (Zero-result queries) กำลังทำลายเวลาในการรับชม (watch time) ของเรา

เป็นเวลาหนึ่งปีที่ TopVideoHub ใช้ SQLite FTS5 สำหรับการค้นหา ซึ่งมันใช้งานได้ดีสำหรับการจับคู่คำที่ตรงกันเป๊ะๆ แต่จะล้มเหลวทันทีเมื่อผู้ใช้พิมพ์ผิด

ผู้คนค้นหาคำว่า "demon slyer" แทนที่จะเป็น "demon slayer" หรือมีการเว้นวรรคเกินมาในชื่อเรื่องภาษาญี่ปุ่นหรือภาษาเกาหลี เนื่องจาก FTS5 จะจับคู่ตาม token ที่ตรงกันเท่านั้น การพิมพ์ผิดเพียงจุดเดียวจึงหมายถึงการไม่พบผลลัพธ์ใดๆ เลย ซึ่งผู้ใช้จะไม่ลองค้นหาใหม่อีกครั้ง แต่พวกเขาจะออกจากระบบไปเลย

ผมจึงย้ายระบบการค้นหาชื่อเรื่องของเราไปใช้ OpenSearch และนี่คือวิธีที่ผมแก้ปัญหานี้สำหรับกลุ่มผู้ใช้งานที่ใช้หลายภาษา

ปัญหาของการใช้ Fuzziness แบบมาตรฐาน

บทช่วยสอนส่วนใหญ่มักจะแนะนำให้ใช้ "fuzziness" เพื่อแก้ไขปัญหาการพิมพ์ผิด ซึ่งวิธีนี้ใช้ได้ผลกับภาษาอังกฤษ แต่ใช้ไม่ได้กับข้อความภาษาจีน ญี่ปุ่น และเกาหลี (CJK)

  • ระยะห่างของการแก้ไข (Edit distance) ใช้ไม่ได้ผลกับ CJK เพราะการพิมพ์ผิดในภาษา CJK มักหมายถึงการใช้ตัวอักษรที่ผิดไปเลยทั้งตัว
  • Fuzziness แบบมาตรฐานสร้างข้อมูลที่ผิดเพี้ยนในเชิงความหมาย (semantic garbage) เช่น มันอาจจะจับคู่คำว่า "fire" กับ "water" เพียงเพราะมันมีระยะห่างของการแก้ไขเพียงหนึ่งขั้นตอน
  • การทำ Tokenization นั้นทำได้ยาก เนื่องจากภาษาในกลุ่ม CJK ไม่มีการใช้ช่องว่างระหว่างคำ

วิธีแก้ปัญหา: การใช้แนวทางแบบหลายฟิลด์ (Multi-Field Approach)

ผมเลิกปฏิบัติกับข้อความทุกประเภทเหมือนกันหมด โดยผมสร้างฟิลด์ชื่อเรื่องเชิงตรรกะ (logical title field) ขึ้นมาหนึ่งฟิลด์ ซึ่งทำดัชนี (index) ด้วยสามวิธีที่แตกต่างกัน:

  • Latin และ Romaji: ผมใช้การทำ tokenization แบบมาตรฐานโดยเปิดใช้งาน fuzziness และตั้งค่า prefix length ไว้ที่ 1 เพื่อให้แน่ใจว่า "demon" จะจับคู่กับ "demn" ได้ แต่ "lemon" จะไม่ถูกจับคู่กับ "demon"
  • ข้อความ CJK: ผมใช้ CJK bigram analyzer และ ICU normalizer โดยปิดการใช้งาน fuzziness แต่เปลี่ยนไปใช้เกณฑ์การจับคู่ขั้นต่ำ (minimum match threshold) ที่ 70% แทน
  • Autocomplete: ผมใช้ฟิลด์ edge-ngram เพื่อรองรับผลลัพธ์แบบ search-as-you-type

สถาปัตยกรรมและความปลอดภัยของข้อมูล

ผมยังคงใช้ SQLite เป็นแหล่งข้อมูลความจริงหนึ่งเดียว (single source of truth) โดยให้ OpenSearch ทำหน้าที่เป็นดัชนีที่รวดเร็วและสามารถสร้างใหม่ได้ (rebuildable index)

  • ผมใช้ PHP ในการส่งข้อมูลอัปเดตไปยัง OpenSearch แบบเป็นชุด (bulk batches)
  • ผมไม่รันการทำ indexing ในระหว่างที่มีการร้องขอ (request) จากผู้ใช้
  • ผมรันสคริปต์ Python เพื่อตรวจสอบ "drift" เพื่อให้แน่ใจว่าจำนวนข้อมูลใน OpenSearch ตรงกับจำนวนใน SQLite

ผลลัพธ์ที่ได้

การเปลี่ยนแปลงครั้งนี้ส่งผลอย่างมหาศาล:

  • การค้นหาที่ไม่พบผลลัพธ์ลดลงจาก 14% เหลือไม่ถึง 3%
  • เซสชันการค้นหา (search sessions) ยาวนานขึ้น เพราะผู้ใช้พบสิ่งที่ต้องการได้ในทันที
  • ค่า Latency ยังคงต่ำอยู่ที่ประมาณ 40ms

หากคุณให้บริการกลุ่มผู้ใช้งานที่ใช้หลายภาษา โปรดจำไว้ว่า: การรองรับการพิมพ์ผิด (typo tolerance) และการจับคู่ภาษา CJK คือสองปัญหาที่แตกต่างกัน และคุณจำเป็นต้องมีสองวิธีแก้ปัญหาที่แตกต่างกัน

Source: https://dev.to/ahmet_gedik778845/how-i-added-typo-tolerant-video-title-search-with-opensearch-and-cjk-3e5d