Elasticsearch

NARAWICH KITTIJIRAYU
Touch Technologies
Published in
7 min readJul 6, 2021

ทำความรู้จัก Elasticsearch

Elasticsearch เป็น NoSQL database ชนิดหนึ่งที่เขียนขึ้นมาโดยใช้ภาษา Java และสร้างขึ้นมาเพื่อเป็นเครื่องมือสำหรับการสร้าง Search Engine System ได้ง่ายเเละมีประสิทธิภาพสูง

คุณภาพของ Elasticsearch การันตีได้ด้วยการที่ Platform ใหญ่ๆได้นำ Elastic search ไปใช้ อย่างเช่น Stack OverFlow, Docker, Github ซึ่งเป็นเว็บไซต์และ Software ขวัญใจเหล่าชาว Developer, Netflix เว็บไซต์สำหรับการชมซีรี่ส์และภาพยนต์, Pfizer บริษัทผลิตวัคซีน mRNA ต้านไวรัสโควิด-19, NCIS หน่วยงานป้องกันและปราบปรามยาเสพติด, Adobe บริษัทผู้สร้าง Creative software ระดับโลก, เเละยังมี Uber, eBay, Facebook, Sound Cloud, LinkedIn และบริษัทอื่นๆอีกมากมายรวมถึงตัว Medium นี้เองด้วย

โดยทุกระบบ Search Engine ต้องมี Index (ดัชนีค้นหา) Relevancy (ระบบคะแนนผลลัพธ์) และระบบอำนวยความสะดวกให้ผู้ใช้ เป็นองค์ประกอบหลัก โดย Index เป็นเครื่องมือในการค้นหาด้วยการลด Scope ในการค้นหาลง โดย Elasticsearch จะใช้วิธีการ Inverted Index เพื่อช่วยในการค้นหา Relevancy จะเป็นการให้คะแนนว่าสิ่งที่จะปรากฏแสดงออกมาจะเรียงลำดับกันอย่างไรให้ใกล้เคียงกับความต้องการของผู้ใช้มากที่สุด

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

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

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

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

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

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

ซึ่งการที่พนักงานคาดเดาคำที่ลูกค้าพูดเปรียบเสมือน ระบบอำนวยความสะดวกให้ผู้ใช้ โดยถ้าเป็น Search Engine อย่างเช่นในกรณีที่เราค้นหาคำว่า “พิก” ระบบจะแสดงผล “พริก” ขึ้นมาให้แทน

สถาปัตยกรรมของ Elasticsearch

สถาปัตยกรรม Elasticsearch แบ่งเป็นสองมุมมองใหญ่ๆ นั่นก็คือ:

  • Logical-layout: มุมมองสำหรับการค้นหา
  • Physical-layout: มุมมองสำหรับการจัดเก็บข้อมูล

Logical-layout ประกอบด้วย Document, Index และ Type

  • Index: คือดัชนีการค้นหา เป็นด่านแรกสุดที่ search engine จะเลือกค้นหา
  • Type: เป็นการจัดเก็บให้เป็นระบบซึ่ง 1 index จะมี 1 type
  • Document: ส่วนเอกสารที่ย่อยที่สุด

Physical-layout: ประกอบด้วย Cluster, Node, Shard และ Replica

  • Node: เครื่อง server ที่เก็บ Index ไว้
  • Cluster: เป็นกลุ่มของ Node
  • Shard: เป็นการแบ่ง Index ออกมาเป็นส่วนๆเพื่อเพิ่มประสิทธิภาพในการค้นหา
  • Replica: เป็นการ copy ข้อมูลเก็บไว้เป็นข้อมูลสำรอง

ข้อมูลทั้งหมดมาจาก: https://medium.com/machinereading/elasticsearch-%E0%B8%A0%E0%B8%B2%E0%B8%84%E0%B8%A5%E0%B8%B8%E0%B8%A2%E0%B8%AA%E0%B8%99%E0%B8%B2%E0%B8%A1-%E0%B8%95%E0%B8%AD%E0%B8%99%E0%B8%97%E0%B8%B5%E0%B9%88-2-4979e03b8e02

ลองใช้ Elastic search

เนื่องจาก Elasticsearch เป็น database ดังนั้นเราจึงต้องสร้าง server หรือ docker เพื่อเป็นที่อยู่ให้กับ Elasticsearch แต่การมี database เราก็ต้องมีที่อยู่ให้กับมันรวมถึงสร้างที่อยู่ให้กับผู้ควบคุมของมันเช่นเดียวกัน ซึ่งก็คือ Kibana นั่นเอง

