[DEV] 기록

[elastic search] document 10,000개 이상 검색 하는 방법

꾸준함. 2021. 2. 5. 23:19

개요

elastic search 같은 경우 효율성을 위해 데이터들을 하나의 db가 아닌 여러 shard들에 데이터를 분산해서 저장하고 데이터들을 모을 때 shard들로부터 데이터를 모아 정렬한 뒤 반환하는 과정을 거치기 때문에 최대 검색 document 개수를 10,000개로 제한하고 있습니다.

현재 개발하는 서비스의 요구사항 중 하나가 모든 이력을 elastic search로부터 불러와 한 페이지당 10개씩 뿌려주는 화면 구현인데, 서비스 런칭 기간이 길어지면 필연적으로 document가 10,000개를 초과할 수밖에 없다는 문제가 발생했습니다.

일반적인 RDBMS를 사용했더라면 하나의 데이터베이스에 모든 데이터가 저장되어있으므로 START와 OFFSET 키워드를 통해 페이징을 간단하게 구현하면 됐겠지만 앞서 설명했다시피 elastic search 같은 경우 데이터들이 여러 shard들에 나뉘어 저장되어있기 때문에 다른 방식으로 구현해야 했습니다.

-> 물론, 전체 document 개수가 10,000개 이하일 경우 from과 size 키워드를 통해 간단하게 구현하시면 됩니다.

(단, from: 9990, size: 20 처럼 index가 10,000이 넘어갈 경우 query_phase_execution_exception 에러가 발생합니다.)

 

해결법

1. index.max_result_window 사이즈 늘리기

가장 근본적인 해결 방법은 설정 내 max_result_window 크기를 default인 10,000보다 크게 설정하는 방법입니다.

PUT index_name/_settings
{
	"max_result_window": [원하는 크기]
}

하지만, 디폴트 값을 변경한다면 과도한 리소스 사용으로 인해 성능 문제를 야기하기 때문에 추천드리지 않습니다.

 

2. search_after 필드 사용하기

search_after 같은 경우 라이브 커서를 제공하여 다음 페이지를 조회할 수 있도록 해줍니다.

검색을 할 때 sort 필드를 통해 정렬 조건을 부여하면 결과 값으로 sort 필드를 반환하는데 다음 페이지를 검색하기 위해 search_after 필드 내 기존 결과 값 sort 필드 내용을 넣어주면 다음 페이지를 조회할 수 있습니다.

현재 버전(7.10)에서는 search_after와 point in time(PIT)를 함께 사용하는 것을 권장합니다.

 

* 주의: collapse나 aggregation은 search_after를 지원하지 않습니다.

 

초기 검색 예시

GET index_name/_search
{
	"size": ...,
	"query": {
    	"bool": {
        	"must": [
            	...
            ]
        }
    },
    "sort": [
    	...
    ]
}

 

검색 결과 예시

"took" : 84,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 10000,
    "max_score" : null,
    "hits" : [
      {
       ...,
        "_source" : {
          ...
        },
        "sort" : [
          temp1
        ]
      },
      {
        ...,
        "_source" : {
          ...
        },
        "sort" : [
          temp2
        ]
      },
      {
        ...,
        "_source" : {
          ...
        },
        "sort" : [
          temp3
        ]
      }
    ]
  }
}

 

다음 페이지 검색 예시

맨 마지막 sort 필드 내용을 search_after 필드 내 넣어주면 다음 페이지를 검색할 수 있습니다.

GET index_name/_search
{
	"size": ...,
	"query": {
    	"bool": {
        	"must": [
            	...
            ]
        }
    },
    "search_after": [temp3],
    "sort": [
    	...
    ]
}

 

저 같은 경우 검색할 때 aggregation을 적용했기 때문에 search_after 필드는 해결책이 되지 못했습니다.

 

3. Scroll API 사용

기존에는 10,000개 이상의 document들에 대해 페이징을 적용할 때 scroll api를 사용하는 것이 권장되었지만 7 버전이 되면서 상황이 바뀌었습니다.

실제 공식 문서를 들어가면 Scroll API와 관련된 설명에 아래와 같은 주의사항이 적혀있습니다.

We no longer recommend using the scroll API for deep pagination. If you need to preserve the index state while paging through more than 10,000 hits, use the search_after parameter with a point in time (PIT).

 

따라서, 사용 방법 설명은 생략하도록 하겠습니다.

 

4. Composite Aggregation 사용

앞서 search_after를 설명했을 때 언급했듯이 저는 검색할 때 aggregation을 적용했기 때문에 composite aggregation을 통해 10,000개 이상의 document들을 페이징을 통해 불러오도록 구현했습니다.

기존에는 aggregation 대신 collapse를 통해 검색을 했었는데 아쉽게도 collapse는 성능이 좋은 대신 페이징을 지원하지 않았습니다.

 

제가 composite aggregation을 적용한 방식은 아래와 같습니다.

 

초기 검색 예시

GET index_name/_search
{
	"size": 0,
	"query": {
    	"bool": {
        	"must": [
            	...
            ],
            "must_not": [
            	...
            ]
        }
    },
    "aggs": {
    	"example": {
        	"composite": {
            	"size": 5000,
                "sources": [
                	"product": {
                    	"terms": {
                        	"field": ...
                        }
                    }
                ]
            }
        }
    }
}

 

검색 결과 예시

{
	"took": 130,
    "timed_out": false,
    "_shards": {
    	"total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
    	"total': {
        	"value": 10000,
            "relation": "gte"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
    	"example": {
        	"after_key": {
            	"product": "temp"
            },
            "buckets": [
            	...
            ]
        }
    }
}

 

다음 검색 예시

after 필드 내 검색 결과로 반환된 after_key 필드 내용을 넣어주면 다음 document들을 조회할 수 있습니다.

GET index_name/_search
{
	"size": 0,
	"query": {
    	"bool": {
        	"must": [
            	...
            ],
            "must_not": [
            	...
            ]
        }
    },
    "aggs": {
    	"example": {
        	"composite": {
            	"size": 5000,
                "sources": [
                	"product": {
                    	"terms": {
                        	"field": ...
                        }
                    }
                ],
                "after": {
                	"product": "temp"
                }
            }
        }
    }
}

 

* 주의할 점: 위 방식을 적용할 경우 정렬을 terms 필드 내 field 기준으로만 할 수 있다는 제약 조건이 있습니다. 따라서 저는 discuss.elastic.co/t/composite-aggregation-order-by/139563 에서 추천한 방법처럼 일단 composite aggregation을 통해 결과물을 모두 받고 서비스단에서 정렬을 진행했습니다.

 

[출처]

discuss.elastic.co/t/paginating-result-set-greater-than-10000-with-aggregations-possible-options/119673/5

stackoverflow.com/questions/41655913/how-do-i-retrieve-more-than-10000-results-events-in-elastic-search

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

discuss.elastic.co/t/how-to-increase-the-default-size-limit-from-10000-to-1000000-in-elasticsearch/208807

www.elastic.co/guide/en/elasticsearch/reference/current/scroll-api.html

medium.com/@sourav.roy_4059/elasticsearch-pagination-by-scroll-api-68d36b8f4972

반응형