본문 바로가기

기타/Elasticsearch

Elasticsearch - 검색 결과 향상을 위한 삽질기

개요

메타 데이터가 어느정도 쌓이자(현재 기준 100만건) 검색을 할 필요가 생겼다.

DHT 네트워크로 공유되는 메타 데이터 특성 상 검색의 대상이 되는 값은 토렌트 이름과 파일 이름뿐이다.

그외에는 있어봐야 파일 크기랑 JPG, MP4와 같은 파일 포맷에 대한 정보 정도이다.

 

검색 시 이름과 파일 이름을 기준으로 검색할 수 있도록 할 필요가 있고 이를 위해 다양한 시도를 해보았다.

 

데이터의 유형

기본 및 언어

DHT 네트워크를 통해 가져오는 데이터인만큼 대부분의 데이터는 모두 문자열 데이터로 구성되어있다.

간혹 특수문자나 바이트 유형의 데이터도 들어오기는 하는데 이는 모두 그대로 저장되지만 표시는 제대로되지 않는다.

언어 또한 다양하게 구성되어 있다. 당연하다면 당연하지만 영어가 가장 많고 한국어, 러시아어, 일본어, 중국어, 아랍어 등등

각종 노드들이 열렬히 데이터들을 보내주고 있기 때문에 이를 분석하기는 쉽지 않을 것 같다...

 

구성

컨텐츠에 대한 요약?을 제목에 하다보니 문장보다는 구분되는 단어와 숫자 위주나 절로 구성되어 있다.

제목 상으로 봤을 때 많은 건 그 카테고리인 것 같고 다음으로는 드라마나 영화 등이 많이 보인다.

 

예시)

HELLO-123

센조.20210310.MP4

어져스:스타트게임.2019.WEB.MP4

재미있는 드라마 제목.200518.HELLo.2021

 

분석

Elasticsearch는 저장소로도 쓰고 있지만 기본적으로 검색 엔진이다.

데이터를 인덱싱을 하는 시점에서 분석기를 돌리기 때문에 사전에 미리 정의를 해둘 필요가 있다.

 

여러가지 상황을 고려하고 테스트해본 결과 다음과 같이 구성하였다.

  • 다국적언어가 모인 인덱스인만큼 언어적 특성을 고려한 특정 언어에 맞는 분석기를 돌리기는 어려움 (Elasticsearch에 이러한 문제를 해결해줄 수 있는 기능을 보았지만 일단 패스)
  • asciifolding - 프랑스 제목을 검색하기 위해 프랑스어를 쓰기는 어렵다 :(
  • lowercase - 대소문자의 구분이 딱히 검색 퀄리티에 영향을 주지 않기 때문에 소문자화
  • char_filter - 데이터 안에 HTML과 같은 유형은 거의 없기 때문에 char_filter는 빈 상태
  • 띄어쓰기(whitespace), 문장 부호(punctuation), 심볼을 기준으로 토크화
"analysis": {
    "normalizer": {
        "keyword_normalizer": {
            "filter": [
                "lowercase",
                "asciifolding"
            ],
            "type": "custom",
            "char_filter": []
        }
    },
    "analyzer": {
        "default": {
            "filter": [
                "lowercase",
                "asciifolding"
            ],
            "type": "custom",
            "tokenizer": "general_tokenizer"
        }
    },
    "tokenizer": {
        "general_tokenizer": {
            "type": "char_group",
            "tokenize_on_chars": [
                "whitespace",
                "punctuation",
                "symbol"
            ]
        }
    }
}

 

위 설정을 인덱스 템플릿에 넣고 이전에 수집된 데이터가 들어가 있는 인덱스를 reindex하여 분석이 진행되도록 하였다.

POST /_reindex
{
    "source": {
        "index": "torrent-rev-001"
    },
    "dest": {
    	"index": "torrent-rev-002"  # 분석기 적용 (템플릿)
    }
}

 

검색

위와 같은 방식으로 분석기를 돌리더라도 검색 쿼리 하나로 모든 유형의 검색을 기대할 수는 없다.

따라서 한번의 요청에 여러 유형의 검색을 포함할 수 있도록 하고 검색 형태에 따라 부스트 값을 부여했다.

다음과 같은 쿼리 유형과 순서로 검색을 하도록 하였다.

  • 검색어가 40자의 hex 형식으로 구성된 경우, term 쿼리 요청
  • 토렌트 이름을 키워드 매칭하며 매칭되는 경우 부스트 값 5를 부여 (완전 동일하게 매칭되는 경우)
  • match_phrase로 토렌트 이름 매칭
  • match_phrase로 파일 이름 매칭
  • bool must로 띄어쓰기로 구분된 검색어들을 검색 must=['hello', 'world']
  • bool should로 토렌트 이름과 파일 이름 매칭
words = query.split()
queryset = Q(
    'bool',
    should=[
        Q('term', torrent_name__keyword={'value': query, 'boost': 5}),
        Q('match_phrase', torrent_name=query),
        Q('match_phrase', files__name__keyword=query),
        Q('bool', must=[Q('match', torrent_name=word) for word in words]),
        Q('bool', should=[
            Q('match', torrent_name=query),
            Q('match', files__name__keyword=query),
        ]),
    ]
)

 

결론

처음에는 단순 bool should 매칭으로도 충분한 검색 결과를 얻었으나 점점 데이터가 늘어나고

같은 검색을 하면 할수록 점점 결과의 정확도가 떨어지는 문제가 발생했다.

 

일반적인 단어를 검색하면 결과에 포함되어 있었으나 지금은 동일하게 검색할 경우, 같은 단어를 포함하는 별의별 데이터가 나온다.

검색 결과를 개선시키기 위해 인덱스 시점에 분석기를 돌려서 전처리를 진행하였고 검색 쿼리 방식을 변경해 이전과 비교해

좀 더 만족스러운 검색 결과를 받아올 수 있었다.