ยกระดับการ Query Elasticsearch ด้วย DSL

Tanapon Pitichat
InsightEra
Published in
4 min readMar 9, 2020
Photo by Caspar Camille Rubin on Unsplash

หลายคนที่ใช้ Elasticsearch แล้วเริ่มที่จะเขียนโปรแกรมเพื่อดึงข้อมูลจาก Elasticsearch ไปสักพัก จะพบปัญหาว่าการดึงข้อมูลจาก Elasticsearch ด้วย Query String นั้นเป็นเรื่องที่ทำได้ไม่ค่อยสะดวกในเชิง Programming เท่าไหร่ ไม่ว่าจะเป็นการที่ต้องคอยรับมือกับ วงเล็บ ใน Query จำนวนมากที่พร้อมจะตกหล่นและทำให้ Query เราผิดได้ทุกเมื่อ หรือเมื่อเขียน Query เสร็จแล้ว การจะไปตามอ่าน Query String เก่าของเรานั้น ก็ทำได้ยากเหลือเกิน ยังไม่รวมไปถึงว่า Query String นั้นมีข้อจำกัดด้านการค้นหาค่อนข้างเยอะอีกด้วย เพราะฉะนั้นบทความสั้นๆ ในวันนี้จะมาแนะนำการ Query Elasticsearch ด้วย DSL เพื่อแก้ปัญหาเหล่านี้กัน

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

ซึ่งบทความก่อนจะอธิบายพื้นฐานของ Elasticsearch รวมถึงการ Query ด้วย Query String ไว้ค่อนข้างละเอียด

DSL คืออะไร

Domain Specific Language หรือ DSL ถ้าจะพูดง่ายๆมันก็คือ JSON ที่ใช้ในการ Query Elasticsearch นั่นแหละ ถ้าเราย้อนไปดูในส่วนของ Query String ที่เคยอธิบายไปในบทความก่อนหน้าจะมีหน้าตาประมาณนี้

(message:elasticsearch AND age:22) OR age:19

ซึ่งเป็นแค่ String ที่ยาวออกไปเรื่อยๆเวลาที่เราต้องเขียนโปรแกรมเพื่อสร้างหรือเพิ่มเงื่อนไขต่อจากเดิมก็จะทำได้ยากและมีโอกาสผิดพลาดสูง ในทางกลับกัน Query ของ DSL จะมีหน้าตาประมาณนี้

{
"query": {
"bool": {
"should": [
{
"bool": {
"must": [
{
"term": {
"age": {
"value": 22
}
}
},
{
"term": {
"message": {
"value": "elasticsearch"
}
}
}
]
}
},
{
"term": {
"age": {
"value": 19
}
}
}
]
}
}
}

ซึ่งถึงแม้ดูแล้วเหมือนว่า DSL จะยาวและเข้าใจยากกว่าแบบ Query String แต่เนื่องจาก Format ที่ DSL ใช้นั้น เป็น JSON ซึ่งมี Library Support ในเกือบทุกภาษา Programming ทำให้การเขียนโปรแกรมเพื่อสร้าง Query Elasticsearch ด้วย DSL นั้นจะสะดวกสบายกว่าและมีข้อผิดพลาดน้อยกว่า อีกทั้งการใช้ DSL นั้นยังเปิดโอกาสให้เราตั้งค่าการ Query ของเราได้ละเอียดมากกว่าอีกด้วย จึงไม่แปลกเลยถ้าหากเราโหลด Library เพื่อเชื่อมต่อกับ Elasticsearch มาสักตัวแล้ว Library ตัวนั้นจะบังคับให้เรา Query ด้วย DSL เพราะฉะนั้นหากเราต้องการเขียนโปรแกรมที่ดึงข้อมูล หรือใช้ความสามารถทั้งหมดของ Elasticsearch การใช้งาน DSL จึงเป็นสิ่งที่หลีกเลี่ยงไม่ได้

ทำความเข้าใจกับ DSL Query

