본문 바로가기

SearchDeveloper/엘라스틱서치 바이블

[4] 데이터 다루기

<색인>

색인 API

PUT [인덱스명]/_doc 덮어쓰기 허용

PUT [인덱스명]/_create 덮어쓰기 금지

refresh

검색 가능하게 만들기

true: 색인 직후 refresh 하고 응답을 반환한다. → 단점: 너무 작은 세그먼트를 많이 생성해 성능 저하와 병합 부하 커짐

wait_for: index.refresh_interval (기본값 1초)만큼 기다린다. refresh 대기하는 문서가 index.max_refresh_listeners 값(기본값 1000) 이상이면 바로 refresh 한다.

false: refresh 하지 않는다

조회 API

GET [인덱스명]/_doc/[_id] , GET [인덱스명]/_source/[_id]

refresh 기다릴 필요 없이 바로 확인할 수 있다!

업데이트 API

detect_noop

업데이트 API 는 기존 문서 먼저 읽고 변경할게 있는지 여부를 확인하는데, 변경할 내용이 기존 내용과 동일하면 no operation 요청임을 확인하고 쓰기작업을 하지 않는다.

"result": "noop"

detect_noop 을 false 로 비활성화 할 수있지만 기존 문서 _source 를 읽는 작업은 무조건 수행한다.

커스텀 플러그인 작성시 IndexingOperationListener 에서 noop 요청이면 preIndex(), postIndex() 메소드 수행 안되므로 유의한다.

POST upd_test/_update/1
{
  "doc": {"views": 36},
  "detect_noop" : false
}

doc_as_upsert

기본적으로 업데이트 할 문서 없으면 실패하는데, doc_as_upsert true 로 하면 업서트 해준다.

스크립트

painless: ES 자체 스크립트 언어

   
params 매개변수 Map
ctx._source 문서의 _source 값 Map 으로 변환한거
ctx.op 작업 종류. [index, none, delete]
ctx._now timestamp 을 ms 로 변환한 값
ctx._index
ctx._id
ctx._type
ctx._routing
ctx._version
메타데이터

벌크 API

-NDJSON 타입이다. (여러 줄 JSON 은 줄바꿈 문자로 구분) (application/x-ndjson)

- bulk API 기술된 작업이 반드시 그 순서대로 수행된다는 보장은 없다. 여러 프라이머리 샤드에 넘어간 요청은 독자적으로 수행되기 때문. 그러나 index, _id, routing 조합 요청이라면 반드시 같은 샤드로 들어간다.

update by query

POST [인덱스명]/_update_by_query

쿼리 요청하면 문서를 일종의 스냅삿을 찍는다. 여기서 버전 충돌 문제가 생기면 conflicts 에 따라 작업한다. abort (기본값) 면 충돌 시 작업 중단하며, proceed 로 하면 무시하고 넘어간다.

중간에 중단되더라도 그때까지 업데이트된 내용이 롤백되진 않는다.

reindex 랑 옵션이 비슷하다.

스로틀링

POST bulk_test/_update_by_query?scroll_size=1000&scroll=1m&requests_per_seconds=500

운영중에 update 하면 부하 줄 수 있으므로 스로틀링을 통해 작업량을 조절할 수 있다.

-scroll_size: 업데이트 전 먼저 검색을 수행하는데 한 번 검색 수행에 가져올 문서 개수

-scoll: 검색한 문서 가져오면 search context(검색 문맥)에 보존하는데, 얼마나 보존할지 지정. 한 배치(scoll_size) 작업에 필요한 시간만 지정하면 된다.

  • 검색 문맥은 힙 메모리, 디스크 공간, file descriptor 를 차지하므로 너무 큰 값을 지정하지 않아야 한다.

-requests_per_second: 초 당 몇개까지 수행할건지 지정한다. scroll_size가 1000이고 requests_per_second가 500 (1초에 500개) 이면 한 번 스크롤하고 2초될때까지 기다리고 한 번 스크롤하는 방식으로 시간을 맞춘다. (-1이면 스로틀링 적용 안 함)

response

{
  "throttled_millis": 2999,
  "requests_per_second":500.0
}

throttled_millis: 작업 진행하지 않고 대기한 총 시간

wait_for_completion

POST [인덱스명]/_update_by_query/wait_for_completion=false

wait_for_completion=false 로 하면 비동기로 update by query 할 수 있다.

작업 확인은 task api 로 한다.

task API

update by query 가 비동기든 아니든 모든 작업은 .task 인덱스에 저장되고, GET .task 혹은 GET_tasks (이거 쓰는게 대세) 로 확인할 수 있다.

작업 취소는 POST tasks/[task id]/_cancle 로 하며 비동기든 아니든 취소 가능하다.

일괄 업데이트 중 운영 트래픽이 급증했을 때

