Elasticsearch KNN에서 k와 num_candidates를 따로 봐야 했던 이유

Elasticsearch에 벡터 검색을 붙이면서 처음에는 knum_candidates를 거의 같은 종류의 설정으로 생각했다.

둘 다 결국 몇 개의 문서를 검색할 것인지 결정하는 숫자처럼 보였기 때문이다.

내 도서 검색 코드에서는 외부에서 받은 k를 그대로 Elasticsearch에 넣지 않고 다음처럼 조금 더 많은 후보를 확보하도록 설정했다.

search_k = k + 10
num_candidates = k + 20

예를 들어 API에서 k=10을 요청했다면 내부에서는 그보다 많은 결과와 후보를 검색하는 방식이다.

처음에는 이것을 단순히 “후보를 조금 넉넉하게 가져온다” 정도로 생각했다.

그런데 KNN 검색을 계속 다루다 보니 중요한 질문은 숫자를 몇 개 더 가져오느냐가 아니었다.

왜 후보가 부족하면 원하는 결과를 놓칠 수 있는가?

그리고 반대로,

후보를 많이 가져오면 정확도가 좋아질 가능성이 있는데 왜 무조건 크게 설정하지 않는가?

이 두 질문을 이해해야 knum_candidates를 제대로 설정할 수 있었다.


먼저 KNN 검색이 모든 벡터를 정확하게 비교하는 것은 아니다

벡터 검색을 처음 접했을 때 나는 검색 과정을 다음과 비슷하게 상상했다.

사용자 검색 벡터
        ↓
모든 도서 벡터와 비교
        ↓
유사도 계산
        ↓
가장 가까운 10개 선택

이 방식이라면 이해하기 쉽다.

도서가 100만 권 있으면 100만 개의 벡터와 모두 비교하고 그중 가장 가까운 10개를 선택하면 된다.

이런 방식은 정확한 nearest-neighbor search에 가깝다.

문제는 데이터가 커질수록 매 검색마다 모든 벡터와 비교하는 비용이 커진다는 것이다.

Elasticsearch의 일반적인 approximate kNN 검색에서는 HNSW 같은 그래프 기반 인덱스를 이용해서 전체 벡터를 모두 탐색하지 않고 가까울 가능성이 높은 영역부터 탐색한다.

여기서 approximate, 즉 근사 검색이라는 말이 중요하다.

빠른 대신 처음부터 모든 문서를 조사하는 것이 아니다.

따라서 실제로 가장 가까운 문서가 존재하더라도 검색 과정에서 그 문서가 후보군 안으로 들어오지 못하면 최종 결과에서도 볼 수 없다.

이 지점에서 num_candidates가 중요해진다.


num_candidates가 부족하면 왜 좋은 결과를 놓칠 수 있을까

Elasticsearch의 approximate kNN 검색에서는 shard마다 먼저 num_candidates만큼의 후보를 수집하고, 그 후보를 이용해 최종적으로 가까운 k개의 결과를 선택한다.

개념적으로 단순화하면 다음과 같다.

전체 벡터
   ↓
HNSW 그래프 탐색
   ↓
num_candidates 만큼 후보 확보
   ↓
후보들의 유사도 비교
   ↓
상위 k개 선택

여기서 굉장히 중요한 점이 있다.

최종 선택은 처음 확보한 후보 안에서만 할 수 있다.

예를 들어 정말 좋은 도서가 전체 데이터에서는 사용자 검색어와 매우 가까운 벡터를 가지고 있다고 하자.

그런데 approximate search의 초기 탐색 과정에서 그 책이 후보군에 포함되지 않았다면 어떻게 될까?

최종 단계에서 아무리 정확하게 유사도를 계산해도 그 책은 선택할 수 없다.

애초에 후보 목록에 없기 때문이다.

즉 문제를 다음처럼 볼 수 있다.

좋은 문서 존재
      ↓
초기 후보 탐색에서 발견하지 못함
      ↓
