Open KimDoubleB opened 11 months ago
수집기 > 색인기 > 스토리지 > 검색기
수집기
스토리지
색인기
검색기
RDBMS에서는 검색을 위해 LIKE Query를 이용. 하지만 텍스트 매칭을 통한 단순한 검색만 가능함.
검색엔진은 데이터베이스에서 불가능한 여러 검색이 가능하다.
인덱스
샤드
ES_JAVA_OPTS="-Des.index.max_number_of_shards={number}"
로 제한 가능
타입
문서
필드
매핑
마스터 노드
데이터 노드
코디네이팅 노드
인제스트 노드
샤드 개수
인덱스 생성 시, 샤드 개수를 설정할 수도 있다.
number_of_shards: 프라이머리 샤드의 개수
number_of_replicas: 레플리카 샤드의 개수
스키마리스 기능은 사용하지 말자
스키마리스는 데이터만 보고 알아서(default로) 매핑이 되도록 설정하는 것
인덱스 생성 시, 주의점
한번 생성된 매핑정보는 변경할 수 없다.
한번 생성한 매핑 타입은 변경할 수 없다. 그렇기에 아래 사항을 고민해 매핑정보를 설정해야한다.
_source
에 어떤 필드를 정의할 것인가?매핑생성
매핑확인
매핑 파라미터
_source
에서 검색이 되지만 색인은 하지 않는 경우메타 필드
_index
: 인덱스 이름. 인덱스 정보 조회 시, 사용._type
: 매핑의 타입정보_id
: 문서를 식별하는 유일 키 값_uid
: #
을 이용해 _type
과 _id
를 조합하여 사용 (내부적으로 사용되는 값)_source
: 문서의 원본 데이터_all
(deprecated)_routing
: 특정 문서를 특정 샤드에 저장하기 위해 사용자가 지정하는 메타 필드필드 데이터 타입
elastic search
데이터를 keyword로 지정하면 elastic
, search
로 검색이 불가능루씬을 기반으로 한 텍스트 기반 검색엔진
텍스트에 분석기를 따로 설정하지 않으면 Standard Analyzer가 사용됨
분석기는 데이터의 특성에 따라 원하는 분석 결과를 미리 예상해보고 해당 결과를 얻기 위한 옵션을 적용해 설정해야 함.
REST API 제공함
_analyze
API를 통해 직접 테스트 해볼 수 있다.책 뒤 단어로 문서 페이지를 찾을 수 있는 구조를 의미.
색인한다는 것이 역색인 파일을 만든다는 것.
기본적으로 제공하는 분석기(Analyzer)들이 있고, 아래 필터들을 조합해 Custom 분석기를 정의해 활용할 수도 있다.
Char filter (전처리 필터)
Tokenizer filter
Token filter
색인하는 것과 검색하는 것 과정에 대해 분석기를 따로 설정하고 싶을 수 있다.
analyzer
)search_analyzer
)색인 데이터를 토큰화 해 저장할 때, 동의어/유의어에 해당하는 단어를 함께 저장해 검색이 가능해지게 할 수 있다.
추가하는 방식
아래와 같은 형태로 파일을 생성하고(txt), synonyms_path
에 해당 파일을 보도록 설정해 적용
Elasticsearch, 엘라스틱서치 // 동의어 추가
Harry => 해리 // 동의어 치환
색인 파일을 변경한다고 자동 reload 되지 않는다.
POST {index_name}/_close
POST {index_name}/_open
아래와 같은 API들이 제공된다.
문서 ID (_id
)
생성 시, Id를 따로 설정하지 않으면 자동으로 문서 ID가 생성된다.
버전관리 (_version
)
색인된 모든 문서는 버전 값을 가짐
이 version 값으로 낙관적 락을 지원할 수 있는 것으로 보인다.
POST movie_dynamic/_doc/1?version=1
오퍼레이션 타입 (op_type
)
일반적으로 ID가 이미 존재하는 경우에는 update 작업이 일어나고, 없으면 create 작업이 일어난다. (upsert)
데이터가 존재할 경우, update 하지않고 색인이 실패하길 원한다면 op_type
을 사용하면 된다.
op_type=create
를 주면 된다.타임아웃(timeout
)
_delete_by_query
를 이용.size
항목을 늘려 수행할 것.색인시점과 검색시점의 동작과정
색인시점
검색시점
검색 방법
검색 방법은 2가지가 제공됨
URI 검색은 _search
path에 q
파라미터 등을 통해 검색(153p)할 수 있는데, 복잡한 질의는 불가능하며 의미를 파악하기 어렵다.
POST movie_search/_search?q=movieNmEn:* AND prdtYear:2017&analyze_wildcard=true&from=0&size=5&sort=_score:desc,movieCd:asc&_source_includes=movieCd, movieNm,mvoieNmEn,typeNm&pretty
그렇기에 간단한 질의, 테스트 용도가 아니라면 RESTful API를 이용하는 것이 낫다.
RESTful API (Query DSL)
위 예제를 아래와 같이 작성해 요청할 수 있다.
POST movie_search/_search
{
"query": {
"query_string": {
"default_field": "movieNmEn",
"query": "movieNmEn:* OR prdtYear:2017"
}
},
"from": 0,
"size": 5,
"sort": [
{
"_score": {
"order": "desc"
},
"movieCd": {
"order": "asc"
}
}
],
"_source": [
"movieCd",
"movieNm",
"mvoieNmEn",
"typeNm"
]
}
Search your data | Elasticsearch Guide [8.10] | Elastic
Request body 필드
size
: 리턴받는 결과의 개수 (default: 10)from
: 몇 번째 문서부터 가져올지 지정 (default: 0)timeout
: 요청해 결과를 받는 데까지 걸리는 시간을 나타냄 (default: -1 (no timeout))_source
: 검색 시 필요한 필드만 출력하고 싶을 때 사용query
: 검색 조건문aggs
: 통계 및 집계 사용sort
: 결과 정렬조건 지정Response body 필드
took
: 쿼리를 실행한 시간timed_out
: 타임아웃 시, true_shards
: 쿼리를 요청한 전체 샤드의 개수
total
, successful
, failed
hits
: 결과 문서 개수 및 스코어 정보
total
, max_score
, hits
Query context: Analyzer에 의한 전문 분석이 필요한 경우
match
필드를 이용POST movie_search/_search
{
"query": {
"match": {
"movieNm": "기묘한 가족"
}
}
}
Filter context: Yes/No로 판단할 수 있는 조건 검색의 경우
filter
필드를 이용create_year
필드의 값이 2018인지 확인POST movie_search/_search
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": {
"term": {
"repGenreNm": "다큐멘터리"
}
}
}
}
}
match_all
검색을 하기 전 filter
조건에 맞는지 필터링 과정을 수행하게 된다.Multi index 검색
여러 인덱스를 한 번에 조회할 수 있음
,
을 이용해 다수의 인덱스 명을 입력하면 된다.POST movie_search,movie_auto/_search
{
"from": 0,
"size": 5,
"query": {
"term": {"repNationNm": "한국"}
}
}
*
와일드카드를 이용해 다수의 인덱스를 대상으로 패턴에 맞는 인덱스만 골라 조회 요청을 할 수도 있다.POST /log-2023-*/_search
중요한건 다수의 인덱스가 다른 스키마를 가지고 있어도 검색이 가능하다는 것.
쿼리 결과 페이징
from
, size
파라미터를 이용해 Pagination를 지원
첫 페이지 - 5개 데이터를 조회
POST movie_search/_search
{
"from": 0, // 생략 가능
"size": 5,
"query": {
"term": {"repNationNm": "한국"}
}
}
두 번째 페이지 - 5개 데이터를 조회
POST movie_search/_search
{
"from": 5,
"size": 5,
"query": {
"term": {"repNationNm": "한국"}
}
}
주의할 점은 Offset pagination 특징을 가진다는 것이다.
쿼리 결과 정렬
sort
파라미터를 통해 결과를 정렬할 수 있다.
기본적으로는 계산한 유사도 스코어(score)로 정렬이 되는데, 어떠한 이유로 인해 다른 조건으로 정렬하고 싶은 경우 이를 활용하면 된다.
여러 정렬 조건을 활용할 수도 있다.
POST movie_search/_search
{
"query": {
"term": {
"repNationNm" : "한국"
}
},
"sort" :{
"prdtYear": {
"order": "asc"
},
"_score": {
"order": "desc"
}
}
}
_source
필드 필터링
결과 문서에 대해 특정 필드만 가져오고 싶다면 _source
를 이용하면 된다.
operator
설정
Query DSL에서는 operator를 통해 명시적으로 설정이 가능하다.
"query": {
"match": {
"movieNm": {
"query": "자전차왕 엄복동",
"operator": "and"
}
}
}
**minimum_should_match
설정**
OR 조건으로 검색하는 경우, 결과가 엄청 많을 수 있다.
이를 방지하기 위해 Term의 개수가 최소 몇 개 이상 매치되는 경우만 결과로 나오도록 설정이 필요할 수 있다.
이 때, minimum_should_match
를 이용하면 된다.
"query": {
"match": {
"movieNm": {
"query": "자전차왕 엄복동",
"minimum_should_match": 2
}
}
}
Fuzzy query
기본적으론 단순한 값을 찾는 Match query를 이용하는데, 유사한 값도 포함시키고 싶은 경우 fuzziness
옵션을 이용해 Fuzzy Query를 사용할 수 있다.
Common options | Elasticsearch Guide [8.10] | Elastic
Fuzzy query | Elasticsearch Guide [8.10] | Elastic
레벤슈타인 편집 거리 알고리즘을 기반으로 문서의 필드 값을 여러번 변경하고, 이를 허용범위의 텀으로 변경해가며 문서를 찾는다.
다양한 쿼리 검색을 제공한다. 사용하기 위해서는 query
필드 하위에 해당 조건을 사용하면 된다.
match_all
): 모든 문서를 검색match
): 문장에 대해 형태소 분석을 통해 분리한 후, 결과 Term을 이용해 검색multi_match
): 여러 개의 필드를 대상으로 검색term
): 형태소 분석을 하지 않고 입력된 텍스트가 존재하는 문서를 검색 (keyword
타입)bool
): 하나 또는 여러 개의 쿼리를 조합해 활용할 때 사용
must: [queries]
: 모든 조건 만족하는 문서만 검색must_not: [queries]
: 모든 조건을 만족하지 않는 문서만 검색should: [queries]
: 여러 조건 중 하나 이상을 만족하는 문서가 검색filter: [queries]
: 조건을 포함하고 있는 문서만 검색wildcard
): *
, ?
옵션을 이용해 일치하는 구문을 문서에서 검색 (형태소 분석기 X)nested
): 문서 내부에 다른 문서가 존재할 때(부모/자식 관계)에서 조건 이용해 검색엘라스틱서치는 성능 상의 이유로 Parent 문서와 Child 문서를 모두 동일한 샤드에 저장함.
버전도 많이 올라갔고, 책에서는 모든 쿼리를 다루지 않고 있기에 더 자세한 내용은 공식문서를 보는 것이 좋다.
Query DSL | Elasticsearch Guide [8.10] | Elastic
내부 동작방식
동적분배 방식
PUT _cluster/settings
{
"transient": {
"cluster.routing.use_adaptive_replica_selection": true
}
}
현재 버전인 8.10에서는 이를 기본 값으로 사용하는 것으로 보인다.
By default, Elasticsearch uses adaptive replica selection to route search requests. This method selects an eligible node using shard allocation awareness and the following criteria:
- Response time of prior requests between the coordinating node and the eligible node
- How long the eligible node took to run previous searches
- Queue size of the eligible node’s
search
threadpool
글로벌 타임아웃 설정
앞서 검색 API에서 타임아웃을 다뤘는데, 기본 값이 무제한(-1)이였다.
다수의 무거운 쿼리가 실행된다면, 전체 시스템에 영향을 줄 수 있으므로 전체 정책으로 타임아웃을 설정할 수 있다.
PUT _cluster/settings
{
"transient": {
"search.default_search_timeout": "1s"
}
}
다양한 부가 쿼리들
Search shards API: 검색이 수행되는 노드 및 샤드에 대한 정보 확인
POST movie_search/_search_shards
Count API: 검색된 문서의 개수가 몇 개인지만 궁금할 때 사용
POST movie_search/_count?q=prdtYear:2017
POST movie_search/_count
{
"query": {
"query_string": {
"default_field": "prdtYear",
"query": "2017"
}
}
}
Validate API: 쿼리가 유효하게 작성됬는지 검증
POST movie_search/_validate/query?q=prdtYear:2017
POST movie_search/_validate/query
{
"query" : {
"match": {
"prdtYear": 2017
}
}
}
왜 실패했는지 더 자세한 정보가 필요한 경우, rewrite=true
query parameter를 추가하면 된다.
Explain API: 요청한 쿼리가 어떻게 스코어가 계산되는지 확인할 때 사용
POST movie_search/_doc/fzzJqmkBjjM-ebDb8PsR/_explain
{
"query" : {
"term": {
"prdtYear": 2017
}
}
}
Profile API: 쿼리에 대한 상세 수행계획과 수행 계획 별 수행된 시간을 확인할 때 사용
매우 상세하게 설명해 결과가 매우 방대하므로 주의하자.
POST movie_search/_search
{
"profile": true,
"query": {
"match_all": {}
}
}
보통 통계 프로그램은 배치 방식으로 데이터를 처리한다.
반면 엘라스틱서치는 많은 양의 데이터를 조각내어 관리함.
캐시
같은 질의에 대해 매번 결과를 계산하는게 아닌 버퍼에 보관된 결과를 반환
캐시 크기는 일반적으로 힙 메모리의 1% 할당.
변경하고 싶은 경우, indices.requests.cache.size
이용.
Shard request cache settings | Elasticsearch Guide [8.10] | Elastic
3가지의 캐시를 지원함
Node query cache
: 모든 샤드가 공유하는 LRU 캐시
index.queries.cache.enabled
로 활성화 가능Shard request cache
: 샤드에서 수행된 쿼리 결과를 캐시
Field data cache
: 집계 계산되는 동안 필드의 값 캐시
Aggregation API
서비스를 운영하다보면 검색 쿼리로 반환된 데이터를 집계하는 경우가 많다.
검색 쿼리의 집계는 다음과 같이 기존 검색 쿼리에 집계 구문을 추가하는 방식으로 수행한다.
{
"query": { ... },
"aggs": { ... }
}
질의가 생략되면 match_all
쿼리로 수행되어 전체문서에 대한 집계가 수행됨.
Aggregations | Elasticsearch Guide [8.10] | Elastic
다양한 메트릭 집계를 제공.
이름에서 알 수 있듯 정수/실수 같은 숫자 연산 값 집계를 수행한다.
_score
값에 따라 정렬을 수행
2가지 집계로 나뉜다.
집계 구조
요청
GET /apache-web-log/_search?size=0 // test를 위해 Size를 0으로 설정
{
"aggs": {
"집계 이름": {
"집계 타입": {
"field": "필드명"
}
}
}
}
응답
{
"took": 78, // 걸린 시간
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": { // 검색 결과
"total": {
"value": 5,
"relation": "eq"
},
"max_score": 1.0,
"hits": [...]
},
"aggregations": {
"my-agg-name": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": []
}
}
}
결과에 대해 변화를 시키고 싶은 경우, script
를 사용할 수 있다.
How to write scripts | Elasticsearch Guide [8.10] | Elastic
painless
언어를 사용한다.
script.lang
옵션을 작성하지 않아도 기본적으로 painless
언어를 사용함.GET my-index-000001/_search
{
"script_fields": {
"my_doubled_field": {
"script": {
"source": "doc['my_field'].value * params['multiplier']",
"params": {
"multiplier": 2
}
}
}
}
}
Cardinality 집계의 경우, 모든 문서에 대해 중복된 값을 집계하는 것은 성능에 큰 영향을 줄 수 있다.
Cardinality aggregation | Elasticsearch Guide [8.10] | Elastic
search.max_buckets
)
range
) - ranges.from
≤ data < ranges.to
date_range
)histogram
)
date_histogram
)terms
)
집계를 수행할 땐 각 샤드에 집계 요청을 전달하고, 각 샤드는 집계 결과에 대해 정렬을 수행한 자체 뷰를 갖는다.
그리고 이것들을 병합해 최종 뷰를 구성하기에 포함되지 않은 문서가 존재하는 경우, 집계가 정확하지 않을 수 있다.
다른 집계로 생성된 버킷을 참조해 집계를 수행하는 집계 (aggs 내 aggs)
buckets_path
파라미터를 사용해 참조할 집계의 경로를 설정
동일 선상의 위치에서 수행되는 새 집계.
ex) 분 단위 데이터량 합산과 가장 큰 데이터량 구하기
GET /apache-web-log/_search?size=0
{
"aggs": {
"histo": {
"date_histogram": {
"field": "timestamp",
"interval": "minute"
},
"aggs": {
"bytes_sum": {
"sum": {
"field": "bytes"
}
}
}
},
"max_bytes": {
"max_bucket": { // 버킷 중 가장 큰 값(Max) 계산
"buckets_path": "histo>bytes_sum" // histo 버킷의 bytes_sum 버킷 참조
}
}
}
}
생성된 버킷을 사용해 집계를 수행하고, 그 결과를 기존 집계에 반영.
갭
gap_policy
)
skip
: 누락된 값을 스킵한다. 버킷을 건너뛰고 음 으로 사용가능한 값으로 계산을 계속 수행.insert_zeros
: 누락된 값을 0으로 대체한다. 파이프라인 집계계산은 정상적으로 수행.
예제
GET /apache-web-log/_search?size=0
{
"aggs": {
"histo": {
"date_histogram": {
"field": "timestamp",
"interval": "day"
},
"aggs": {
"bytes_sum": {
"sum": {
"field": "bytes"
}
},
"sum_deriv": {
"derivative": { // 현재 값과 이전 값을 비교하는 집계 수행
"buckets_path": "bytes_sum"
}
}
}
}
}
}
다시 한번 집계 정리
이 가운데 실제로 수학적인 계산을 하는 것은 메트릭 집계 뿐이다.
집계 프로세스는 앞서 이야기했듯 Coordinating node가 Data node에게 집계를 명령하고, 그 결과를 집계해 Coordinating node가 최종 집계해 결과를 반환한다.
Min, Max 같은 한 값에 대한 집계의 경우, Data node에서 Coordinating node로 문서 전체를 넘길 필요가 없다. 계산한 값만 넘기고, Coordination node에서 이를 모아 한번 더 연산하면 끝이다.
문제는 Cardinality 집계이다. 이를 정확히 계산하기 위해서는 Data node에서 중복을 제거한 모든 문서를 Coordinating node로 전달하고, 이를 다 받고나서 다시 한번 중복제거를 해야만 최종 결과를 만들어낼 수 있다.
문제
제안
해결
엘라스틱서치는 위 제안 중 정확도 포기를 선택했다.
데이터를 실제로 가지고 있는 노드의 모음
여러 클러스터 간 한번에 검색해야하는 경우도 있다.
Cross Cluster Search
기능을 이용해 다수의 클러스터에 대하여 한 번에 검색할 수 있다.물리적으로 실행된 런타임 상태의 엘라스틱서치 (엘라스틱서치 인스턴스)
같은 클러스터 내 모든 노드는 서로 다른 노드와 수시로 정보를 주고받음.
유사한 특성을 가지고 있는 문서들을 모아둔 컬렉션
원하는 만큼의 많은 문서를 저장할 수 있으며, 물리적인 샤드형태로 나눠서 다수의 노드에 저장됨.
검색 대상이 되는 실제 물리적인 데이터
데이터를 분산해 저장하는 방식, 개념, 단위.
인덱스에 쿼리하면, 다음과 같은 과정을 거침.
이로써 2가지 좋은 점이 있음
샤드의 복제본.
특정 노드가 문제가 생긴 경우, 페일오버 메커니즘을 통해 레플리카를 이용해 안정적인 클러스터 운영을 보장함.
문서가 저장되는 데이터 자료구조.
루씬 라이브러리가 핵심이며, 크게 2가지 기능을 가지고 있음.
IndexWriter
: 데이터 색인하는 녀석IndexSearch
: 색인된 데이터를 검색 결과로 제공하는 녀석루씬 인덱스: 이 2가지 기능을 가지고 색인 및 검색을 제공
엘라스틱서치는 이 독립적인 루씬 인덱스를 엘라스틱서치 샤드라는 형태로 확장해 서비스하는 것
하나의 루씬 인덱스는 내부적으로 다수의 세그먼트로 구성되어 있음.
루씬에서는 세그먼트를 관리하기 위한 용도로 커밋 포인트(Commit Point) 자료구조를 사용
IndexSearcher
는 커밋 포인트를 이용해 가장 오래된 세그먼트부터 차례대로 검색 후 결과를 합쳐 제공IndexWriter
에 의해 색인작업이 이뤄지고, 결과물로 하나의 세그먼트가 생성됨.
즉, 시간이 흐를수록 세그먼트들의 개수는 늘어나는 것인데, 너무 많이 생성되면 읽기 성능저하가 일어남.
왜이리 복잡하게 할까? 그냥 기존 세그먼트 파일에 덮어씌우면 되지 않나?
수정/삭제는 어떻게 일어나는가?
Flush: 읽기 가능하게 만드는 작업
Commit: 쓰기 작업 ⇒ Flush
Merge: 세그먼트 통합 작업 ⇒ Optimize API
엘라스틱서치 Refresh (루씬 Flush)
refresh_interval
필드를 통해 설정이 가능
-1
로 설정하여 잠시 비활성화 시켜놓을 수 있다.엘라스틱서치 Flush (루씬 Commit)
엘라스틱서치 Optimize API (루씬 Merge)
레플리카 수는 앞서 이야기했듯 운영 중 언제든 변경이 가능하다.
ES_JAVA_OPTS="-Des.index.max_number_of_shards
환경변수로 이 제한을 따로 설정할 수도 있다.클러스터 내 모든 샤드는 마스터 노드에서 관리된다.
마스터 노드 입장에서는 샤드가 가지고 있는 데이터 수보다 데이터의 물리적인 크기가 더 중요하다.
일단 장애가 발생하면,
number_of_shards
옵션에 따라 인덱스당 생성할 수 있는 최대 샤드는 기본적으로 1024개
개별 샤드가 가질 수 있는 최대 문서 수
java.lang.Integer.MAX_VALUE - 127
. 즉, 20억개 정도를 저장할 수 있음.즉, 1024 * 20억 = 약 2조.
안정적인 클러스터 운영을 위해서는 자신의 환경에 맞게 다방면에 걸친 충분한 테스트가 필요.
Suggest API가 키워드 자동완성을 지원하지만 한글 키워드 대상에서는 제대로 동작하지 않는다.
Suggest API는 4가지 방식을 제공한다.
Edit distance(편집거리)를 이용해 비슷한 단어를 제안
ICU 분석기를 통해 한글 오타 교정 가능
ICU는 국제화 처리를 위해 개발된 분석기로, 내부에 한국어 자소 분해/융합 기능을 가지고 있음.
간단한 한글 처리는 가능하나, 한글 기반으로 정교한 오타 교정 등 처리하기 위해선 전문적인 플러그인 개발이 요구됨.
다나와에서는 이를 이용해 오타교정을 한 사례가 있음.
검색을 효율적으로 돕기 위해 자동완성 기능을 제공
Example
PUT movie_term_completion
{
"mappings": {
"_doc": {
"properties": {
"movieNmEnComple": {
"type": "completion"
}
}
}
}
}
POST movie_term_completion/_search
{
"suggest": {
"movie_completion": {
"prefix": "l",
"completion": {
"field": "movieNmEnComple",
"size": 5
}
}
}
}
일반적으로 전방일치(prefix)만 가능함. 실 환경과 맞지 않음.
부분일치를 해야한다면, 부분일치 하고자 하는 부분을 분리해 색인 데이터를 구성해야 함.
배열 형태로 데이터를 분리해 저장
PUT movie_term_completion/_doc/1
{
"movieNmEnComple": {
"input": ["After", "Love"]
}
}
오타 교정, 한영/영한 오타 교정 등을 Analyzer를 직접 구현해 사용할 수 있다.
책에서는 직접 구현하진 않고, 구현해놓은 소스를 이용하여 테스트 한다.
PUT /company_spellchecker
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"korean_spell_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"trim",
"lowercase",
"javacafe_spell"
]
}
}
}
}
}
}
PUT /company_spellchecker/_doc/_mappings
{
"properties": {
"name": {
"type": "keyword",
"copy_to":["suggest"]
},
"suggest": {
"type": "completion",
"analyzer": "korean_spell_analyzer"
}
}
}
--- 검색 ---
POST /company_spellchecker/_doc/_search
{
"suggest": {
"my-suggestion": {
"text": "샴성전자",
"term": {
"field": "suggest"
}
}
}
}
프로세스
교정 과정에서 중요한 것
Completion Suggest API를 이용하면 간단하게 자동완성을 이용할 수 있다.
하지만 앞서 이야기했듯 한글은 유니코드 변함의 이슈로 인해 제대로 지원되지 않는다.
이를 해결하기 위해 루씬에서 제공하는 다양한 분석기능(API)을 활용할 수 있다.
💡 복습 Analyzer Tokenizer + Token Filter. 데이터가 들어오면 검색 가능하도록 정제/가공
Tokenizer 정해진 Separator로 토큰을 분리하는 역할. Analyzer에는 하나 이상의 Tokenizer가 있어야만 함 ****ex) Whitespace Tokenizer
Token Filter Tokenizer로 분리된 토큰을 가공하는 역할. ex) Lowercase filter
초성 검색을 지원하고 싶다?
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori-tokenizer.html
user_dictionary
는 수정해도 바로 반영되지 않음.
user_dictionary
를 읽음. 로딩된 이후로는 다시 읽어들이지 않는다._close
/_open
하면 된다.하지만 이 방식을 적용해도 이미 인덱싱 된 문서에는 적용되지 않는다.
user_dictionary
를 이용해 토큰을 생성해두었기 때문.update_by_query api
활용.OpenJDK 기준
근데 사실 최근에 와서는 Container를 사용하고, 이에 따라 따로 Java를 따로 설치해야할 필요가 있을까 싶다.
8.10.2 Elasticsearch container image는 어떤 Java/Version을 사용할까?
IMPLEMENTOR="Oracle Corporation"
JAVA_RUNTIME_VERSION="20.0.2+9-78"
JAVA_VERSION="20.0.2"
JAVA_VERSION_DATE="2023-07-18"
현재 버전 유지보수는 해줄까? EOL이 언제까지인가?
ES/Lucene도 어찌됬든 Java로 만들어진 프로젝트들.
ES는 다년간의 경험으로 적합한 JVM 옵션을 사용하고 있다.
Advanced configuration | Elasticsearch Guide [8.11] | Elastic
만약 업데이트 하고 싶다면,
/usr/share/elasticsearch/config/jvm.options.d/
.ES_JAVA_OPTS
환경변수 이용root의 jvm.options
파일은 건드리지 말고 환경 별 jvm.options.d/
를 사용하라고 가이드하고 있다.
8.10.2 docker file 기준으로 아래와 같이 jvm.options
가 설정되어 있다.
-XX:+UseG1GC
## JVM temporary directory
-Djava.io.tmpdir=${ES_TMPDIR}
# Leverages accelerated vector hardware instructions; removing this may
# result in less optimal vector performance
20:--add-modules=jdk.incubator.vector
## heap dumps
# generate a heap dump when an allocation from the Java heap fails; heap dumps
# are created in the working directory of the JVM unless an alternative path is
# specified
-XX:+HeapDumpOnOutOfMemoryError
# exit right after heap dump on out of memory error
-XX:+ExitOnOutOfMemoryError
# specify an alternative path for heap dumps; ensure the directory exists and
# has sufficient space
-XX:HeapDumpPath=data
# specify an alternative path for JVM fatal error logs
-XX:ErrorFile=logs/hs_err_pid%p.log
## GC logging
-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m
전체 옵션을 보고 싶다면, GET _nodes/_all/jvm
으로 확인해볼 수 있다.
{
"_nodes": {
"total": 1,
"successful": 1,
"failed": 0
},
"cluster_name": "docker-cluster",
"nodes": {
"JqgoQyD3QwidYsJXMonygw": {
"name": "es",
"transport_address": "192.168.228.2:9300",
"host": "192.168.228.2",
"ip": "192.168.228.2",
"version": "8.10.2",
"transport_version": 8500061,
"build_flavor": "default",
"build_type": "docker",
"build_hash": "6d20dd8ce62365be9b1aca96427de4622e970e9e",
"roles": [
"data",
"data_cold",
"data_content",
"data_frozen",
"data_hot",
"data_warm",
"ingest",
"master",
"ml",
"remote_cluster_client",
"transform"
],
"attributes": {
"ml.max_jvm_size": "536870912",
"ml.config_version": "10.0.0",
"xpack.installed": "true",
"transform.config_version": "10.0.0",
"ml.machine_memory": "1073741824",
"ml.allocated_processors": "10",
"ml.allocated_processors_double": "10.0"
},
"jvm": {
"pid": 121,
"version": "20.0.2",
"vm_name": "OpenJDK 64-Bit Server VM",
"vm_version": "20.0.2+9-78",
"vm_vendor": "Oracle Corporation",
"using_bundled_jdk": true,
"start_time_in_millis": 1699961395643,
"mem": {
"heap_init_in_bytes": 536870912,
"heap_max_in_bytes": 536870912,
"non_heap_init_in_bytes": 7667712,
"non_heap_max_in_bytes": 0,
"direct_max_in_bytes": 0
},
"gc_collectors": [
"G1 Young Generation",
"G1 Concurrent GC",
"G1 Old Generation"
],
"memory_pools": [
"CodeHeap 'non-nmethods'",
"Metaspace",
"CodeHeap 'profiled nmethods'",
"Compressed Class Space",
"G1 Eden Space",
"G1 Old Gen",
"G1 Survivor Space",
"CodeHeap 'non-profiled nmethods'"
],
"using_compressed_ordinary_object_pointers": "true",
"input_arguments": [
"-Des.networkaddress.cache.ttl=60",
"-Des.networkaddress.cache.negative.ttl=10",
"-Djava.security.manager=allow",
"-XX:+AlwaysPreTouch",
"-Xss1m",
"-Djava.awt.headless=true",
"-Dfile.encoding=UTF-8",
"-Djna.nosys=true",
"-XX:-OmitStackTraceInFastThrow",
"-Dio.netty.noUnsafe=true",
"-Dio.netty.noKeySetOptimization=true",
"-Dio.netty.recycler.maxCapacityPerThread=0",
"-Dlog4j.shutdownHookEnabled=false",
"-Dlog4j2.disable.jmx=true",
"-Dlog4j2.formatMsgNoLookups=true",
"-Djava.locale.providers=SPI,COMPAT",
"--add-opens=java.base/java.io=org.elasticsearch.preallocate",
"-Des.cgroups.hierarchy.override=/",
"-XX:+UseG1GC",
"-Djava.io.tmpdir=/tmp/elasticsearch-15691817446927015418",
"--add-modules=jdk.incubator.vector",
"-XX:+HeapDumpOnOutOfMemoryError",
"-XX:+ExitOnOutOfMemoryError",
"-XX:HeapDumpPath=data",
"-XX:ErrorFile=logs/hs_err_pid%p.log",
"-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m",
"-Xms512m",
"-Xmx512m",
"-XX:MaxDirectMemorySize=268435456",
"-XX:G1HeapRegionSize=4m",
"-XX:InitiatingHeapOccupancyPercent=30",
"-XX:G1ReservePercent=15",
"-Des.distribution.type=docker",
"--module-path=/usr/share/elasticsearch/lib",
"--add-modules=jdk.net",
"--add-modules=org.elasticsearch.preallocate",
"-Djdk.module.main=org.elasticsearch.server"
]
}
}
}
}
JVM heap size
jvm.options
파일도 그렇고, 공식문서에도 현재 사용가능한 메모리를 기반으로 자동으로 설정되니 업데이트 하지 않아도 됨을 명시하고 있다.By default, Elasticsearch automatically sets the JVM heap size based on a node’s roles and total memory. Using the default sizing is recommended for most production environments.
**-Xms
, -Xmx
의 크기를 같게 설정해야 한다.**
운영체제의 50% 메모리 공간을 보장하자.
Heap 사이즈를 Compressed ordinary object pointers(oops) threshold보다 높게 설정하지 말자.
Object Pointer는 개체의 메모리 번지를 표현하는 주소값.
[Java] Compressed Class Space와 Compressed Object Pointer
Compressed ordinary object pointers threshold가 항상 정확하진 않으나 26GB 정도가 안전하며, 특정 시스템에서는 30GB정도 되는 경우도 있다.
Set
Xms
andXmx
to no more than the threshold for compressed ordinary object pointers (oops). The exact threshold varies but 26GB is safe on most systems and can be as large as 30GB on some systems.
사용하고 있는지 알고 싶다면, 엘라스틱로그에서 아래 로그를 확인하거나 위에서 언급한 jvm option 조회에서 jvm.using_compressed_ordinary_object_pointers
를 확인하면 된다.
heap size [1.9gb], compressed ordinary object pointers [true]
Zero based Compressed OOP
가상 메모리
메모리 스와핑
적은 메모리로도 프로그램이 돌아가고 있다?
Lucene은 내부적으로 Java에서 제공하는 NIO를 이용하여 운영체제 커널에서 제공하는 mmap 시스템 콜을 직접 호출함.
ES에서 스와핑은 노드 안정성에 치명적임. 그래서 최대한 피해야 함.
Disable swapping | Elasticsearch Guide [8.11] | Elastic
swapoff -a
등을 통해 스와핑을 비활성화하는게 제일 좋음.ulimit 명령어
[Linux] 5분이면 가능! ulimit 확인 및 설정 방법(feat. open files)
sysctl 명령어
GET /_cluster/health
를 통해 Health 체크를 지원함.
Cluster health API | Elasticsearch Guide [8.11] | Elastic
{
"cluster_name" : "testcluster",
"status" : "yellow",
"timed_out" : false,
"number_of_nodes" : 1,
"number_of_data_nodes" : 1,
"active_primary_shards" : 1,
"active_shards" : 1,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 1,
"delayed_unassigned_shards": 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 50.0
}
status
는 클러스터의 상태를 나타내며 green, yellow, red 값을 가진다.
green
: 클러스터의 모든 샤드가 정상임yellow
: 프라이머리 샤드는 정상 할당됬으나, 일부 레플리카 샤드가 정상적으로 할당되지 못함red
: 일부 프라이머리 샤드가 정상적으로 할당되지 못함. 클러스터가 시작하며 프라이머리가 아직 할당되지 않았을 때 잠시 red 상태가 될 수 있음level=indices
를 붙이면 된다.level=shard
를 붙이면 된다.GET /_cluster/health/{index_name}
으로 하면 된다.
엘라스틱서치는 실행 시 config 디렉터리에 설정된 환경설정 정보를 바탕으로 노드를 인스턴스화한다.
이 때, 사용 가능한 리소스를 적절히 분산해 각 모듈에서 나눠 사용할 수 있도록 자동으로 리소스를 분배한다.
클러스터가 구성되면 물리적인 노드가 실제로 어떤 설정을 가지고 있으며, 사용 중인 리소스가 어떤지 상태정보 API를 통해 확인할 수 있다.
클러스터 레벨 물리상태 조회
GET /_cluster/state
metadata
정보, routing_table
정보, Restore/Snapshot
정보 등을 제공노드 레벨 물리상태 조회
GET /_nodes
GET /_nodes/{node ip}
GET /_nodes/{node id}
GET /_nodes/_local
: 실제로 API 요청을 받았었던 노드만을 대상으로 정보를 조회할 수 있음제공되는 정보는 다음과 같다.
settings
): elasticsearch.yml
에 설정된 설정사항을 보여준다.
os
): 인스턴스가 실행된 운영체제 정보process
): 인스턴스 생성 시, Memory lock이 수행됬는지 알 수 있음
jvm
): 설정된 JVM 옵션들thread_pool
): 설정된 스레드풀 관련 정책. 적절한 스레드를 생성하도록 CPU 코어 개수 기반으로 자동 설정transport
): 클러스터 내부의 노드 간의 통신을 위해 transport 모듈 이용. 어떤 포트 바인딩 되었는지 확인 가능.http
): 클러스터 외부 통신을 위해 http 모듈 이용. 어떤 포트 바인딩 되었는지 확인 가능.plugins
): 설치된 플러그인 목록 확인가능
modules
): 어떤 모듈 동작하고 있는지 확인 가능elasticsearch에서는 실시간 모니터링을 위해 여러 기준별로 상세한 정보를 API로 제공한다.
GET /_cluster/stats
GET /_nodes/stats
GET /_stats
상세해서 레벨마다 많은 양의 데이터를 제공한다. 필요할 때마다 찾아보는게 좋을 듯.
대충 아래와 같은 정보들을 원한다면 찾아보면 된다.
Compact and aligned text (CAT) APIs | Elasticsearch Guide [8.11] | Elastic
cat? 리눅스 cat에서 영감받아 만들어진 API로 콘솔에서 모니터링 하기 위한 목적으로 만들어졌음.
결과를 보면 대충 다음과 같다.
여러 파라미터를 지원한다.
help
: API에 대한 정보v
: 헤더 정보 포함여부. v=true
시, 헤더가 추가되어 전달된다.h
: 특정 헤더만 포함시키고 싶은 경우format
: text, yaml 등 다양한 결과 포멧을 지원한다.
엘라스틱서치 실무 가이드 책을 읽고 정리
챕터 별 바로가기
1장 - 검색 시스템 이해하기 2장 - 엘라스틱서치 살펴보기 3장 - 데이터 모델링 4장 - 데이터 검색 5장 - 데이터 집계 9장 - 엘라스틱서치와 루씬 이야기 10장 - 대용량 처리를 위한 시스템 최적화 11장 - 장애 방지를 위한 실시간 모니터링
docker
https://github.com/KimDoubleB/LAB/tree/master/docker/elastic-search
버전 별 변경사항
Elasticsearch 7
Elasticsearch 8
한글 검색용 필터
https://github.com/yainage90/hanhinsam
8.10.2 사용 시 참고사항
버전에 맞게 gradle 수정: 8.1.2 -> 8.10.2
elasticsearch 버전이 올라가며 코드 수정이 이뤄졌음 (특히, AbstractTokenFilterFactory)
고로 FilterFactory들을 다음과 같이 코드 수정해줘야 함. (https://github.com/elastic/elasticsearch/pull/88113, 8.4.0 이후인 듯)
super(indexSettings, name, settings);
super(name, settings);
./gradlew clean assemble
빌드 후,build/distributions/hanhinsam-0.1.zip
확인bin/elasticsearch-plugin install file:///file-path/hanhinsam-0.1.zip
설치검색 FE 예제 코드
https://github.com/KimDoubleB/es-movie-finder