โปรแกรมที่ทำงานกับดาต้าเบสจะมีชะตากรรมเหมือนกันอย่างหนึ่ง เมื่อผ่านระยะเวลาใช้งานมาเนิ่นนาน ข้อมูลที่สะสมมากขึ้น แล้วก็จะเริ่มช้า บางทีถึงขั้นใช้งานไม่ได้
สถานการณ์เช่นนี้ไม่เคยเกิดขึ้นตอนที่ส่งมอบโปรแกรม ช่วงหนึ่งถึงสองปีแรกมักไม่มีปัญหา เพราะข้อมูลสะสมยังไม่เกินกำลังดาต้าเบส
กระบวนท่ามาตรฐานเมื่อเกิดปัญหา มักถูกวินิจฉัยว่าเป็นที่ดาต้าเบสเซิร์ฟเวอร์ทำงานไม่ทัน แล้วก็จัดการอัพเกรดให้ใหญ่ขึ้น ใหญ่ขึ้น และใหญ่ขึ้น
คำถามหนึ่งที่เราไม่ทันได้ถาม
"เป็นไปได้หรือไม่ว่า เพราะโปรแกรมเลือกใช้ query ไม่ดีพอ?"
เรื่องนี้เป็นเรื่องที่พิสูจน์ยาก ตอนที่ข้อมูลน้อยเราไม่สามารถเห็นความแตกต่างระหว่าง "query ที่ใช้ได้" กับ "query ที่ดี" เพราะผลลัพธ์แทบไม่ต่างกัน โปรแกรมที่ใช้งานได้มาเป็นตลอดย่อมไม่ถูกสงสัยว่าเป็นจำเลย
เป็น Technical Debt หรือหนี้สินทางเทคนิคที่ทุกฝ่ายมักมองไม่เห็น ไม่ค่อยให้ความสำคัญ เพราะต้องใช้เวลากว่าระเบิดเวลาจะทำงาน การทดสอบ ณ เวลาที่รับมอบโปรแกรมก็ทำไม่ได้ ผู้พัฒนาโปรแกรมอาจละเลยหรือไม่รู้เท่าทัน ที่สำคัญเมื่อถึงเวลานั้นต่างก็แยกย้ายไปทำอย่างอื่นแล้ว
ประสบการณ์เรื่องนี้ผมได้มาด้วยบาดแผลเช่นกัน
จากโปรแกรม ERP ที่ได้ปกติกับทุกแห่งที่ผ่านมา จนมาเจอกับไซต์ขนาดใหญ่แห่งหนึ่ง เมื่อใช้งานมาระยะหนึ่งกลับทำงานได้ไม่ดี ช้า ค้างไม่ตอบสนองบ่อยๆ
หลังจากที่พยายาม scale up เซิร์ฟเวอร์หลายครั้ง ก็เริ่มคิดได้ว่าควรกลับมาทบทวนการออกแบบโปรแกรม
ค้นหาอะไรก็ได้
ทำไมเรื่องนี้เป็นเรื่องใหญ่ สำหรับบางโปรแกรมที่ควบคุมฟิลด์ที่อนุญาตให้ค้นหาแบบจำกัดอาจไม่ค่อยเกิดปัญหานี้ บังเอิญโปรแกรมของเราออกแบบด้วยแนวคิดให้ผู้ใช้สามารถค้นหาอะไรก็ได้ จากช่องค้นหาช่องเดียว ผู้ใช้สามารถใส่คำค้นอะไรก็ได้
ผู้ใช้อาจใส่คำค้นบางส่วนที่ไม่ได้บอกว่าคืออะไร หรือต้องการหาอะไร ไม่ว่าจะชื่อลูกค้า, เบอร์โทร, พนักงานขาย, สินค้า, เลขที่บิล หรือใบสั่งซื้อของลูกค้า หลังจากนั้นโปรแกรมก็จะให้คำตอบว่าเจอคำนี้ที่ไหนบ้าง ในจักรวาลข้อมูลขององค์กร
หากใครรู้จัก Elastic Search ประมาณนั้นเลย เพียงแต่ไม่เลือกใช้ เพราะไม่ต้องการให้การจัดการระบบด้าต้าเบสเบื้องหลังซับซ้อนเกินไป
หัวใจสำคัญของความสามารถ "ค้นหาอะไรก็ได้" อยู่ที่การเก็บประวัติ "คำค้น" มาวิเคราะห์เพื่อปรับปรุงโปรแกรมให้ตอบตรงกับที่ผู้ใช้คาดหวังมากที่สุด พูดง่ายๆ คือ พยายามทำให้ผู้ใช้รู้สึกว่า "รู้ใจ"
พฤติกรรมต่อเนื่องหลังจากได้คำตอบ เป็นเงื่อนงำที่ต้องตีความเพื่อวัดผล
บางครั้งเมื่อค้นแล้ว ทำงานต่อไม่ได้ ต้องขยายคำค้นซ้ำอีก แปลว่ายังไม่ได้คำตอบที่ต้องการ
บางครั้งก็เจอเทคนิคที่ไม่เคยคาดคิด เช่น ใส่เลขแค่ 3–4 ตัว ต้องแกะร่องรอยจนพบว่าผู้ใช้กำลังหาเอกสารโดยค้นจากเลขท้ายของบิล
การเรียนรู้พฤติกรรมค้นหานำไปสู่ความเข้าใจผู้ใช้ และพบรูปแบบบางอย่างที่ช่วยปรับปรุงเทคนิคที่ใช้ค้นหาข้อมูล
Simple Query
ก่อนอื่นผมขออธิบายการเก็บข้อมูลในดาต้าเบสของโปรแกรมให้เห็นภาพตรงกันก่อน เนื่องจากเป็น MongoDB การเก็บข้อมูล ERP จึงมีเพียง 3 Collections ที่สำคัญได้แก่
เอกสาร เก็บข้อมูลธุรกรรมซื้อ-ขาย-จ่าย-รับ ฯลฯ
ผู้เกี่ยวข้อง อาจเป็นลูกค้า,ลูกหนี้ หรือผู้ขายก็ได้
สิ่งที่เกี่ยวข้อง อาจเป็นสินค้าหรือบริการเอาไว้ใช้อ้างอิง
ปริมาณของธุรกรรมที่เกิดขึ้นทุกวัน จึงมีผลต่อขนาดของดาต้าเบสมากที่สุด นั่นคือ ข้อมูลเอกสาร
เราอาจจะเรียกข้อมูลเอกสารว่าเป็น transactional data บันทึกเหตุการณ์ที่เกิดขึ้นในกิจการ เพียงแต่ Document Database แตกต่างจาก Relational Database ตรงที่เป็นข้อมูลที่เก็บมีความสมบูรณ์ในตัวเอง
ผลพวงที่ตามมา ทำให้เป็นการค้นหาที่ตรงไปตรงมา คนทั่วไปก็เข้าใจได้ เช่น หากใส่คำค้นว่า "สยาม" ก็เป็นการหา เอกสารที่มีคำนี้อยู่ในที่ใดที่หนึ่งในนั้น อาจจะเป็นชื่อลูกค้าในบิลขาย เป็นชื่อผู้ขายในบิลซื้อ เป็นชื่อพนักงานขาย เป็นยี่ห้อสินค้า หรือเป็นที่อยู่ในบิล
ไม่ว่าอย่างไรก็เก็บอยู่ในข้อมูลเอกสารนั้นที่เดียว ไม่ได้แยกเก็บแล้วต้อง join table กลับมาหลายตลบ
โปรแกรมยุคแรกผมทำเช่นนั้นจริงๆ เอาคำค้นไปสร้างเป็น query เพื่อสั่งให้ค้นหาจากทุกฟิลด์ที่อาจเป็นไปได้ในเอกสาร แปลเป็นภาษาที่อ่านเข้าใจได้ดังนี้
ค้นหา "เอกสาร" ที่ "ชื่อผู้เกี่ยวข้อง" มีคำว่า "สยาม" หรือ "ที่อยู่" มีคำว่า "สยาม" หรือ "ชื่อสินค้า" มีคำว่า "สยาม" หรือ … มีคำว่า "สยาม"
{"$match":
{"$or":
[
{"info.who": {"$regex": "สยาม"}},
{"info.address": {"$regex": "สยาม"}},
{"items.name": {"$regex": "สยาม"}},
...
]
}
}
ซึ่งใช้งานได้ดีกับทุกไซต์ในช่วงเริ่มต้น จนกระทั่งเวลาผ่านไปเกือบปี
ความแตกต่างระหว่างบริษัทที่เปิดบิลวันละ 1,000 ใบ กับ 100 ใบเริ่มแสดงเห็นถึงความช้าเร็วในการค้นหาที่ต่างกัน
Narrative Scope
เห็นสัญญาณเตือนจากไซต์ที่เปิดบิลวันละ 1,000 ใบ เมื่อสะสมกลายเป็นแสน สองแสน มากขึ้นเรื่อยๆ คำอธิบายทางเทคนิคคือ ขนาดของข้อมูลใหญ่กว่าขนาดของแคช ระหว่างการค้นหาถ้าข้อมูลไหนไม่อยู่ในแคช ทำให้ต้องเสียเวลาอ่านเข้ามาใหม่
นอกจากอัตราเพิ่มที่รวดเร็ว จำนวนผู้ใช้พร้อมกันก็เป็นอีกตัวแปรสำคัญ โดยธรรมชาติบริษัทที่ข้อมูลเยอะ มักจะมีจำนวนผู้ใช้ช่วยกันทำงานมากกว่าด้วย ความช้าในการค้นหาจึงถูกตัวคูณจำนวนผู้ใช้
วิธีการทำให้ค้นหาข้อมูลได้เร็วขึ้นอย่างหนึ่งคือ "ตัดข้อมูล" ก้อนใหญ่ออกด้วยฟิลด์ที่เป็น index เป็นการทำให้ขอบเขตข้อมูลที่ต้องค้นแคบลง ก่อนที่จะค้นหาละเอียดภายในข้อมูล (MongoDB เรียกว่า Object Scanning)
Multi Step Search
แต่ข้อเสียของการตัดข้อมูล อาจทำให้หาคำตอบไม่เจอ หากเกิดขึ้นบ่อยจะกลายเป็นระบบค้นหาที่ไม่น่าเชื่อถือ
ดังนั้นกระบวนการค้นหาแบบใหม่ที่ต้องนำมาใช้คู่กันคือ การค้นหา 2 จังหวะ
จังหวะแรก ค้นหาเร็ว "ตัดข้อมูล" ก่อน หากพบข้อมูลก็ตัดจบ ไม่ต้องทำจังหวะสอง
จังหวะสอง กรณีไม่พบข้อมูลเลย ก็ค้นหาช้าตามเดิมอีกรอบหนึ่ง
ด้วยวิธีนี้ผู้ใช้จะได้รับคำตอบเสมอ อาจเร็วหรือช้าต่างกัน ขึ้นอยู่กับเทคนิคการตัดข้อมูลของโปรแกรม ทำอย่างไรจึงหลุดไปจังหวะสองน้อยที่สุด
ขายบ่อยกว่า
ข้อมูลเอกสารมีฟิลด์ "ประเภท" ที่ใช้แบ่งกลุ่มใหญ่ๆ เช่น "บิลขาย", "บิลซื้อ", "ใบรับ", "ใบจ่าย", "ใบหักภาษี" ฯลฯ
หากค้นหาโดยไม่เลือกประเภท ก็จะเป็นการหาเอกสารทั้งหมดที่มีในระบบ และเท่าที่เจอมาแทบทุกไซต์ไม่ว่าธุรกิจประเภทไหน มักมีลูกค้ามากกว่าผู้ขาย มีเอกสารฝั่งขายมากกว่าซื้อ ยกเว้นบางธุรกิจเช่น ก่อสร้างที่เป็นตรงข้ามกัน
ข้อสังเกตที่ได้สำรวจจากประวัติการค้นหาของทุกไซต์พบว่า การค้นหาที่บ่อยที่สุดมักเกี่ยวข้องกับงานขาย เช่น ค้นเลขที่ใบเสนอราคา หรือเลขที่ใบ PO ของลูกค้า, ค้นชื่อลูกค้าหาประวัติว่าเคยเสนอราคาและเคยขาย และค้นชื่อสินค้า ว่าเคยเสนอราคาหรือขายให้ใครบ้าง
ดังนั้นการค้นหาแบบเร็ว เราสามารถออกแบบให้ตัดข้อมูล โดยค้นหาเฉพาะบางประเภท โดยแยกเป็นการค้นหาย่อยตามกลุ่มข้อมูล จัดลำดับก่อนหลังไม่เท่ากัน
จากคำค้น "สยาม" เราสามารถเพิ่ม narrative scope ใน query pipeline เพื่อตัดจำนวนข้อมูลเหลือเพียง "บิลขาย" ที่ต้องกรองตาม query คำค้นเดิม ได้ดังนี้้
ค้นหา "เอกสาร" ที่ "ประเภท" เท่ากับ "บิลขาย" และ.. "ชื่อผู้เกี่ยวข้อง" มีคำว่า "สยาม" หรือ "ที่อยู่" มีคำว่า "สยาม" หรือ "ชื่อสินค้า" มีคำว่า "สยาม" หรือ … มีคำว่า "สยาม"
{"$match":
{"_type": "บิลขาย"}
},
{"$match":
{"$or":
[
{"info.who": {"$regex": "สยาม"}},
{"info.address": {"$regex": "สยาม"}},
{"items.name": {"$regex": "สยาม"}},
...
]
}
}
ล่าสุดก่อน
ผู้ใช้ที่ทำงานประจำวันส่วนใหญ่เมื่อค้นหา มักคาดหวังว่าจะเจอข้อมูลล่าสุดก่อน ซึ่งเป็นความต้องการไม่เหมือนกับการเรียกข้อมูลเพื่อจัดทำรายงาน ให้ผู้บริหารหรือนักวิเคราะห์
และหลายครั้งการย้อนไปค้นข้อมูลอดีตหรือเรื่องที่จบไปแล้ว นอกจากกินแรงดาต้าเบสแล้ว ก็ยังไม่ค่อยมีประโยชน์กับผู้ใช้ส่วนใหญ่ด้วย
ดังนั้นเราควรหาทางนิยามความหมายของ "ปัจจุบัน" ที่เป็น Working Period ที่สัมพันธ์กับงานประจำวันว่ายาวนานแค่ไหน เพื่อให้ได้คำตอบที่สอดคล้องกับธรรมชาติการทำงานของธุรกิจนั้นมากที่สุด
การตัดข้อมูลจากกรอบเวลาที่เป็นวันที่เอกสาร โดยทั่วไปอาจกำหนดเป็น "12 เดือนล่าสุด" เท่ากับย้อนหลังไม่เกิน 1 ปี บางทีอาจสั้นยาวกว่านั้น ธุรกิจที่ให้เครดิตลูกค้านานถึง 180 วัน ก็จะมี Working Period ยาวนานตามไปด้วย
จากคำค้น "สยาม" เราสามารถเพิ่ม narrative scope ใน query pipeline เพื่อตัดจำนวนข้อมูลที่ต้องกรองตาม query คำค้นเดิม ได้ดังนี้
ค้นหา "เอกสาร" ที่ "ประเภท" เท่ากับ "บิลขาย" และ “วันที่” มากกว่าหรือเท่ากับ “2023–09–01” และ.. "ชื่อผู้เกี่ยวข้อง" มีคำว่า "สยาม" หรือ "ที่อยู่" มีคำว่า "สยาม" หรือ "ชื่อสินค้า" มีคำว่า "สยาม" หรือ … มีคำว่า "สยาม"
{"$match":
{"_type": "บิลขาย"},
{"info.date": {"$gte": "2023-09-01"}},
},
{"$match":
{"$or":
[
{"info.who": {"$regex": "สยาม"}},
{"info.address": {"$regex": "สยาม"}},
{"items.name": {"$regex": "สยาม"}},
...
]
}
}
คาดเดาจากคำค้น
เทคนิคถัดมาที่ช่วยให้สามารถค้นข้อมูลได้เร็วขึ้น คือการสร้าง query โดยพยายามพิจารณาคำค้นนั้น เพื่อตัดฟิลด์ที่ไม่จำเป็นต้องค้นออกไป
ตัวอย่างเช่น คำค้นที่เป็น "เลขที่เอกสาร" จะมีรูปแบบที่สังเกตได้ง่าย มักจะต้องลงท้ายด้วยตัวเลขอย่างน้อย 3–4 ตัว เช่น "IV6706–0121" และไม่มีเว้นวรรค
ดังนั้นคำค้นที่สามารถคาดเดาเป้าหมายได้ เราสามารถสร้าง query ให้ค้นหาเฉพาะฟิลด์เลขที่ก่อน เพื่อให้ได้คำตอบที่เร็วที่สุด หากไม่เจอจึงค่อยค้นหาอะไรก็ได้ จากหลายฟิลด์อีกทีหนึ่ง
หรือเมื่อเจอคำค้นที่เป็นเลข 3–4 ตัว เช่น "0121" สามารถตีความว่า ต้องการค้นหา "เลขท้าย" ของเอกสาร
ในทางกลับกัน หากเจอคำค้นที่มีเว้นวรรค ไม่ใช่เลขที่เอกสารแน่ๆ มีโอกาสเป็น ชื่อลูกค้า, ชื่อสินค้า ฯลฯ ดังนั้น query ที่ค้นหาอะไรก็ได้ก็ไม่ต้องค้นฟิลด์เลขที่
คำค้นเชิงซ้อน
พนักงานขายที่ต้องเสนอราคาและติดต่อลูกค้า หลายครั้งที่ต้องการค้นหาใบเสนอราคาของลูกค้ารายนั้น
เราสามารถออกแบบและแนะนำให้ใช้คำค้นเชิงซ้อนโดยให้ระบุ "หมวดบิล" และ "ชื่อลูกค้า" ไปพร้อมกัน เพื่อช่วยตัดข้อมูลให้ค้นได้เร็วยิ่งขึ้น
เช่น "QT && ปตท" หมายถึงให้ค้นเอกสารหมวด "QT" ที่มีชื่อลูกค้า "ปตท"
ปัจจัยสำคัญ
โปรแกรมที่เริ่มต้นจากกล่องค้นหาเพียงกล่องเดียว อาจจะเรียกว่าเป็นกล่องค้นหาโง่ๆ ที่แสนขยัน สั่งให้หาอะไรก็พยายามวิ่งไปค้นทุกซอกทุกมุม จนกระทั่งเยอะไปจนวิ่งไม่ไหว
สิ่งที่ทำให้แตกต่างจากโปรแกรมอื่นคือ "ระยะเวลา" ที่ได้อยู่กับมันยาวนานพอ ได้มีโอกาสเฝ้าดูการใช้งานของผู้ใช้ที่แตกต่างหลากหลาย มีโอกาสเข้าใจองค์ประกอบที่เชื่อมโยงกัน
อาจเป็นเพราะโชคดีที่ผมได้รับเงื่อนไขต่างจากงานโปรแกรมทั่วไป ไม่ได้ทำโปรแกรมเหมือนสร้างบ้าน ไม่ใช่ผู้รับเหมา ที่ทำตามสเปคเพื่อส่งมอบ แล้วย้ายไปทำโปรเจกต์อื่น แต่เป็นเหมือนงานปลูกป่า ดูแลต้นไม้ให้เติบโต คอยปรับปรุงสภาพแวดล้อมให้เหมาะสมกับชุมชนที่เติบโตขึ้นเรื่อยๆ
การปรับปรุงเล็กๆ น้อยๆ ด้วยเวลายาวนานพอ เมื่อมองย้อนกลับไปจึงเห็นว่ามีรายละเอียดเปลี่ยนแปลงไปมากเพียงใด
หากถามว่าสามารถออกแบบไว้ตั้งแต่วันแรกได้หรือไม่ บางอย่างก็อาจได้ แต่บางอย่างก็ไม่ได้ จนกว่าจะถึงเวลาที่เหมาะสม
เหมือนกับการสะสมของดาต้าเบสที่กว่าจะมีขนาดใหญ่จนเริ่มรู้สึกว่าช้า จนตระหนักว่าวิธีการค้นหาแบบเดิมอาจไม่ดีพอ ก็มีเวลาของมัน หากแลกกับการออกแบบที่ซับซ้อนใช้เวลาพัฒนานานย่อมไม่คุ้มที่จะทำตั้งแต่วันแรก แถมยังไม่สามารถทดสอบตอนที่เงื่อนไขขนาดยังไม่ใช่
โลกเทคโนโลยีเปลี่ยนแปลงเร็วมาก การเลือกสิ่งที่ดีที่สุด ณ เวลานั้น อาจไม่เป็นจริงเมื่อเวลาผ่านไป โลกธุรกิจมีแนวคิดท้าทายใหม่ๆ ซึ่งไม่มีใครรับประกันว่าจะสำเร็จหรือล้มเหลว บางทีการออกแบบโปรแกรมยุคใหม่ก็ต้องการพื้นที่ว่าง มีส่วนที่ยืดหยุ่นสำหรับได้ทดลองแนวทางใหม่ ได้ลองผิดลองถูก แล้วปรับเปลี่ยนไปกัน ไม่ใช่การคาดคั้นหาแบบพิมพ์เขียวทุกครั้ง
การทำให้ดูเหมือนง่าย ส่งมอบผลลัพธ์ให้ตรงกับความคาดหวังกับผู้ใช้ ทั้งความเร็วและความแม่นยำในสัดส่วนที่เหมาะสม กลับมีเบื้องหลังที่หลายคนอาจไม่ทันคาดคิด
Roger Federer เคยกล่าวถึงการเล่นเทนนิสของเขาไว้ว่า "กว่าที่จะทำให้ดูเหมือนง่ายไม่ต้องใช้ความพยายาม เบื้องหลังเกิดจากความพยายามทุ่มเทอย่างหนักมาก่อน"
2024-08-04
Comments