เราสามารถใช้ Kibana ในการเข้าไปดูเพื่อสำรวจข้อมูลใน Elasticsearch ได้ รวมถึงการลองใช้การ query ต่างๆด้วยนั่นเอง

ในกรณีตัวอย่าง ผมจะใช้ Docker เป็นตัว Host สำหรับ Elasticsearch และ Kibana

  • โดยขั้นแรก เราจะสร้าง docker-compose file เพื่อสร้าง container ให้กับ Elasticsearch และ Kibana โดยจะตั้งชื่อว่า docker-compose.yml ใน file docker-compose จะประกอบด้วยสอง container คือ Elasticsearch และ Kibana
version: '3' 
services:
elas:
image: docker.elastic.co/elasticsearch/elasticsearch:7.13.1
environment:
- discovery.type=single-node
ulimits:
memlock:
soft: -1
hard: -1
ports:
- 9200:9200
restart: always

kibana:
image: docker.elastic.co/kibana/kibana:7.13.1
env_file:
- ../../Documents/elasticsearch/kibana.env
ports:
- "5601:5601"
environment:
ELASTICSEARCH_URL: http://elas:9200/
ELASTICSEARCH_HOSTS: http://elas:9200/
depends_on:
- elas
  • เมื่อเราใส่ configuration ลงไปจนครบดังรูปเเล้ว ต่อไปเราต้องสร้าง file environment ชื่อ “ kibana.env “สำหรับ Kibana ใน directory เดียวกัน
ELASTICSEARCH_URL=http://elas:9200/ 
XPACK_SECURITY_ENABLED=false
  • ต่อไปเราจะรันโดยการเปิด Terminal เเล้วพิมพ์ command ลงไปตามนี้ ขั้นแรกต้องมั่นใจว่าเราอยู่ใน directory ที่มีไฟล์ของ “ docker-compose ” อยู่เเล้วโดยการพิมพ์ “ ls ” ลงไปใน Terminal เเล้วตามด้วย Enter
  • ต่อไปเมื่อเราเห็น “ docker-compose.yml ” ขึ้นมาเเล้วให้เราสั่ง run โดยการพิมพ์ว่า “ sudo docker-compose up ” ในกรณีที่เราอยากดู error หรือ output ของ docker แต่ถ้าเราไม่ต้องการดูให้พิมพ์คำว่า “ sudo docker-compose up -d ”
  • ต่อไปให้ลองสร้าง Index ไว้สำหรับใส่ data ให้เข้าไปที่ web browser เเล้วพิมพ์คำว่า “ http://localhost:5601/
  • เเล้วหน้าจะเจอหน้า Home Page ของ Kibana ดังรูป
  • ต่อไปให้เข้าไปที่มุมซ้ายบนเเล้วเลื่อนลงไปล่างสุด กดเข้าไปยัง “ Dev tools ”
  • ต่อไปเราจะสร้าง Index เพื่อใช้ในการ search โดยพิมพ์ command “ PUT /ชื่อindex+?pretty ” เเล้วกด Run
  • เมื่อสร้าง Index เสร็จเรียบร้อยเเล้วเราจะเช็คว่า Index ที่เราสร้างนั้นเข้าไปอยู่ใน Elasticsearch เเล้วหรือไม่โดยการพิมพ์ command “ GET /_cat/indices ” เเล้วกด Run เราควรจะเห็นชื่อ Index ของเราอยู่ในรายชื่อ Index
  • เมื่อเราสร้าง Index เสร็จเเล้วเราต้องสร้าง Sample data เพื่อนำมาใช้ในการ search ของเรา เเล้วกด Run
  • ขั้นตอนต่อไปในการ search เราจะเริ่มจาก search ตัวแปลที่เป็น type string โดยเราจะใส่ query เข้าไปตามในรูป ใน “ fields ” ให้ใส่ array ของชื่อ field ที่เราต้องการจะ search ส่วนใน “ query ” ให้ใส่ keyword ของเราเข้าไปด้วย format ดังนี้ “ * + keyword + * ” เเล้วกด Run
  • ถ้าเรากด “ ctrl + f ” เพื่อค้นหน้าในหน้า web browser เราควรจะเจอ keyword ของเราอยู่ใน field ที่เราเลือกใน Result ทางด้านขวามือของเรา
  • ในกรณีที่เราต้องการ search ข้อมูลในหลาย field พร้อมกัน เรายังสามารถใส่ชื่อ field หลาย field เข้าไปได้เช่นกัน
  • ถ้าสังเกตในรูป result ทางด้านฝั่งขวาจะสังเกตได้ว่าเราใส่ keyword เป็น “ b “ เเต่ชื่อของ “ Christina “ นั้นไม่มีตัว “ b “ อยู่เลย แต่มี “ b “ ในชื่อสัตว์เลี้ยง

