포스트

Elasticsearch 관련도 점수: query/filter 컨텍스트, boost, 그리고 dis_max

filter 컨텍스트에서 boost가 무시되는 이유, boost가 절대 점수가 아니라 곱셈 계수인 탓에 우선순위 계단이 뒤집히는 구조, 그리고 dis_max와 constant_score로 계단을 보장하는 법.

Elasticsearch 관련도 점수: query/filter 컨텍스트, boost, 그리고 dis_max

should 절에 boost를 100, 80, 10, 1로 매겨 우선순위 계단을 만드는 건 흔한 패턴이다. 문제는 그 계단이 생각대로 서지 않는 경우가 많다는 것이다. 심지어 아예 무시되기도 한다.

원인은 두 가지다 — 점수를 아예 계산하지 않는 컨텍스트에 넣었거나, boost가 절대 점수가 아니라 곱셈 계수라는 걸 놓쳤거나.

multi_match와 정렬 사용법은 MultiMatch와 다중 정렬에서 다뤘다. 여기서는 점수가 만들어지는 과정 자체를 본다.

query 컨텍스트와 filter 컨텍스트

Elasticsearch의 모든 절은 둘 중 하나의 컨텍스트에서 실행된다. 이 구분이 점수의 존재 여부를 결정한다.

query 컨텍스트 — must / should "얼마나 잘 맞는가"를 계산한다 · 문서마다 _score를 산출 · boost가 실제로 적용된다 · 결과 캐시 안 됨 (점수가 문서마다 다르므로) bool.must / bool.should filter 컨텍스트 — filter / must_not "통과하는가"만 판정한다 · 점수를 계산하지 않는다 (yes/no) · boost가 전부 무시된다 · 비트셋으로 캐시 가능 → 재사용 시 빠름 bool.filter / bool.must_not filter에 넣은 절의 boost는 오류를 내지 않는다. 조용히 무시될 뿐이다. 정렬까지 필드 기준(_score가 아님)이면 점수가 아예 쓰이지 않으므로 증상도 안 보인다. 그러다 나중에 누군가 _score 정렬로 바꾸면, 잠자던 boost가 살아나면서 순서가 뒤집힌다.
1
2
3
4
5
{ "bool": {
    "filter": [ { "bool": { "should": [
        { "prefix": { "name": { "value": "강남", "boost": 100 } } }
    ] } } ]
} }

이 코드는 동작하지 않는다. filter 안에 있으므로 boost: 100은 무시된다. 문법 오류가 아니라서 조용히 지나가고, 정렬이 필드 기준이면 아무 증상도 없다. 의도가 코드에 적혀 있는데 작동하지 않는 상태가 남는다.

판단 기준은 명확하다.

이 절의 목적넣을 곳
관련도에 기여해야 한다 (_score 정렬을 쓴다)must 또는 should
걸러내기만 하면 된다 (필드 정렬을 쓴다)filter — 캐시 이득까지 챙긴다

같은 서비스라도 화면마다 다를 수 있다. 관련도순 검색 화면은 must + _score 정렬, 마감일순 목록은 filter + 필드 정렬 — 이 조합이 자연스럽다. 모듈마다 다르게 가는 게 잘못이 아니라, 다르다는 걸 모르고 공통 유틸을 공유하는 게 사고의 원인이다.

boost는 절대 점수가 아니라 곱셈 계수다

must/should에 제대로 넣었다고 해도 계단이 보장되지 않는다. boost는 “이 절의 점수를 N점으로 만들라”가 아니라 “이 절이 낸 점수에 N을 곱하라” 이기 때문이다.

그리고 절 종류에 따라 곱해지는 원본 점수가 다르다.

의도한 계단 prefix 100 > wildcard 80 > phrase 10 prefix (boost 100) 100 constant score 1.0 × 100 — 고정 wildcard (boost 80) 80 constant score 1.0 × 80 — 고정 match phrase (boost 10) 120 BM25 12 × 10 — 변동! 0 50 100 150 결과: phrase(120) > prefix(100) > wildcard(80) — 계단이 뒤집혔다 MultiTermQuery(prefix·wildcard·fuzzy)는 constant score 1.0을 내므로 boost 값이 곧 점수가 된다. match·match_phrase는 BM25 점수를 내는데, 이 값은 term 빈도·문서 길이·필드 통계에 따라 문서마다 다르다. 즉 한쪽은 고정이고 한쪽은 변동이라, boost 숫자만으로는 순서를 보장할 수 없다.

bool.should의 점수는 매칭된 절들의 합이다

두 번째 뒤집힘 원인이 여기 있다. should는 매칭된 절 하나를 고르는 게 아니라 매칭된 모든 절의 점수를 더한다.

