Updated:

4 minute read

개요

  • 분산형 분석 및 검색 엔진
  • 다양한 유형의 데이터를 지원
  • 전문 검색 지원
    • 텍스트 역색인(inverted index) 사용
    • 숫자, geo는 BKD trees 사용
  • 모든 항목이 색인되므로 빠른 엑세스 가능
  • 확장성, 정확도, 복원력에서 강점을 가짐
  • 저장과 검색에 강점을 가짐
    • 저장
      • Analysis, Tokenizer, Stemming, …
    • 검색
      • Full text queries, relevancy, score, Term Frequency, …
    • 예시
      • 쇼핑몰에서 특정 키워드(무선 이어폰)로 검색 시 해당 키워드만 있거나 정확히 포함된 제품뿐만 아니라 관련된 상품들(무선 xxx 이어폰, 이어폰 xxx 무선, 무선, 이어폰)이 다 나오고 정확도 순이라던가의 정렬을 제공
  • 라이센스
    • Elastic License 2.0과 Apache License 2.0이 혼재
    • 라이센스는 헤더에 명시
    • x-pack 폴더는 Elastic License 2.0만 부여
  • 멀티테넌시(multitenancy)
    • 하나의 서비스를 여러 사용자에게 제공하는 아키텍쳐
    • Elasticsearch에서는 둘 이상의 인덱스를 하나의 쿼리로 검색 및 출력
  • 자바로 구현된 루씬(Lucene)(정보 검색 라이브러리)을 이용하여 개발
  • 쿼리문이나 쿼리에 대한 결과도 모두 JSON 형식으로 전달되고 리턴


용어

  • cluster
    • 연결된 node의 모음
  • node
  • index
    • 하나 이상의 물리적 샤드를 가리키는 논리적 네임스페이스
    • settings
      • curl -XGET "http://elasticsearch:9200/index_1"
      • number_of_shards, number_of_replicas, …
    • mappings
      • curl -XGET "http://elasticsearch:9200/index_1"
      • 데이터 유형 및 색인 방법을 정의
      • Dynamic mapping
        • document 추가 시 자동으로 mapping 생성
      • Explicit mapping
        • 데이터 유형 및 색인 방법을 명시적으로 정의
  • shard
    • 인덱스에 있는 모든 데이터의 조각
    • 데이터의 컨테이너
    • primary shard, replica shard로 나뉨
    • replica shard는 primary shard의 복제본이며 서로 다른 노드에 저장
      • 데이터의 가용성과 무결성을 보장
  • document
    • 단일 테이터 단위
    • document는 shard에 저장되고 shard는 node에 저장


Query DSL(Domain Specific Language)