num_candidates 후보에 포함되지 않음
      ↓
최종 k 선정 대상에도 없음
      ↓
검색 결과에서 누락

이것이 num_candidates를 지나치게 작게 잡았을 때 검색 품질이 떨어질 수 있는 가장 중요한 이유다.

단순히:

후보가 적으니까 정확도가 떨어진다.

가 아니다.

좀 더 정확하게 말하면:

approximate search가 실제 nearest neighbor를 후보군 안으로 가져올 기회가 줄어들 수 있기 때문에 최종 결과의 recall이 떨어질 수 있다.

Elastic 공식 문서에서도 HNSW 검색에서 num_candidates를 늘리는 것이 일반적으로 recall과 정확도를 개선할 가능성이 있으며, 그 대신 latency가 증가하는 trade-off가 있다고 설명한다.


예를 들어 왜 k=10, num_candidates=10이 답답할 수 있을까

설명을 위해 단순한 상황을 생각해볼 수 있다.

내가 최종적으로 10개의 책을 가져오고 싶다고 하자.

k = 10

그리고 후보도 정확히 10개만 확보한다면:

num_candidates = 10

검색 엔진 입장에서는 사실상 처음 탐색한 후보 10개에서 최종 10개를 결정해야 하는 여유 없는 구조가 된다.

후보 탐색 단계에서 조금이라도 더 좋은 문서를 놓쳤다면 최종 단계에서 이를 보완할 여지가 작다.

반면 후보를 더 넓게 확보한다면:

k = 10
num_candidates = 50

최종적으로 보여줄 책은 여전히 10개지만 검색 엔진은 더 넓은 후보를 비교할 수 있다.

후보 50개
    ↓
유사도 비교
    ↓
그중 상위 10개

따라서 최종 10개에 포함될 만한 좋은 문서가 후보군 안으로 들어올 가능성을 높일 수 있다.

다만 여기서 50이라는 숫자가 좋다는 의미는 아니다.

이것은 원리를 설명하기 위한 예일 뿐이며, 적절한 후보 수는 데이터 규모와 인덱스 구성, shard 수, 필터 조건 등에 따라 달라진다.


그렇다면 num_candidates를 아주 크게 하면 되지 않을까

여기까지 이해하면 자연스럽게 다음 생각이 든다.

후보를 많이 찾을수록 놓칠 가능성이 줄어든다면 그냥 num_candidates를 매우 크게 하면 되는 것 아닌가?

실제로 검색 품질 하나만 놓고 보면 후보를 더 많이 탐색하는 것이 도움이 될 가능성이 있다.

하지만 검색 서비스에는 정확도만 있는 것이 아니다.

후보를 많이 탐색하려면 검색 엔진이 그만큼 더 많은 그래프 노드를 살펴보고 더 많은 후보를 평가해야 한다.

즉 다음 두 목표가 충돌한다.

더 많은 후보 탐색
      ↓
좋은 문서를 찾을 가능성 ↑

하지만

더 많은 후보 탐색
      ↓
검색 작업량 ↑
      ↓
응답 지연 가능성 ↑

Elastic에서도 num_candidates를 HNSW approximate kNN의 주요한 검색 시점 speed/accuracy 조절값으로 설명한다. 값을 증가시키면 recall과 accuracy 개선 가능성이 있는 대신 latency가 높아질 수 있다.

따라서 목표는:

최대한 많은 후보를 찾는다.

가 아니다.

오히려:

검색 결과가 충분히 안정되는 범위까지 후보를 늘리고, 그 이후에는 불필요하게 검색 비용을 증가시키지 않는다.

에 가깝다.


여기서 knum_candidates의 역할이 갈린다

이제 두 숫자를 따로 볼 수 있다.

내가 이해하는 기준은 다음과 같다.

k

최종적으로 얼마나 많은 가까운 결과가 필요한가?

num_candidates

그 결과를 찾기 위해 검색 과정에서 얼마나 넓은 후보를 살펴볼 것인가?