update by query 가 72시간 수행하고 나머지 2시간 남았는데 운영 트래픽이 급증하는 상황이라면, 두 가지 해결책이 있다.

  1. 일단 중지하고 검색 조건을 수정해 나머지 변경 안된 건만 대상으로 트래픽 해결한뒤 재수행한다.
  2. 변경 안된 건만 거를 수 없는 상황이라면, _rethrottle api 로 스로틀링을 조절한다.

POST _update_by_query/[task id]/_rethrottle?requests_per_seconds=5000

슬라이싱

POST [인덱스명]/_update_by_query?slices=auto

-스로틀링과 반대로 서비스 중지 후 공지한 시간 안에 업데이트 성능을 최대로 끌어내 빠른 시간 안에 끝내고자 할 때 쓴다.

-검색과 업데이트를 지정한 개수로 쪼개 병렬적으로 수행한다.

-기본값은 1로 병렬이 아니며, slices:auto 로 하면 기본 쪼개는 개수는 프라이머리 샤드 수이다.

-requests_per_second 값도 각 슬라이스에 분배된다. ex. requests_per_second 가 1000이고 슬라이스 가 5개면 각 슬라이스는 200ms만 기다린다.


<검색>

term 쿼리

text 타입일때, 질의어는 normalizer 처리되고 text 타입 필드 값은 analyzer 를 거쳐 역색인을 이용한다. 역색인 결과가 단일텀이고 노멀라이저 거친 질의어와 완전 일치하는 경우에만 검색 결과에 걸린다.

range 쿼리

문자열 필드 range 쿼리는 부하가 큰 쿼리로 분류한다.

prefix 쿼리

단발성 쿼리는 괜찮으나 서비스 쿼리로는 적절하지 못하다. prefix 를 서비스성으로 사용하고 싶으면 mappings 에 index_prefixes 를 넣는 방법이 있다.

PUT prefix_index_test
{
  "mappings": {
    "properties": {
       "fieldName": {
         "type": "text",
         "index_prefixes": {
           "min_chars": 3,
           "max_chars": 5
         }
       }
    }
  }
}

min_chars(디폴트: 2) 와 max_chars(디폴트: 5) 사이의 prefix 를 미리 별도로 색인한다.

elasticsearh.ymlsearch.allow_expensive_queries:false 로 하면 indx_prefixes 적용안된 prefix 는 사용할 수 없다.

쿼리 문맥과 필터 문맥

필터 문맥(filter context): 점수 매기지 않고 단순히 조건 만족하는지 여부만 따진다.

  • filter, must_not, exists, range, constant_score 쿼리

쿼리 문맥(query context): 유사도 점수를 매긴다.

  • must, should, match, term 쿼리

쿼리 수행 순서

-must, must_not, filter, should 사이에서 어떤 쿼리가 먼저 수행된다는 규칙은 없다. 루씬의 쿼리로 재조합에 비용 추정 후 유리할 것으로 생각되는 부분을 먼저 수행한다.

-쿼리문맥이어도 일단 조건이 문서와 매치되는지를 참 거짓으로 판단한다. 그 이후에 쿼리문맥은 점수를 계산하고 필터 문맥은 점수 계산을 안 할 뿐이다.

-쿼리문맥이어도 랭킹안에 못 들거같은 문서는 점수 계산을 건너뛰기도 한다.

-그래서 사용자가 임의로 쿼리 순서를 컨트롤하는게 어렵다. 하고싶으면 커스텀 플러그인에서 커스텀 쿼리를 만들어야 한다.

constant_score

filter 쿼리에 유사도 점수를 줄 수 있다. (기본값:1)

GET /_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "user.id": "kimchy" }
      },
      "boost": 1.2
    }
  }
}

search_type

GET [인덱스명]/_search?search_type=dfs_query_then_fetch

query_then_fetch: (기본값) 각 샤드 레벨에서 유사도 점수 계산을 끝낸다. 약간 부정확할 순 있지만 검색 성능 차이가 크다.

dfs_query_then_fetch: 모든 샤드로부터 정보를 모어 유사도 점수를 글로벌하게 계산한다. 정확도는 올라가나 검색 성능이 떨어진다.

페이지네이션

from, size (비추)

from, size 로 검색하면 from+size 개수 문서를 모두 검색한 뒤 잘라서 응답으로 보낸다.

그러면 CPU와 메모리 사용량을 크게 증가시킨다. 또 문제는 다음 페이지 검색할 때의 인덱스 변경이 있었다면 인덱스 상태가 동일하지 않기 때문에 누락될 수 도 있으므로 from,size 페이지네이션은 사용하지 말아햐 한다.

from+size 가 기본 1만이 넘어가면 검색은 수행이 거부된다.

scroll (전체 순회할 때 유용)

-검색 조건 만족하는 전체 문서 순회할 때 사용한다. 최초 검색 시의 문맥(search context) 이 유지되므로 중복이나 누락이 발생하지 않는다.

-"score":"1m" 에서 시간 설정은 배치와 배치 사이를 유지할 정도의 시간으로 지정하면 된다.