CRUD

  • REST API를 이용
  • https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html
  • 단일 document 접근 url 구조
    • http://://_doc/<_id>
  • PUT
    • _doc
      • curl -XPUT "http://elasticsearch:9200/index_1/_doc/1" -H 'Content-Type: application/json' -d'{ "field_1": "value_1", "field_2": "value_2"}'
      • 동일한 url에 다른 내용을 입력하면 update가 되는데 이를 방지하기 위해 _doc 대신 _create 이용
    • _create
      • curl -XPUT "http://elasticsearch:9200/index_1/_create/1" -H 'Content-Type: application/json' -d'{ "field_1": "value_1", "field_2": "value_2"}'
  • GET
    • _doc
      • curl -XGET "http://elasticsearch:9200/index_1/_doc/1"
    • _search
      • value 검색
        • curl -XGET "http://elasticsearch:9200/index_1/_search?q=value_2"
      • field 검색
        • curl -XGET "http://elasticsearch:9200/index_1/_search?q=field_1:*"
      • field, value 검색
        • curl -XGET "http://elasticsearch:9200/index_1/_search?q=field_1:value_1"
        • curl -XGET "http://elasticsearch:9200/index_1/_search" -H 'Content-Type: application/json' -d'{ "query": { "match": { "field_1": "value_1" } }}'
      • field, value AND 검색
        • curl -XGET "http://elasticsearch:9200/index_1/_search" -H 'Content-Type: application/json' -d'{"query":{"bool":{"must":[{"match":{"field_1":"value_1"}},{"match":{"field_2":"value_2"}}]}}}'
      • value AND 검색
        • curl -XGET "http://elasticsearch:9200/index_1/_search?q=value_1 AND value_2"
  • DELETE
    • 하나의 document 삭제
      • curl -XDELETE "http://elasticsearch:9200/index_1/_doc/1"
    • 인덱스 삭제
      • curl -XDELETE "http://elasticsearch:9200/index_1"
    • query
      • curl -XPOST "http://localhost:9200/my-index-000001/_delete_by_query" -H 'Content-Type: application/json' -d'{ "query": { "match": { "name": "chp" } }}'
    • 주의사항
  • POST
    • _doc
      • PUT과 유사하나 doc id를 입력하지 않으면 doc id가 자동 생성
      • curl -XPOST "http://elasticsearch:9200/index_1/_doc" -H 'Content-Type: application/json' -d'{ "field_1": "value_1", "field_2": "value_2"}'
    • _create
      • curl -XPOST "http://elasticsearch:9200/index_1/_create/1" -H 'Content-Type: application/json' -d'{ "field_1": "value_1", "field_2": "value_2"}'
    • _update
      • 수정을 위해 전체 내용을 다시 PUT하는 대신 변경할 필드만을 내용으로 해서 POST
      • curl -XPOST "http://elasticsearch:9200/index_1/_update/1" -H 'Content-Type: application/json' -d'{ "doc": { "field_1": "value_1_1" }}'
    • _bulk
      • curl -XPOST "http://elasticsearch:9200/_bulk" -H 'Content-Type: application/json' -d'{"index":{"_index":"index_1","_id":"1"}}{"field_1":"value_1"}{"delete":{"_index":"index_1","_id":"2"}}{"create":{"_index":"index_1","_id":"3"}}{"field_1":"value_3"}{"update":{"_id":"1","_index":"index_1"}}{"doc":{"field_2":"value_2"}}'


인덱스 목록 별 용량 조회

  • localhost:9200/_cat/indices
  • localhost:9200/_cat/indices?v
  • localhost:9200/_cat/indices?format=json
  • localhost:9200/_cat/indices?format=json&pretty


설치

  • docker
    • docker run --name elasticsearch -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.17.9
  • Kubernetes yaml
     apiVersion: apps/v1
     kind: StatefulSet
     metadata:
       name: test-es
       namespace: elasticsearch-test
     spec:
       serviceName: elasticsearch
       replicas: 3
       selector:
         matchLabels:
           app: elasticsearch
       template:
         metadata:
           labels:
             app: elasticsearch
         spec:
           containers:
           - name: elasticsearch
             image: docker.elastic.co/elasticsearch/elasticsearch:7.11.2
             ports:
             - containerPort: 9200
               name: rest 
               protocol: TCP
             - containerPort: 9300
               name: node
               protocol: TCP
             env:
               - name: cluster.name
                 value: k8s-logs
               - name: node.name
                 valueFrom:
                   fieldRef:
                     fieldPath: metadata.name
               - name: network.host
                 value: "0.0.0.0"
               - name: discovery.seed_hosts
                 value: "test-es-0.elasticsearch,test-es-1.elasticsearch,test-es-2.elasticsearch"
               - name: cluster.initial_master_nodes
                 value: "test-es-0,test-es-1,test-es-2"
               - name: ES_JAVA_OPTS
                 value: "-Xms512m -Xmx512m"
        
     ---
     apiVersion: v1
     kind: Service
     metadata:
       name: elasticsearch
       namespace: elasticsearch-test
       labels:
         app: elasticsearch
     spec:
       type: NodePort
       selector:
         app: elasticsearch
       ports:
         - name: rest
           port: 9200
           nodePort: 30200
           protocol: TCP
         - name: node
           port: 9300
           nodePort: 30201
           protocol: TCP
    