예를 들어 최종 서비스에서 10개의 후보가 필요한데 검색 엔진 내부에서는 30개나 50개의 후보를 탐색할 수도 있다.

서비스가 필요한 결과 수
         ↓
         k

검색 엔진이 살펴볼 후보 폭
         ↓
 num_candidates

두 숫자가 관련되어 있지만 같은 목적의 숫자는 아니다.

현재 Elastic의 kNN retriever 문서에서도 k는 반환할 nearest neighbors 수이고, num_candidates는 shard별 초기 후보군의 크기로 정의되어 있다. 또한 knum_candidates보다 클 수 없다.


내 서비스에서는 libCode 필터 때문에 한 가지를 더 생각해야 했다

내가 구현한 도서 검색에서는 전체 도서를 대상으로 검색하지 않는다.

사용자가 특정 도서관을 선택했다면 해당 도서관이 보유한 데이터 안에서 검색해야 한다.

그래서 libCode를 필터 조건으로 사용했다.

구조를 단순화하면 다음과 같다.

{
  "knn": {
    "field": "embedding",
    "query_vector": [ ... ],
    "k": 20,
    "num_candidates": 30,
    "filter": {
      "terms": {
        "libCode": ["128057"]
      }
    }
  }
}

처음에는 필터가 붙으면 오히려 검색할 데이터가 줄어드니 항상 더 빨라질 것이라고 생각하기 쉽다.

일반적인 데이터베이스 검색에서는 상당히 자연스러운 생각이다.

100만 건에서 찾는 것보다 조건을 걸어 1만 건에서 찾는 것이 일반적으로 쉬울 것처럼 보인다.

그런데 HNSW 기반 approximate kNN에서는 필터가 반드시 그런 식으로 동작하지 않는다.

Elastic 공식 문서에서도 필터가 있는 approximate kNN에서는 조건에 맞는 num_candidates를 확보하기 위해 그래프를 더 탐색해야 할 수 있기 때문에, 필터가 오히려 검색 비용을 증가시킬 수 있다고 설명한다.

왜 그런지 생각해보면 이해하기 쉽다.


필터가 있으면 왜 더 많은 탐색이 필요할 수 있을까

예를 들어 벡터상 가까운 책을 탐색했다고 하자.

검색 엔진이 가까운 후보를 하나 발견했다.

그런데 그 책의 libCode가 사용자가 선택한 도서관과 다르다.

벡터상 매우 가까운 책
        ↓
하지만 libCode 불일치
        ↓
결과 후보로 사용할 수 없음

또 다른 가까운 책을 찾았다.

그것도 다른 도서관의 책이다.

가까운 책 발견
  → 필터 탈락

가까운 책 발견
  → 필터 탈락

가까운 책 발견
  → 필터 통과

검색 엔진 입장에서는 num_candidates=30이라고 해도 단순히 벡터상 가까운 문서 30개만 발견하면 끝나는 문제가 아니다.

필터까지 통과하는 후보를 충분히 확보해야 한다.

Elastic의 현재 kNN filter는 approximate kNN 과정에서 적용되는 pre-filter이며, 조건을 만족하는 후보들을 확보하도록 검색 과정에 반영된다.

그래서 필터 조건이 강하면 그래프를 더 깊게 탐색해야 하는 상황이 생길 수 있다.

이 부분은 내가 libCode처럼 서비스 자체의 필터를 KNN에 적용할 때 특히 중요하게 생각해야 하는 지점이다.


반대로 필터가 너무 강하면 brute force로 바뀔 수도 있다

더 흥미로운 부분도 있다.

현재 Elasticsearch/Lucene 구현에서는 필터를 적용한 결과 문서 수가 num_candidates 이하라면 HNSW 그래프 검색 대신 필터를 통과한 문서에 대해 brute-force 방식으로 검색할 수 있다.

또 HNSW 탐색 도중 검색해야 할 그래프 노드 수가 필터 조건에 맞는 문서 수를 넘어서는 상황에서도 brute-force 방식으로 전환할 수 있다.

