Pagination (PIT)

기본적으로 Elasticsearch에서의 검색은 상위 10개의 결과를 반환합니다.
더 많은 양의 데이터를 탐색하려면 search API의 from, size 파라미터를 조정하여 사용하면 됩니다.

from: 탐색의 시작점. default = 0
size: 탐색할 문서 수.

GET /_search

{

  "from": 5,

  "size": 20,

  "query": {

    "match": {

      "user.id": "kimchy"

    }

  }

}

from과 size를 이용해 너무 깊이(많게) 페이징하거나 요청하지 않는 것이 좋습니다.

search request는 일반적으로 여러 샤드에 대해서 요청됩니다. 대규모의 Request가 들어올 경우 각 샤드들에서 많은 정보들을 메모리에 로드하게 되어 메모리와 CPU 사용량이 크게 증가해 성능이 저하되거나 노드 오류가 발생할 수 있습니다.

기본적으로 from, size 파라미터를 사용해서 최대 10000건의 document를 검색할 수 있습니다.
해당 값은 index.max_result_window 인덱스 세팅으로 설정되어 있습니다.

search_after

10000건 이상을 검색하기 위해 search_after 파라미터를 사용합니다. search_after 파라미터를 사용해 이전에 검색이 완료된 부분의 다음 부분을 이어서 검색을 할 수 있습니다.

search_after를 사용하기 위해서는 같은 query와 sort 기준을 가진 여러 개의 search request가 필요합니다.
문제는, search request 중간에 refresh가 발생하게 되면 검색 결과의 순서에 차이가 발생하게 되어 각 페이지들 사이의 결과들이 차이가 나게 됩니다. 

그러한 문제를 막기 위해서 PIT를 사용하여 현재 인덱스의 상황을 유지한 후, 검색을 실행합니다.

Point in Time API (PIT)

search request는 대상 인덱스의 최신 데이터에 대해서 실행됩니다. 따라서 앞서 이야기한 것처럼 여러 번의 search request를 사용해 검색을 진행하게 되면 검색 대상 데이터에 변화가 있는 상태에서 결과를 받게 되는 경우가 발생할 수 있습니다. 

PIT는 특정 시점에서의 해당 인덱스의 데이터를 보여주는 View입니다. PIT를 생성 후 search_after 검색을 하게 되면 인덱스가 중간에 업데이트 되어도, 검색 시점에서의 인덱스 데이터를 기준으로 하여 검색이 진행되므로 검색 결과의 변동 없이 검색을 실행할 수 있습니다.


모든 PIT 검색 결과는 명시적으로 제공되는 _shard_doc이라는 정렬 순위 결정(tiebreaker) 필드를 제공합니다. 순위 결정 필드가 없을 경우, 페이징된 결과가 누락되거나 중복될 수 있으므로, pit를 사용하지 못하는 경우에도 임의로 순위 결정(tiebreaker) 필드를 포함하는 것을 추천합니다.

조건 확인

PIT API를 사용하기 위해 read 인덱스 권한이 필요합니다.

사용 예시

search request를 보내기 전에 명시적으로 PIT API를 사용해야 합니다.

keep_alive 파라미터는 Elasticsearch에 PIT를 활성 상태로 유지해야 하는 기간을 설정합니다.

요청

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

결과

{

  "id" : "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFmNWVWppcGIzU0NLYjhKYmgzSmVibmcAAAAAAAAdvRoWX2JfVXNiY3NUS3FhdFp3MjdhZlVnUQABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA=="

}

결과에서 확인한 id를 search request의 pit 파라미터에 적용시킵니다.

POST /_search