DSL Query นั้นประกอบขึ้นจาก JSON สองประเภทหลักคือ

  1. Leaf query clauses คือเงื่อนไขที่ใช้ระบุว่าเราต้องการหา document โดยมี field ไหนมีค่าเป็นอะไร เช่น ระบุว่า field message ต้องมีค่า เป็น elasticsearch หรือ ระบุว่า field age ต้องมีค่าเป็น 19 เป็นต้น ถ้าเปรียบเทียบกับ Query String แล้วก็จะเหมือนกับเครื่องหมาย Collon : ที่ใช้ระบุ Field และ ค่าที่ต้องการ
  2. Compound query clauses คือเงื่อนไขที่รวมกลุ่มของ Leaf query clauses เข้าด้วยกันเช่น บอกว่า Leaf query clauses นี้ทำ Operation AND หรือ OR กัน เป็นต้น ถ้าอธิบายให้เห็นภาพมากขึ้น Compound query clauses ก็เหมือนกับ วงเล็บ () ใน Query String นั่นเอง

ลองดูตัวอย่างของ Query ก่อนหน้านี้ก็จะแยกประเภทของ JSON ตามสีได้เป็น 2 ชนิดคือ Leaf query clauses สีฟ้า และ Compound query clauses สีเหลือง

โดยจากรูปจะสังเกตได้ว่า JSON ทั้งสองแบบนี้สามารถใช้งานควบคู่กัน เพื่อนำมาสร้างเป็น Query ที่ซับซ้อนมากๆ ตามที่เราต้องการได้ และวิธีการที่เราจะใช้ DSL Query นั้นก็ทำได้โดยส่ง GET หรือ POST Request ไปที่ path /_search โดยมี Body เป็น JSON ที่เราต้องการเพื่อค้นหาข้อมูล

GET,POST /_search
{
"query": {
Leaf query clauses / Compound query clauses
}
}

ซึ่งที่เราเห็นจากรูปนี้เป็นแค่เพียงส่วนเล็กๆ จาก Function ทั้งหมดที่ Elasticsearch มีให้ แต่การจะอธิบายทั้งหมดนั้นก็จะทำให้บทความนี้ยาวจนเกินไป เพราะฉะนั้นบทความนี้จะนำเสนอ Leaf query clauses และ Compound query clauses หลัก ที่ได้ใช้งานบ่อยๆ จากประสบการณ์จริง โดยเริ่มจาก Leaf query clauses ก่อน

Leaf query clauses หลักที่ได้ใช้ประจำ

Term(s) query ใช้เมื่อต้องการตั้งเงื่อนไขว่า field ที่เราสนใจต้องมีค่าเป๊ะๆ เป็นอะไร จึงไม่ควรใช้กับ field ที่มี mapping type เป็น text แต่ควรจะใช้กับ field ที่เป็น keyword หรือใช้กับตัวเลขก็ได้ ซึ่งมีหน้าตา Query แบบนี้

{
"query": {
"term": {
"FIELD_NAME": {
"value": FIELD_VALUE,
"boost": SCORE_BOOSTING
}
}
}
}

จะสังเกตได้ว่านอกจากการระบุ field และ ค่าของ field แล้ว term query ยังให้เรากำหนดได้ด้วยว่าอยากให้ query นี้ส่งผลต่อ search score มากแค่ไหน เพราะในกรณีที่เราต้องการค้นหาจากหลาย field ถ้าเราให้น้ำหนักความสำคัญของแต่ละ field ไม่เท่ากัน เราก็สามารถกำหนดค่าด้วย boost ได้ ยกตัวอย่างเช่น ถ้าต้องการ Query ข้อมูลที่มี field age = 19 โดยให้ Query นี้มีความสำคัญต่อ search score 1.75 เท่า ก็จะทำได้โดยการใช้ Query นี้

{
"query": {
"term": {
"age": {
"value": 19,
"boost": 1.75
}
}
}
}

และหากต้องการหาข้อมูลที่อยู่ใน list ก็สามารถทำได้โดยแค่เปลี่ยนจาก term เป็น terms เช่นนี้

