본문 바로가기
데이터 엔지니어링

Elasticsearch search_after vs scroll API(cursor)

by 내기록 2022. 10. 8.
반응형

 

Elasticsearch의 pagination 검색 방법에 대한 고민

 

ES를 통한 데이터 조회에서 scroll API(cursor) 방식을 사용하다가 한 노드 당 500개 이상의 cursor가 생성되면, cursor들이 삭제되기 전까지 추가적인 cursor가 생성되지 않는 것을 발견했다. 

cursor의 live time을 20m으로 설정했기 때문에 누군가가 고의적으로 혹은 실수로 500개 이상의 요청을 날리고 cursor를 사용하지 않는다면 일시적으로 장애가 발생할 수 있는 상황이었다. 참고로, cursor를 발급받고 사용하면 문제가 되지 않는다. 사용함과 동시에 scroll index count값이 줄어든다.

 

이를 해결하기 위해 알아본 결과 ES에서는 7버전부터 scroll API(cursor) 대신 search_after 방식을 권장한다는 것을 알게 되었다.

하지만 현재 cursor로 조회 시 50ms 가 걸리던 검색이 search_after를 사용하니 150ms 정도가 걸린다는 것을 알게되었다. (kibana 기준) 이를 해결하기 위해 search_after에 대해 더 알아보았다.

 

[아래 내용을 정리하며 든 생각]

현재 pit와 search_after를 함께 사용하고 있고, sort 조건에 string 타입의 컬럼을 넣고있었다. 내용을 정리하며 pit를 사용하면 _shard_doc 이 자동으로 생성되며 한 pit 내에서는 유니크한 값을 가진다는 것을 알게 되었다.

우리는 순서 상관 없이 원하는 데이터를 잘 가져오기만 하면 되기 때문에 따로 sort 조건을 넣지 않는 방법을 사용해 보려고 한다.

 

2022.10.13 추가

pit를 사용하면 sort 조건에 string이 있어도 속도가 크게 차이나지 않는다. 하지만 Pit를 사용하지 않고 string만 sort 조건으로 사용하면 속도가 현저히 느려진다.

그리고 pit의 _shard_doc만 사용해서 search_after를 사용해도 속도의 개선은 거의 없었다.

cursor에 비해 속도가 느리다는 것은 인지하고 사용하는 것이 좋을 것 같다.

 

 

Search_after 방식

If you need to page through more than 10,000 hits, use the search_after parameter instead.

10,000개 이상의 데이터를 조회해야 할 때 search_after 파라미터를 사용해야 합니다.

 search_after는 이전 검색 요청의 마지막 hit에서 tie-breaker를 사용합니다. (think of bookmark)

 

모든 PIT 검색 요청은 명시적으로 제공될 수 있는 _shard_doc이라는 암시적 정렬 타이브레이커 필드를 추가합니다.

 

Search_after 요청에는 정렬 순서가 _shard_doc이고 총 히트가 추적되지 않을 때 더 빠르게 하는 최적화가 있습니다. 
순서에 상관없이 모든 문서를 반복하려면 이 방법이 가장 효율적인 선택사항입니다.

 

_shard_doc 값은 PIT 내의 샤드 인덱스와 Lucene의 내부 문서 ID의 조합으로, 문서마다 고유하며 PIT 내에서 일정합니다. 검색 요청에 타이브레이커를 명시적으로 추가하여 순서를 사용자 정의할 수도 있습니다.

 

 

(요청)

GET /_search
{
  "size": 10000,
  "query": {
    "match" : {
      "user.id" : "elkbee"
    }
  },
  "pit": {
    "id":  "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", 
    "keep_alive": "1m"
  },
  "sort": [ 
    {"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos"}},
    {"_shard_doc": "desc"} // (1)
  ]
}

sort에 (1)로 표시된 값을 보면 shard_doc을 desc로 정렬할 것을 명시하고 있습니다.

이렇게 명시하지 않아도 암시적으로 기본값인 asc로 정렬되어 결과 값에서 확인할 수 있습니다.

 

 

 

(결과)