go-elasticsearch

  • 코드
     package main
    	
     import (
         "bytes"
         "context"
         "encoding/json"
         "errors"
         "fmt"
         "log"
         "os"
         "strings"
    	
         "github.com/elastic/elastic-transport-go/v8/elastictransport"
         "github.com/elastic/go-elasticsearch/v8"
         "github.com/elastic/go-elasticsearch/v8/esapi"
     )
    	
     func main() {
         config := elasticsearch.Config{
             Addresses: []string{
                 "http://elasticsearch:9200",
             },
             Logger: &elastictransport.ColorLogger{Output: os.Stdout},
         }
    	
         client, err := elasticsearch.NewClient(config)
         if err != nil {
             log.Fatalf("new client fail : %s", err)
         }
    	
         log.Println(elasticsearch.Version)
    	
         response, err := client.Info()
         if err != nil {
             log.Fatalf("get info fail : %s", err)
         }
    	
         defer response.Body.Close()
         log.Println(response)
    	
         log.Printf("=== put data start ===")
         err = PutData(client, "index_1", "id_1", "{\"field_1\" : \"value_2\"}")
         if err != nil {
             log.Fatalf("put data fail : %s", err)
         }
         log.Printf("=== put data end ===")
    	
         log.Printf("=== get data start ===")
         err = GetData(client, "index_1", map[string]interface{}{
             "query": map[string]interface{}{
                 "match": map[string]interface{}{
                     "field_1": "value_2",
                 },
             },
         })
         if err != nil {
             log.Fatalf("get data fail : %s", err)
         }
         log.Printf("=== get data end ===")
     }
    	
     func PutData(client *elasticsearch.Client, index, id, data string) error {
         var builder strings.Builder
         builder.WriteString(data)
    	
         request := esapi.IndexRequest{
             Index:      index,
             DocumentID: id,
             Body:       strings.NewReader(builder.String()),
             Refresh:    "true",
         }
    	
         response, err := request.Do(context.Background(), client)
         if err != nil {
             return err
         }
         defer response.Body.Close()
    	
         if response.IsError() {
             return errors.New(fmt.Sprintf("response error - id : (%s), status : (%s)", id, response.Status()))
         }
    	
         var result map[string]interface{}
         err = json.NewDecoder(response.Body).Decode(&result)
         if err != nil {
             return err
         }
         log.Printf("status : (%s), version : (%d), result : (%s)", response.Status(), int(result["_version"].(float64)), result["result"])
    	
         return nil
     }
    	
     func GetData(client *elasticsearch.Client, index string, query interface{}) error {
         var buffer bytes.Buffer
         err := json.NewEncoder(&buffer).Encode(query)
         if err != nil {
             return err
         }
    	
         response, err := client.Search(
             client.Search.WithContext(context.Background()),
             client.Search.WithIndex(index),
             client.Search.WithBody(&buffer),
             client.Search.WithTrackTotalHits(true),
             client.Search.WithPretty(),
         )
         if err != nil {
             return err
         }
         defer response.Body.Close()
    	
         var result map[string]interface{}
         err = json.NewDecoder(response.Body).Decode(&result)
         if err != nil {
             return err
         }
    	
         if response.IsError() {
             return errors.New(fmt.Sprintf("response error - type : (%s), reason : (%s), status : (%s)",
                 result["error"].(map[string]interface{})["type"],
                 result["error"].(map[string]interface{})["reason"],
                 response.Status()))
         }
    	
         log.Printf("status : (%s), hits : (%d), took : (%dms)",
             response.Status(),
             int(result["hits"].(map[string]interface{})["total"].(map[string]interface{})["value"].(float64)),
             int(result["took"].(float64)))
    	
         for _, hit := range result["hits"].(map[string]interface{})["hits"].([]interface{}) {
             log.Printf("id : (%s), source : (%s)", hit.(map[string]interface{})["_id"], hit.(map[string]interface{})["_source"])
         }
    	
         return nil
     }