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

Tanakorn Numrubporn
Machine Reading
Published in
8 min readMay 2, 2018

เริ่มค้นหาข้อมูล

ตอนนี้เราจะมาสู่แกนกลางของตัว Elasticsearch แล้วนะครับ เหตุผลที่เครื่องมือตัวนี้เกิดมาก็เพราะมันต้องการช่วยให้เราสามารถสร้าง Search Engine และเจ้า Search Engine เนี่ย ก็คือระบบที่ใช้สำหรับค้นหาข้อมูล

และบทนี้เราจะมาว่ากันด้วยการค้นหาล้วนๆ เริ่มกันตั้งแต่โครงสร้างการค้นหาใน Elasticsearch ไปจนถึงคำสั่งต่างๆ ที่ใช้ในการค้นหาแบบพิศดาร

มาเริ่มกันเลย

โครงสร้างการค้นหาของ Elasticsearch

เนื่องจากโครงสร้างการเก็บข้อมูลของ Elasticsearch ตามที่เราได้รู้กันมาแล้วว่า จะเก็บแบบกระจายตัวตาม Node ต่างๆ โดยแยกเก็บเป็นก้อนๆ เรียกว่า Shard ซึ่งเจ้า Shard เนี่ยก็จะมีทั้งตัวต้นแบบที่เรียกว่า Primary Shard และตัวสำรองที่เรียกว่า Replica Shard ดังนั้น คงจะเป็นการดี หากเราเข้าใจว่า กลไกการค้นหาภายในของ Elastic มันทำงานยังไง

จากรูปข้างต้น สมมติให้เราคือเครื่อง Client ต้องการค้นหาข้อมูลผ่าน Elasticsearch Cluster โดยการส่งคำค้นหา หรือ Search Query เข้าไป จากนั้น ทาง Elasticsearch จะทำการค้นหาภายในข้อมูล Index ของตน เพื่อได้ผลลัพธ์แล้ว ก็จะส่งรายการกลับไปให้ผู้ค้นหา

สมมติให้ภายใน Cluster มี Node หรือ Server อยู่ 3 เครื่อง แต่ละเครื่องเก็บ Shard อยู่ 3 ตัว และแต่ละ Shard จะมี Replica อยู่ 2 ตัว เช่น Shard A จะมี Replica A1, A2

เมื่อผู้ใช้ยิง Search Query เข้ามา ใน Cluster มันจะวิ่งเข้าไปที่ Node ใด Node หนึ่ง ซึ่ง Node นั้นจะทำหน้าที่เป็นผู้ประสานงานโดยตัวมันเองจะมีข้อมูลทั้งหมดว่า แต่ละ Node มีข้อมูล Index อะไรบ้าง (ซึ่ง Node ทุกสามารถทำหน้าที่นี้ได้ทุกตัว ขึ้นอยู่กับว่า ตัวไหนจะขึ้นมารับหน้าที่ประสานงานในจังหวะนั้นๆ)

จากนั้น Node ผู้ประสานงานจะทำการกระจายคำสั่ง หรือ Boardcast ออกไปยัง Node อื่นๆ ภายใน Cluster

แต่ละ Node ที่ได้รับคำสั่งค้นหาจาก Node ประสานงาน ก็จะค้นหาข้อมูลจาก Index ภายในตัวมันเอง แล้วเมื่อได้ผลลัพธ์ก็จะส่งผลลัพธ์กลับไปมารวมที่ Node ประสานงาน

จากนั้น Node ประสานงานจะทำการรวมรวม จัดระเบียบ และเรียงลำดับเพื่อส่งคืนผลลัพธ์ทั้งหมดกลับไปที่ ผู้ใช้งาน

การ Search ข้อมูล

ในตอนนี้เราจะมาดูกันว่า วิธีการค้นหา หรือการ Query ข้อมูลำภายใน Elasticsearch มีวิธีใดบ้าง โดยผมจะยกมาแต่ส่วนที่สำคัญ และใช้บ่อยที่สุดนะครับ

ค้นหาด้วย URL

เนื่องจากระบบของ Elasticsearch เป็นระบบที่รองรับการทำงานแบบ REST API ซึ่งแปลว่า เราสามารถยิงคำขอผ่านการใช้ URL ได้ โดยจากนี้ไปให้ใช้ Kibana ในการทำงาน

