Elasticsearch 검색 점수 가중치 조정 기법

애플리케이션이 성장함에 따라 검색 품질 향상에 대한 요구가 점점 커집니다. 이를 검색 경험이라고 부릅니다. 사용자에게 무엇이 중요한지, 사용자가 검색 기능을 어떻게 활용하는지 이해하는 것이 필요합니다. 이로 인해 특정 문서가 다른 문서보다 더 중요하거나, 특정 쿼리에서 특정 필드를 강조하고 다른 필드는 약화시켜야 한다는 결론에 도달합니다. 이때 가중치(boost)를 활용할 수 있습니다.

검색 경험을 더 깊이 생각해보면, 우리는 검색 결과 중에서 가장 원하는 데이터를 얻고자 합니다. 이는 문서의 관련성 점수(relevance score)와 직접적으로 연결됩니다.

더 자세히 설명하면, 우리가 쿼리한 모든 문서는 내부적으로 관련성 점수인 score가 계산되며, 이 score를 기준으로 내림차순 정렬되어 클라이언트에 표시됩니다.

그렇다면 점수는 어떻게 계산될까요?

Elasticsearch가 사용하는 TF-IDF 알고리즘의 실용적인 계산 공식은 다음과 같습니다:
score(q,d) = coord(q,d) * queryNorm(q) * Σ(tf(t in d) * idf(t)² * boost(t) * norm(t,d))
  • TF(Term Frequency): 단어 빈도, 특정 단어가 문서 내에서 얼마나 자주 등장하는지를 나타냅니다. 빈도가 높을수록 가중치가 높아집니다.
  • IDF(Inverse Document Frequency): 역문서 빈도, 특정 단어가 전체 문서 집합에서 얼마나 자주 나타나는지를 나타냅니다. 빈도가 높을수록 가중치가 낮아집니다.

실무에서는 종종 boost 값을 조정하여 score를 제어합니다(boost 기본값은 1입니다).

인덱스 및 매핑 생성

1. 인덱스 생성

@Test
public void createIndex() {
    client.admin().indices().prepareCreate("blog").get();
}

2. 매핑 생성

@Test
public void testCreateIndexMapping_boost() throws Exception {
    XContentBuilder mappingBuilder = XContentFactory.jsonBuilder()
        .startObject()
            .startObject("document")
                .startObject("properties")
                    .startObject("id").field("type", "integer").field("store", "yes").endObject()
                    .startObject("title").field("type", "string").field("store", "yes").field("analyzer", "ik_max_word").endObject()
                    .startObject("content").field("type", "string").field("store", "yes").field("analyzer", "ik_max_word").endObject()
                    .startObject("comment").field("type", "string").field("store", "yes").field("analyzer", "ik_max_word").endObject()
                .endObject()
            .endObject()
        .endObject();

    PutMappingRequest request = Requests.putMappingRequest("blog")
            .type("document")
            .source(mappingBuilder);
    client.admin().indices().putMapping(request).get();
}

3. Document 엔티티 클래스

package com.elasticsearch.bean;

public class Document {
    private Integer id;
    private String title;
    private String content;
    private String comment;

    public Integer getId() { return id; }
    public String getComment() { return comment; }
    public String getContent() { return content; }
    public String getTitle() { return title; }
    public void setComment(String comment) { this.comment = comment; }
    public void setContent(String content) { this.content = content; }
    public void setId(Integer id) { this.id = id; }
    public void setTitle(String title) { this.title = title; }
}

4. 문서 생성

@Test
public void createDocument() throws JsonProcessingException {
    Document document = new Document();
    document.setId(3);
    document.setTitle("Elasticsearch의 용도");
    document.setContent("Elasticsearch는 대량 데이터 검색에 사용됩니다");
    document.setComment("Elasticsearch 정말 대단하다");

    ObjectMapper objectMapper = new ObjectMapper();
    String source = objectMapper.writeValueAsString(document);
    System.out.println("source:" + source);

    IndexResponse indexResponse = client.prepareIndex("blog", "document", document.getId().toString()).setSource(source).get();
    System.out.println("인덱스 이름: " + indexResponse.getIndex());
    System.out.println("문서 타입: " + indexResponse.getType());
    System.out.println("ID: " + indexResponse.getId());
    System.out.println("버전: " + indexResponse.getVersion());
    System.out.println("생성 성공 여부: " + indexResponse.status());
    client.close();
}

5. 가중치 테스트

// TODO: id2가 id1보다 앞에 오도록 하는 방법
@Test
public void BoolQuery_boost() {
    SearchResponse searchResponse = client.prepareSearch("blog").setTypes("document")
        .setQuery(QueryBuilders.boolQuery()
            .should(QueryBuilders.termQuery("title", "검색"))
            .should(QueryBuilders.termQuery("content", "검색"))
            .should(QueryBuilders.termQuery("comment", "검색"))
        ).get();

    SearchHits hits = searchResponse.getHits();
    printSearch(hits);
}

public void printSearch(SearchHits hits) {
    System.out.println("검색 결과 수: " + hits.getTotalHits() + "개");
    System.out.println("결과 최고 점수: " + hits.getMaxScore());

    Iterator<SearchHit> iterator = hits.iterator();
    while (iterator.hasNext()) {
        SearchHit searchHit = iterator.next();
        System.out.println("전체 데이터 JSON: " + searchHit.getSourceAsString());
        System.out.println("각 결과 점수: " + searchHit.getScore());
        System.out.println("id: " + searchHit.getSource().get("id"));
        System.out.println("title: " + searchHit.getSource().get("title"));
        System.out.println("content: " + searchHit.getSource().get("content"));
        System.out.println("**********************************************");

        for (Iterator<SearchHitField> ite = searchHit.iterator(); ite.hasNext();) {
            SearchHitField next = ite.next();
            System.out.println(next.getValues());
        }
    }
}

태그: elasticsearch TF-IDF 검색점수 가중치 BooleanQuery

6월 5일 22:33에 게시됨