[컴][db] elasticsearch 에서 paginate (v6, v7)

 pagination / paging / page / offset

elasticsearch 에서 paginate (v6, v7)

from, size

1만개 이하의 hits 라면 from, size 을 이용하면 된다.

GET /_search
{
  "from": 5,
  "size": 20,
  "query": {
    "match": {
      "user.id": "kimchy"
    }
  }
}

그런데 너무 깊게, 또는 한번에 많은 결과를 가져올 것이라면, 이녀석을 사용하지 말라고 한다.

이유는 다음과 같다.

Search request 들은 일반적으로 여러 shard 들에 걸쳐있는데, 각 shard 는 “요청받은 hit들” 과 “이전 모든 page들의 hit들”(any previous pages) 메모리에 load해야만 한다. “deep page” 들과 “많은양의 결과” 에게 이 작업들은 memory 와 cpu 사용을 현저하게 높여서 성능저하 또는 node failure 를 일으키게 된다.[ref. 1]

그래서 기본 설정은 10,000 hit들 보다 많은 hits 를 paginate 할 것이라면, from, size 를 사용할 수 없다. 이 값은 index.max_result_window index setting 에 의해 설정된다.[ref. 1]

그래서 1만개 이상의 hits 를 paging 하려면, search_after parameter 를 사용하라고 한다.

scroll 도 있지만, v7에선 search_after 를 사용하는 것을 권장하는 듯 하다.(참고)

version 6 에서는 아직 search_after에 PIT 가 없기 때문에, scroll 을 사용하는 것이 나은 듯 하다.

명확하진 않지만, “1만개의 hits” 를 확인할 때는 query 를 했을때 나오는 결과값에 보면, hits.total 값이 있는데, 이 값을 보고 1만 hits 가 넘는 경우에는 사용하지 않으면 될 것 같다.

search after

_shard_doc

PIT 는 version 7 부터 들어갔다.

  • 모든 PIT search request 에는 sort tiebreaker field 인 _shard_doc 라는 field 가 들어가게 된다. 이 tiebreaker field 는 각 document 마다 unique 한 값을 가져야만 한다. 이 tiebreaker field 가 없으면 paged result 는 hits 를 빼먹거나, 중복된 hits 를 보여줄 수 있다.
  • sort 할 때 document 마다 있는 unique 값이 사용되어져야 한다. 그렇지 않으면, paged result 의 값이 중복되거나, 빠지거나 한다.[ref. 2, v6 의 document]
  • _id 가 unique 한 field 이지만, 이것을 사용하는 것은 추천하지 않는다. _id field 에 대해서 doc value 가 disabled 되어 있기 때문이다. 그래서 version 6 에서는 _id field 의 복사본을 사용하는 방법을 취한다. [ref. 2, v6 의 document]
  • ref. 3 에 따르면, version 6에서는 _id 를 대신할만한 unique value 를 직접 넣어줘야 하는 것 같다. 글의 느낌은 document 에 아예 기록해 놓으라는 느낌이다.
  • search_after 가 완전하게 또는 부분적으로 tiebreaker 값과 match 되는 첫번째 document 를 찾는다.
  • search after request 들은 sort order 가 _shard_doc 이면서 total hits 가 추적되지 않을때 더 빠르게 최적화 되어 있다. 순서에 상관없이 모든 documents 를 iterate 하길 원한다면, 이것이 가장 효과적이다.
  1. pit id 를 얻어온다.
  2. pit 를 넣고, query
POST /my-index-000001/_pit?keep_alive=1m
GET my-index-000001/_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", "numeric_type" : "date_nanos" }}
  ]
}
GET my-index-000001/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    },
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}      
    ]
}

scroll

pagination 을 위한 함수는 아닌듯 하다. 그저 많은 page 를 불러오려할 때 유용해 보인다. 그래서 v6에서 거대한 page에 대한 pagination 을 하려면, sort, 와 page 의 구분은 다른 query 조건을 줘서 스스로 해야 할 듯 보인다. search_after 도 존재는 하지만, v7에 비하면 불완전해 보인다.

code example

const client = new Client({ node: 'http://3.112.194.162:9200' })
const result = await client.search({
  index: 'my-index-001',
  // keep the search results "scrollable" for 30 seconds
  scroll: '30s',
  size: 10,
  // filter the source to only include the quote field
  _source: ['s.d'],
  body: {
    "query": {
      "bool": {
        "must": [
          {
            "match": {
              "myid": "3220"
            }
          },
          {
            "range": {
              "s.date": {
                "gte": 1627776000000, // 2021-08-01 GMT
                "lt": 1630454400000,  // 2021-09-01 GMT
                "format": "epoch_millis"
              }
            }
          }
        ],
        "filter": [],
        "should": [],
        "must_not": []
      }
    }
  }
})
if (result.statusCode != 200) {
  console.error(result)
  return
}
result.body.hits.hits.map((val, i)=>{
  console.log(JSON.stringify(val))
})


// scroll next
const scrollId = result.body._scroll_id
let totalReqHitCount = 0
while(true){
  const res2 = await client.scroll({
    scroll_id: scrollId,
    scroll: '30s'
  })
  if(res2.statusCode != 200){
    console.error(JSON.stringify(res2))
    break
  }
  this._doSomething(res2)
  const hits = res2.body.hits
  totalReqHitCount += hits.hits.length
  if (hits.total === totalReqHitCount) {
    break
  } 
}


client.clearScroll({
    scroll_id: result.body._scroll_id,
})

결과:

'{"_index":"my-index-001","_type":"as","_id":"478274297bf453483e792499fd6ad4a0ad9c1b11::f2b8577cfc7c41bdb18ec2b57c881676","_score":2,"_source":{"s":{"date":1627782547273}}}'

'{"_index":"uh-as-202108","_type":"as","_id":"b496b241b720d98687de9df760ec497245fbc7b3::230f53df51aa4617a88b76f573e73a1a","_score":2,"_source":{"s":{"date":1627928148753}}}'

sort _doc

search after 의 _shard_doc 같은 느낌이다. 순서에 상관없이 빠르게 가져올 때 좋다고 한다.

GET /_search?scroll=1m
{
  "sort": [
    "_doc"
  ]
}

Reference

  1. Paginate search results | Elasticsearch Guide [7.15] | Elastic
  2. Search After | Elasticsearch Guide [6.7] | Elastic
  3. [Alerting] Add a tie breaker field to alerts · Issue #62002 · elastic/kibana · GitHub

댓글 없음:

댓글 쓰기