วิธีดึงข้อมูลทั้ง Index ของ product ออกมา

GET product/default/_search?q=*

วิธีนี้จะดึงข้อมูลทั้งหมดจาก Index ของ product ออกมาทั้งหมด ซึ่งจะมี 1,000 เอกสาร โดยในที่นี้มีการเรียกใช้ฟังก์ชั่น _search แล้วส่งพารามิเตอร์ q (ซึ่งก็ย่อมาจาก query นั่นเอง)

เนื่องจากว่าเราต้องการดึงข้อมูลทั้งหมดออก จึงไม่จำเป็นต้องยิงพารามิเตอร์ q ให้เสียเวลา โดยเขียนได้ดังนี้ (ตัด ?q=* ออก)

GET /product/default/_search

ซึ่งจะให้ผลลัพธ์เหมือนกัน

ต่อไปจะเป็นการค้นหาเอกสารที่มีคำว่า lobster อยู่ในฟิลด์ name

GET product/default/_search?q=name:Lobster

โปรดสังเกตตรงพารามิเตอร์ q ให้ดี เรากรอกลงไปว่า name:Lobster แปลว่า ให้หาคำว่า โดย name คือชื่อฟิลด์ และ Lobster คือ คำที่ต้องการหาจากฟิลด์นั้น

ต่อไปเป็นการค้นหาเอกสารที่มีคำว่า Meat อยู่ในฟิลด์ tags

GET product/default/_search?q=tags:Meat

และสุดท้าย เป็นการค้นหาเอกสารที่มีคำว่า Meat อยู่ในฟิลด์ tags และคำว่า Tuna อยู่ในฟิลด์ name

ทำความเข้าใจโครงสร้างผลลัพธ์ของการค้นหา

จากแบบฝึกหัดที่ผ่านมา เราคงเห็นแล้วว่า เวลาเรา query ไป Elasticsearch จะยิงผลลัพธ์กลับมาด้วยฟอร์แมต JSON โดยใน section จะขออธิบายความหมายของแต่ละฟิลด์แบบคร่าวๆ ครับ

โดยตัวอย่างที่ยกมาในรูปข้างล่างเป็นผลลัพธ์ของ query

GET product/default/_search?1=tags:Meat

การค้นหาด้วย Query DSL

เนื่องการค้นหาข้อมูลด้วย url นั้น อาจจะไม่สะดวกหากประโยคที่ใช้ query มีความซับซ้อนมาก เพราะจะทำให้เสี่ยงต่อการพิมพ์ผิดได้ และจัดเก็บยาก ดังนั้น Elasticsearch จึงเตรียมวิธีการ query ที่ตอบจุดประสงค์เหล่านี้มาให้ ซึ่งเรียกว่ามันคือการ Query ด้วย JSON หรือที่เรียกว่า Query DSL

ตัวอย่างต่อไปนี้คือการดึงข้อมูลของเอกสารมาทั้งหมด

GET product/default/_search
{
"query": {
"match_all": {}
}
}

เป็นการใช้ฟังก์ชั่น match_all ซึ่งก็คือการ ดึงเอกสารทุกตัวทั้งหมดใน index ออกมาให้หมดนั่นเอง

Term Query

เรามาลองเริ่มดึงข้อมูลกันโดยเอาเอกสารเฉพาะที่มีคำว่า lobster ในฟิลด์ที่ชื่อ name โดยเขียนเป็น query ได้ดังนี้

GET /product/default/_search
{
"query": {
"term": {
"name": "lobster"
}
}
}

จะเห็นว่ามีผลลัพธ์ออกมา 5 เอกสาร

แต่หากเราเปลี่ยนคำค้นหาจาก lobster เป็น Lobster จะได้ผลลัพธ์ดังนี้

{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}

แปลว่า หากไม่เจอเลย ทั้งๆ ที่เปลี่ยนแค่พยัญชนะจาก l เป็น L เท่านั้น

อธิบายได้ตามรูปนี้

term query จะค้นคำที่ตรงกับคำใน index เท่านั้น โดย lobster กับ Lobster ถือเป็นคนละคำกัน

