Elastic Search

[Elastic Search] 검색 κ΅¬ν˜„ν•˜κΈ°(with Fast API)

Tempo 2022. 2. 15. 20:03

ES둜 검색엔진을 κ΅¬ν˜„ν•˜μ˜€λ‹€. κ΅¬ν˜„λœ 검색 엔진을 μ‹€μ œ μ„œλΉ„μŠ€μ²˜λŸΌ μ‚¬μš©ν•˜κΈ° μœ„ν•΄ REST APIλ₯Ό κ΅¬ν˜„ν•΄λ³΄μž.

REST API의 λ‘œμ§μ€ λ‹¨μˆœν•˜κ²Œ λ³Έλ‹€λ©΄ 2단계이닀.

1. μ‚¬μš©μžκ°€ 검색 ν‚€μ›Œλ“œλ₯Ό μž…λ ₯ν•œλ‹€. 2. 검색 ν‚€μ›Œλ“œμ— ν•΄λ‹Ήν•˜λŠ” λ¬Έμ„œλ₯Ό μ°ΎλŠ”λ‹€.

μ‚¬μš©μž μž…λ ₯ κ΅¬ν˜„

μ‚¬μš©μž μž…λ ₯ κ΅¬ν˜„μ—μ„œ κ³ λ €ν•  점은 λ‹¨μˆœν•˜κ²Œ ν•˜λ‚˜μ˜ ν‚€μ›Œλ“œλ§Œ μž…λ ₯λ°›μ•„μ„œ λ¬Έμ„œλ₯Ό μƒμ„Έν•˜κ²Œ 검색할 μˆ˜λŠ” μ—†λ‹€λŠ” 것이닀.
μœ μ‚¬μ–΄, μ œμ™Έμ–΄, μ—¬λŸ¬ ν‚€μ›Œλ“œ, And 쑰건, Or 쑰건 λ“± λ‹€μ–‘ν•œ 쑰건으둜 검색이 κ°€λŠ₯ν•˜λ©΄ μ‚¬μš©μžμ—κ²Œ 더 쒋은 검색 μ‹œμŠ€ν…œμ΄ 될 수 μžˆλ‹€.
κ·Έλž˜μ„œ 검색 ν‚€μ›Œλ“œμ—μ„œ μ‚¬μš©ν•  수 μžˆλ„λ‘ 특수 μ»€λ§¨λ“œλ₯Ό μ •λ¦¬ν•΄λ³΄μ•˜λ‹€.

단일 검색: search=<ν‚€μ›Œλ“œ>                      예) 맨투맨
μœ μ‚¬μ–΄ 검색: search=<Keyword-A + keyword-B>    예) 맨투맨 +μ•„λ””λ‹€μŠ€
    => 맨투맨이 ν¬ν•¨λœ λ¬Έμ„œμ—μ„œ μ•„λ””λ‹€μŠ€κ°€ ν¬ν•¨λœ λ¬Έμ„œ
μ œμ™Έμ–΄ 검색: search=<Keyword-A - keyword-B>    예) 맨투맨 -μ•„λ””λ‹€μŠ€
    => 맨투맨이 ν¬ν•¨λœ λ¬Έμ„œμ—μ„œ μ•„λ””λ‹€μŠ€λΌλŠ” 단어가 μžˆλ‹€λ©΄ μ œμ™Έ
μ—¬λŸ¬ ν‚€μ›Œλ“œ 검색: search=<keyword-A, keyword-B> 예) 맨투맨, μ•„λ””λ‹€μŠ€
    => 맨투맨이 ν¬ν•¨λœ λ¬Έμ„œμ—μ„œ μ•„λ””λ‹€μŠ€λŸ¬λŠ” 단어가 ν¬ν•¨λœ λ¬Έμ„œ(ν•˜μ§€λ§Œ κ°•μ œλŠ” μ•„λ‹˜)
And 쑰건 검색: search=<Keyword-A & keyword-B> 예) 맨투맨 & μ•„λ””λ‹€μŠ€
    => 맨투맨과 μ•„λ””λ‹€μŠ€κ°€ 무쑰건 λ“€μ–΄κ°„ λ¬Έμ„œ
OR 쑰건 검색: search=<Keyword-A | Keyword-B>  예) λ‚˜μ΄ν‚€ | μ•„λ””λ‹€μŠ€
    => 맨투맨 λ˜λŠ” μ•„λ””λ‹€μŠ€κ°€ ν¬ν•¨λœ λ¬Έμ„œ
