Elasticsearch 관련도 점수: query/filter 컨텍스트, boost, 그리고 dis_max
filter 컨텍스트에서 boost가 무시되는 이유, boost가 절대 점수가 아니라 곱셈 계수인 탓에 우선순위 계단이 뒤집히는 구조, 그리고 dis_max와 constant_score로 계단을 보장하는 법.
should 절에 boost를 100, 80, 10, 1로 매겨 우선순위 계단을 만드는 건 흔한 패턴이다. 문제는 그 계단이 생각대로 서지 않는 경우가 많다는 것이다. 심지어 아예 무시되기도 한다.
원인은 두 가지다 — 점수를 아예 계산하지 않는 컨텍스트에 넣었거나, boost가 절대 점수가 아니라 곱셈 계수라는 걸 놓쳤거나.
multi_match와 정렬 사용법은 MultiMatch와 다중 정렬에서 다뤘다. 여기서는 점수가 만들어지는 과정 자체를 본다.
query 컨텍스트와 filter 컨텍스트
Elasticsearch의 모든 절은 둘 중 하나의 컨텍스트에서 실행된다. 이 구분이 점수의 존재 여부를 결정한다.
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을 곱하라” 이기 때문이다.
그리고 절 종류에 따라 곱해지는 원본 점수가 다르다.
bool.should의 점수는 매칭된 절들의 합이다
두 번째 뒤집힘 원인이 여기 있다. should는 매칭된 절 하나를 고르는 게 아니라 매칭된 모든 절의 점수를 더한다.
필드 목록을 루프 돌며 필드마다 절을 추가하는 코드가 흔한데, 이 경우 여러 필드에서 매칭된 문서는 점수가 누적된다.
계단을 보장하려면
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가 합산 누적을 막는다. 이러면 의도한 계단이 실제로 선다.
점수가 왜 그렇게 나왔는지 모르겠으면
_explainAPI를 쓴다. 문서 하나에 대해 각 절이 얼마를 냈고 어떻게 합쳐졌는지 트리로 보여준다. 추측하지 말고 열어보는 게 빠르다.
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.
_explainAPI로 문서 하나에 대해 각 절의 기여를 트리로 확인한다.