4 분 소요

개요

  • 분산형 분석 및 검색 엔진
  • 다양한 유형의 데이터를 지원
  • 전문 검색 지원
    • 텍스트 역색인(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": "xxx" } }}'
    • 주의사항
  • 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
	}