성별 검색결과 λΆ€μŠ€νŒ…: search=<keyword-A> (μ—¬μ„±: ^^, 남성:^~) 
    => ν‚€μ›Œλ“œ λ’€ ν•΄λ‹Ή 특수문자λ₯Ό λΆ™μ—¬ μ—¬μ„± λ˜λŠ” 남성이 μž‘μ„±ν•œ 것 같은 κ²°κ³Όλ₯Ό λΆ€μŠ€νŒ…
λͺ…μ‚¬λ‘œλ§Œ 검색: search=<keyword-A>^&            예) 예쁨^&
    => μž…λ ₯ ν‚€μ›Œλ“œλ₯Ό λͺ…μ‚¬λ‘œ λΆ„λ₯˜, ν•΄λ‹Ή λͺ…사가 ν¬ν•¨λœ λ¬Έμ„œλ§Œ κ°€μ Έμ˜΄

ν•΄λ‹Ή 검색 ν‚€μ›Œλ“œλ₯Ό 받을 수 μžˆλ„λ‘ APIλ₯Ό κ΅¬ν˜„ν•΄λ³΄λ©΄ μ•„λž˜μ™€ 같은 μ½”λ“œκ°€ λœλ‹€. κ°„λ‹¨ν•œ API κ΅¬ν˜„μ„ μœ„ν•΄ Python의 Fast APIλ₯Ό μ‚¬μš©ν•˜μ˜€λ‹€.

from typing import Optional
from fastapi import FastAPI
from create_es_query import CreateEsIndex
import uvicorn

import json
import elasticsearch

app = FastAPI()
# Elastic Search config
f = open('config.json')
config = json.loads(f.read())
es = elasticsearch.Elasticsearch([f"http://{config['host_url']}:9200"], http_auth=(config['username'], config['password']))

# νŽ˜μ΄μ§€ κΈ°λ³Έ 접속을 μœ„ν•œ API
@app.get("/")
def read_root():
    return {"Hello": "World"}

# 검색 ν‚€μ›Œλ“œλ₯Ό μž…λ ₯λ°›κΈ° μœ„ν•œ API
@app.get("/search/keyword")
def search_result(search: str = None):
    qry = CreateEsIndex()
    search_key = search
    query = qry.create_complex_query(search_key)

    t = es.search(index="review_pos", body=query)
    doc_list = t.body['hits']['hits']
    hit_docs = [d['_source'] for d in doc_list]
    return {"result": hit_docs}

if __name__ == '__main__':
    uvicorn.run(app, host="localhost", port=8000)

μœ„ μ½”λ“œμ—μ„œ search_result ν•¨μˆ˜λ‘œ 검색어λ₯Ό λ°›λŠ” APIλ₯Ό κ΅¬ν˜„ν•˜μ˜€λ‹€. μ‹€μ œλ‘œ ν•΄λ‹Ή API둜 μš”μ²­ν•˜λ €λ©΄
http://localhost:8000/search/keyword?search=<keyword>둜 μš”μ²­ν•˜λ©΄ λœλ‹€.

μž…λ ₯ ν‚€μ›Œλ“œ μ „μ²˜λ¦¬ 둜직 κ΅¬ν˜„

μ‚¬μš©μžλ‘œ λΆ€ν„° μž…λ ₯받은 ν‚€μ›Œλ“œμ— μœ μ‚¬μ–΄κ°€ ν¬ν•¨λ˜μ—ˆλŠ”μ§€, μ œμ™Έμ–΄κ°€ ν¬ν•¨λ˜μ—ˆλŠ”μ§€ μ»΄ν“¨ν„°λŠ” μ•Œμ§€ λͺ»ν•œλ‹€. 이λ₯Ό μœ„ν•΄ λ³„λ„μ˜ λͺ¨λ“ˆμ„ ν•˜λ‚˜ λ§Œλ“€μ–΄ μž…λ ₯받은 ν‚€μ›Œλ“œλ₯Ό ꡬ뢄할 수 μžˆλ„λ‘ ν•˜μ˜€λ‹€.