{
"query": {
"terms": {
"FIELD_NAME": {
"value": [FIELD_VALUE_1,FIELD_VALUE_2],
"boost": SCORE_BOOSTING
}
}
}
}

Range query ใช้เมื่อต้องการค้นหาข้อมูลจาก field ที่ต้องการใช้ค่าค้นหาเป็น ช่วง เช่น หาคนในช่วงอายุ 18 ถึง 20 หรือหาเอกสารในช่วงวันที่ 1–30 โดยมี format ดังนี้

{
"query": {
"range" : {
"FIELD_NAME" : {
"gte" : FROM_VALUE,
"lte" : TO_VALUE,
"boost" : SCORE_BOOSTING
}
}
}
}

ซึ่งใน Range query นั้นมี property ที่ใช้ระบุขอบเขตของช่วงดังนี้

gt : มากกว่า
gte : มากกว่าเท่ากับ
lt : น้อยกว่า
lte : น้อยกว่าเท่ากับ

โดยเราจะใส่ทั้ง gt(e) และ lt(e) เพื่อค้นหาในช่วง หรือ แค่อย่างใดอย่างหนึ่งเพื่อค้นหาแบบปลายเปิดก็ได้

Wildcard query ใช้เมื่อต้องการค้นหา field จากค่าที่เป็น pattern เช่น ต้องการ user ที่มีชื่อขึ้นต้นด้วย Jo และลงท้ายด้วย y เป็นต้น โดยจะมีหน้าตา Query ดังนี้

{
"query": {
"wildcard": {
"FIELD_NAME": {
"value": "PATTERN_VALUE",
"boost": SCORE_BOOSTING
}
}
}
}

ในส่วนของ PATTERN_VALUE นั้นคือ String ที่ผสมกับเครื่องหมายพิเศษที่ใส่ได้สองอย่างคือ

* : แทนตัวอักษรอะไรก็ได้ตั้งแต่ 0 ตัวหรือมากกว่า
? : แทนตัวอักษรอะไรก็ได้เพียงตัวเดียว

ถ้าเกิดเรามี User ทั้งหมดคือ Joy, Johny , Joey, Jimmy ยกตัวอย่าง Pattern เช่น
Jo*y อาจจะหมายถึง Joy, Johny, หรือ Joey ก็ได้
Jo?y จะหมายถึง Joey เพียงคนเดียว

Exists query ใช้เมื่อต้องการค้นหาว่าข้อมูลไหนมี field ที่กำหนด โดยรูปแบบ Query เป็นดังนี้

{
"query": {
"exists": {
"field": "FIELD_NAME"
}
}
}

ซึ่งเงื่อนไขที่ Elasticsearch จะมองว่า field นี้ไม่มีอยู่ ได้แก่
1. Field มีค่าเป็น String เปล่า (“”)
2. Field มีค่าเป็น null (null)
3. Field เป็น Array ที่มีค่าใดค่าหนึ่งเป็น null ([null, “foo”])
4. Field นั่นไม่ได้ถูก index ไว้

Match query ใช้เมื่อต้องการทำ Full text search กับ field ที่มี mapping type เป็น text จะต่างจาก term query ตรงที่ match query นั้นสามารถช่วยเราหาข้อมูลที่ใกล้เคียงกับคำค้นได้ด้วย เช่น ต้องการหาจากคำที่สะกดผิด หรือ ต้องการค้นหาข้อความที่เกี่ยวข้องกับคำค้น เป็นต้น มี Format ของ Query ดังนี้

{
"query": {
"match" : {
"FIELD_NAME" : {
"query" : "FULL_TEXT"
}
}
}
}

จะสังเกตได้ว่าใน Query นี้ไม่มี SCORE_BOOSTING ให้ใส่เหมือน Query ก่อนๆ นั่นเพราะคะแนนของแต่ละผลลัพธ์ของการ Search จะคำนวนจากความใกล้เคียงของ FULL_TEXT ที่ใส่เข้ามา นั่นคือยิ่งผลลัพธ์มีความตรงกับ FULL_TEXT เท่าไหร่ Score ก็ยิ่งสูงเท่านั้น