이 의미는 꽤 중요하다.

같은 KNN Query를 사용하고 있어도 항상 내부 실행 방식이 똑같다고 단정할 수 없다는 것이다.

예를 들어:

전체 도서 대상 검색

과:

아주 작은 도서관의 libCode 하나만 대상으로 검색

은 데이터 범위가 상당히 다를 수 있다.

필터 결과가 매우 작다면 HNSW approximate search의 장점보다 필터된 문서 전체를 직접 비교하는 것이 더 효율적이라고 Elasticsearch가 판단할 수 있다.

따라서 내 서비스에서 KNN 성능을 측정할 때도 단순히:

num_candidates=30일 때 몇 ms

만 기록하면 부족하다.

어떤 libCode 조건에서 테스트했는지도 같이 기록해야 한다.


shard가 여러 개면 num_candidates를 이해할 때 더 조심해야 한다

또 하나 처음에는 놓치기 쉬운 부분이 shard다.

num_candidates는 전체 Elasticsearch 인덱스에서 딱 그 숫자만큼의 후보를 본다는 의미가 아니다.

Elastic 문서에서는 num_candidatesshard별 후보 수라고 설명한다. 각 shard에서 후보를 확보한 뒤 그 결과를 병합해서 최종 top k를 찾는다.

예를 들어 개념적으로 shard가 여러 개라면:

Shard 1
→ 후보 확보

Shard 2
→ 후보 확보

Shard 3
→ 후보 확보

      ↓

후보 결과 병합

      ↓

Global Top K

와 같은 과정이 들어간다.

따라서 인덱스가 shard 하나일 때와 shard 여러 개일 때 같은 num_candidates를 사용한다고 해서 검색 비용까지 완전히 동일하다고 생각해서는 안 된다.

이것도 단순히 다른 블로그에서:

num_candidates=100 추천

같은 숫자를 가져오면 안 되는 이유 중 하나다.


k가 너무 작을 때는 또 다른 종류의 문제가 생긴다

num_candidates는 검색 품질과 탐색 폭의 문제지만 k 자체도 너무 작으면 문제가 될 수 있다.

특히 내 서비스처럼 검색 결과를 RAG나 추천 단계에서 다시 사용하는 경우 그렇다.

예를 들어 최종 생성 단계에서 충분히 좋은 책을 고르려면 검색 단계에서 여러 후보가 필요하다고 하자.

그런데 KNN 단계에서 처음부터:

k=1

로 한 권만 가져오면 LLM이나 후속 추천 로직은 사실상 선택할 수 있는 것이 없다.

KNN 결과 1권
        ↓
RAG Context 1권
        ↓
LLM

첫 번째 검색 결과가 조금만 빗나가도 뒤 단계에서 복구하기 어렵다.

검색 엔진이 제공하지 않은 책을 후속 로직이 선택할 수 없기 때문이다.

이것은 RAG에서도 상당히 중요한 문제다.

Retrieval 단계에서 빠진 정보는 Generation 단계에서 정상적으로 복원하기 어렵다.

따라서 k는:

사용자 화면에서 몇 개 보여줄 것인가

만 가지고 결정할 숫자는 아니다.

그 검색 결과가 이후 어떤 과정에 사용되는지도 봐야 한다.


그렇다고 k를 크게 만드는 것이 항상 좋은 것도 아니다

반대 방향의 문제도 있다.

RAG를 사용하는 서비스에서:

k=50

으로 책을 50권 가져온다고 생각해보자.

후보가 많으니 좋은 책이 포함될 가능성은 높아질 수 있다.

하지만 그 50권의 설명을 모두 LLM Context에 넣는다면 다른 문제가 생긴다.

검색 결과 증가
     ↓
Context 증가
     ↓
입력 Token 증가
     ↓
비용 증가

그리고

관련성이 낮은 자료까지 포함
     ↓
