มาทำ website search service ด้วย Logstash และ Elasticsearch กันเถอะ
Published: 2023-07-08 09:10:57

สวัสดีครับทุกคน จริงๆที่มาของบทความนี้คือเนื่องจากได้รับมอบหมายให้ทำ search service ให้กับเว็บไซค์นึงโดยต้องใช้ 
Logstash และ Elasticsearch ในการทำงาน ไหนๆก็ได้ POC ละ ก็ลองเขียนบทความมันซะเลยจะได้เป็นการทบทวนตัวเอง หรือมีใครที่กำลังศึกษาเพื่อพอที่จะได้เป็นแนวทางให้ได้บ้าง

## Logstash และ Elasticsearch คืออะไร
เรามาเริ่มต้นด้วยการทำความรู้จักกับเจ้าเครื่องมือสองตัวที่เรากำลังจะนำมาใช้งานกันก่อนดีกว่าครับ เจ้า Logstash และ Elasticsearch เนื่ยเป็น 2 ใน 3 เครื่องมือที่อยู่ใน ELK Stack ซึ่งเป็น Application open source ที่ถูกจัดทำโดยทีมงาน Elastic (อีกตัวคือ Kibana แต่เรายังไม่ได้ใช้ ฮ่าๆ) มีจุดประสงค์ในการทำ data management ต่างๆเช่น การดึงข้อมูลจากแหล่งนึงมาวิเคาะร์และทำการส่งต่อไปให้แหล่งต่างๆต่อไป โดยการทำงานของ ELK Stack ก็ประมาณนี้