필드 목록을 루프 돌며 필드마다 절을 추가하는 코드가 흔한데, 이 경우 여러 필드에서 매칭된 문서는 점수가 누적된다.

bool.should — 매칭된 절의 점수를 모두 더한다 문서 A 이름 필드에 정확히 일치 prefix(이름) 100 100 문서 B 세 필드에 애매하게 걸림 주소 100 비고 100 담당 100 300 "정확히 하나 일치"보다 "여러 필드에 걸쳐 애매하게 일치"가 3배 높은 점수를 받는다. 검색 품질이 나쁘다는 신고의 상당수가 이 구조에서 나온다.

계단을 보장하려면

boost 숫자만으로는 순서를 못 만든다. 구조를 바꿔야 한다.

방법무엇을 하나언제
dis_max + tie_breaker여러 절 중 가장 높은 점수 하나를 쓰고, 나머지는 tie_breaker 비율만큼만 더한다여러 필드에 같은 검색어를 걸 때 (합산 누적 방지)
constant_score절의 점수를 지정한 값으로 고정한다. BM25 변동을 없앤다계단을 숫자로 확정하고 싶을 때
multi_match (best_fields)내부적으로 dis_max로 동작한다필드별 절을 직접 만들 이유가 없을 때
function_score / rescore상위 N건만 다시 점수 매김비싼 재랭킹을 상위에만 적용할 때
1
2
3
4
5
6
7
{ "dis_max": {
    "tie_breaker": 0.3,
    "queries": [
      { "constant_score": { "filter": { "prefix": { "name": "강남" } }, "boost": 100 } },
      { "constant_score": { "filter": { "match_phrase": { "address": "강남" } }, "boost": 80 } }
    ]
} }

constant_score로 감싸면 BM25 변동이 사라져 boost 값이 곧 점수가 되고, dis_max가 합산 누적을 막는다. 이러면 의도한 계단이 실제로 선다.

점수가 왜 그렇게 나왔는지 모르겠으면 _explain API를 쓴다. 문서 하나에 대해 각 절이 얼마를 냈고 어떻게 합쳐졌는지 트리로 보여준다. 추측하지 말고 열어보는 게 빠르다.

1
2
GET /my-index/_explain/{document_id}
{ "query": { ... } }

운영 함정

함정 1 — filter에 boost를 넣기. 오류가 안 나고 조용히 무시된다. 정렬이 필드 기준이면 증상도 안 보이다가, 나중에 _score 정렬로 바뀌는 순간 순서가 뒤집힌다.

함정 2 — boost 숫자를 절대 점수로 착각하기. 곱셈 계수다. MultiTermQuery는 constant score 1.0을 내지만 match 계열은 BM25를 내므로, 같은 boost라도 결과 점수가 다르다.

함정 3 — 필드마다 should 절을 추가하기. 점수가 합산돼 “여러 필드에 애매하게”가 “한 필드에 정확히”를 이긴다. dis_max나 multi_match(best_fields)를 쓴다.

함정 4 — 모듈마다 다른 컨텍스트인데 공통 유틸을 공유하기. 한쪽은 _score 정렬, 다른 쪽은 필드 정렬이면 같은 절 생성기를 쓰면 안 된다. boost가 한쪽에서만 의미를 갖는다.

면접 한 줄 Q&A

  • Q. query 컨텍스트와 filter 컨텍스트의 차이는? A. query(must/should)는 “얼마나 잘 맞는가”를 계산해 _score를 내고, filter(filter/must_not)는 “통과하는가”만 판정한다. filter는 점수를 계산하지 않으므로 boost가 무시되고, 대신 비트셋으로 캐시된다.
  • Q. boost: 100을 주면 100점이 되나? A. 아니다. 곱셈 계수다. prefix·wildcard 같은 MultiTermQuery는 constant score 1.0을 내므로 결과가 100이 되지만, match_phrase는 BM25 점수를 내므로 그 값에 100이 곱해진다.
  • Q. 우선순위 계단이 뒤집히는 이유는? A. 두 가지다. 고정 점수 절과 BM25 변동 절을 같은 boost 체계로 섞었거나, bool.should가 매칭된 모든 절의 점수를 합산하기 때문이다.
  • Q. 계단을 보장하려면? A. constant_score로 점수를 고정하고 dis_max + tie_breaker로 합산 누적을 막는다. 여러 필드에 같은 검색어를 거는 경우라면 multi_match의 best_fields가 내부적으로 같은 일을 한다.
  • Q. 점수가 왜 이렇게 나왔는지 확인하려면? A. _explain API로 문서 하나에 대해 각 절의 기여를 트리로 확인한다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.