class CreateEsIndex:

    def __init__(self):
        self.main_keyword = []
        self.synonym, self.exclude, self.multi_key, self.and_cond, \
        self.or_cond, self.woman, self.man, self.noun = [], [], [], [], [], [], [], []
        self.condition_map = {
            '-': self.exclude, '+': self.synonym, '|': self.or_cond, '&': self.and_cond,
            ',': self.multi_key, '^^': self.woman, '^~': self.man, '^&': self.noun
        }
        return

    @staticmethod
    def text_clear(keyword: str):
        rep_list = ['-', '+', '|', '&', ',', '^^', '^~', '^&']
        for r in rep_list:
            keyword = keyword.replace(r, '')
        return keyword

    def process_word(self, search_word: str):
        word_list = search_word.split(' ')
        for i in range(0, len(word_list)):
            w = word_list[i]
            if i == 0:
                self.main_keyword.append({"match": {"review": self.text_clear(w)}})
            [self.condition_map[s].append(self.text_clear(w)) if s in w else None for s in self.condition_map.keys()]

    def create_complex_query(self, search_word: str):
        self.process_word(search_word)
        q = {
            "query": {
                "bool": {
                    "must": self.main_keyword,
                    "must_not": [],
                    "should": []
                }
            }
        }
        bol = q['query']['bool']
        for k, v in self.condition_map.items():
            if not v:
                continue
            words = [{"match": {"review": w}} for w in v]
            if k == "+":
                bol['should'].extend(words)
            if k == "-":
                bol['must_not'].extend(words)
            if k == ",":
                bol['should'].extend(words)
            if k == "&":
                bol['must'].extend(words)
            if k == "|":
                bol['should'].extend(words)
            if k == "^^":
                boost = {"rank_feature": {"field": "genders.female", "boost": 4}}
                bol['should'].append(boost)
            if k == "^~":
                boost = {"rank_feature": {"field": "genders.male", "boost": 4}}
                bol['should'].append(boost)
            if k == "^&":
                words = [{"match": {"review.nori_noun": w}} for w in v]
                bol['should'].extend(words)
        return q

μœ„ μ½”λ“œμ—μ„œ CreateEsIndex 클래슀의 초기 λ³€μˆ˜λ‘œ 검색 쑰건에 ν•΄λ‹Ήν•˜λŠ” 리슀트λ₯Ό μƒμ„±ν•˜μ˜€λ‹€.
여기에 μ μ ˆν•œ 단어λ₯Ό μΆ”κ°€ν•˜κΈ° μœ„ν•΄ process_word ν•¨μˆ˜λ‘œ μ‚¬μš©μžλ‘œλΆ€ν„° μž…λ ₯받은 ν‚€μ›Œλ“œλ₯Ό λΆ„λ₯˜ν•œλ‹€. 이λ₯Ό 톡해 ES에 μ‘°νšŒν•  수 μžˆλŠ” 쿼리λ₯Ό μƒμ„±ν•˜μ˜€λ‹€.

μ˜ˆμ‹œ

ν‚€μ›Œλ“œ: 맨투맨

μž…λ ₯ Query

{'query': {'bool': {'must': [{'match': {'review': '맨투맨'}}], 'must_not': [], 'should': []}}}

κ²°κ³Ό

{
  "result": [
    {
      "gender": {
        "female": 0.385272741317749,
        "male": 0.614727258682251
      },
      "prd_id": 957878,
      "review": "μ•„λ””λ‹€μŠ€ 맨투맨 ν›„κΈ°μ§„μ§œ μ˜€λ²„ν• μ œλŒ€λ‘œμΈ 맨투맨 인생 맨투맨!!!!!",
      "review_id": 7325803
    },
    {
      "gender": {
        "female": 0.5234223008155823,
        "male": 0.4765776991844177
      },
      "prd_id": 897632,
      "review": "맨투맨 뭐살지 κ³ λ―Όν•˜μ‹ λ‹€λ©΄ 이 맨투맨 μΆ”μ²œλ“œλ €μš”",
      "review_id": 4934238
    },
    ...
  ]
}

ν‚€μ›Œλ“œ: 맨투맨 -μ•„λ””λ‹€μŠ€

μž…λ ₯ Query

{'query': {'bool': {'must': [{'match': {'review': '맨투맨'}}], 'must_not': [{'match': {'review': 'μ•„λ””λ‹€μŠ€'}}], 'should': []}}}

κ²°κ³Ό

{
  "result": [
    {
      "gender": {
        "female": 0.5234223008155823,
        "male": 0.4765776991844177
      },
      "prd_id": 897632,
      "review": "맨투맨 뭐살지 κ³ λ―Όν•˜μ‹ λ‹€λ©΄ 이 맨투맨 μΆ”μ²œλ“œλ €μš”",
      "review_id": 4934238
    },
    {
      "gender": {
        "female": 0.6838095188140869,
        "male": 0.3161904811859131
      },
      "prd_id": 1163605,
      "review": "λΈŒλŸ¬μ‹œνŠΈ 클럽 맨투맨 λΈ”λž™μž…λ‹ˆλ‹€λΈŒλŸ¬μ‹œνŠΈ 클럽 맨투맨 λΈ”λž™μž…λ‹ˆλ‹€",
      "review_id": 7883291
    },...
   ]
}
λ°˜μ‘ν˜•