![ELK stack work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsa2hpu6psl94v22x00p.png)
<figcaption>ภาพที่.1 - How ELK work. (ที่มา https://www.loggly.com/use-cases/what-is-the-elk-stack/) </figcaption></br>

โดยในบทความนี้เราจะใช้ Logstash ในการ query ข้อมูลจาก Database และทำการส่งต่อไปให้ ElasticSearch ในการทำ indexing

## Project structure
งั้นก่อนเริ่มลงมือเรามาดูการทำงานของ search service ที่เรากำลังจะ implement กันดีกว่า สมมุติว่าเรามีเว็บไซค์อยู่เว็บไซค์นึงและมี content ต่างๆในเว็บมากมายแล้วที่นี้เราต้องการให้ user ของเรานั้นสามารถ search content ต่างๆภายในเว็บไซค์ของเราได้เราจึงทำการ implement API ขึ้นมาเพื่อรองรับการทำงานในการค้นหา content ต่างๆแบบนี้

![ภาพที่ 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7nl1d6rlxxxpqnbl861.jpg)

จากรูปจะเห็นได้ว่ามีการทำงานที่ไม่ซับซ้อนเลย โดยเริ่มจากเราจะใช้ logstash ดึงข้อมูลจาก database และทำการส่งต่อไปให้ Elasticsearch ในการทำ indexing จากนั้นเราจะทำการสร้าง API ขึ้นมาหนึ่งตัวเพื่อเป็นการดึงข้อมูลจาก Elasticsearch อีกที

## How to setup
จริงๆแล้วในการ setup ELK stack เราจะ download จาก [official](https://www.elastic.co/) มาติดตั้งภายในเครื่องก็ได้ แต่ในบทความนี้เราจะใช้ docker เป็นหลัก โดยเราจะกำหนด Docker compose ตามนี้

```
version: '3'
services:
  postgres:
    image: postgres:15-alpine
    container_name: postgresql
    environment:
      - POSTGRES_PASSWORD=secret
      - POSTGRES_USER=admin
      - POSTGRES_DB=web_content
    ports:
      - 5432:5432
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - search-network
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1
    container_name: elasticsearch
    ports:
      - 9200:9200
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    networks:
      - search-network
  logstash:
    image: docker.elastic.co/logstash/logstash:8.7.1
    container_name: logstash
    environment:
      discovery.seed_hosts: logstash
    volumes:
      - ./logstash-config/postgresql-42.6.0.jar:/usr/share/logstash/postgresql-42.6.0.jar
      - ./logstash-config/pipelines.yml:/usr/share/logstash/config/pipelines.yml
      - ./logstash-config/web-search.conf:/usr/share/logstash/config/web-search.conf
      - ./logstash-config/template.json:/usr/share/logstash/config/template.json
    depends_on:
      - elasticsearch
      - postgres
    networks:
      - search-network
    links:
      - elasticsearch
      - postgres
volumes:
  elasticsearch_data:
  postgres_data:
networks:
  search-network:
```
ในที่นี้เราจะใช้ database เป็น postgresql การตั้งค่าต่างๆก็ไม่มีอะไรพิเศษใช้ตามปกติได้เลย ส่วน Elasticsearch เราจะตั้ง xpack.security เป็น false ไว้เพื่อให้ง่ายต่อการ develop

มาถึงส่วนที่สำคัญและมีขั้นตอนเยอะกว่าส่วนอื่นๆ นั้นก็คือการ setup logstash อย่างแรกเลยเรามาดูการทำงานของ Logstash กันซะหน่อย ตัว Logstash จริงๆแล้วจะเรียนกว่าเป็น data processing pipeline ก็ได้ ซึ่งสิ่งที่ Logstash สามารถทำได้ก็เช่น  ingest data จาก source ต่างๆเช่น database, log file เป็นต้น tranform data จากนั้นก็ส่ง data ไปยังที่ที่ต้องการ ซึ่งการทำงานของ Logstash จะประกอบด้วย 3 ส่วนหลักๆดังนี้

![ภาพที่ 3 ที่มา [Logstash](https://www.bmc.com/blogs/logstash-using-data-pipeline/)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uafazmg6hxqdolv7qcjj.png)
<figcaption>ภาพที่.2 - How Logstash work. (ที่มา https://www.bmc.com/blogs/logstash-using-data-pipeline/) </figcaption></br>

1. Input หรือ data ขาเข้าต่างๆ คือแหล่ง data ที่เราต้องการส่งมา process ที่ Logstash (ในที่นี้ input data ของเราจะมาจาก postgresql)
2. Filter ส่วนนี้จะเป็นการทำ tranformation data ให้อยู่ในรูปแบบที่เราต้องการ
3. Output เป็นที่ที่เราต้องการนำ data ไปจัดเก็บหรือส่ง data ออกไป (ในที่นี้ของเราคือ Elasticsearch)

เราพอจะเข้าใจการทำงานคร่าวๆของ logstash แล้วงั้นมาลงมือกันเลยดีกว่าอย่างแรกทำการสร้างไฟล์ .yml ซึ่งเป็นที่ configure global settings ต่างๆของ logstash

```
1 - pipeline.id: web-search
2   path.config: "/usr/share/logstash/config/web-search.conf"
3   config.support_escapes: true
```
จาก code ด้านบน
บรรทัดที่ 1 เรามีการ set id ให้กับ pipeline ตัวนี้ แน่นอนว่าเราสามารถมี pipeline ได้มากกว่าหนึ่งตัว
บรรทัดที่ 2 คือ path config ตัวนี้ใช้สำหรับบอก Logsatsh ว่าจะไปเอาตัว config ของเราได้ที่ไหน (เดี๋ยวจะให้ดูต่อไปว่า config file คืออะไร) 
บรรทัดต่อมาไม่มีอะไรแค่บอก logstash ให้รองรับตัว escape sequences ต่างๆ

หลังจากที่เราทำการสร้าง configure ของ Logstash แล้ว ทีนี้เรามากำหนด data processing pipelines ของ Logstash กันบ้างซึ่งจะอยู่ในรูปของ .conf file ซึ่งจะมีหน้าตาแบบนี้

```
1 input{
2     jdbc{
3         jdbc_connection_string => 
4   "jdbc:postgresql://postgresql:5432/web_content"
5         jdbc_user => "admin"
6         jdbc_password => "secret"
7         jdbc_driver_library => 
8   "/usr/share/logstash/postgresql-42.6.0.jar"
9         jdbc_driver_class => "org.postgresql.Driver"
10        statement => "select title, description, keyword, 
11  'localhost:3000' || url_path as url from \"content\""
12     }
13  }
14
15  filter {}
16
17  output {
18      elasticsearch{
19          hosts => ["elasticsearch:9200"]
20          index => "content_search"
21          template => 
22   "/usr/share/logstash/config/template.json"
23      }
24  }
```
เราจะเห็นได้ว่า file ของเราอันนี้ ก็คือการทำงานของ Logstash ที่อ้างอิงจากภาพที่ 2 นั้นเอง 
Input ของเรานั้นเราจะดึงข้อมูลมาจาก postgresql ส่วน output ของเรานั้นจะส่งข้อมูลที่ได้ไปยัง Elasticsearch นั้นเอง (filter ไม่จำเป็นต้องมีก็ได้ในกรณีนี้) 
งั้นเรามาดูกันที่ละตัว 

## Logstash input 
เราจะเห็นคำว่า JDBC (Java Database Connectivity.)อยู่ สิ่งนั้นก็คือ plugin นั้นเอง logstash Allow ให้เรานำเข้า data จากแหล่งใดๆก็ได้ ตราบเท่าที่มี plugin support ในที่นี้เราจะใช้เจ้าตัว JDBC จาก code
บรรทัดที่ 3-6 จะเป็น database connection ของเรา
บรรทัดที่ 7 เป็นการกำหนดที่อยู่ driver ของ JDBC สำหรับ postgresql สามารถ download ได้ที่นี่ [JDBC DRIVER](https://jdbc.postgresql.org/ ) 
บรรทัดที่ 10 จะเป็น query statment ของเราในที่นี้จาก query จะเห็นว่ามีการเลือก title, description, และ keywords ออกมาเพราะต้องการใช้ค่าจาก field ต่างๆพวกนี้ในการทำ indexing นั้นเอง

## Logstash output 
ในที่นี้เราจะใช้ elasticsearch plugin สำหรับส่งข้อมูลออกไปที่ Elasticsearch เรามาดู option ต่างๆของ plugin ตัวนี้กัน
	- hosts : กำหนด Elasticsearch cluster's host(s) ว่าจะให้ไป connect ทีไหน
	- index : ชื่อของ index 
	- template : กำหนด template ที่ใช้สำหรับการสร้าง index

template คืออะไร index template ก็คือ config file ตัวนึงที่เราใช้บอก Elasticsearch ว่าเราอยากสร้าง index อย่างไร

```
{
  "index_patterns": ["content-search*"]
  "template": {
    "settings": {
      "index": {
        "analysis": {
          "analyzer": {
            "thai_analyzer": {
              "tokenizer": "thai",
              "filter": ["lowercase"]
            }
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "thai_analyzer"
        },
        "description": {
          "type": "text",
          "analyzer": "thai_analyzer"
        },
        "keyword": {
          "type": "text",
          "analyzer": "thai_analyzer"
        }
      }
    }
  }
}
```
index template ของเรามีหน้าตาประมาณนี้ และทำไมต้องใช้ ในกรณีนี้เราอยากจะสร้าง index ด้วยการบอก Elasticsearch อยากให้มีการตัดคำแบบภาษาไทยได้ 
โดยปกติแล้ว Elasticsearch จะมีการ process ข้อความด้วยสิ่งที่เรียกว่า Analyzer เช่นการตัดคำ, เปลี่ยนเป็น lowercase, ตัด stop word 
ซึ่ง default แล้ว ค่าเริ่มต้นจะไม่รองรับภาษาไทยทำให้เวลาเราต้องการ search คำด้วยภาษาไทยต้องมีการกำหนดค่า Analyzer ภาษาไทยเข้าไปด้วย
สมมุติว่าเรามีข้อความว่า “สวัสดีครับนี่คือการทดสอบ” ถ้าเรา search ด้วยแบบนี้ ก็จะไม่ได้ result

`http://localhost:9200/my_index/_search?q=สวัสดีครับ`

ถ้าอยากได้ result เราจำเป็นที่จะต้อง search ด้วยข้อความเต็มทั้งหมดแบบนี้

`http://localhost:9200/my_index/_search?q=สวัสดีครับนี่คือการทดสอบ`

จาก code จะเห็นว่ามีการกำหนด custom analyzer โดยใช้ชื่อว่า thai_analyzer โดยเราได้กำหนดว่า Analyzer นี้จะ process โดยการปรับข้อความเป็น lowercase และตัดคำด้วย Tokenizer ภาษาไทยของ Elasticsearch และเรามีการกำหนดว่า field, title, description, keyword ให้มีการใช้ Analyzer แบบภาษาไทย โดยใช้ตัว custom analyzer ของเรานั้นเอง `"analyzer": "thai_analyzer"`

จาก setup ทั้งหมดน่าจะเพียงพอแล้วสำหรับการ run Logstash และ Elasticsearch ถ้าพร้อมแล้วก็ docker-compse up โลดดด ถ้าไม่มี Error message ใดๆปรากฎก็แสดงว่าทำงานถูกต้อง 
แล้วเราจะรู้ได้ยังไงละว่า data ที่เราอุตส่าห์ดึงมาเข้าไปอยู่ใน Elasticsearch แล้ว วิธีที่เราสามารถเช็คได้ง่ายๆก็คือเช็คผ่านตัว Elasticsearch API นั้นเอง ตัว Elasticsearch provide API ให้เราสามารถเข้าไปเรียกได้เลย ส่วนใครอยากรู้รายละเอียดต่างๆว่ามีอะไรให้ใช้มั้ง สามารถเข้าไปดูได้ที่นี่เลย [Elasticsearch API](https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html )
โอเคก่อนอื่นงั้นเรามาลองดูดีกว่า ว่า Elasticsearch ของเรานั้นมันทำงานได้แล้วจริงๆโดยการส่ง Get request หรือเปิด Browser ไปที่ `http://localhost:9200/`
หากไม่มีข้อผิดผลาดใดๆ เราก็จะได้รับข้อความหน้าตาประมาณนี้

```
{
  "name" : "50bc0eda2e16",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "fBx14wctQeCEWchxU9oeKQ",
  "version" : {
    "number" : "8.7.1",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "f229ed3f893a515d590d0f39b05f68913e2d9b53",
    "build_date" : "2023-04-27T04:33:42.127815583Z",
    "build_snapshot" : false,
    "lucene_version" : "9.5.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}
```
ต่อมาเพื่อเป็นการตรวจสอบว่า index ของเราสามารใช้งานได้จริงๆแล้วเราสามารถดูได้จาก 

`http://localhost:9200/_cat/indices`

เราจะได้ list index ของเราออกมา ซึ่งถ้ามีข้อความประมาณนี้ปรากฎเป็นใช้ได้ (สังเกตชื่อ index ของเราด้วยว่าตรงกับที่เราตั้งไว้ไหม ในที่นี้เราใช้ชื่อว่า content_search)

`yellow open content_search V3ewHt6XS-uhSLouGrl2eA 1 1 1 0 8.3kb 8.3kb`

งั้นเรามาทดสอบอีกซักหน่อยดีกว่าว่า Data ที่เราได้มาจาก Logstash input นั้นมันถูกส่งมาที่ Elasticsearch จริงๆหรือเปล่าโดยมาที่นี่

`http://localhost:9200/{index_name}/_search.`

(Replace index_name ด้วย index name ของเรา) 

```
{
	"took":12,"timed_out":false,"_shards":{
		"total":1,
		"successful":1,
		"skipped":0,"failed":0
	},
	"hits":{
		"total":{
			"value":1,
			"relation":"eq"
	},
	"max_score":1.0,
	"hits":[{
		"_index":"content_search",
		"_id":"luFcC4kBpCWRwcQmXoAD",
		"_score":1.0,
		"_source":{
			"title":"สวัสดีครับ",
			"url":"localhost:3000/test",
			"@version":"1",
			"keyword":["ทดสอบ","เทส"],
			"@timestamp":"2023-06-30T08:12:29.863354418Z",
			"description":"ทดสอบเทสทดสอบเทส"
	}
     }
   ]
 }
}
```
ซึ่งเราจะได้รับของหน้าตาตามรูปสังเกตว่าภายใต้ hits จะมี field ที่เป็น Data ที่เราต้องการ (title, url, keyword, description) ซึ่งจะสัมพันธ์กับ query ใน statement ที่เราได้เพิ่มไปใน Logstash input 

เท่านี้เป็นเรียบร้อยสำหรับ setup และ ใช้งาน logstash คู่กับ Elasticsearch สำหรับทำการดึงข้อมูลจาก Postgresql มาทำ indexing บน Elasticsearch 

## API Service
หลังจากที่เรามีข้อมูลที่เราต้องการบน ElasticSearch แล้วต่อมาเราจะทำ API ขึ้นมาหนึ่งเส้น สำหรับให้หน้าบ้านเรียกใช้ search content ซึ่งเราจะ response ออกไปตามที่หน้าบ้านต้องการ โดยในการทำ API ครั้งนี้เราจะใช้ Golang 

สำหรับการทำงานร่วมกับ Elasticsearch จริงๆแล้วการใช้งานตัว Elasticsearch กับ Golang นั้นเราสามารถใช้งานผ่าน HTTP requests ใน go ได้เลย แต่เพื่อความง่ายและสะดวกเราจะใช้งานผ่านตัว client library ที่ชื่อว่า go-elasticsearch [library](https://github.com/elastic/go-elasticsearch )

สิ่งแรกที่เราต้องทำก็คือ connect ตัว Elasticsearch กันก่อนเลย
```
1 func NewElasticClient(es_end_point string) (*ElasticClient, 2 error) {
3	cfg := elasticsearch.Config{
4		Addresses: []string{
5			es_end_point,
6		},
7	}
8
9	//Connect to Elasticsearch
10	es, err := elasticsearch.NewClient(cfg)
11	if err != nil {
12		log.Fatalf("Error creating the client: %s", 
13  err)
14		return nil, err
15	}
16
17	res, err := es.Info()
18	if err != nil {
19		log.Fatalf("Error getting response: %s", err)
20		return nil, err
21	}
22
23	defer res.Body.Close()
24	log.Println(res)
25
26	return &ElasticClient{es}, nil
27 }
```
จาก code จะเห็นว่ามีการสร้าง function ขึ้นมาตัวนึงที่ใช้สำหรับการ connect ตัว Elasticsearch ชื่อว่า elasticConnect() ซึ่งจะคืนของเป็น Elasticsearch client object.
 จากรูปที่ code 
บรรทัดที่ 3 - 7 นั้นจะเป็นการกำหนด configure สำหรับการสร้าง  Elasticsearch client. configure ในที่นี้ของเราก็จะมีแค่ตัว Addresses สำหรับกำหนดตัว Elasticsearch endpoint(s). เท่านั้น
บรรทัดที่ 10 จะเป็นการสร้าง instant ของ Elasticsearch client ขึ้นมาโดย base on มาจากตัว config ที่เราได้สร้างไว้ในบรรทัดที่ 27 - 30
บรรทัดที่ 17 นั้นเราจะเรียก function Info() สำหรับการเรียกดูข้อมูลต่างๆใน Cluster ของตัว Elasticsearch เช่น เวอร์ชั่น,ชื่อ, และ พวกรายละเอียดๆต่างๆ.

หลังจากที่เรา connect ตัว Elasticsearch ไปแล้วเราจะมาทำการสร้าง function สำหรับการ search content ของเว็บไซค์ของเราโดยทั้งหมดจะทำงานผ่านตัว esapi ซึ่งเป็นอีก package นึงของตัว go-elasticsearch แต่ก่อนที่เราจะทำตัว search นั้นเรามาเตรียม query กันก่อนดีกว่า query คืออะไร จริงๆแล้วจะบอกว่าตัว query นี้เป็น search condition ก็ได้นะ เรามาดูหน้าตากันดีกว่า

```
1 type SearchRequest struct {
2	KeyWord string `form:"keyword" binding:"required"`
3 }
4
5 func setQuery(req SearchRequest) ([]byte, error) {
6
7	query := map[string]interface{}{
8		"query": map[string]interface{}{
9			"multi_match": map[string]interface{}{
10				"query":  req.KeyWord,
11				"fields": []string{"keyword", 12 "title", "description"},
13			},
14		},
15	}
16
17	jsonQuery, err := json.Marshal(query)
18	if err != nil {
19		log.Printf("Can not marshal query: %s", err)
20 		return []byte{}, err
21	}
22
23	return jsonQuery, nil
24 }
```
จาก code จะเห็นว่ามีการสร้าง function มาหนึ่งตัวชื่อว่า setQuery มีหน้าที่ในการเตรียม query สำหรับใช้ในการ search ที่ Elasticsearch นั้นเอง
โดยเจ้า function ตัวนี้จะรับ parameter หนึ่งตัวเป็น keyword ที่เราใช้ในการค้นหา และคืนของเป็น query statement ในรูปของ json
mult_match    : เป็นตัวกำหนดชนิดของการ search ที่เราใส่เข้ามานั้นจะให้ match อย่างไร (multi_match ในที่นี้หมายถึงให้ match คำค้นหามากกว่า 1 field นั้นเอง)
query              : เป็นตัวกำหนดคำค้นหานั้นเอง (keyword หรือคำ ที่เราต้องการจะใช้สำหรับการ search content)
Field                : คือตัวกำหนดว่าจะให้ keyword ของเรา search จาก filed ไหนบ้าง (โดย Elasticsearch จะ match keyword ของเราเข้ากับ field ที่เรากำหนด) 

หลังจากที่เราเตรียม query เรียบร้อยก็ถึงเวลาของการทำ function search  กันแล้วโดยเราจะทำการสร้างฟังค์ชั่นมาอีกหนึ่งตัว

```
1 func (ec *ElasticClient) GetContentByKeyword(searchIndex 
2 string, jsonQuery []byte) (*esapi.Response, error) {
3	// Set up the search request
4	result := esapi.SearchRequest{
5		Index: []string{searchIndex},
6		Body:  bytes.NewReader(jsonQuery),
7	}
8
9	res, err := result.Do(context.Background(), ec)
10	if err != nil {
11		log.Printf("Error executing search request: 
12 %s", err)
13		return nil, err
14	}
15
16	if res.IsError() {
17		log.Printf("Search request failed: %s", 
18 res.String())
19		return nil, err
20	}
21
22	return res, nil
23 }
```
จากรูป ฟังค์ชั่น GetContentByKeyword ของเราจะรับของสองตัว และคืนออกไปเป็น *esapi.Response
	- indexName คือชื่อ index ที่เราจะใช้สำหรับค้นหา (อันเดียวกับที่อยู่ใน logstash pipeline config file ส่วนของ output)
	- jsonQuery คือ search condition ที่เราจะใช้สำหรับการค้นหา (ได้มาจาก setQuery ฟังค์ชั่น)

บรรทัดที่ 4 - 7 จะเป็นการ setting search request สำหรับการค้นหา โดยจะรับของสำคัญสองตัว คือ index name และ ตัว query condition
บรรทัดที่ 9 เราจะเรียกใช้ method Do สำหรับการ executed search request ถ้าไม่มี error ใดๆ เกิดขึ้นเราจะได้ result หน้าตาประมาณนี้

```
{
	"took":12,"timed_out":false,"_shards":{
		"total":1,
		"successful":1,
		"skipped":0,"failed":0
	},
	"hits":{
		"total":{
			"value":1,
			"relation":"eq"
	},
	"max_score":1.0,
	"hits":[{
		"_index":"content_search",
		"_id":"luFcC4kBpCWRwcQmXoAD",
		"_score":1.0,
		"_source":{
			"title":"สวัสดีครับ",
			"url":"localhost:3000/test",
			"@version":"1",
			"keyword":["ทดสอบ","เทส"],
			"@timestamp":"2023-06-30T08:12:29.863354418Z",
			"description":"ทดสอบเทสทดสอบเทส"
	}
     }
   ]
 }
}
```
ซึ่งก็เป็น response หน้าตาแบบเดียวกับเวลาเราเรียกไปที่ Elasticsearch url เลย สำหรับใครที่อยากรู้รายละเอียดต่างๆของ response สามารถไปตามอ่านกันได้ที่นี่เลย [Elasticsearch API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-api-response-body)

แต่ๆ เราคงไม่ได้อยากคืนของทั้งหมดนี่ให้กับหน้าบ้าน เพราะจริงๆแล้วหน้าบ้านแค่อยากจะได้ title, description และ keyword เท่านั้นเอง

```
type SearchResponse struct {
	Url         string `json:"url"`
	Title       string `json:"title"`
	Description string `json:"description"`
}
```
หน้าบ้านอยากได้ของที่มีหน้าตาแบบนี้ไว้สำหรับแสดงให้ user งั้นเราจะมาเพิ่มอีกหนึ่งฟังค์ชั่น
สำหรับการ convert ตัว espapi.response ไปเป็น SearchResponse

```
func convertResponse(res *esapi.Response) ([]SearchResponse, error) {
	searchResults := make([]SearchResponse, 0)

	var esapiResponse struct {
		Hits struct {
			Hits []struct {
				Source SearchResponse `json:"_source"`
			} `json:"hits"`
		} `json:"hits"`
	}
	if err := json.NewDecoder(res.Body).Decode(&esapiResponse); err != nil {
		log.Fatalf("Error Decoder response: %s", err)
	}

	for _, hit := range esapiResponse.Hits.Hits {
		result := SearchResponse{
			Url:         hit.Source.Url,
			Title:       hit.Source.Title,
			Description: hit.Source.Description,
		}

		searchResults = append(searchResults, result)
	}

	return searchResults, nil
}
```
โดยจะมีหน้าตาแบบนี้ จริงๆแล้วฟังค์ชั่นนี้ก็ไม่มีอะไรมากแค่รับตัว response ที่มี type เป็น esapi.response 
แล้วทำการ loop เอา data ที่อยู่ใน Hits มา convert ให้เป็น SearchResponse แบบที่เราต้องการแล้วคืนของออกไป
ก็น่าจะประมาณนี้สำหรับ การทำ search service api ด้วย Logstash และ Elasticsearch ของเรา

สุดท้ายนี้ก็อยากจะขอขอบคุณทุกคนมากๆที่อ่านมาถึงตรงนี้ ถ้าใครพบเห็นขอผิดพลาดใดๆ มีวิธีที่ดีกว่าหรือข้อแนะนำติชมใดๆ
ก็อยากรบกวนให้มาแชร์กันใน comment ได้เลย ยินดีน้อมรับทุกความคิดเห็นครับ 
สำหรับ code แบบ full version สามารถเข้าไปดูได้ที่ [Github](https://github.com/Je-Devlop/web_search)

resources;
[1](https://www.elastic.co/?ultron=B-Stack-Trials-APJ-PHS&gambit=Stack-Core&blade=adwords-s&hulk=paid&Device=c&thor=elastic&gclid=Cj0KCQjwkqSlBhDaARIsAFJANkgleWZ02MukvIDxA9CTSlsEmyI7F0ZtI1-mnRK9bPeGzrrx-SvWSeUaAoKBEALw_wcB) [2](https://medium.com/insightera/%E0%B9%80%E0%B8%A3%E0%B8%B4%E0%B9%88%E0%B8%A1%E0%B8%95%E0%B9%89%E0%B8%99%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%88%E0%B8%B1%E0%B8%94%E0%B9%80%E0%B8%81%E0%B9%87%E0%B8%9A%E0%B8%82%E0%B9%89%E0%B8%AD%E0%B8%A1%E0%B8%B9%E0%B8%A5%E0%B8%82%E0%B8%99%E0%B8%B2%E0%B8%94%E0%B9%83%E0%B8%AB%E0%B8%8D%E0%B9%88%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-elasticsearch-4d70dbe6a79d) [3](https://www.elastic.co/logstash/)