[Elastic][Elasticsearch] 개요
Updated:
개요
- 분산형 분석 및 검색 엔진
- 다양한 유형의 데이터를 지원
- 전문 검색 지원
- 텍스트 역색인(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
- https://www.elastic.co/guide/en/elasticsearch/reference/7.9/modules-node.html
- 종류 : master, data, ingest, ml, remote_cluster_client, transform
- node.roles을 설정하면 지정된 역할만 수행
- 노드는 다른 노드들의 정보를 알고 있고 클라이언트의 요청을 적절한 노드로 전달
- 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)
- https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
- 검색을 위한 json 기반의 쿼리
- Full text queries
- match_all, match, match_phrase, multi_match, query_string
- Compound queries
- bool
- must, filter, should, must_not
- boosting
- positive, negative, negative_boost
- constant_score
- filter, boost
- dis_max
- queries, tie_breaker
- function_score
- bool
CRUD
- REST API를 이용
- https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html
- 단일 document 접근 url 구조
- http://
: / /_doc/<_id>
- http://
- 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"}'
- _doc
- 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"
- value 검색
- _doc
- 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" } }}'
- 주의사항
- https://discuss.elastic.co/t/free-disk-space-monitoring-after-deleting-records/146651
- 삭제한다고해서 디스크 용량이 줄어들지는 않음
- reindex 또는 forcemerge 수행 필요
- 하나의 document 삭제
- 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"}}'
- _doc
인덱스 목록 별 용량 조회
- 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 }