Elastic/Elasticsearch: 개요
개요
- 분산형 분석 및 검색 엔진
- 다양한 유형의 데이터를 지원
- 전문 검색 지원
- 텍스트 역색인(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
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"}'
- 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" } }}'
- 주의사항
- https://discuss.elastic.co/t/free-disk-space-monitoring-after-deleting-records/146651
- 삭제한다고해서 디스크 용량이 줄어들지는 않음
- reindex 또는 forcemerge 수행 필요
- 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
}