Compound query clauses หลักที่ได้ใช้ประจำ

สำหรับ Compound Query ที่ใช้บ่อยที่สุดมีเพียงอันเดียวก็คือ Bool query ซึ่งก็อย่างที่อธิบายไปแล้วในข้างต้นว่า Query นี้สามารถนำมาจัดกลุ่มของ Leaf query เหมือนเครื่องหมายวงเล็บได้ โดยมีหน้าตา Query แบบนี้

{
"query": {
"bool" : {
"must" : [LEAF_QUERY_CLAUSE, COMPOUND_QUERY_CLAUSE],
"filter": [LEAF_QUERY_CLAUSE, COMPOUND_QUERY_CLAUSE],
"must_not" : [LEAF_QUERY_CLAUSE, COMPOUND_QUERY_CLAUSE],
"should" : [LEAF_QUERY_CLAUSE, COMPOUND_QUERY_CLAUSE],
"minimum_should_match" : MINIMUM_SHOULD_MATCH_VALUE,
"boost" : SCORE_BOOSTING
}
}
}

Bool query นั้นสามารถใส่ LEAF_QUERY_CLAUSE ต่างๆ ที่ได้อธิบายไปก่อนหน้านี้ได้ หรือจะเป็น COMPOUND_QUERY_CLAUSE ซ้อนทับไปอีกชั้นก็ได้เช่นกัน โดย Bool query แบ่งออกได้เป็น 4 ประเภทหลักๆ คือ

1. must หมายถึง query ทั้งหมดใน list ที่ระบุไว้ต้องเป็นจริง ถึงจะนำ Document ที่ค้นหามาแสดง เป็นเหมือนเครื่องหมาย AND ใน Query string

2. filter เหมือนกับ must แต่ search score ที่เกิดจาก query ใน filter นั้นจะไม่ถูกนำมาคำนวนด้วย

3. must_not ตรงกันข้ามกับ must นั่นคือ query ทั้งหมดใน list ที่ระบุไว้ต้องเป็นเท็จ ถึงจะนำ Document ที่ค้นหามาแสดง

4. should หมายถึง query อันใดก็ได้ใน list ที่ระบุไว้เป็นจริงครบตามจำนวนที่ระบุไว้ใน MINIMUM_SHOULD_MATCH_VALUE ก็จะนำ Document ที่ค้นหามาแสดง เป็นเหมือนเครื่องหมาย OR ใน Query string

จะสังเกตได้ว่า Leaf query clauses เพียงอย่างเดียวไม่สามารถสร้าง Query ที่ซับซ้อนได้ แต่เมื่อนำมาใช้คู่กับ Bool query นั้นจะทำให้เรากำหนดเงื่อนไข AND, OR ระหว่างแต่ละ Leaf query clauses ได้ ซึ่งทำให้เราเจาะจงเลือก Document ที่เราต้องการได้แม่นยำขึ้นนั่นเอง

ถึงแม้ Query ที่อธิบายมาทั้งหมดนี้ จะเพียงพอต่อการ Query Elasticsearch ได้จริงแล้ว แต่ทั้งหมดนี้ ยังเป็นเพียงส่วนเล็กๆ ของ Query ทั้งหมดที่ Elasticsearch สามารถทำได้ ยังมี Feature เจ๋งๆ อีกมากที่บทความนี้ไม่ได้มีเนื้อหาครอบคลุม หากผู้อ่านท่านใดสนใจก็สามารถไปอ่านเพิ่มเติมได้ที่

ทั้งนี้ ทางผู้เขียนเชื่อว่า การที่จะเชี่ยวชาญหรือเข้าใจการ Query Elasticsearch อย่างแท้จริงนั้น จะต้องเกิดจากการลองทำจริงเยอะๆ จึงอยากแนะนำให้ผู้อ่านลองสร้าง Query หลายๆ รูปแบบไปทดสอบกับ Data หลายๆ รูปแบบ เพื่อความเข้าใจที่มากขึ้น ขอบคุณที่อ่านจนจบครับ

--

--