본문 바로가기

Elastic Search

[Elastic Search] MBTI 검색 프로젝트 - 3. API 구축하기

반응형

MBTI 검색엔진 데이터를 API 형태로 전달합니다.

API 목록

  • 전체 문서에서 각 MBTI 타입별 상위 100개 키워드 출력 - 문서 수 기준
  • MBTI 유형 중 E 또는 I 유형에 따라 상위 100개 키워드 출력
  • MBTI 유형 중 E 또는 I 유형에 따라 검색어를 입력하여 검색된 상위 100개 키워드 출력

구현한 API는 다음과 같습니다.

전체 문서에서 각 MBTI 타입별 상위 100개 키워드를 출력

@app.get('/top/keywords/{mbti_type}')
def get_top_keywords(mbti_type: str, q:Optional[str]=None):
    es_query = {
        "size": 0,
        "query": {"match": {"keyword": mbti_type}},
        "aggs": {
            "term_cnt": {
                "terms": {
                    "field": "contents.nori_noun",
                    "size": 1000
                }
            }
        }
    }
    res = es.search(index="mbti_term", body=es_query)
    bkt_list = res.body["aggregations"]["term_cnt"]["buckets"]
    refine_bkt_list = [b for b in bkt_list if b['key'] not in stopwords]
    refine_bkt_list = [r for r in refine_bkt_list if not r['key'].isdigit()]
    return {"data": refine_bkt_list}

es_query를 보면 “aggs”에서 합계를 내고 있는 필드는 contents.nori_noun 입니다. 이전 ES 포스트에서 말했듯이, nori_noun 은 명사(Mecab 형태소 분석기 분류상 명사)만 들어있습니다. 보통 의미있는 단어는 명사인 경우가 많아 명사만 특정 필드에 저장했습니다. 그리고 여기에 있는 terms 를 카운트 했습니다.

ENFP로 검색한 결과입니다.

MBTI 유형 중 E 또는 I 유형에 따라 상위 100개 키워드 출력

@app.get('/top/keywords/regex/{mbti_type}')
def get_top_keywords_regex(mbti_type: str, q: Optional[str]=None):
    if mbti_type not in ["I", "E"]:
        raise

    regex_q = f"{mbti_type}.*"
    es_query = {
        "size": 0,
        "query": {"regexp": {"keyword": regex_q}},
        "aggs": {
            "term_cnt": {
                "terms": {
                    "field": "contents.nori_noun",
                    "size": 1000
                }
            }
        }
    }
    res = es.search(index="mbti_term", body=es_query)
    bkt_list = res.body["aggregations"]["term_cnt"]["buckets"]
    refine_bkt_list = [b for b in bkt_list if b['key'] not in stopwords]
    refine_bkt_list = [r for r in refine_bkt_list if not r['key'].isdigit()]
    return {"data": refine_bkt_list}

여기서는 ES 내에 regex를 사용했습니다(regexp). 제가 분석하고 싶은것은 E(외향형) 또는 I(내향형)에 따라 어떤 키워드를 더 많이 언급할까 또는 어떤 비중으로 특정 단어들을 언급할까 궁금했기 때문입니다.

"I" 로 검색한 결과입니다.

MBTI 유형 중 E 또는 I 유형에 따라 검색어를 입력하여 검색된 상위 100개 키워드 출력

이번에는 Regex 와 키워드 검색을 결합하여 봅시다.

@app.get('/top/keywords/search/{mbti_type}/{keyword}')
def get_search_keyword(mbti_type: str, keyword: str, q: Optional[str]=None):
    if mbti_type not in ["I", "E"]:
        raise

    regex_q = f"{mbti_type}.*"
    es_query = {
        "size": 0,
        "query": {
            "bool": {
                "must": [
                    {
                        "match": {
                            "contents": keyword
                        }
                    },
                    {
                        "regexp": {
                            "keyword": regex_q
                        }
                    }
                ]
            }
        },
        "aggs": {
            "term_cnt": {
                "terms": {
                    "field": "contents.nori_noun",
                    "size": 1000
                }
            }
        }
    }
    res = es.search(index="mbti_term", body=es_query)
    bkt_list = res.body["aggregations"]["term_cnt"]["buckets"]
    refine_bkt_list = [b for b in bkt_list if b['key'] not in stopwords]
    refine_bkt_list = [r for r in refine_bkt_list if not r['key'].isdigit()]
    return {"data": refine_bkt_list}

regex와 키워드 검색을 결합하는 것은 어렵지 않습니다. 간단하게 bool 쿼리를 사용하여 match와 regexp를 사용하여 검색하였습니다. 이 쿼리는 특정 키워드가 포함된 문서들이면서 E 또는 I 유형에 대해 언급한 문서들을 가져온 뒤 Term Vector를 분석하여 그 수를 세는 쿼리입니다.

"I" 와 "여행"을 검색했을 때
"E"와 "여행"을 검색했을 때

위 코드들 중 공통적인 부분이 있습니다.

# 불용어 제거
refine_bkt_list = [b for b in bkt_list if b['key'] not in stopwords]
# 숫자 제거
refine_bkt_list = [r for r in refine_bkt_list if not r['key'].isdigit()]

ES에서 명사들만 필드에 넣다보니 Mecab 에서는 숫자도 명사로 인식하기 때문에 API에서는 최종적으로 숫자 형태의 문자는 제거하고 Response 합니다.

또한 불용어(이, 것 등) 역시 제거합니다. Mecab 에서는 수사, 단위명사, 대명사 등도 명사로 인식되어 포함되기 때문에 불용어 사전을 이용하여 최대한 제거합니다.

 

API가 완성되었으니 앱 또는 웹 어플리케이션으로 결과를 보여주는 화면까지 연결해보겠습니다.

반응형