Elasticsearch ภาคลุยสนาม ตอนที่ 2
สถาปัตยกรรม และการจัดการข้อมูลเอกสารใน Elasticsearch
สองมุมมอง
การจะทำความเข้าใจสถาปัตยกรรมของ Elasticsearch นั้น ต้องมองจากสองมุมมอง ได้แก่
- Logical Layout: เป็นมุมมองในส่วนที่ใช้สำหรับการค้นหา คิดอะไรไม่ออกก็ให้มองว่ามันเหมือนกับฐานข้อมูลนั่นแหละ ซึ่งในโลกของ Elasticsearch นั้นเรียกมันว่าเป็น Index (ซึ่งก็คือฝั่งซ้ายของรูปข้างบน)
- Physical Layout: เป็นมุมมองในส่วนที่ใช้ในการจัดเก็บข้อมูลในระดับ Hardware ซึ่งตรงนี้เป็นส่วนงานที่ใช้ในการจัดการ Performance, Scalability และ Availability ของระบบค้นหา (ฝั่งขวาของรูปข้างบน)
Logical Layout: Document, Type และ Index
ภาพต่อไปนี้จะสรุปรูปแบบการทำงานในส่วนของ Logical ของ Elasticsearch ทั้งหมด
ก่อนอื่นเรามาดูในแกนกลางกล่าวคือ Elasticsearch กันก่อน จะเห็นว่า ข้อมูลที่จัดเก็บภายใน Elasticsearch นั้นจะแบ่งเป็น 3 ระดับ
Index: จากภาพข้างต้นเราจะเห็นว่า ระบบ search ตัวนี้จะมี Index อยู่สองตัว get-together และ get-together-blog
Type: ใช้สำหรับจัดระเบียบเอกสารภายในแต่ละ Index (ปัจจุบันทาง Elasticsearch บังคับให้เราใช้ 1 type ต่อ 1 index เพราะแนวคิดเรื่อง type ทำให้คนสับสน) ในที่นี้จะเห็นว่า get-together มี type 2 ตัว ได้แก่ event, group ส่วน get-together-blog มีแค่ blog เพียงตัวเดียว
Document: มันเอกข้อมูลดิบ และเป็นหน่วยย่อยที่สุดที่จัดเก็บใน Elasticsearch โดยรูปแบบของ Document ที่จัดเก็บภายใน Elasticsearch จะอยู่ในรูปแบบของ JSON ตัวอย่างเช่น
{
"name": "Elasticsearch Denver",
"organizer": "Lee",
"location": "Denver, Colorado, USA"
}
จากเอกสารข้างต้น มีฟิลด์อยู่ 3 ฟิลด์ได้แก่ name, organizer และ location
Physical Layout: Cluster, Node, Shard และ Replica
Node: คือเครื่อง Server ที่ใช้จัดเก็บข้อมูล Index (จำไว้เลยว่า Node = Server เพื่อป้องกันการสับสนในภายหลัง)
Cluster: คือกลุ่มของ Node (Server) ที่อยู่ด้วยกัน เป็นการจัดระเบียบ Hardware ให้เป็นกลุ่มก้อน
จากรูปข้างล่าง จะเห็นว่าใน Cluster นี้จะประกอบไปด้วย Node 9 ตัวซึ่งแต่ละตัวก็มีข้อมูล Index ภายในตัวเอง
Shard: เป็นตัวแบ่งข้อมูล Index ให้แยกย่อยออกมาเพื่อประโยชน์ในการทำให้การค้นหา และการเก็บข้อมูลมีประสิทธิภาพ
ที่ต้องมี Shard นั้นเพราะว่า การยัดข้อมูลเอกสารทั้งหมดเข้าไปในอยู่ใน Index เพียงตัวเดียว บางครั้งมันอาจจะไม่เหมาะ โดยเฉพาะอย่างยิ่งหากข้อมูลนั้นมันใหญ่เกินไป เช่น 1TB ดังนั้น ทาง Elasticsearch จึงใช้ Shard ในการแตก Index ให้แยกย่อยออกมาดังรูป
สรุปคือ Shard คือการแบ่งข้อมูลของ Index ออกมาแยกเก็บเป็นส่วนย่อยๆ
คราวนี้ขอเวลาเราจัดเก็บใน Node ของจริง เราจะไม่ได้เก็บ Index ตรงๆ แต่จะเก็บเป็น Shard แล้วกระจายเก็บแยกกัน จากตัวอย่างข้างต้น Shard ทั้ง 4 ตัวจะต้องนำไปเก็บไว้ใน Node โดยขอยกตัวอย่างให้เก็บ Shard A และ B ไว้ที่ Node เดียวกัน และ C กับ D ไว้ที่ Node อีกตัวหนึ่ง ดังรูป
การจัดเก็บแบบนี้ นอกจากจะช่วยในการแตก Index ที่ใหญ่เกินไปให้มีขนาดเล็กลงแล้ว ยังช่วยการค้นหาทำได้อย่างรวดเร็ว (เพราะค้นหาจาก 2 Node พร้อมกัน แทนที่จะค้นใน Index ใหญ่ๆ เพียงตัวเดียว)
ตัวสุดท้ายในเรื่องของ Physical Layout ก็คือ Replica หรือตัวก๊อปปี้ของ Shard
เนื่องจาก Elasticsearch ค่อนข้างซีเรียสกับเรื่องของ Avaiability (Server ไม่ล่ม) ค่อนข้างมาก ดังนั้น จึงใส่แนวคิดเรื่อง Replica (ตัวก๊อปปี้) เข้ามา
แนวคิดก็ไม่มีอะไรมาก Shard ทุกตัวต้องมีตัวสำรองที่เรียกว่า Replica ขึ้นมา โดยมันจะมีข้อมูลเหมือน Shard ต้นทาง หรือ Primary Shard ทุกประการ และควรจะนำไปวางไว้คนละ Node กับ Primary Shard ของมัน โดยหากใช้ตัวอย่างเดิม หากเราเพิ่ม Replica Shard ให้กับ Primary Shard ทุกตัว แล้วแบ่งไปวางใน Node ทั้งสองตัว จะได้ผลลัพธ์ดังรูปข้างล่าง
จากรูปเราจะสร้าง Replica Shard ขึ้นมาแล้วใส่ชื่อเดียวกับ Primary Shard เช่น Shard A จะมี Replica A เป็นตัวสำรอง จากนั้นก็นำมันไปวางไว้ที่ Node B (ซึ่งเป็นคนละตัวกับ Shard A)
หากสมมติว่า Node A หรือ Server A พังลง ก็แปลว่า ระบบยังมีข้อมูล Index โดยสมบูรณ์ เพราะใน Node (Server) B ยังมีข้อมูลอยู่ครบทั้ง A, B ที่อยู่ในรูปของ Replica Shard และ C, D ที่อยู่ในรูปของ Primary Shard
การจัดการเอกสารใน Elasticsearch
ก่อนอื่นเราต้องเปิดใช้งาน Elasticsearch เสียก่อน เพราะเรากำลังจะทำแบบฝึกหัดเพื่อการสร้าง ลบ อัพเดตข้อมูลใน Index บน Elasticsearch ด้วยตัวเอง
ให้เราเปิด Terminal แล้วเข้าไปยังโฟลเดอร์ที่เราวาง Elasticsearchไว้ แล้วพิมพ์คำสั่งต่อไปนี้
bin/elasticsearch
เราจะใช้เครื่องมือที่ช่วยให้เราทำงานได้สะดวกขึ้น ซึ่งเครื่องมือนั่นคือ Kibana ให้เราเปิด Terminal อีกตัวหนึ่งแยกต่างหากจาก Elasticsearch แล้วพิมพ์คำสั่งต่อไปนี้
bin/kibana
จากนั้นเข้าไปที่ http://localhost:5601 แล้วคลิกเข้า Dev Tools ที่เมนูด้านซ้ายของ Kibana
เพียงเท่านั้นคุณก็พร้อมจะลุยไปกับผมแล้วครับ
Delete Index
จากแบบฝึกหัดในตอนที่ 1 เราได้ทำการสร้าง Index ชื่อ product ขึ้นมา ซึ่งจริงๆ แล้วจะใช้เป็น Index หลักสำหรับบทความนี้เกือบทั้งหมด ดังนั้น ก่อนอื่น ผมอยากจะให้คุณทำการเรียนรู้วิธีลบ Index ไปพร้อมกับการลบข้อมูลจริงๆ เลย
ในหน้า Dev Tools ของ Kibana ให้พิมพ์คำสั่งต่อไปนี้
DELETE product
จากนั้นก็คลิกปุ่มสามเหลี่ยมสีเขียว หากลบสำเร็จ หน้า console จะแสดงผล
{
"acknowledged": true
}
Create Index
เรามาต่อกันด้วยการสร้าง Index ที่ชื่อว่า product ด้วยคำสั่งต่อไปนี้
PUT product
ซึ่งจะเป็นการสร้าง index ที่ชื่อว่า product ให้กับ
Adding Document
เมื่อเรามี Index แล้ว ขั้นต่อไปก็คือการเพิ่มข้อมูลเข้าไปใน Index ซึ่งข้อมูลตัวนี้เราเรียกกันว่าเป็น Document โดยเราสามารถเพิ่ม Document เข้าไปใน Index ได้สองแบบคือ
แบบไม่ระบุ ID (ให้ Elasticsearch สร้าง ID ให้เราโดยอัตโนมัติ) โดยใช้คำสั่งดังต่อไปนี้
POST product/default
{
"name": "Processing Events with Logstash",
"instructor": {
"firstName": "Tim",
"lastName": "Tana"
}
}
สังเกตมั๊ยครับว่า คำสั่งนี้ เวลาระบุ Index เราจะต้องระบุคำว่า default ด้วย
POST product/default
Default คือ type ที่ถูกสร้างมาโดยอัตโนมัติสำหรับ Index นี้ แปลว่า จากนี้ไป เราจะอ้างอิง type ที่ชื่อ default ตลอดไป (และ Elasticsearch ก็สนับสนุนให้ใช้ type ตัวนี้ด้วย เพราะในอนาคตบริษัทอาจจะเลิกใช้ type ไป)
เมื่อเพิ่ม เอกสารแล้วมันจะแสดงผลดังนี้
จะเห็นว่าระบบจะสร้าง id ให้กับเอกสารเราโดยอัตโนมัติ
เราสามารถเพิ่ม Document แบบระบุ ID ของเราเองได้ ด้วยการใช้คำสั่งต่อไปนี้
POST product/default/1
{
"name": "Elasticsearch in action summary",
"instructor": {
"firstName": "Him",
"lastName": "Machine Reading"
}
}
เมื่อรันคำสั่งเสร็จ จะเห็นว่า ID ของเอกสารใหม่จะมีค่าเป็น 1
ดึงข้อมูล Document ด้วย ID
เมื่อเรามี ID อยู่กับตัวแล้ว สามารถดึงข้อทูลของเอกสารมาแสดงผลได้ โดยพิมพ์คำสั่งต่อไปนี้
GET product/default/1
ผลลัพธ์ที่ได้คือ
{
"_index": "product",
"_type": "default",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"name": "Elasticsearch in action summary",
"instructor": {
"firstName": "Him",
"lastName": "Machine Reading"
}
}
}
นำ Document ใหม่มาทับ Document เดิม
หากต้องการนำ Document ใหม่มาทับเอกสารเดิม ให้ใช้คำสั่งดังนี้
PUT product/default/1
{
"name": "Elasticsearch in action summary",
"price": 195,
"instructor": {
"firstName": "Machine",
"lastName": "Reading"
}
}
ซึ่งตรงนี้จะเป็นการเพิ่มฟิลด์ price และเปลี่ยนข้อมูลใน firstName กับ lastName
จากนั้นทดลองดึงข้อมูลออกมาจากคำสั่งต่อไปนี้
GET product/default/1
ก็จะได้ข้อมูลดังนี้
{
"_index": "product",
"_type": "default",
"_id": "1",
"_version": 2,
"found": true,
"_source": {
"name": "Elasticsearch in action summary",
"price": 195,
"instructor": {
"firstName": "Machine",
"lastName": "Reading"
}
}
}
Update Document
หมายเหตุ: ใน Elasticsearch จะไม่มีแนวคิดเรื่องการ Update ข้อมูลเหมือนใน Database เพราะระบบมองทุกอย่างเป็นเอกสาร และมีความ immutable อยู่ ดังนั้น การ update แท้ที่จริงแล้วก็คือ การลบเอกสารเก่า แล้วนำเอกสารใหม่ไปวางแทนที่
เราอยากจะปรับข้อมูลราคาของเอกสารเบอร์ 1 จาก 195 เป็น 95 แล้วเพิ่มฟิลด์ tags เข้าไป ให้พิมพ์คำสั่งดังต่อไปนี้
POST product/default/1/_update
{
"doc": { "price": 95, "tags": [ "Elasticsearch" ] }
}
จากนั้นให้ลองดึงข้อมูล
GET product/default/1
จะได้ผลลัพธ์ดังนี้
{
"_index": "product",
"_type": "default",
"_id": "1",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
สังเกตมั๊ยครับว่า เวอร์ชั่นของเอกสารเพิ่มจาก 2 เป็น 3 แล้ว
Update แบบใช้ Scripted
สมมติว่าเราต้องการ update เอกสารแบบ dynamic ไม่ได้กรอกตัวเลขตรงๆ เข้าไปแบบตัวอย่างที่แล้ว เราสามารถใช้วิธีเขียน Scripted เข้ามาช่วยได้
สมมติให้เราอยากจะเพิ่มราคาขึ้นไปอีก 10 บาท เราสามารถทำได้โดยคำสั่งดังต่อไปนี้
POST /product/default/1/_update
{
"script" : "ctx._source.price += 10"
}
เมื่อทำการดึงข้อมูลออกมา จะพบว่า
{
"_index": "product",
"_type": "default",
"_id": "1",
"_version": 4,
"found": true,
"_source": {
"name": "Elasticsearch in action summary",
"price": 105,
"instructor": {
"firstName": "Machine",
"lastName": "Reading"
},
"tags": [
"Elasticsearch"
]
}
}
Upsert
หากเรามีเอกสารตัวหนึ่งที่อยากใส่เข้าไปใน Index แต่ไม่แน่ใจว่า มีเอกสารนั้นอยู่แล้วหรือไม่ หากมีอยู่แล้วให้ทำการ Update แต่หากไม่มีก็ให้เพิ่มเข้าไป เราจะทำอย่างไรได้บ้าง?
Elasticsearch มีคำตอบให้ครับ นั่นคือการ Upsert โดยเราจะทดลองลบ Document เดิมทิ้งไปก่อน
DELETE product/default/1
จากนั้นก็ให้ทำการ Upsert เอกสารเข้าไป ดังนี้
POST /product/default/1/_update
{
"script" : "ctx._source.price += 5",
"upsert" : {
"price" : 100
}
}
คำสั่งนี้บอกว่า หากมีเอกสารเดิมอยู่ให้เพิ่มราคาเข้าไป 5 บาท แต่หากไม่มีเอกสารนี้อยู่ ให้ตั้งราคาเป็น 100
ให้ลองดึงข้อมูลเอกสารออกมา
GET product/default/1
จะเห็นว่าเอกสารใหม่จะมีค่าบนฟิลด์ price อยู่ที่ 100 เพราะเราได้ลบเอกสารเบอร์ 1 ไปแล้ว
Delete Document
เมื่อตะกี้นี้เราได้เห็นวิธีในการลบ Document โดยใช้ ID ไปแล้ว ดังนี้
DELETE product/default/1
คราวนี้เราจะลองเพิ่มเอกสารสองตัวเข้าไป ดังนี้
POST /product/default
{
"name": "Processing Events with Logstash",
"category": "course"
}POST /product/default
{
"name": "The Art of Scalability",
"category": "book"
}
จากนั้นเรามาลองลบ Document โดยใช้การวางเงื่อนไข Query ซึ่งเราจะลบเอกสารที่มีค่าในฟิลด์ category เป็น book วิธีการคือดังนี้
POST /product/_delete_by_query
{
"query": {
"match": {
"category": "book"
}
}
}
Batch Processing
คราวนี้เราอยากจะเพิ่ม Document เข้าไปหลายฉบับพร้อมๆ กัน เราสามารถทำได้โดยวิธีที่เรียกว่า _bulk
ตัวอย่างแรกของ _bulk
คือการเพิ่มเอกสารสองตัวโดยให้มี ID: 100 และ 101 ตามลำดับ ดังนี้
POST /product/default/_bulk
{ "index": { "_id": "100" } }
{ "price": 100 }
{ "index": { "_id": "101" } }
{ "price": 101 }
นี่คือการเพิ่มเอกสาร ID 100 ที่มี price = 100 และ ID 101 มี price = 101
ต่อไปเราจะทำการ update ข้อมูล price ในเอกสาร ID 100 ให้เป็น 1000 และทำการลบเอกสาร ID 101 ไปพร้อมๆ กัน
POST /product/default/_bulk
{ "update": { "_id": "100" } }
{ "doc": { "price": 1000 } }
{ "delete": { "_id": "101" } }
ให้ลองดึงข้อมูลของ document 100 ออกมา
GET product/default/100
จะได้ข้อมูลที่มีค่า price เป็น 1000 ตามคาด
จากนั้นลองดึงข้อมูล 101 ออกมา
GET product/default/101
จะหาไม่พบ (ตามคาดเช่นกัน)
Import Data from file
จากนั้นเราจะทำการนำข้อมูลเอกสารจากไฟล์เข้ามาอยู่ใน Index (ซึ่งเป็นสิ่งที่เราทำไปแล้วในตอนที่แล้ว) คราวนี้เราจะมาทำกันใหม่ตั้งแต่ต้น
ก่อนอื่นให้ทำการลบ Index product ออกไปก่อน
DELETE product
คราวนี้หากในตอนที่แล้วใครยังไม่ได้ดาวน์โหลดข้อมูล test-data.json ก็ให้ดาวน์โหลดได้จาก
https://github.com/himaeng/elasticsearchtutorial/archive/master.zip
หรือหากใครอยากจะใช้ git ในการดาวน์โหลด ดังนี้
git clone https://github.com/himaeng/elasticsearchtutorial.git
เมื่อแตกไฟล์เสร็จแล้ว ก็เปิด terminal แล้วเข้าไปที่ folder จากนั้นพิมพ์คำสั่งดังต่อไปนี้
curl -H "content-type: application/json" -XPOST "http://localhost:9200/product/default/_bulk?pretty" --data-binary "@test-data.json"
เท่านั้นคุณก็จะได้ข้อมูลทั้งหมดจากไฟล์ test-data.json มาเก็บไว้ใน index ที่ชื่อ product
มาแยกคำสั่งดูกันทีละส่วน
-H "content-type: application/json"
ทุกครั้งที่เราเรียกใช้ Elasticsearch เพื่อการเปลี่ยนแปลงข้อมูล (Create, Update, Delete) เราจะต้องระบุข้อมูลส่วนนี้ไปด้วย -H ย่อมาจาก header
-XPOST "http://localhost:9200/product/default/_bulk?pretty"
POST นี่คือคำสั่งที่ใช้สร้าง index ที่ชื่อว่า product โดยใช้ type ชื่อ default และใช้ฟังก์ชั่น _bulk
เพื่อเพิ่มข้อมูลแบบ batch processing
ส่วน ?pretty
เพื่อบอกให้การแสดงผลบน Terminal/command line ดูง่าย (หากไม่ใช้คำสั่งนี้ ผลลัพธ์จะแสดงออกมาแบบติดๆ กัน อ่านยากมาก แต่หากใช้ ?pretty
จะแสดงผลแบบนี้บน terminal
สำรวจ Cluster
คราวนี้เราลองมาตรวจดูสถานะของพวก server และ cluster กันบ้าง
ก่อนอื่นลองมาดู health ของ cluster กันหน่อย โดยให้กลับมารันคำสั่งใน Kibana เหมือนเดิมดังนี้
GET /_cat/health?v
เมื่อรันคำสั่งแล้วจะเห็นหมดเลยว่า ตอนนี้ cluster เรามีกี่ Node มีกี่ shard และเป็น primary shard เท่าไหร่
ต่อไปจะเป็นคำสั่งสำหรับลิสต์รายการ node ทั้งหมดออกมา
GET /_cat/nodes?v
หากคุณอยากจะดูว่าตอนนี้ cluster ของคุณมี index อะไรบ้าง ก็ให้รับคำสั่งต่อไปนี้
GET /_cat/indices?v
ต่อไปนี้คือวิธีในการแสดงข้อมูลภาพรวมของ shard
GET /_cat/allocation?v
และสุดท้ายจะเป็นคำสั่งที่แสดงว่ามี node แต่ละตัวมี shard และแต่ละ shard มีเก็บข้อมูลของ index อะไรบ้าง
GET /_cat/shards?v