จากรูป จะเห็นว่า product index ของเราเก็บ keyword เป็นอังกฤษตัวเล็กทั้งหมด ซึ่งมันเกิดขึ้นในตอนจัดเก็บ Index

Elasticsearch จะทำการแยกคำออกเป็นคำๆ แล้วทำให้มันเป็นตัวพิมพ์เล็กทั้งหมด ซึ่งตัวที่ทำหน้าที่ตรงนี้มีชื่อว่า Analysis (ซึ่งจะเป็นเนื้อหาในตอนถัดไป) โดยสรุปก็คือ ใน index ของเราทั้งหมดจะจัดเก็บเป็นตัวพิมพ์เล็กทั้งหมด ทำให้เวลาเรา query ด้วยคำว่า Lobster จึงหาไม่เจอ เพราะใน index มีแค่คำว่า lobster เท่านั้น

ซึ่งปัญหานี้เราจะแก้ด้วย Match Query

Match Query

เรามาลองเขียน query ใหม่ดังนี้

GET /product/default/_search
{
"query": {
"match": {
"name": "Lobster"
}
}
}

จะเห็นว่า มันแสดงผลว่าหาข้อมูลเจอเอกสาร 5 ฉบับอย่างถูกต้อง

สาเหตุเป็นเพราะว่า match query ก่อนจะทำการค้นหาใน index จะนำคำค้นหาเข้าไปผ่าน analysis ก่อน ด้วยเหตุนี้ คำค้นที่เราส่งเข้าไปคือ Lobster จึงถูกเปลี่ยนเป็น lobster แล้วจากนั้นจึงค่อยนำเข้าไปค้นหาใน Index ดังรูป

Match query จะทำการใช้ Analysis ทำการปรับ keyword ก่อนจะนำไปค้นหาใน Index

รายละเอียดของ Match Query ทั้งหมดจะยกไปพูดในส่วนท้ายของบทความตอนนี้ต่อไป

ค้นหาเอกสารด้วย boolean field

คราวนี้เรามาลองค้นหาเอกสารโดยระบุค่าที่ใช้ค้นหาเป็น boolean โดยจะค้นที่ฟิลด์ชื่อ is_active ดังนี้

GET /product/default/_search
{
"query": {
"term": {
"is_active": true
}
}
}

ค้นหาด้วย term หลายตัว

หากเราดูโครงสร้างของ index ที่ชื่อ product เราจะเห็นข้อมูลของฟิลด์ที่ชื่อ tags ดังนี้

GET product/default/_mapping[ผลลัพธ์]
...
},
"tags": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
...

จะเห็นว่าภายใต้ฟิลด์ที่ชื่อว่า tags จะมีฟิลด์ลูกหรือ nested field ที่ชื่อว่า keyword อยู่ ดังนั้น หากเราต้องการจะอ้างอิงถึงฟิลด์ลูกตัวนี้เราจะต้องเขียนชื่อฟิลด์ใน query ว่า tags.keyword

คราวนี้เราลองมาเขียน query ที่ค้นหาจากคำหลายๆ คำพร้อมกัน โดยจะใช้ฟังก์ชั่นที่ชื่อ terms (อันนี้เติม S นะครับ) โดยเราจะค้นหาจากฟิลด์ tags.keyword ซึ่งฟิลด์นี้มีธรรมชาติมีค่าหลายค่าอยู่แล้ว ดังนั้นเราจึงต้องค้นหาด้วยวิธีแบบนี้ โดยให้ลองค้นหาโดยใช้ query ดังนี้

GET /product/default/_search
{
"query": {
"terms": {
"tags.keyword": [ "Soup", "Cake" ]
}
}
}

จะได้ผลลัพธ์ดังนี้

จะเห็นว่า ผลลัพธ์ที่ได้จะมีค่า tags.keyword ที่มีทั้ง Soup และ Cake

การดึงข้อมูลจาก ID ของเอกสาร

เราสามารถ query จากการดึงข้อมูลเอกสารจากการเรียก id ตรงๆ ได้ โดยใช้ประโยคดังต่อไปนี้

GET product/default/_search
{
"query": {
"ids": {
"values": [ 1, 2, 3 ]
}
}
}

ค้นหาข้อมูลแบบเป็น Range