เมื่อเราทำตามด้านบนได้เรียบร้อยเเล้วต่อไปเราก็สามารถสร้าง Search Engine ขนาดเล็กของเราเพื่อไว้ใช้งานเองได้เเล้ว

การใช้ Elasticsearch ด้วย Golang

เริ่มแรกเราต้องสร้าง file เอาไว้ run ตัว code ของเราก่อน ชื่อว่า “elasticsearch.go”

ต่อไปเราต้องสร้าง struct และ Sample data ที่เราต้องการจะใส่เข้าไปใน Elasticsearch เพื่อใช้ในการ search

  • ตัวอย่าง struct
type user struct{
Name string `json:"name"`
Nickname string `json:"nick_name"`
Lastname string `json:"lastname"`
Email string `json:"email"`
Gender string `json:"gender"`
PetName string `json:"pet_name"`
}
  • ตัวอย่าง Sample data
var userList = []user{
{"Johnnystar","John","Antonio","johnloveantonio@gmail.com","male","franklylovelydog"},
{"Debpra","Mel","oahna","debby@gmail.com","female","putty"}, {"Christina","Chris","Missuniverse","chrissy@gmail.com","female","sibrena"}, {"Jorderna","Jordan","Jackson","jordanhandsome@gmail.com","male","catslayer"}, {"Willington","Willy","wool","willywool@gmail.com","male","sharktank"},
}

ต่อไปเราจะสร้าง Function สำหรับการนำ Sample data ของเราที่สร้างขึ้นมาเข้าไปเก็บไว้ใน Elasticsearch

  • Function Create
func create(es *elasticsearch.Client){
for k ,v := range userList{
out, err := json.Marshal(v)
if err != nil {
panic (err)
}
var b strings.Builder
b.WriteString(string(out))
req := esapi.IndexRequest{
Index: "info",
DocumentID: strconv.Itoa(k + 1),
Body: strings.NewReader(b.String()),
Refresh: "true",
}
res, err := req.Do(context.Background(), es)
if err != nil {
log.Fatalf("Error getting response: %s", err)
}
err = res.Body.Close()
if err != nil {
return
}
}
}

ต่อไปเราจะสร้าง Function เอาไว้สร้าง request query ที่จะนำไปใช้ query หรือ search ข้อมูลออกมา

  • Function BuildSearchRequest
func BuildSearchRequest(keyword string,field string)bytes.Buffer{
var buf bytes.Buffer
query := map[string]interface{}{
"query": map[string]interface{}{
"query_string": map[string]interface{}{
"query" : "*"+keyword+"*",
"fields" : []interface{}{
field,
},
},
},
}
if err := json.NewEncoder(&buf).Encode(query); err != nil {
log.Fatalf("Error encoding query: %s", err)
}
return buf
}

เมื่อสร้างตัว request เสร็จเเล้วเราก็ต้องสร้าง Function ตัว query เพื่อเข้าไป query ข้อมูลออกมาให้เรา

  • Function Query
func query(buf bytes.Buffer, es *elasticsearch.Client) map[string]interface{}{
var r map[string]interface{}
res, err := es.Search(
es.Search.WithContext(context.Background()),
es.Search.WithIndex("info"),
es.Search.WithBody(&buf),
es.Search.WithTrackTotalHits(true),
es.Search.WithPretty(),
)
if err != nil {
log.Fatalf("Error getting response: %s", err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
}
}(res.Body)
if res.IsError() {
var e map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
log.Fatalf("Error parsing the response body: %s", err)
} else {
log.Fatalf("[%s] %s: %s",
res.Status(),
e["error"].(map[string]interface{})["type"],
e["error"].(map[string]interface{})["reason"],
)
}
}
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
log.Fatalf("Error parsing the response body: %s", err)
}
return r
}

เมื่อ query ออกมามันจะอยู่ในรูปแบบของ map[string]interface{} ซึ่งเราต้องแปลงมันให้กลับไปเป็น struct เหมือนที่เราสร้างไว้ตั้งแต่แรกโดยสร้าง Function เอาไว้จับเข้า struct

  • Function InToStruct

เมื่อทุก function ครบเเล้วเราก็ต้อง init ตัว Elasticsearch ของเรา โดยการสร้าง Function main

  • Function main
func main(){
es, err := elasticsearch.NewDefaultClient()
if err != nil {
log.Fatalf("Error creating the client: %s", err)
}
create(es)
result := search("j",es,"name")
for _,i := range result{
fmt.Println(i)
}
fmt.Println("finish")
}

