본문 바로가기

DB

검색 결과 순서가 바뀌는 원리 — TF-IDF, BM25, 쿼리 튜닝 한 번에 정리 (Elasticsearch)

반응형

 

Elasticsearch 검색 점수의 비밀 — TF-IDF부터 BM25 튜닝까지 한 번에 정리했습니다

Elasticsearch로 검색 기능을 만들다 보면 이런 의문이 생기실 거예요.

"왜 이 문서가 1등이지? 검색 점수는 도대체 어떻게 계산되는 거야?"

이번 글에서는 Elasticsearch 검색 점수의 핵심 알고리즘인 TF-IDFBM25, 그리고 실무에서 쓰는 튜닝 방법까지 한 번에 정리해 드릴게요.


1. 핵심 원리 — TF-IDF와 BM25는 뭘 보는가

두 알고리즘의 평가 기준은 같습니다.

"문서 안에서 자주 등장하지만(TF 높음), 전체 문서에서는 희귀한 단어(IDF 높음)일수록 그 문서를 대표하는 핵심 키워드다."

쉽게 말하면 "엘라스틱서치"라는 단어가 전체 문서 중 딱 몇 개에만 나오는데, 그 문서 안에서 여러 번 등장한다면 — 그 문서는 엘라스틱서치 관련 핵심 문서일 확률이 높다고 판단하는 거예요.


2. TF-IDF vs BM25 — 뭐가 다른가

BM25는 TF-IDF의 업그레이드 버전입니다. Elasticsearch는 v5.0부터 BM25를 기본 알고리즘으로 채택했어요.

구분 TF-IDF BM25

단어 빈도 평가 반복될수록 점수 무한 증가 일정 횟수 이상이면 점수 상승이 꺾임
문서 길이 보정 고려하지 않음 평균 길이와 비교해 긴 문서에 페널티
키워드 도배 매우 취약 도배에 속지 않음

TF-IDF의 가장 큰 문제는 두 가지였어요.

첫째, 키워드 도배입니다. "엘라스틱서치"를 본문에 1,000번 쑤셔 넣으면 점수가 1,000배가 됩니다. BM25는 어느 횟수 이상 반복되면 점수 상승이 포화 상태로 꺾여서 도배가 통하지 않아요.

둘째, 문서 길이 불공평입니다. 100장짜리 문서는 우연히라도 단어가 많이 등장할 수밖에 없어요. BM25는 전체 문서 평균 길이와 비교해서 쓸데없이 긴 문서에는 페널티를 줍니다. 1장짜리 핵심 요약본에서 5번 나온 게 100장짜리에서 5번 나온 것보다 더 관련성 높다고 보는 거죠.


3. BM25 파라미터 튜닝 — k1과 b

BM25의 검색 품질을 좌우하는 파라미터는 딱 두 개입니다.

k1 (단어 빈도 포화도 조절)

  • 기본값: 1.2
  • 높일수록 → 단어 반복에 따라 점수가 계속 올라감 (TF-IDF에 가까워짐)
  • 낮출수록 → 단어가 한 번 나오든 백 번 나오든 점수 차이 없음

b (문서 길이 페널티 강도)

  • 기본값: 0.75 (0~1 사이)
  • 1에 가까울수록 → 긴 문서에 강한 페널티
  • 0에 가까울수록 → 문서 길이 무시하고 단어 빈도로만 평가

인덱스 생성 시 아래처럼 커스텀 설정을 적용할 수 있어요.

PUT /my_custom_index
{
  "settings": {
    "index": {
      "similarity": {
        "my_custom_bm25": {
          "type": "BM25",
          "k1": 1.5,
          "b": 0.8
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "similarity": "my_custom_bm25"
      },
      "description": {
        "type": "text"
      }
    }
  }
}

💡 실무 팁: 사실 k1, b를 직접 건드리는 경우는 생각보다 많지 않아요. 기본값이 이미 충분히 최적화되어 있거든요. 대신 아래에서 설명할 쿼리 단 튜닝을 훨씬 더 많이 씁니다.


4. 쿼리 단 튜닝 — 인덱스 건드리지 않고 검색 품질 올리기

인덱스를 재생성하지 않고 쿼리만으로 검색 결과를 조작하는 방법 3가지입니다.

방법 1. Boost — 필드 가중치

"제목에서 발견되면 본문보다 3배 더 중요하게 봐라" 식으로 가중치를 주는 방법이에요.

POST /news_index/_search
{
  "query": {
    "multi_match": {
      "query": "인공지능 로봇",
      "fields": [
        "title^3",
        "summary^1.5",
        "content"
      ]
    }
  }
}

본문에 키워드가 100번 도배된 문서보다, 제목에 딱 1번 들어간 핵심 문서를 상단으로 끌어올릴 수 있어요.

방법 2. function_score — 비즈니스 로직 결합

텍스트 관련도(BM25)에 조회수, 최신 글 여부 같은 비즈니스 수치를 점수에 반영할 때 씁니다.

POST /news_index/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "테마주 급등",
          "fields": ["title^2", "content"]
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "view_count",
            "modifier": "log1p",
            "factor": 1.2
          }
        },
        {
          "gauss": {
            "published_date": {
              "origin": "now",
              "scale": "7d",
              "offset": "1d",
              "decay": 0.5
            }
          }
        }
      ],
      "score_mode": "multiply",
      "boost_mode": "multiply"
    }
  }
}

"검색어와 관련도 높고 + 조회수 많고 + 최신 글"이 1등으로 나오게 됩니다. log1p를 쓰는 이유는 조회수가 100만이라고 점수가 100만 배가 되면 안 되니까, 로그를 씌워서 완만하게 꺾어주는 거예요.

방법 3. script_score — 커스텀 수식

기본 제공 함수로 부족할 때, Painless 스크립트로 직접 수식을 작성하는 방법이에요.

POST /news_index/_search
{
  "query": {
    "script_score": {
      "query": {
        "match": { "content": "주식 배당금" }
      },
      "script": {
        "source": "_score + (doc['likes'].value / 10.0)"
      }
    }
  }
}

(좋아요 * 0.7) + (공유 횟수 * 0.3) 같은 복잡한 비즈니스 수식을 그대로 랭킹 로직에 이식할 수 있어요. 다만 연산 비용이 크니 데이터가 많을 때는 주의하세요.


마무리

정리하면 이렇습니다.

  • TF-IDF → 직관적이지만 도배와 문서 길이에 취약
  • BM25 → TF-IDF에 도배 방지와 길이 보정을 추가한 완성형
  • k1, b 튜닝 → 특수한 도메인이 아니면 기본값으로 충분
  • 쿼리 단 튜닝 → 실무에서 가장 많이 쓰는 방법, 인덱스 건드릴 필요 없음

검색 품질을 올리고 싶다면 BM25 파라미터보다 Boost와 function_score부터 먼저 시도해 보세요. 훨씬 빠르고 안전하게 원하는 결과를 얻을 수 있습니다. 😄


 

반응형