Elasticsearch เปิดให้เราสามารถค้นหาเอกสารโดยระบุข้อมูลเป็นช่วงได้ เช่นประโยคดังต่อไปนี้ จะเป็นการดึง product ที่มีปริมาณของในคลังตั้งแต่ 1 ถึง 5 ชิ้น

GET product/default/_search
{
"query": {
"range": {
"in_stock": {
"gte": 1,
"lte": 5
}
}
}
}

หมายเหตุ: gte คือ มากกว่าหรือเท่ากับ (Greater Than or Equal) ส่วน lte คือ น้อยกว่าหรือเท่ากับ (Less Than or Equal)

ผลที่ได้ก็คือ

      {
...
"_source": {
"name": "Bread - Rolls Rye",
"price": 157,
"in_stock": 2,
...
}
},
{
...
"_source": {
"name": "Sherry - Dry",
"price": 62,
"in_stock": 1,
...

และนอกเหนือจากตัวเลขแล้ว เรายังสามารถ query ด้วย range ของ date ได้ด้วย

query ต่อไปนี้ต้องการเอกสารที่สร้างขึ้นระหว่างวันที่ 1 มกราคม 2010 ไปจนถึง 31 ธันวาคม 2010

GET product/default/_search
{
"query": {
"range": {
"created": {
"gte": "2010/01/01",
"lte": "2010/12/31"
}
}
}
}

และเราสามารถกรอกวันตาม format ของเราเองได้ โดยให้ระบุ format ไว้ดังนี้

GET product/default/_search
{
"query": {
"range": {
"created": {
"gte": "01-01-2010",
"lte": "31-12-2010",
"format": "dd-MM-yyyy"
}
}
}
}

โดยข้างบนเป็นการปรับ format แบบ วัน-เดือน-ปี แทนที่จะเป็น ปี/เดือน/วัน โดยระบุด้วย format

ดึงเอกสารที่มีค่า (non-null)

ต่อไปนี้คือการดึงเอกสารที่ฟิลด์ tags มีค่า (non-null) เพื่อป้องกันการแสดงผลที่เป็นค่าว่าง

GET product/default/_search
{
"query": {
"exists": {
"field": "tags"
}
}
}

การหาเอกสารโดยใช้ Prefix Query

ในบางกรณีเราอยากค้นหาเอกสารโดยไม่ได้ใช้คำทั้งหมด แต่เป็นตัวอักษรที่เป็นคำขึ้นต้น เช่น ต่อไปนี้เป็นการค้นหาเอกสารที่มีค่าในฟิลด์ tags ขึ้นต้นด้วยตัวอักษร Vege

GET product/default/_search
{
"query": {
"prefix": {
"tags.keyword": "Vege"
}
}
}

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

จะเห็นว่า ผลลัพธ์ทุกอันที่ติดมา ภายในฟิลด์ tags จะมีตัวอักษร Vege นำหน้าเสมอ

หมายเหตุ: การค้นหาด้วย prefix หรือ wildcard จะเปลืองทรัพยากร และช้ากว่า query แบบธรรมดา ดังนั้น หากเลี่ยงได้ ก็ควรเลี่ยง

ค้นหาด้วย Wildcard

ต่อไปนี้คือการค้นหาด้วยการกำหนด wildcard เพื่อหาเอกสารที่มีค่าสอดคล้องกัน

หากเราใช้ * แปลว่าจำนวนตัวอักษรจะมีตั้งแต่ 0 ถึงกี่ตัวก็ได้ เช่น

GET product/default/_search
{
"query": {
"wildcard": {
"tags.keyword": "Veg*ble"
}
}
}

Veg*ble นั้น จะตรงกับ Vegable, Vegetable, Vegble เป็นต้น

หากเราใช้อักขระ ? แปลว่าต้องมีเพียงหนึ่งตัวอักษรตรงตำแหน่งนี้เท่านั้น ห้ามมากกว่าหรือน้อยกว่า เช่น

GET product/default/_search
{
"query": {
"wildcard": {
"tags.keyword": "Veg?ble"
}
}
}

ใน Veg?ble จะตรงกับ Vegable, Vegeble แต่จะไม่ตรงกับ Vegetable, Vegble ดังนั้นผลลัพธ์ที่ได้ของ query นี้จึงไม่เจออะไรเลย

คราวนี้ขอปรับ query นิดหน่อย

GET product/default/_search
{
"query": {
"wildcard": {
"tags.keyword": "Veget?ble"
}
}
}

คราวนี้ก็จะค้นเจอข้อมูลแล้ว เพราะ Veget?ble ตรงกับ Vegetable

Full Text Query (Match Query)

จากที่ผมได้ติดผู้อ่านไว้ในตอนต้นของบทความตอนนี้เรื่อง Match Query ซึ่งเป็น Query ที่น่าจะเป็นตัวหลักที่เรานำไปใช้งานจริง เรามาเริ่มต้นโดยการ import ข้อมูลเพื่อสร้าง index ใหม่ที่ชื่อ recipe กันก่อน

ให้ไปดาวน์โหลดไฟล์จาก

https://github.com/himaeng/elasticsearchtutorial/archive/master.zip

หรือจะใช้คำสั่งของ git ก็ได้

git clone https://github.com/himaeng/elasticsearchtutorial.git

จากนั้นก็เปิด terminal เพื่อเข้าไปตรงโฟลเดอร์ที่เราดาวน์โหลดไฟล์ (แล้วแตก) จากนั้นก็ทำการ import ข้อมูลเพื่อสร้าง recipe index ดังนี้

curl -H "content-type: application/json" -XPOST 'http://localhost:9200/recipe/default/_bulk?pretty' --data-binary "@recipe.json"

จากนั้นก็เปิด Kibana (http://localhost:5601) แล้วกรอกคำสั่งต่อไปนี้เพื่อตรวจดู Mapping ของ Index (เรื่อง Mapping กับ Analysis จะขอยกยอดไปพูดในตอนหน้านะครับ)

GET recipe/default/_mapping

หากไม่มีอะไรผิดพลาดเราจะเห็นโครงสร้างข้อมูลที่ Elasticsearch สร้างให้เราโดยอัตโนมัติในตอนที่เรา import

Match Query มาตรฐาน

ในตอนที่เราใช้ term query เราต้องค้นเป็นคำๆ ซึ่งบอกตรงๆ ว่าดูไม่ค่อยยืดหยุ่นเท่าไหร่ แต่สำหรับ match query นั้นเราสามารถค้นหาได้แบบเป็น free text หรือการกรอกประโยคเข้าไปได้เลย แล้วเดี๋ยวทาง Elasticsearch จะทำการค้นหาให้เอง เช่น Query ดังต่อไปนี้

GET recipe/default/_search
{
"query": {
"match": {
"title": "Recipes with pasta or spaghetti"
}
}
}

เมื่อลองตรวจดูผลลัพธ์จะเห็นว่าเอกสารที่ขึ้นเป็นอันดับแรกจะมีคำว่า Pasta or Spaghetti ทำให้มีคะแนน (Relevance Score อยู่ที่ 2.6974046) ดังนี้

แต่หากดูที่อันดับสอง จะมีแค่คำว่า Pasta เท่านั้น และมี Relevance Score อยู่ที่ 1.7715604 (เรื่องรายละเอียดของการคำนวน Relevance Score แบบพิศดารจะมีกล่างถึงในตอนต่อๆ ไป)

จะเห็นว่า เอกสารที่ได้คะแนนอันดับหนึ่งจะมีคำที่ตรงกับคำค้นหามากกว่าอันดับสอง จึงได้คะแนนมากกว่า

Match Query โดยใช้ Boolean Operator เข้าช่วย

จากตัวอย่างที่แล้ว เวลาเราค้นหาด้วย match แล้วในคำค้นหามีหลายคำ Elasticseard จะนำทุกคนมา OR นั่นทำให้ผลลัพธ์ของการค้นหาจึงแสดงผลไม่ครบทุกคำแบบตัวอย่างก่อนหน้านี้

คราวนี้เราลองมาใช้ query เดิม แต่เพิ่ม operator ให้เป็น and ดังนี้

GET /recipe/default/_search
{
"query": {
"match": {
"title": {
"query": "Recipes with pasta or spaghetti",
"operator": "and"
}
}
}
}

นั่นนำคำสั่งมารัน จะค้นหาไม่เจอเอกสารใดๆ เลย เพราะการใช้ and หมายถึงต้องมีทุกตัวอักษร

คราวนี้ลองปรับ query ใหม่โดยเราจะสนใจเฉพาะคำว่า pasta กับ spaghetti เท่านั้น

GET recipe/default/_search
{
"query": {
"match": {
"title": {
"query": "pasta spaghetti",
"operator": "and"
}
}
}
}

คราวนี้ผลลัพธ์จะได้ 1 เอกสาร ที่มี title จะมีทั้งคำว่า pasta และ spaghetti ทั้งคู่

ค้นหาจากหลายๆ ฟิลด์

ที่ผ่านมาเวลาเราค้นหาด้วย match query เราจะค้นหาจากฟิลด์เพียงฟิลด์เดียว ในกรณีที่เราต้องการค้นหาผ่านหลายๆ ฟิลด์ในเวลาเดียวกัน เราสามารถใช้คำสั่ง multi_match ได้

ยกตัวอย่างเช่น หากเราต้องการค้นหาเอกสารที่มีคำว่า pasta อยู่ในฟิลด์ title หรือ description เราจะสามารถค้นหาได้ด้วยคำสั่งดังนี้

GET /recipe/default/_search
{
"query": {
"multi_match": {
"query": "pasta",
"fields": [ "title", "description" ]
}
}
}

Multiple Queries with Boolean Logic

ที่ผ่านมาทั้งหมดเราใช้ query เดี่ยวในการค้นหา แต่ Elasticsearch มีความสามารถในการใส่ query หลายๆ ตัวพร้อมๆ กัน เพื่อทำให้ logic ในการค้นหาทำได้อย่างมีประสิทธิภาพ

จากตัวอย่างต่อไปนี้ เราจะนำ match query มาผสมกับ range query โดยเชื่อมกันด้วย must ซึ่งแปลว่าเอกสารที่ค้นหาได้ ต้องตอบทั้งสอง Query โดย query ดังต่อไปนี้ เอกสารที่ต้องการคือ เอกสารที่ใน ingredients.name ต้องมีคำว่า parmesan และมีค่า preparation_time_minutes (เวลาในการเตรียมอาหาร) น้อยกว่าหรือเท่ากับ 15

GET recipe/default/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"ingredients.name": "parmesan"
}
},
{
"range": {
"preparation_time_minutes": {
"lte": 15
}
}
}
]
}
}
}

