본문 바로가기
Coding/Python

ElasticSearch Ngram 활용하여 검색하는 방법

by Hide­ 2021. 8. 11.
반응형

개요

많은 케이스가 있겠지만 보통 검색 용도로 ElasticSearch를 사용하는 것으로 알고 있다. MySQL의 경우 LIKE쿼리를 사용하여 검색을 할 수 있는데, %파이썬% 과 같이 앞뒤로 %를 사용하여 검색하는 경우 인덱스를 태우지 못하고 Full scan을 해버린다. 데이터베이스는 데이터베이스일 뿐 검색용도가 아니기에 애초에 적합하지 않다. 그렇다면 엘라스틱 서치를 사용하여 단순히 검색 쿼리를 날리면 바로 해결이 가능할까?

보통 DB를 접하다가 ES를 처음 접하는 사람들은 LIKE쿼리와 같이 Wildcard를 줘서 검색하는 방법을 찾곤 한다. (나또한 그랬다) 아무리 엘라스틱 서치가 검색 용도에 적합하다고는 하지만, 데이터베이스와 마찬가지로 와일드 카드를 통해 검색을 진행하면 속도 저하가 발생할 수 밖에 없다. 

따라서 엘라스틱 서치에서 제공하는 Ngram을 활용하여 문제를 해결해야하는데 이 방법에 대해 간단하게 설명한다.

환경 설정

실습은 도커를 활용하여 진행하겠다. 먼저 아래의 명령어를 터미널에 입력하여 이미지를 땡겨온다. (버전은 알아서 수정해도 된다)

docker pull docker.elastic.co/elasticsearch/elasticsearch:6.8.1

다음으로 아래의 명령어를 통해 실행시킨다.

docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.8.18

인덱스 생성 및 설정

다음으로 아래의 리퀘스트를 보내서 Ngram설정과 동시에 인덱스를 생성한다.

PUT http://localhost:9200/event-video

    "settings": {
        "analysis": {
            "analyzer": {
                "ngram_analyzer": {
                    "tokenizer": "ngram_tokenizer"
                }
            },
            "tokenizer": {
                "ngram_tokenizer": {
                    "type": "nGram",
                    "min_gram": "2",
                    "max_gram": "10",
                    "token_chars": ["letter", "digit"]
                }
            }
        }
    },
    "mappings": {
        "event-video-doc": {
            "properties": {
                "title": {
                    "type": "text",
                    "analyzer": "ngram_analyzer"
                },
                "description": {
                    "type": "text",
                    "analyzer": "ngram_analyzer"
                }
            }
        }
    }
}

properties내부를 보면 필드들의 속성에 "analyzer": "ngram_analyzer" 를 넣어줘서 해당 토크나이저를 사용한다고 명시해놨다. 이 부분을 빼먹는다면 ngram 설정을 해줬어도 해당 토크나이저를 사용하지 않기 때문에 정상적으로 동작하지 않는다.

위처럼 나오면 정상적으로 인덱스가 생성된 것이다. 이제 실제 인덱스 정보를 요청하여 생성된 인덱스의 내용을 확인한다.

GET http://localhost:9200/event-video/_settings

{
    "event-video": {
        "settings": {
            "index": {
                "number_of_shards": "5",
                "provided_name": "event-video",
                "creation_date": "1628647550590",
                "analysis": {
                    "analyzer": {
                        "ngram_analyzer": {
                            "tokenizer": "ngram_tokenizer"
                        }
                    },
                    "tokenizer": {
                        "ngram_tokenizer": {
                            "token_chars": [
                                "letter",
                                "digit"
                            ],
                            "min_gram": "2",
                            "type": "nGram",
                            "max_gram": "10"
                        }
                    }
                },
                "number_of_replicas": "1",
                "uuid": "FbU8JPQaS66vpp2zruc__g",
                "version": {
                    "created": "6081899"
                }
            }
        }
    }
}

데이터 삽입

먼저 나는 파이썬을 사용하고 있기 때문에 elasticsearch라는 라이브러리를 사용할 것이다. (https://elasticsearch-py.readthedocs.io/en/v7.14.0/) 또한 FastAPI를 통해 웹서버를 구동하고 있기 때문에 AsyncElasticsearch를 통해 클라이언트 인스턴스를 생성할 예정이다. 물론 예제에서는 asyncio를 통해 run하는 형태로 진행한다.

import asyncio
from elasticsearch import Elasticsearch, AsyncElasticsearch


es = AsyncElasticsearch()

index_name = "event-video"
doc_type = "event-video-doc"

doc1 = {
    "title": "파이썬 실습 1",
    "description": "fastapi로 구현하는 웹서버"
}
doc2 = {
    "title": "안녕 파이썬",
    "description": "테스트"
}
doc3 = {
    "title": "웹서버 실습",
    "description": "파이썬 fastapi로 구현한다"
}

async def main():
    await es.index(index=index_name, doc_type=doc_type, body=doc1)
    await es.index(index=index_name, doc_type=doc_type, body=doc2)
    await es.index(index=index_name, doc_type=doc_type, body=doc3)

asyncio.run(main())

위처럼 코드를 작성하고 실행하면 총 3개의 도큐먼트가 생성됐을 것이다. 만약 위 코드를 실행했는데

Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x105ff0250>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x105ff2280>, 0.359082876)]']
connector: <aiohttp.connector.TCPConnector object at 0x105ff0160>

 와 같은 오류가 발생한다면 es.close() 라인을 추가해주면 된다. (https://elasticsearch-py.readthedocs.io/en/v7.14.0/async.html#receiving-unclosed-client-session-connector-warning)

테스트

import asyncio
from elasticsearch import Elasticsearch, AsyncElasticsearch


es = AsyncElasticsearch()

index_name = "event-video"
doc_type = "event-video-doc"


query = {
    "bool": {
        "should": [
            {
                "term": {"title": "파이"},
            },
            {
                "term": {"description": "파이"}
            }
        ]
    }
}


async def main():
    result = await es.search(index=index_name, body={
            "query": query,
        }
    )
    for r in result['hits']['hits']:
        print(r)

    await es.close()


asyncio.run(main())

title 또는 description에 "파이"라는 문자열이 포함된 도큐먼트를 뽑아내는 쿼리이다. 실행시켜보면,

{'_index': 'event-video', '_type': 'event-video-doc', '_id': 'iFL1MnsBQDWXkE5cpeCX', '_score': 0.2876821, '_source': {'title': '파이썬 실습 1', 'description': 'fastapi로 구현하는 웹서버'}}
{'_index': 'event-video', '_type': 'event-video-doc', '_id': 'iVL1MnsBQDWXkE5cpeCk', '_score': 0.2876821, '_source': {'title': '안녕 파이썬', 'description': '테스트'}}
{'_index': 'event-video', '_type': 'event-video-doc', '_id': 'ilL1MnsBQDWXkE5cpeCs', '_score': 0.2876821, '_source': {'title': '웹서버 실습', 'description': '파이썬 fastapi로 구현한다'}}

위처럼 정상적으로 값이 뽑혀나온다.