LLM이 참고할 정보가 복잡해짐

따라서 RAG에서는 검색 recall만 극단적으로 높이는 것이 목표가 아니다.

필요한 정보는 놓치지 않으면서 관련성이 낮은 정보는 최대한 줄이는 것이 더 중요하다.

그래서 내 서비스에서 k를 판단할 때는 Elasticsearch만 보지 않고 그 뒤의 생성 과정까지 같이 봐야 한다.

사용자 질문
   ↓
KNN
   ↓
k개의 도서
   ↓
RAG Context
   ↓
LLM
   ↓
최종 추천

앞 단계의 숫자 하나가 뒤의 토큰 사용량과 추천 품질에도 영향을 준다.


그래서 k+10, k+20이 부족할 수도 있다

내가 사용한 설정은:

search_k = k + 10
num_candidates = k + 20

이었다.

이제 이 값을 조금 더 비판적으로 볼 수 있다.

예를 들어 외부 k가 작으면:

k = 5

search_k = 15
num_candidates = 25

가 된다.

반대로:

k = 100

search_k = 110
num_candidates = 120

이 된다.

문제는 데이터 규모가 커졌다고 해서 +20이라는 고정된 차이가 항상 같은 수준의 탐색 여유를 제공하지는 않는다는 것이다.

k=5에서 25개의 후보와 k=100에서 120개의 후보는 k 대비 후보 비율이 크게 다르다.

k=5
num_candidates=25
→ k의 5배

k=100
num_candidates=120
→ k의 1.2배

따라서 +20이라는 단순한 규칙은 구현하기는 쉽지만 데이터가 커지거나 요청 k가 크게 달라지는 환경에서는 적절한지 다시 검증해야 한다.

이 부분이 지금 내 설정에서 가장 먼저 실험해보고 싶은 지점이다.

현재 Elastic 문서의 최신 kNN API에서는 별도로 지정하지 않았을 때 num_candidates 기본값을 대체로 1.5 × k 범위로 계산하는 동작도 설명되어 있지만, 실제 기본값과 지원 방식은 Elasticsearch 버전/API 형태에 따라 확인해야 한다.

따라서 내가 사용한 k+20 역시 정답으로 보기보다 튜닝이 필요한 초기 운영값이라고 보는 편이 맞다.


“부족하다”를 느낌으로 판단하면 안 된다

검색 결과를 보면서:

결과가 좀 별로네.

라고 느끼는 것만으로 num_candidates가 부족하다고 결론내릴 수도 없다.

검색 품질이 나쁜 원인은 훨씬 많기 때문이다.

예를 들어 기대한 책이 나오지 않았다고 하자.

원인은 다음 중 하나일 수도 있다.

1. 애초에 그 책에 embedding이 없다

아무리 num_candidates를 늘려도 검색할 벡터가 없다면 나올 수 없다.

2. libCode 필터에서 제외됐다

사용자가 선택한 도서관에 해당 책이 없으면 KNN 후보가 되는 것 자체가 맞지 않을 수 있다.

3. embedding을 만들 때 넣은 텍스트가 부족하다

예를 들어 도서 제목만 embedding했다면:

직장 생활이 지쳤을 때 위로받을 책

같은 자연어 질문과 의미적으로 연결할 정보 자체가 부족할 수 있다.

이 경우 num_candidates=1000으로 올려도 embedding 품질 문제는 해결되지 않는다.

4. Query embedding 자체의 문제일 수 있다

문서와 검색어가 같은 임베딩 공간에서 비교 가능한 형태로 생성되지 않았다면 검색 파라미터를 튜닝해도 결과는 계속 좋지 않을 수 있다.

5. 실제로 num_candidates가 부족할 수 있다

앞의 조건들이 정상인데 후보 폭을 늘렸을 때 기대 결과가 반복적으로 상위권에 들어온다면 그때 num_candidates 영향을 의심할 근거가 생긴다.

