Elasticsearch ภาคลุยสนาม ตอนที่ 2

Tanakorn Numrubporn
Machine Reading
Published in
6 min readApr 30, 2018

สถาปัตยกรรม และการจัดการข้อมูลเอกสารใน Elasticsearch

Elasticsearch Architecture

สองมุมมอง

การจะทำความเข้าใจสถาปัตยกรรมของ Elasticsearch นั้น ต้องมองจากสองมุมมอง ได้แก่

  • Logical Layout: เป็นมุมมองในส่วนที่ใช้สำหรับการค้นหา คิดอะไรไม่ออกก็ให้มองว่ามันเหมือนกับฐานข้อมูลนั่นแหละ ซึ่งในโลกของ Elasticsearch นั้นเรียกมันว่าเป็น Index (ซึ่งก็คือฝั่งซ้ายของรูปข้างบน)
  • Physical Layout: เป็นมุมมองในส่วนที่ใช้ในการจัดเก็บข้อมูลในระดับ Hardware ซึ่งตรงนี้เป็นส่วนงานที่ใช้ในการจัดการ Performance, Scalability และ Availability ของระบบค้นหา (ฝั่งขวาของรูปข้างบน)

Logical Layout: Document, Type และ Index

ภาพต่อไปนี้จะสรุปรูปแบบการทำงานในส่วนของ Logical ของ Elasticsearch ทั้งหมด

Elasticsearch Logical Layout

ก่อนอื่นเรามาดูในแกนกลางกล่าวคือ 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 ภายในตัวเอง

Node และ Cluster

Shard: เป็นตัวแบ่งข้อมูล Index ให้แยกย่อยออกมาเพื่อประโยชน์ในการทำให้การค้นหา และการเก็บข้อมูลมีประสิทธิภาพ

ที่ต้องมี Shard นั้นเพราะว่า การยัดข้อมูลเอกสารทั้งหมดเข้าไปในอยู่ใน Index เพียงตัวเดียว บางครั้งมันอาจจะไม่เหมาะ โดยเฉพาะอย่างยิ่งหากข้อมูลนั้นมันใหญ่เกินไป เช่น 1TB ดังนั้น ทาง Elasticsearch จึงใช้ Shard ในการแตก Index ให้แยกย่อยออกมาดังรูป

แสดงเรื่องการแบ่งข้อมูลบน Index ออกมาเป็น Shard ย่อย (ภาพจากคอร์ส Elasticsearch Complete Guide)

สรุปคือ Shard คือการแบ่งข้อมูลของ Index ออกมาแยกเก็บเป็นส่วนย่อยๆ

คราวนี้ขอเวลาเราจัดเก็บใน Node ของจริง เราจะไม่ได้เก็บ Index ตรงๆ แต่จะเก็บเป็น Shard แล้วกระจายเก็บแยกกัน จากตัวอย่างข้างต้น Shard ทั้ง 4 ตัวจะต้องนำไปเก็บไว้ใน Node โดยขอยกตัวอย่างให้เก็บ Shard A และ B ไว้ที่ Node เดียวกัน และ C กับ D ไว้ที่ Node อีกตัวหนึ่ง ดังรูป

แตก Index เป็น 4 Shards (A, B, C, D) แล้วนำ A,B ไปเก็บไว้ที่ Node A ส่วน C, D เก็บไว้ที่ Node B

การจัดเก็บแบบนี้ นอกจากจะช่วยในการแตก 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 ทุกตัว แล้วนำไปวางไว้คนละ 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
}
ผลลัพธ์การ Delete Product index

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 ที่ถูกสร้างโดย Elasticsearch แบบอัตโนมัติ

จะเห็นว่าระบบจะสร้าง 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

ผลลัพธ์ของการรันคำสั่งโดยใช้ ?pretty

สำรวจ 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

--

--