{

    "size": 100,

    "query": {

        "match" : {

            "data" : "1"

        }

    },

    "pit": {

    "id":  "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFmNWVWppcGIzU0NLYjhKYmgzSmVibmcAAAAAAAAdvRoWX2JfVXNiY3NUS3FhdFp3MjdhZlVnUQABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA==", 

    "keep_alive": "1m"  

    }

PIT 파라미터를 사용하는 search request에는 인덱스 이름, Routing, Preference 값을 지정하지 않습니다.

PIT 내에 이미 해당 정보들이 복사되어있기 때문에 PIT ID만을 사용하여 검색을 진행합니다.

PIT 활성 유지

keep_alive를 통해 앞서 PIT의 유지 기간을 설정했습니다.

keep_alive 파라미터의 값은 다음 Search request를 위해서 유지할 수 있을 정도의 시간만 설정해주는 것을 권장합니다. PIT가 활성화 된 기간이 길 수록 Elasticsearch의 전반적인 속도가 느려질 수 있기 때문입니다.

Elasticsearch의 백그라운드 Merge 프로세스는 작은 세그먼트들을 새로운 큰 세그먼트로 Merge해 최적화합니다. 그 과정에서 필요 없어진 작은 세그먼트들이 삭제가 되며 검색 속도 최적화, 용량 확보등의 작업이 이루어집니다. 하지만 PIT가 활성화된 경우, PIT가 가지고 있는 순간의 데이터를 유지하기 위해 오래된 세그먼트들이 삭제되는 것을 막게 됩니다. 오래된 세그먼트를 유지하기 위해서는 더 많은 디스크 공간과 파일 핸들링이 필요하기 때문에, 필요 이상의 기간동안 PIT가 활성화되지 않도록 확인이 필요합니다.

PIT API 종료

PIT는 자동적으로 keep_alive에 설정된 시간이 지나게 되면 종료됩니다.

하지만 앞서 이야기한 것처럼 PIT를 유지하는데는 비용이 들기 때문에, search_request에 더 이상 PIT가 사용되지 않으면 종료하는 것이 좋습니다.

요청

DELETE /_pit

{

    "id" : "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFk5IMlNOUnhDUWdhS1oxZlFLRnBlS0EAAAAAAAAPkJgWWVdwS2lPSkNTZ09pZFA5Ui1hdDN5ZwABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA=="

}

결과

{

  "succeeded" : true,

  "num_freed" : 1

}

검색 슬라이싱

 문서의 양이 많은 인덱스에 대해서 pagination 검색을 수행할 때에는 검색 속도의 향상을 위해 여러 조각으로 슬라이싱해 검색하는 것이 도움이 될 수 있습니다.

요청

GET /_search

{

  "slice": {

    "id": 0,                      

    "max": 2                      

  },

  "query": {

    "match": {

      "data": "5"

    }

  },

  "pit": {

    "id": "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFmNWVWppcGIzU0NLYjhKYmgzSmVibmcAAAAAAAAd0RYWX2JfVXNiY3NUS3FhdFp3MjdhZlVnUQABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA=="

  }

}


GET /_search

{

  "slice": {

    "id": 1,

    "max": 2

  },

"query": {

    "match": {

      "data": "5"

    }

  },

  "pit": {

    "id": "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFmNWVWppcGIzU0NLYjhKYmgzSmVibmcAAAAAAAAd0RYWX2JfVXNiY3NUS3FhdFp3MjdhZlVnUQABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA=="

  }

}

결과

{

  "pit_id" : "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFmNWVWppcGIzU0NLYjhKYmgzSmVibmcAAAAAAAAd0RYWX2JfVXNiY3NUS3FhdFp3MjdhZlVnUQABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA==",

  "took" : 0,

  "timed_out" : false,

  "_shards" : {

    "total" : 1,

    "successful" : 1,

    "skipped" : 0,

    "failed" : 0

  },

  "hits" : {

    "total" : {

      "value" : 1,

      "relation" : "eq"

    },

    "max_score" : 2.0794415,

    "hits" : [

      {

        "_index" : "my-index-000001",

        "_id" : "z8YU1oEBiPJISYxEVGON",

        "_score" : 2.0794415,

        "_source" : {

          "data" : "5"

        }

      }

    ]

  }

}


{

  "pit_id" : "58HpAwEPbXktaW5kZXgtMDAwMDAxFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAFmNWVWppcGIzU0NLYjhKYmgzSmVibmcAAAAAAAAd0RYWX2JfVXNiY3NUS3FhdFp3MjdhZlVnUQABFllqVnlBYUJuVER5VzRrc1VIbmdzV2cAAA==",

  "took" : 0,

  "timed_out" : false,

  "_shards" : {

    "total" : 1,

    "successful" : 1,

    "skipped" : 0,

    "failed" : 0

  },

  "hits" : {

    "total" : {

      "value" : 0,

      "relation" : "eq"

    },

    "max_score" : null,

    "hits" : [ ]

  }

}

첫 번째 request는 첫 번째 슬라이스의 검색 결과를, 두 번째 request는 두 번째 슬라이스의 검색 결과를 반환합니다. 최대 슬라이스 수가 2로 설정되어 있으므로 두 request의 요청 결과의 합은 슬라이싱 없이 전체에 대한 pit 검색과 동일합니다.

슬라이싱은 샤드에서 먼저 수행된 후 각 샤드에서 로컬로 수행됩니다. 로컬에서는 Lucene document ID를 기반으로 인접한 범위로 분할합니다.

예를 들어 샤드의 수가 2이고 유저가 슬라이스를 4개 요청한 경우 슬라이스 0, 2는 첫 번째 샤드에, 슬라이스 1, 3은 두 번째 샤드에 할당됩니다.

출처, 참고 문서

LinkedIn

이동훈 (Donghoon Lee)