그래서 나는 검색 결과가 좋지 않을 때 다음 순서로 보는 편이 더 적절하다고 생각한다.

원하는 도서가 인덱스에 있는가?
        ↓
embedding이 존재하는가?
        ↓
libCode 조건을 만족하는가?
        ↓
query vector가 정상인가?
        ↓
현재 KNN 결과 확인
        ↓
k / num_candidates 조정

파라미터 튜닝은 생각보다 뒤쪽에 있다.


실제로 num_candidates가 부족한지 확인하려면 비교 실험이 필요하다

이제는 +10, +20을 그대로 사용하면서 결과가 괜찮아 보인다고 끝내고 싶지는 않다.

실제로 부족한지를 확인하려면 정답에 가까운 기준 데이터를 먼저 만들어야 한다.

예를 들어 검색 질문 30개를 준비한다고 하자.

질문의 종류도 나눈다.

정확한 도서 검색

채식주의자
데미안 헤르만 헤세

주제 검색

파이썬 입문 책
투자를 처음 공부하는 책

자연어 검색

회사 생활 때문에 지쳤을 때 부담 없이 읽을 책

그리고 각 질문에 대해:

최소한 이 책들은 상위권에 들어왔으면 좋겠다.

라는 기대 결과를 직접 정한다.

그 후 같은 query vector를 사용하면서 num_candidates만 변경한다.

20
50
100
200

이때 봐야 할 것은 단순한 검색시간 하나가 아니다.

확인 항목이유
기대 도서가 Top 5에 들어오는가실제 검색 품질 확인
기대 도서의 순위가 바뀌는가후보 증가의 효과 확인
Top 10 결과가 얼마나 변하는가검색 안정성 확인
응답시간은 얼마나 달라지는가품질 대비 비용 확인
libCode에 따라 차이가 있는가필터 영향 확인

이 실험에서:

num_candidates 50
→ 기대 결과 충분

num_candidates 100
→ 거의 동일

num_candidates 200
→ 거의 동일하지만 느려짐

같은 패턴이 실제로 나온다면 200까지 늘릴 이유가 작다.

반대로 후보 수를 늘릴수록 놓치던 결과가 반복적으로 들어온다면 현재 설정이 부족할 가능성을 의심할 수 있다.

이런 비교가 있어야 “부족하다”는 말을 근거 있게 할 수 있다.


검색 품질에서는 결국 Recall을 봐야 한다

이 문제를 좀 더 정확하게 표현하면 num_candidates 튜닝은 Recall과 비용의 균형에 가깝다.

내가 원하는 관련 도서가 실제로 10권 있다고 하자.

검색 결과가 그중 4권만 찾아낸다면 많은 관련 결과를 놓치고 있는 것이다.

후보 폭을 늘렸더니 8권을 찾아낸다면 recall이 좋아졌다고 볼 수 있다.

하지만 후보를 더 늘렸는데도 8권에서 변화가 없다면 그 이후부터는 후보 탐색보다 embedding이나 데이터 품질을 확인하는 것이 더 의미 있을 수도 있다.

그래서 앞으로는 단순히:

검색 결과가 좋아 보인다.

보다:

미리 정한 관련 도서를 몇 개나 다시 찾아왔는가?

를 측정하는 방향으로 검색 품질을 보고 싶다.


결국 숫자보다 중요한 것은 검색 파이프라인 전체였다

KNN을 처음 만들 때는 관심이 숫자에 있었다.

k를 몇으로 하지?

num_candidates를 몇으로 하지?

하지만 실제 서비스를 만들면서는 숫자 하나보다 그 숫자가 들어가는 전체 구조가 더 중요하다는 생각이 들었다.

내 서비스에서는 대략 다음 흐름을 가진다.

사용자 자연어 검색
        ↓
Embedding 생성
        ↓
libCode 필터 적용
        ↓
HNSW approximate KNN
        ↓
num_candidates 후보 탐색
        ↓
상위 k 결과
        ↓
도서 메타데이터
        ↓