จะได้ผลลัพธ์ดังนี้

{
...
"preparation_time_minutes": 12,
...
],
"ingredients": [
...
{
"name": "Parmesan cheese"
}
],

กรองผลลัพธ์ด้วย filter

การค้นหาข้อมูลของ Elasticsearch นั้น หากดูตั้งแต่ต้นมา จะเห็นว่า ทั้ง term และ match query จะค้นหาโดยใช้การดึงเอกสารจากคะแนนความแม่นยำหรือ Relevance Score เป็นหลัก

แต่ยังมีการดึงข้อมูลอีกวิธีหนึ่ง ซึ่งเป็นวิธีธรรมดาๆ ที่เป็นวิธีดั้งเดิมแบบ Database นั่นคือ การกรองข้อมูลที่ตรงกับที่เราต้องการมาใช้โดยไม่สนใจคะแนน วิธีนี้เรียกว่า filter

ความแตกต่างระหว่าง query กับ filter (ภาพจากหนังสือ Elasticsearch in Action)

จากรูปข้างต้น ทางซ้ายมือคือการดึงข้อมูลโดยใช้ filter จะเห็นว่า วิธีการมันเรียบง่ายมาก คือค้นหาแค่ว่าเอกสารมีคำตรงกับคำค้นหาหรือไม่ หากมีก็นำมาแสดงผลใน Cache หากไม่มีก็ข้ามไป ต่างกับ Query ที่หากเอกสารมีคำที่ต้องกับคำค้นหา มันจะทำการคำการคำนวน Relevance Score ก่อน แล้วจากนั้นจึงส่งผลลัพธ์กลับไปยังผู้ใช้

