- นำฐานข้อมูลเดิมมาใช้เพื่อสร้าง โครงสร้างเสิร์ชเอนจินที่ทำงานได้โดยไม่ต้องพึ่งบริการภายนอก โดยเน้นที่การทำโทเค็น น้ำหนัก และการให้คะแนน
- แนวคิดหลักคือ ทำโทเค็นข้อความทั้งหมดแล้วจัดเก็บไว้ จากนั้นตอนค้นหาก็จับคู่โทเค็นด้วยวิธีเดียวกันเพื่อคำนวณความเกี่ยวข้อง
- ผสาน Word, Prefix, N-Gram tokenizer เพื่อรองรับทั้งการตรงกันแบบเป๊ะ การตรงกันบางส่วน และการรับมือคำพิมพ์ผิด โดยแต่ละ tokenizer มีค่าน้ำหนักเฉพาะของตนเอง
- ใช้ ระบบน้ำหนักและอัลกอริทึมการให้คะแนนบน SQL เพื่อประเมินทั้งความยาวเอกสาร ความหลากหลายของโทเค็น คุณภาพเฉลี่ย และปัจจัยอื่น ๆ แบบรวมกัน
- มี ความสามารถในการขยายและความโปร่งใส สูง จึงเพิ่ม tokenizer หรือประเภทเอกสารใหม่ ปรับน้ำหนัก หรือแก้ไข scoring ได้อย่างอิสระ
เหตุผลที่ต้องสร้างเสิร์ชเอนจินเอง
- บริการภายนอกอย่าง Elasticsearch หรือ Algolia นั้นทรงพลัง แต่ก็มาพร้อม ภาระในการเรียนรู้ API ที่ซับซ้อนและการดูแลโครงสร้างพื้นฐาน
- หากต้องการเพียง ฟีเจอร์ค้นหาที่ทำงานร่วมกับฐานข้อมูลเดิมและดีบักได้ง่าย การสร้างเองจะมีประโยชน์มาก
- เป้าหมายคือ เสิร์ชเอนจินแบบเรียบง่ายที่คืนผลลัพธ์ได้ตรงประเด็นโดยไม่ต้องพึ่งพาภายนอก
แนวคิดหลัก: การทำโทเค็นและการจับคู่
- หลักการพื้นฐานคือ ทำโทเค็น (tokenize) ข้อความทั้งหมดแล้วจัดเก็บไว้ และเมื่อค้นหาก็สร้างโทเค็นด้วยวิธีเดียวกันเพื่อนำมาจับคู่
- ในขั้นตอน indexing จะตัดเนื้อหาออกเป็นหน่วยโทเค็นและเก็บพร้อมค่าน้ำหนัก
- ในขั้นตอน search จะทำโทเค็นกับคิวรีแบบเดียวกันเพื่อหาโทเค็นที่ตรงกันและคำนวณคะแนน
- ในขั้นตอน scoring จะใช้ค่าน้ำหนักที่จัดเก็บไว้เพื่อคำนวณคะแนนความเกี่ยวข้อง
การออกแบบสคีมาฐานข้อมูล
- ใช้สองตาราง:
index_tokens และ index_entries
index_tokens: เก็บโทเค็นที่ไม่ซ้ำและค่าน้ำหนักตาม tokenizer
index_entries: เชื่อมโทเค็นกับเอกสาร และเก็บคะแนนสุดท้ายที่สะท้อนน้ำหนักของฟิลด์ เอกสาร และ tokenizer
- สูตรคำนวณน้ำหนักสุดท้าย:
field_weight × tokenizer_weight × ceil(sqrt(token_length))
- มีการตั้งดัชนีเพื่อรองรับการค้นหาเอกสาร การค้นหาโทเค็น คิวรีตามฟิลด์ และการกรองตามน้ำหนัก
ระบบการทำโทเค็น
- WordTokenizer: แยกตามคำ ตัดคำสั้นทิ้ง ใช้สำหรับการตรงกันแบบเป๊ะ (น้ำหนัก 20)
- PrefixTokenizer: สร้าง prefix ของคำ ใช้สำหรับ autocomplete และการตรงกันบางส่วน (น้ำหนัก 5)
- NGramsTokenizer: สร้างชุดอักขระความยาวคงที่ ใช้รองรับคำพิมพ์ผิดและการตรงกันบางส่วน (น้ำหนัก 1)
- tokenizer ทั้งหมดจะทำงานร่วมกันในส่วนของ การแปลงเป็นตัวพิมพ์เล็ก การลบอักขระพิเศษ และการทำช่องว่างให้เป็นรูปแบบมาตรฐาน
ระบบน้ำหนัก
- น้ำหนักของฟิลด์: สะท้อนความสำคัญของชื่อเรื่อง เนื้อหา คีย์เวิร์ด ฯลฯ
- น้ำหนักของ tokenizer: Word > Prefix > N-Gram
- น้ำหนักของเอกสาร: คำนวณโดยผสานสองปัจจัยข้างต้นเข้ากับความยาวของโทเค็น
- ใช้ฟังก์ชัน
ceil(sqrt()) เพื่อลดอิทธิพลของโทเค็นที่ยาวเกินไป และสามารถปรับเป็นฟังก์ชัน log หรือเชิงเส้นได้หากจำเป็น
บริการทำดัชนี
- สามารถทำดัชนีได้เฉพาะเอกสารที่ implement
IndexableDocumentInterface
- เมื่อเอกสารถูกสร้างหรือแก้ไข จะทำดัชนีผ่าน event listener หรือคำสั่ง (
app:index-document, app:reindex-documents)
- ขั้นตอน:
- ลบดัชนีเดิมก่อน แล้วสร้างโทเค็นใหม่
- รัน tokenizer ทุกตัวกับแต่ละฟิลด์
- ตรวจสอบว่าโทเค็นมีอยู่แล้วหรือไม่ แล้วจึงสร้าง (
findOrCreateToken)
- ใช้น้ำหนักที่คำนวณได้ทำ batch insert ลงใน
index_entries
- โครงสร้างนี้รองรับการป้องกันข้อมูลซ้ำ การเพิ่มประสิทธิภาพ และการอัปเดต
บริการค้นหา
- ประมวลผลคิวรีด้วย tokenizer ชุดเดียวกันเพื่อให้ได้ ชุดโทเค็นที่เหมือนกับตอน indexing
- ลบโทเค็นซ้ำ เรียงตามความยาวโดยให้โทเค็นที่ยาวกว่านำหน้า และจำกัดสูงสุด 300 รายการ
- ใช้ SQL query เพื่อ join โทเค็นกับเอกสาร พร้อม คำนวณและจัดเรียงคะแนนความเกี่ยวข้อง
- ส่งคืนผลลัพธ์ในรูปแบบ
SearchResult(documentId, score)
อัลกอริทึมการให้คะแนน
- คะแนนพื้นฐาน:
SUM(sd.weight)
- การปรับตามความหลากหลายของโทเค็น:
LOG(1 + COUNT(DISTINCT token_id))
- การปรับตามน้ำหนักเฉลี่ย:
LOG(1 + AVG(weight))
- ค่าปรับโทษตามความยาวเอกสาร:
1 / (1 + LOG(1 + token_count))
- Normalization: หารด้วยคะแนนสูงสุดเพื่อปรับให้อยู่ในช่วง 0~1
- กรองค่าน้ำหนักโทเค็นขั้นต่ำ (
st2.weight >= ?) เพื่อ ตัดการตรงกันที่ไม่มีความหมายและมีน้ำหนักต่ำ
การจัดการผลลัพธ์
- ผลการค้นหาจะส่งคืนเป็น ID เอกสารและคะแนน แล้วแปลงเป็นเอกสารจริงผ่าน repository
- ใช้ฟังก์ชัน
FIELD() เพื่อ ดึงเอกสารโดยยังคงลำดับผลการค้นหาไว้
ความสามารถในการขยายของระบบ
- เพิ่ม tokenizer ใหม่ได้ด้วยการ implement
TokenizerInterface
- ลงทะเบียนเอกสารประเภทใหม่ได้ด้วยการ implement
IndexableDocumentInterface
- ปรับน้ำหนักหรือ logic ของ scoring ได้ เพียงแก้ SQL
บทสรุป
- โครงสร้างนี้เป็น เสิร์ชเอนจินที่เรียบง่ายแต่ใช้งานได้จริง และให้ประสิทธิภาพเพียงพอโดยไม่ต้องมีโครงสร้างพื้นฐานภายนอก
- จุดเด่นคือ ตรรกะที่ชัดเจน การควบคุมได้ทั้งหมด และการดีบักที่ง่าย
- เน้นย้ำว่า เมื่อเทียบกับระบบที่ซับซ้อนแล้ว โค้ดที่เข้าใจและควบคุมได้ด้วยตัวเอง อาจมีคุณค่ามากกว่า
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
แนวคิดพื้นฐานของการค้นหานั้นเรียบง่ายและเป็นขอบเขตปัญหาที่น่าสนใจ
แต่สิ่งที่ยากจริง ๆ คือการจัดการกับ ข้อมูลปริมาณมหาศาล และการรับมือกับ คำค้นที่กำกวม
แนวทางที่อิง DBMS นั้นใช้ได้ดีกับเว็บไซต์ขนาดเล็ก แต่พอถึงระดับ Wikipedia ภาษาอังกฤษก็จะชนเพดานอย่างรวดเร็ว
สำหรับผู้เริ่มต้น SeIRP e-book เป็นแหล่งเรียนฟรีที่ดี
การที่ไม่มีคำตอบที่ชัดเจน นี่แหละที่ทำให้มันยากเป็นพิเศษ
Google บางครั้งก็แสดงโฆษณาเป็น ‘ผลลัพธ์ที่เกี่ยวข้องที่สุด’ ด้วย ดังนั้น Marginalia Search จึงเป็นกรณีเปรียบเทียบที่ดี
อยากรู้ว่าเคยอ้างอิง งานวิจัยของ TREC บ้างหรือไม่
เสิร์ชเอนจินต้องต่อสู้กับ ผู้เล่นที่เป็นปฏิปักษ์ ซึ่งหวังหารายได้จากโฆษณาอยู่ตลอดเวลา
มันกลายเป็น เกมแมวไล่จับหนูที่ไม่มีวันจบ ที่ต้องคอยเปลี่ยนตัวชี้วัดคุณภาพเพื่อไม่ให้พวกเขาหาประโยชน์จากมันได้
ถ้าคิดจากความเร็วระดับ 5 วินาทีต่อ 1 คำค้น และประมาณ 12 คำค้นต่อนาที อยากรู้ว่าจะค้นในคอร์ปัสขนาดประมาณไหนได้
ตัวอย่างเช่น มันยากที่จะตัดสินว่าระหว่างบทความวิกิของ Gilligan’s Island กับแฟนบล็อก อันไหนเป็นผลลัพธ์ที่ “ดีกว่า”
และพอมี การบิดเบือนอันดับ หรือ keyword stuffing เพิ่มเข้ามา มันก็กลายเป็นความท้าทายที่ซับซ้อนกว่าปัญหาการสเกลเสียอีก
การค้นหานั้นยากมากจริง ๆ
ต่อให้เป็นบริษัทที่มีทรัพยากรและความสามารถล้นเหลืออย่าง Apple, Microsoft, OpenAI ก็ยังมี คุณภาพฟังก์ชันค้นหาต่ำ
นี่ไม่ใช่แค่ปัญหาทางเทคนิค
การเพิ่มคุณภาพการค้นหาต้องอาศัย การปรับจูนพารามิเตอร์การจัดอันดับอย่างละเอียด ซึ่งเป็นงานที่วางแผนผ่านระบบจัดการอย่าง sprint หรือ Jira ได้ยาก
ท้ายที่สุดแล้ว นี่เป็นงานที่ต้องการ ความไว้วางใจและอิสระในการทำงาน ให้กับนักพัฒนา
พวกเขาลงทุนกับโมเดล AI เป็นหลักหมื่นล้าน แต่เว็บแอปหรือระบบค้นหาถูกมองเป็นเรื่องรอง เลยออกมาเป็นแบบนั้น
เมื่อประมาณ 10 ปีก่อน ผมเคยทำงานกับ เพื่อนร่วมงานที่กำลังเรียนปริญญาเอกด้านการออกแบบเสิร์ชเอนจิน
เขาพูดถึงการผสานการค้นหากับฐานข้อมูลอย่างกระตือรือร้นมาก และผมก็ได้เรียนรู้อะไรเยอะจากเขา
สักวันหนึ่งผมอยากลงลึกกับภายในของ Apache Solr และ Lucene ให้มากกว่านี้
สมัยก่อนยังไม่มีโซลูชันค้นหาแบบโอเพนซอร์ส จึงต้องทำเอง
บทเรียนที่ได้จากประสบการณ์นั้นคือ “อย่าสร้างเสิร์ชเอนจินเอง”
มีผู้คนมากมายทุ่มเวลาหลายปีให้กับปัญหานี้ และถ้าทำเองจะตกไปอยู่ใน นรกแห่งการบำรุงรักษาที่ไม่สิ้นสุด
พอมีคำขออย่าง “ช่วยเพิ่มการแก้คำสะกดหน่อย” หรือ “ปีหน้าใส่ระบบจัดหมวดหมู่ด้วย” เข้ามาต่อเนื่องก็จบเลย
ผมเคยสนุกมากกับคอร์สสร้างเสิร์ชเอนจินของ ศาสตราจารย์ David Evans แห่ง Virginia University
การได้สร้าง “เสิร์ชเอนจินแบบคลาสสิก” ด้วยตัวเองเป็นโปรเจกต์ที่สนุกมาก
ดูได้ที่ ลิงก์คอร์ส และ เพลย์ลิสต์ YouTube
สิ่งที่ผมหงุดหงิดกับเสิร์ชเอนจินที่ใช้บ่อยคือมันมัก ละเลยตัวย่อหรือคำที่ยาว 2~3 ตัวอักษร
เวลาเสิร์ชคำสั้น ๆ อย่าง “mp3” หรือ “PHP” แล้วโดนตัดทิ้ง มันน่าหงุดหงิดมาก
ผมเคยอ่าน Programming Collective Intelligence ของ Toby Segaran แล้วได้แรงบันดาลใจจากไอเดียหลากหลายอย่าง เช่น การค้นหา ระบบแนะนำ และตัวจำแนก
เป็นบทความที่น่าสนใจ
ทำให้อยากรู้ว่าระดับการปรับแต่ง tokenizer ของเสิร์ชเอนจินยอดนิยมสูงแค่ไหน
อยากรู้ว่าระบบนี้จะ ทำงานได้ขยายขนาดแค่ไหน
Elasticsearch แสดงประสิทธิภาพที่น่าประทับใจทีเดียว แม้จะเกินสเกลที่แนะนำไปแล้ว
การสร้างเสิร์ชเอนจินค้นหาข้อความแบบง่าย ๆ ไม่ใช่เรื่องยาก
แต่การสร้าง เสิร์ชเอนจินที่ดี เป็นคนละเรื่องกันเลย
แค่ทำอัลกอริทึมอย่าง BM25 ได้ยังไม่พอ
บริษัทส่วนใหญ่ที่ผมเคยไปให้คำปรึกษาใช้โซลูชันของตัวเองอยู่พักหนึ่ง สุดท้ายก็ย้ายไป Elasticsearch หรือ Opensearch
การทำเองตอนแรกดูเรียบง่าย แต่พอเวลาผ่านไปก็ซับซ้อนขึ้นเพราะ ปัญหาเรื่องอันดับและประสิทธิภาพที่ตกลง
อาการอย่าง “ช้า” หรือ “ได้ผลลัพธ์เพี้ยน ๆ” จะวนกลับมาซ้ำ ๆ
Elasticsearch แก้ปัญหาแบบนี้มาตั้งแต่ 10 ปีก่อนแล้ว และตอนนี้ก็พัฒนาไปไกลกว่านั้นมาก
แม้จะมีคนบอกว่า “ตั้งค่ายาก” แต่ทุกวันนี้ส่วนใหญ่ คอนฟิกอัตโนมัติ กันแล้ว และก็มีบริการแบบ managed มากมาย
มันอาจใช้ง่ายกว่า Postgres เสียอีก
สุดท้ายสิ่งสำคัญคือ การปรับ index mapping ให้เหมาะสม
บางคนอาจบอกว่า “ฟีเจอร์ขั้นสูงพวกนั้นไม่จำเป็น” แต่ในความเป็นจริง คุณภาพการค้นหาส่งผลตรงต่อการอยู่รอดของธุรกิจ
ถ้าต้องการระบบค้นหาที่ดีจริง ๆ สุดท้ายก็ต้องยอมรับความซับซ้อนเหล่านี้
ทางเลือกใหม่ ๆ อย่าง SeekStorm ที่ถูกพูดถึงบ่อยใน HN ช่วงหลังก็ดูน่าสนใจ แต่ผมยังไม่เคยเห็นเคสใช้งานจริงในโปรดักชัน
โดยเฉพาะ ทิปเรื่องปิด dynamic mapping และกันไม่ให้ index ฟิลด์ที่ไม่จำเป็น นั้นมีประโยชน์มาก
เท่าที่รู้มันเป็นโปรเจกต์ที่เก่ากว่า Lucene