RAG Context
        ↓
LLM 추천 문장

여기서 검색 결과가 이상하다고 num_candidates 하나만 바꾸면 앞뒤의 문제를 놓칠 수 있다.

Embedding이 잘못됐을 수도 있다.

필터가 문제일 수도 있다.

후보 수가 부족할 수도 있다.

RAG에서 너무 많은 문서를 사용하고 있을 수도 있다.

그래서 knum_candidates검색 품질을 결정하는 중요한 설정이지만 검색 품질 전체를 결정하는 설정은 아니다.


지금 내 설정을 다시 본다면

현재 사용했던:

search_k = k + 10
num_candidates = k + 20

이라는 방식은 구현 초기에는 이해하기 쉽고 관리하기 편했다.

외부 API가 원하는 결과 수보다 조금 더 넉넉한 후보를 확보한다는 의도도 명확했다.

하지만 지금 다시 보면 검증해야 할 부분이 있다.

첫째, +20이라는 고정값이 k 크기가 달라져도 적절한가.

둘째, libCode별 데이터 규모 차이가 큰데 같은 값을 적용해도 되는가.

셋째, shard 구성이 바뀌었을 때 같은 검색 특성을 유지하는가.

넷째, 후보 수를 더 늘렸을 때 실제 검색 결과가 얼마나 개선되는가.

다섯째, 개선되는 결과에 비해 응답시간 증가가 받아들일 만한 수준인가.

이 다섯 가지를 실제 데이터로 확인하지 않은 이상 현재 설정을 최적값이라고 표현하면 안 된다.

지금 단계에서는 가장 정확하게 다음과 같이 말할 수 있다.

k+10, num_candidates=k+20은 실제 서비스에서 사용한 운영값이다. 하지만 이것이 최적의 값이라는 근거는 아직 부족하고, 검색어 테스트 세트를 이용한 별도의 평가가 필요하다.


마무리

Elasticsearch KNN에서 knum_candidates를 처음 보면 둘 다 단순히 검색 개수를 결정하는 옵션처럼 보인다.

하지만 내부 동작을 이해하고 나면 역할이 상당히 다르다.

k최종적으로 확보하려는 nearest neighbor 수와 관련된다.

num_candidates그 결과를 찾기 위해 approximate search 과정에서 얼마나 넓게 후보를 탐색할 것인지와 관련된다. Elastic 역시 num_candidates를 HNSW 검색에서 speed와 accuracy 사이를 조절하는 주요 검색 파라미터로 설명한다.

후보가 너무 적으면 실제로 관련성이 높은 문서가 초기 후보에 들어오지 못해 최종 결과에서도 사라질 수 있다.

반대로 후보를 무작정 크게 만들면 검색 품질 개선은 거의 없는데 탐색 비용만 증가할 수도 있다.

여기에 내 서비스처럼 libCode 필터가 들어가면 조건을 만족하는 후보를 찾기 위해 그래프를 더 탐색해야 하는 상황도 생길 수 있다. 필터 결과 규모에 따라 Elasticsearch 내부에서 brute-force 방식으로 전환되는 경우도 있다.

그래서 지금은:

num_candidates는 몇으로 하면 좋을까?

라는 질문 자체가 조금 부족하다고 생각한다.

더 정확한 질문은 다음과 같다.

내 데이터와 검색어, 필터 조건에서 원하는 결과를 놓치지 않으면서도 불필요한 탐색을 하지 않는 후보 수는 얼마인가?

이 질문에 답하려면 결국 실제 검색어 세트와 기대 결과를 만들어 측정해야 한다.

그리고 그 실험까지 하고 나면 지금 사용하고 있는 k+10, num_candidates=k+20을 유지할지, 비율 기반으로 바꿀지, 검색 조건에 따라 동적으로 조절할지 판단할 수 있을 것이다.

그 결과는 별도의 테스트를 진행한 뒤 실제 검색 순위와 응답시간을 기준으로 다시 기록하려고 한다.