ตัวอย่าง filter ต่อไปนี้เป็นการคัดเฉพาะสูตรอาหารที่ใช้ parmesan เป็นส่วนผสมอ โดยจะมีการกรองเอาเฉพาะข้อมูลที่มีค่า preparation_time_minutes ไม่เกิน 15 นาทีเท่านั้น

GET recipe/default/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"ingredients.name": "parmesan"
}
}
],
"filter": [
{
"range": {
"preparation_time_minutes": {
"lte": 15
}
}
}
]
}
}
}

ในจังหวะค้นหา ระบบจะทำการกรองข้อมูลมาก่อน แล้วจึงค่อยค้นหาโดยใช้ Query สังเกตมั๊ยครับว่า ผลลัพธ์ของ query ข้างต้น จะเหมือนกับของตัวอย่างก่อนหน้านี้เลย

การใช้ must_not operator

จากตัวอย่างก่อนหน้า เราจะเพิ่มเงื่อนไขของ query โดยระบุว่า สูตรอาหารที่หามาได้นั้น ต้องไม่มี tuna ซึ่งตรงนี้เราก็แค่เพิ่ม clause ที่ใช้ must_not operator เข้าไป ดังนี้

GET recipe/default/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"ingredients.name": "parmesan"
}
}
],
"must_not": [
{
"match": {
"ingredients.name": "tuna"
}
}
],
"filter": [
{
"range": {
"preparation_time_minutes": {
"lte": 15
}
}
}
]
}
}
}

