우리 프로젝트에서 매칭 조건에 따라 검색 결과를 정확하게 도출하려면, Elasticsearch의 bool 쿼리 내에서 must와 filter 방식을 적절히 활용하는 것이 핵심인데 이 방식이 비슷해서 정리하고자 한다.우선 두 방식 모두 "이 조건을 반드시 만족해야 한다"는 공통점이 있지만, 실제 동작 방식과 성능, 그리고 사용 목적에 있어 차이가 뚜렷하다. must는 검색어와 문서의 관련성 점수를 계산해 순위를 매길 때 사용되고, filter는 점수 계산 없이 빠르고 정확하게 필수 조건을 필터링할 때 적합하다. 아래에서는 각 방식의 특징과 실제 활용법, 그리고 우리 프로젝트에서 어떤 방식을 선택해야 하는지 정리했다
must와 filter의 차이점 한눈에 보기
구분 | must | filter |
점수 계산 | 문서의 _score에 영향 | 점수 계산 무시 (_score = 0) |
성능 | 상대적으로 느림 | 캐싱 가능 → 빠름 |
사용 목적 | 관련성 순위가 중요한 검색 | 정확한 필터링 (카테고리, 날짜) |
쿼리 유형 | 텍스트 분석(match, term 등) | 정확 값 매칭(term, range 등) |
must의 특징과 장점
- 필수 조건: 해당 조건을 반드시 만족해야 하며, 관련성 점수(_score)를 계산한다.
- 예시: 검색어 "Elasticsearch"가 제목에 포함된 문서를 유사도 순으로 정렬하고 싶을 때 사용함
{
"query": {
"bool": {
"must": { "match": { "title": "Elasticsearch" } }
}
}
}
- 장점
- 관련성 기반 정렬: 검색어와 문서의 유사도를 반영해 순위를 매길 때 필수적
- 복합 조건 조합: should와 함께 사용해 유연한 검색 로직 구현이 가능
filter의 특징과 장점
- 필수 조건: 해당 조건을 반드시 만족해야 하지만, 점수 계산은 하지 않는다.
- 예시: 카테고리가 "IT"이고 가격이 10,000원 이상인 문서만 필터링할 때 사용
{
"query": {
"bool": {
"filter": [
{ "term": { "category": "IT" } },
{ "range": { "price": { "gte": 10000 } } }
]
}
}
}
- 장점
- 빠른 성능: filter 쿼리는 캐싱되어 반복 쿼리 시 성능이 극대화됨
- 정확한 필터링: 나이, 지역, 상태 등 구조화된 데이터 처리에 최적화됨
언제 must를, 언제 filter를 써야 할까?
- must를 써야 하는 경우
- 검색어의 유사도에 따라 결과 순위를 매겨야 할 때
- boost 파라미터 등 가중치 조정이 필요한 경우
- should와 결합해 옵션 조건을 추가할 때
- filter를 써야 하는 경우
- 정확한 값 매칭이 필요할 때 (예: status = "active")
- 범위 검색 (예: date > "2024-01-01")
- 성능 최적화가 필요한 빈번한 쿼리에서
성능 비교 예시
조건 | must | filter |
100만 문서 검색 | 120ms | 45ms |
동일 조건 반복 검색 | 110ms | 15ms (캐시 히트) |
💡 filter는 캐싱으로 인해 동일 쿼리 재실행 시 3배 이상 빠르다.
must와 filter 혼합 사용 예시
{
"query": {
"bool": {
"must": { "match": { "title": "Elasticsearch" } },
"filter": { "term": { "category": "IT" } }
}
}
}
- 검색어(유사도)는 must, 카테고리 등 구조화 필드는 filter로 분리하면 성능과 검색 품질을 모두 잡을 수 있다.
실제 프로젝트 적용 가이드
- 필수 조건(티어, 라인 등 100% 일치해야 하는 값): filter 사용 → 점수 계산 제외, 캐싱 효과로 성능 극대화
- 가중치 조건(챔피언 선호도 등): should + function_score 사용 → 일치할수록 점수 상승, 랭킹 반영
- 즉시 제외 조건(비선호 챔피언 등): must_not 사용
Must(should, must_not)만 사용시
{
"query": {
"bool": {
"must": [ // 필수 조건 (모두 만족해야 함)
{ "term": { "member_info.season_infos.tier": "SILVER" } }, // 티어가 SILVER인 유저
{ "term": { "preferred_partner.wantLine.partnerLine": "MID" } }, // 상대가 원하는 라인이 MID
{ "term": { "preferred_partner.wantLine.myLine": "TOP" } } // 내가 원하는 라인이 TOP
],
"should": [ // 선택 조건 (일치하면 가산점)
{
"nested": { // 중첩 객체(nested) 검색
"path": "recent_twenty_match.most_three_champions_stats", // 챔피언 통계 배열 경로
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Ahri" // 최근 20경기 중 Ahri를 사용한 기록
}
}
}
}
],
"must_not": [ // 제외 조건 (일치 시 결과에서 제외)
{
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Yasuo" // Yasuo 사용 기록이 있는 유저 제외
}
}
}
}
]
}
}
}
Filte만 사용시
{
"query": {
"bool": {
"filter": [ // 모든 조건이 필수 (must → filter로 대체)
{ "term": { "member_info.season_infos.tier": "SILVER" } },
{ "term": { "preferred_partner.wantLine.partnerLine": "MID" } },
{ "term": { "preferred_partner.wantLine.myLine": "TOP" } },
{ // 기존 should → filter 내 must로 강제화
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Ahri"
}
}
}
}
],
"must_not": [ // 제외 조건 유지
{
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Yasuo"
}
}
}
}
]
}
}
}
추천 쿼리 구조
{
"query": {
"function_score": {
"query": {
"bool": {
"filter": [
// 필수 조건 (점수 미반영)
{ "term": { "member_info.season_infos.tier": "SILVER" } },
{ "term": { "preferred_partner.wantLine.partnerLine": "MID" } },
{ "term": { "preferred_partner.wantLine.myLine": "TOP" } },
// must_not 조건 이동
{
"bool": {
"must_not": [
{
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Yasuo"
}
}
}
}
]
}
}
]
}
},
"functions": [
// 1순위 챔피언 가중치 (10%)
{
"filter": {
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Ahri"
}
}
}
},
"weight": 1.10
},
// 2순위 챔피언 가중치 (5%)
{
"filter": {
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Lux"
}
}
}
},
"weight": 1.05
},
// 3순위 챔피언 가중치 (1%)
{
"filter": {
"nested": {
"path": "recent_twenty_match.most_three_champions_stats",
"query": {
"term": {
"recent_twenty_match.most_three_champions_stats.champion_name.keyword": "Ezreal"
}
}
}
},
"weight": 1.01
},
// 승률 가중치 추가
{
"field_value_factor": {
"field": "member_info.season_infos.win_rate",
"factor": 0.1,
"modifier": "log1p"
}
}
],
"score_mode": "sum", // 점수 합산 방식
"boost_mode": "multiply" // 기존 점수와의 결합 방식
}
}
}
결론
- must = 유사도 기반 검색 (검색어, 랭킹)
- filter = 정확한 필터링 (카테고리, 날짜, 상태)
- 성능이 중요한 구조화된 데이터 필터링에는 filter를, 관련성 순위가 중요한 검색에는 must를 선택하는 것이 최적
- 실제 구현 시 explain: true 옵션으로 점수 계산 방식을 반드시 검증해야한다.
- 이 구조를 적용하면 초당 수천 건의 매칭 요청도 안정적으로 처리할 수 있다.
'개발일기 > TIL(Since24.04.19)' 카테고리의 다른 글
CRLF vs LF 문제 (0) | 2025.05.07 |
---|---|
Kotiln 공부하기 (1) | 2025.04.26 |
타임리프에서 SPA로? SSR과 CSR 개념 정리 (0) | 2025.04.21 |
DB 변경 감지 (flyway) (0) | 2025.04.20 |
테이블 컬럼이 변경되었을때 DB가 동작하는 방식 (0) | 2025.04.20 |