เมื่อเสร็จทุกอย่างหมดเเล้ว code จะออกมาอยู่ในรูปแบบนี้

package mainimport (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
"io"
"log"
"strconv"
"strings"
)
type user struct{
Name string `json:"name"`
Nickname string `json:"nick_name"`
Lastname string `json:"lastname"`
Email string `json:"email"`
Gender string `json:"gender"`
PetName string `json:"pet_name"`
}
var userList = []user{
{"Johnnystar","John","Antonio","johnloveantonio@gmail.com","male","franklylovelydog"},
{"Debpra","Mel","oahna","debby@gmail.com","female","putty"},
{"Christina","Chris","Missuniverse","chrissy@gmail.com","female","sibrena"},
{"Jorderna","Jordan","Jackson","jordanhandsome@gmail.com","male","catslayer"},
{"Willington","Willy","wool","willywool@gmail.com","male","sharktank"},
}
func search(keyword string,es *elasticsearch.Client,field string)[]user{
return InToStruct(query(BuildSearchRequest(keyword,field),es))
}
func create(es *elasticsearch.Client){
for k ,v := range userList{
out, err := json.Marshal(v)
if err != nil {
panic (err)
}
var b strings.Builder
b.WriteString(string(out))
req := esapi.IndexRequest{
Index: "info",
DocumentID: strconv.Itoa(k + 1),
Body: strings.NewReader(b.String()),
Refresh: "true",
}
res, err := req.Do(context.Background(), es)
if err != nil {
log.Fatalf("Error getting response: %s", err)
}
err = res.Body.Close()
if err != nil {
return
}
}
}
func BuildSearchRequest(keyword string,field string)bytes.Buffer{
var buf bytes.Buffer
query := map[string]interface{}{
"query": map[string]interface{}{
"query_string": map[string]interface{}{
"query" : "*"+keyword+"*",
"fields" : []interface{}{
field,
},
},
},
}
if err := json.NewEncoder(&buf).Encode(query); err != nil {
log.Fatalf("Error encoding query: %s", err)
}
return buf
}
func query(buf bytes.Buffer, es *elasticsearch.Client) map[string]interface{}{
var r map[string]interface{}
res, err := es.Search(
es.Search.WithContext(context.Background()),
es.Search.WithIndex("info"),
es.Search.WithBody(&buf),
es.Search.WithTrackTotalHits(true),
es.Search.WithPretty(),
)
if err != nil {
log.Fatalf("Error getting response: %s", err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
}
}(res.Body)
if res.IsError() {
var e map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
log.Fatalf("Error parsing the response body: %s", err)
} else {
log.Fatalf("[%s] %s: %s",
res.Status(),
e["error"].(map[string]interface{})["type"],
e["error"].(map[string]interface{})["reason"],
)
}
}
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
log.Fatalf("Error parsing the response body: %s", err)
}
return r
}
func InToStruct(r map[string]interface{}) []user{
var temp user
var result []user
for _, hit := range r["hits"].(map[string]interface{})["hits"].([]interface{}) {
s := hit.(map[string]interface{})["_source"]
temp.Name = fmt.Sprintf("%v", s.(map[string]interface{})["name"])
temp.Nickname = fmt.Sprintf("%v", s.(map[string]interface{})["nick_name"])
temp.Lastname = fmt.Sprintf("%v", s.(map[string]interface{})["lastname"])
temp.Email = fmt.Sprintf("%v", s.(map[string]interface{})["email"])
temp.Gender = fmt.Sprintf("%v", s.(map[string]interface{})["gender"])
temp.PetName = fmt.Sprintf("%v", s.(map[string]interface{})["pet_name"])
result = append(result, temp)
}
return result
}
func main(){
es, err := elasticsearch.NewDefaultClient()
if err != nil {
log.Fatalf("Error creating the client: %s", err)
}
create(es)
result := search("j",es,"name")
for _,i := range result{
fmt.Println(i)
}
fmt.Println("finish")
}

เมื่อสร้าง Code file หรือ Gofile เสร็จเเล้วก็สามารถ run command “ sudo docker-compose up -d ” ใน Terminal เเล้วเปิดอีก Terminal เพื่อ run command “ go run elasticsearch.go “ ถ้าไม่มีอะไรผิดพลาดผลลัพธ์จะออกมาเป็นดังนี้

Touch Technologies

“ เราไม่ได้ถูกต้องที่สุด แต่เราแสดงสิ่งที่เราทำ ”

--

--