-정렬여부과 상관없다면 “sort”: [”_doc”] _doc 정렬을 지정하는 것이 좋다. 그러면 유사도 점수를 계산하지 않으며 정렬을 위한 별도의 자원도 사용하지 않아 성능 끌어올릴 수 있다.

search_after (추천)

동점 제거용(tiebreaker) 필드 정렬이 필요하다.

GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}      
    ]
}

다음 페이지 검색 시

가장 마지막 문서의 sort 값을 search_after 에 넣는다.

GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "search_after": ["1463538857", "654323"],
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}
    ]
}

tiebreaker 필드로 _id 를 넣는 것은 좋지 않다. doc_values 가 꺼져 있어서 많은 메모리를 사용하기 때문이다. 필요하면 _id 값이 있는 새 필드를 파는 게 좋다.

point in time API

POST /my-index-000001/_pit?keep_alive=1m

검색 대상의 상태 고정할 때 사용한다. id 난수 값을 반환한다.

keep_alive: 상태 유지할 시간

GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "pit": {
         "id":  "[_pit 에서 준 값]", 
          "keep_alive": "1m"
    },
    "sort": [
        {"date": "asc"}
    ]
}

pit 값으로 검색하면

  • 인덱스명을 지정안해도 된다. pit 지정하는 것 자체가 검색 대상을 지정하는 것이기 때문이다.
  • tie breaker 필드를 지정안 해도 된다. 정렬이 하나있다면 _shard_doc 이라는 동점 제거용 필드 오름차순 정렬이 맨 마지막에 자동으로 추가된다.
    • 그래서 search_after 사용하려면 정렬 필드 최소 하나는 지정해야 한다.

<정렬>

size:0 으로 하면 문서 수집도 안 하고 점수도 계산 안해서 성능 상 이득이다.

cardinality

POST /sales/_search?size=0
{
  "aggs": {
    "type_count": {
      "cardinality": {
        "field": "type",
        "precision_threshold": 100 
      }
    }
  }
}

precision_threshold

  • 높일수록 정확도가 높고 메모리를 더 사용한다.
  • precision_threshold 가 최종 cardinality 보다 높으면 정확도가 충분히 높다.
  • 기본값은 3000, 최대 4000
  • 필드의 cardinality 가 높고 낮고 상관없이 메모리 사용량은 precision_threshold에만 영향 받는다.

집계 캐시

-집계 요청이 들어오면 샤드 요청 캐시에 올린다. 이후 동일한 집계요청이 같은 샤드로 올라오면 캐시를 활용해 그대로 반환한다. 동일한 집계 요청인지 여부는 요청 본문이 동일한가로 구분한다.

-now 가 포함된 집계 요청은 캐시되지 않은다.

-새 데이터가 색인돼 인덱스 상태가 달라지면 샤드 요청 캐시는 무효화되기 때문에 고정된 인덱스 아니면 캐시 활용도가 떨어진다.

composite 집계

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html

페이지네이션으로 집계 데이터를 가져올 수 있다.

GET my-index-000001/_search?size=0
{
  "aggs": {
    "my_buckets": {
      "composite" : {
        "sources" : [
          {
            "date": {
              "date_histogram" : {
                "field": "date",
                "calendar_interval": "day",
                "offset": "+6h",
                "format": "iso8601"
              }
            }
          }
        ]
      }
    }
  }
}
{
  ...
  "aggregations": {
    "my_buckets": {
      "after_key": { "date": "2015-10-01T06:00:00.000Z" },
      "buckets": [
        {
          "key": { "date": "2015-09-30T06:00:00.000Z" },
          "doc_count": 1
        },
        {
          "key": { "date": "2015-10-01T06:00:00.000Z" },
          "doc_count": 1
        }
      ]
    }
  }
}

after_key 에 가장 마지막 버킷의 key 를 넣어준다.

파이프라인 집계

다른 집계 결과를 집계 대상으로 한다.

<클라이언트 코드>

-transport 클라이언트: 8 버전부터 완전히 제거됨

-고수준 REST 클라이언트: 저수준 클라이언트를 추상화해서 쉽게 쓸 수 있도록 래핑. 7.15 부터 지원 중단.

-자바클라이언트: 8버전 이상이면 고려해라. 8.7 이상이면 쓰는 게 좋다. 7.15 는 베타 버전이고 7.16부터 정식 버전이라 고수준이랑 혼용하는게 좋다.

  • jackson 같은 라이브러리 사용해서 사용자가 지정한 클래스로 직렬화 역직렬화 알아서 해준다. 내부 구현과 결합도를 낮춤
  • 검색 메소드를 쿼리 DSL 과 유사한 느낌으로 내 가독성이 좋다.
  • 8.7 부터 고수준 rest client 의 BulkProcessor 를 대체할 수 있는 BulkIngester 가 추가됐다.

 

레퍼런스

엘라스틱서치 바이블 4장