개발일기/TIL(Since24.04.19)

ES 조건 별 검색하는방식 Must, Filter

w.llama 2025. 4. 22. 14:37

우리 프로젝트에서 매칭 조건에 따라 검색 결과를 정확하게 도출하려면, Elasticsearch의 bool 쿼리 내에서 mustfilter 방식을 적절히 활용하는 것이 핵심인데 이 방식이 비슷해서 정리하고자 한다.우선 두 방식 모두 "이 조건을 반드시 만족해야 한다"는 공통점이 있지만, 실제 동작 방식과 성능, 그리고 사용 목적에 있어 차이가 뚜렷하다. 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 옵션으로 점수 계산 방식을 반드시 검증해야한다.
  • 이 구조를 적용하면 초당 수천 건의 매칭 요청도 안정적으로 처리할 수 있다.