{
  "pit_id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", 
  "took" : 17,
  "timed_out" : false,
  "_shards" : ...,
  "hits" : {
    "total" : ...,
    "max_score" : null,
    "hits" : [
      ...
      {
        "_index" : "my-index-000001",
        "_id" : "FaslK3QBySSL_rrj9zM5",
        "_score" : null,
        "_source" : ...,
        "sort" : [                                
          "2021-05-20T05:30:04.832Z",
          4294967298 // (1)                             
        ]
      }
    ]
  }
}

sort에 (1)로 표시된 부분을 보면, shard_doc 값을 확인할 수 있습니다. shard_doc 값은 pit_id 내에서 유니크한 값을 가집니다.

 

 

호출 예)

GET _search
{
  "size": 10,
  "query": {
	"match": {
  	"name": "bike"
	}
  },
  "pit": {
	"id": "85ezAwEIcHJvZHVjdHMWSzJZVTdKOU1RLU9SRVBsck43SGg3dwAWS3ptRzhJX3FUZE9iaGVpY3J5VmhTdwAAAAAAAAAACRZVYm5aNWF2U1NqcWJJdXhPc1dyS2hBAAEWSzJZVTdKOU1RLU9SRVBsck43SGg3dwAA",
	"keep_alive" : "2m"
  },
  "sort": [
	{
  	"_score": "desc"
	},
	{
  	"_shard_doc": "asc"
	}
  ]
}

sort 조건으로 _score과 _shard_doc(default)를 사용하기도 합니다.

순서 상관 없이 조건에 맞는 데이터 전체를 조회하는 것이 목적이라면 string type대신 위와 같은 조건을 넣으면 속도가 개선될 수 있지 않을까 합니다.

 

 

* Scroll API에 비해 search_after 속도가 잘 나오지 않는다면 아래 글 참고

참고 : https://discuss.elastic.co/t/recommendation-to-use-search-after-instead-of-scrolling/268293

 

위 글에 의하면 _id로 sort조건을 넣어서 search_after를 사용했을 때 기존의 scroll 방식에 비해 10배 속도가 저하되었다고 합니다.

{"size":5000,"sort":[{"_id.keyword":{"order":"asc"}}]}

and the next chunk query:

{"size":5000,"sort":[{"_id.keyword":{"order":"asc"}}],"search_after":["25acabb7-6a31-470a-a658-7e6a0eeb3f99"]}

 

with 100,000 document we have the following times:

if i use a number field for sorting, i have with search_after ~ 5,0s.

10만개의 document를 조회 했을 때 걸린 시간은 아래처럼 10배 차이가 나지만 number field를 사용해서 정렬을 하면 search_after의 조회시간도 5.0s 정도 걸린다고 합니다.

scoll: 5.1s
search_after: 49s

 

 

 

 

 

Scroll API (cursor)

https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#paginate-search-results

ES에서 scroll API를 사용하는 것을 권장하지 않습니다. 만약 만 건 이상의 데이터를 조회해야 한다면 search_after를 PIT와 함께 사용하는 것을 권장합니다.

 

스크롤은 실시간 사용자 요청을 위한 것이 아니라 대량의 데이터를 처리하기 위한 것입니다.
예를 들어, 하나의 데이터 스트림 또는 인덱스의 내용을 다른 구성의 새 데이터 스트림 또는 index로 reindex할 때

 

스크롤 요청에서 반환되는 결과는 스냅샷과 같이 초기 검색 요청이 이루어진 당시의 데이터 스트림 또는 인덱스의 상태를 반영합니다. 이후 문서 변경(인덱스, 업데이트 또는 삭제)은 이후 검색 요청에만 영향을 미칩니다.

 

 

 

 

 

References

https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#paginate-search-results

https://discuss.elastic.co/t/recommendation-to-use-search-after-instead-of-scrolling/268293

https://opster.com/guides/elasticsearch/how-tos/elasticsearch-pagination-techniques/

https://www.elastic.co/kr/blog/optimizing-sort-queries-in-elasticsearch-for-faster-results

 

반응형

댓글