จะเห็นว่า ผลลัพธ์ของ Query นี้ id 11 จะหายไป เพราะมันมีส่วนผสมที่ชื่อว่า Con oil-packed tuna อยู่ จึงถูกตัดออกจากผลลัพธ์

ค้นหาด้วย should operator

operator ตัวสุดท้ายของ boolean query มีชื่อว่า should วิธีทำความเข้าใจง่ายๆ นะครับ

must = and

should = or

มันก็แค่นี้เองครับ การใช้ should จะเป็นการ OR ว่า query ไหนเป็นจริงเพียงตัวใดตัวหนึ่ง เอกสารนั้นก็ถูกเลือกมาแล้ว มาลองดูกันเลยครับ

GET recipe/default/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"ingredients.name": "parmesan"
}
},
{
"range": {
"preparation_time_minutes": {
"lte": 15
}
}
}
]
}
}
}

นี่คือ query ชุดเดิม เพียงแต่เปลี่ยนจาก must เป็น should เท่านั้น และผลลัพธ์ที่ได้ก็คือ มีเอกสารที่ติดมากับผลลัพธ์จำนวน 11 ฉบับ (ในขณะที่ must มีแค่ 3 ฉบับ)

ตรวจสอบการคำนวนผลลัพธ์ ด้วย Explain API

บางครั้ง เราก็ไม่ควรไว้ใจให้ Elasticsearch ทำการคำนวณคะแนน และเรียงลำดับผลการค้นหาให้เราโดยเราไม่สามารถเข้าไปดูที่มาที่ไปได้เลย เราสามารถใช้ฟังก์ชั่นนึงของ Elasticsearch ที่ช่วยให้เราสืบสาวเข้าไปได้ว่าเอกสารแต่ละตัวที่นำมาแสดงนั้น มันคำนวณคะแนนออกได้อย่างไร

เราจะใช้ query เดิมจากตัวอย่างที่แล้ว เพิ่มเติมเพียง explain เท่านั้น ดังนี้

GET /product/default/_search
{
"explain": true,
"query": {
"term": {
"name": "lobster"
}
}
}

เมื่อรันคำสั่งนี้ไป จะเราจะเห็นเอกสารผลลัพธ์แต่ละชุดมีข้อมูลเพิ่มขึ้นมานั่นคือ _explaination ซึ่งจะแสดงให้เราเห็นถึงวิธีในการคำนวณคะแนนของเอกสารอย่างละเอียดว่า คำนวนอย่างไรและใช้วิธีอะไรบ้างโดยบอกทั้งสูตร และตัวเลขอย่างละเอียด สามารถศึกษาได้เลย

ก็จบกันไปแล้วนะครับ สำหรับตอนที่น่าจะยาวที่สุดของซีรี่ย์ tutorial ตัวนี้ เพราะการค้นหาถือเป็นฟังก์ชั่นหลักของ Elasticsearch ดังนั้นผมจึงค่อนข้างเน้นมากหน่อย

เจอกันตอนหน้าครับ ว่าด้วยเรื่อง Relevance Score & Analysis

--

--