Dev/Django

Elasticsearch와 Django를 연동해 검색 API 개발

sincerely10 2020. 8. 17. 21:16
반응형

안녕하세요. 이 포스트는 시리즈로 구성되어있습니다.

이번 포스트는 지난 포스트의 로그인/회원가입, 소셜로그인을 이어 검색 API에 대해서 작성해보겠습니다.
검색 API를 적용해본 것은 기존에도 ElasticSearch를 비롯한 ELK(ElasicSearch, LogStash, Kibanna) 기술 스택에 관심이 있었고 1차로 진행한 프로젝트에서 새로운 것을 적용해보자는 마음이 있었습니다.

elasticsearch(엘라스틱 서치)에 대해 간략하게 소개하자면, 오픈소스의 검색엔진입니다. 자세한 건 추후에 포스트 할 기회가 있다면 하도록 하겠습니다.

구현한 과정 절차 따라 서술하도록 하겠습니다.
제가 reference 한 글은 https://blog.nerdfactory.ai/2019/04/29/django-elasticsearch-restframework.html 입니다.

1. ElasticSearch 설치

가장 먼저 elastic search의 설치입니다. 저는 mac OS를 사용하기 때문에 brew로 설치를 진행했습니다.
저는 공식 홈페이지에 있는 명령어를 통해 "brew install elastic/tap/elasticsearch-full" 명령어로 설치를 진행했습니다.

제가 설치한 version은 2020년 8월 17일 기준으로 latest 버전으로 7.8.1이 설치됐습니다.
또한 설치 경로는 /usr/local/Cellar/elasticsearch-full/7.8.1 입니다.

2. 한글 형태소 분석기 nori 설치

한글 형태소 분석을 위한 nori라는 elastic search의 플러그인을 설치해줍니다. 설치를 실행해주는 파일은 /usr/local/Cellar/elasticsearch-full/7.8.1/bin 디렉터리 밑에 elasticsearch-plugin 파일입니다. 설치 명령어는 elasticsearch-plugin install analysis-nori 입니다.

3. elastic search 실행

설치한 elastic search를 실행해줍니다. elasticsearch라고만 입력해줘도 path가 지정되어 있기 때문에 실행이 됩니다. 초기 설정값을 불러오고 실행이 되는 것을 확인할 수 있습니다.
참고하실 점은 저는 elastic search를 단일 Node로 설치했다는 점입니다.(elastic search도 분산 저장 및 처리를 지원합니다.)

마찬가지로 기회가 된다면 elasticsearch에 대한 개괄적인 설명도 포스트 하겠습니다.

4. python elasticsearch 설치

pip install elasticsearch==7.8.1 로 python에서 elasticsearch를 컨트롤하기 위한 모듈을 설치합니다.

pip freeze로 원하는 버전의 elasticsearch가 설치됐는지 확인합니다.

5. elasticsearch index 생성 및 bulk data import

DB의 역할과 비슷한 index를 생성해줍니다. 그리고 제가 단어를 찾고자 하는 대상(칼럼의 역할과 같은) properties를 생성해줍니다. 제가 작성한 bulk_setting.py를 공유하면 다음과 같습니다.

 1 import requests, json, os
 2 from elasticsearch import Elasticsearch
 3
 4 directory_path = 'path'
 5 res = requests.get('http://localhost:9200')
 6 es = Elasticsearch([{'host':'localhost','port':'9200'}])
 7
 8 es.indices.create(
 9     index='dictionary',
10     body={
11         "settings": {
12             "index": {
13                 "analysis": {
14                     "analyzer": {
15                         "my_analyzer": {
16                             "type": "custom",
17                             "tokenizer": "nori_tokenizer"
18                         }
19                     }
20                 }
21             }
22         },
23         "mappings": {
24             "properties": {
25                 "product_id": {
26                     "type": "long",
27                 },
28                 "product_name": {
29                     "type": "text",
30                     "analyzer": "my_analyzer"
31                 },
32                 "sub_category_name": {
33                     "type": "text",
34                     "analyzer": "my_analyzer"
35                 },
36                 "creator": {
37                     "type": "text",
38                     "analyzer": "my_analyzer"
39                 }
40             }
41         }
42     }
43 )

제가 찾고자 하는 검색 대상은 강의의 이름(product_name), 강의가 속한 카테고리(sub_category_name), 크리에이터의 이름(creator)입니다. 원래는 nested로 강의의 챕터와 강의 컨텐츠를 검색 대상에 포함시키려 했는데 nested를 사용하려면 중간에 비어있지 않아야 해서 결국 포함시키지 못했습니다.

그리고 제가 reference 하는 글은 mapping 아래 dictionary_datas라는 mapping type을 추가했는데 7.x 대 버전부터는 이 mapping type을 적으면 에러가 발생했습니다.

6. JSON 파일 import

Json 포맷의 데이터를 한 줄씩 읽어서 index에 import 하기 좋은 형태로 만들어줍니다. 비교적 간단한 내용이기 때문에 코드만 첨부하겠습니다.

60 with open(directory_path+"dictionary_data.json", encoding='utf-8') as json_file:
61     json_data = json.loads(json_file.read())
62 
63 body = ""
64 j=1
65 for i in json_data:
66     body = body + json.dumps({"index": {"_index": "dictionary", "_id":j}}) + '\n'
67     body = body + json.dumps(i, ensure_ascii=False) + '\n'
68     if j == 1:
69         print(body)
70     j += 1
71 
72 f = open(directory_path+'input.json', 'w')
73 f.write(body)
74 f.close()
75
76 es.bulk(body) 

결과는 아래와 같습니다.

1 {"index": {"_index": "dictionary", "_id": 1}}
2 {product_id: 1, product_name: 지금 가장 새로운 음악, sub_category_name: 음악, creator: 그루비룸}
3 {index: {_index: dictionary, _id: 2}}
4 {product_id: 2, product_name: 언제 어디서나 힐링악기 칼림바와 함께 마음을 두드려요, sub_category_name: 음악, creator: 알찬ALCHAN}
5 {index: {_index: dictionary, _id: 3}}
6 {product_id: 3, product_name: 홈레코딩 101 : 작사, 작곡부터 싱글발매까지, sub_category_name: 음악, creator: 주니}
7 {index: {_index: dictionary, _id: 4}}

 

7. Json Data Import 하기

이제 elasticsearch의 index에 생성한 JSON 포맷의 파일을 import 해주겠습니다. postman이나 python 명령어로 import 할 수 있지만, 터미널에서 curl이 비교적 간단하므로 해당 방법을 사용하겠습니다.

curl -X POST http://localhost:9200/dictionary/_bulk\pretty --data-binary @search/input.json

8. Django Setting

7번 까지가 elasticsearch의 모든 관련 설정이었습니다. 이제 Django에서 elasticsearch를 활용할 수 있도록 변경해보겠습니다.

8.1 먼저 Django에 search app을 생성해보겠습니다.

django-admin startapp search

8.2 project의 settings.py에 INSTALLED_APPS에 app을 등록

8.3 url 등록
저는 ip:8000/search/?search=검색어 라는 형태로 url을 만들 예정이기 때문에 이에 맞게 설정하고 views를 등록하였습니다.

9. Search App views.py 등록

드디어 마지막 단계인 search app의 views.py의 생성입니다. 저는 비교적 간단하게 index를 만들었고, 이에 해당하는 검색이 되도록 views.py를 만들었습니다. 더 효율적인 방법이 있겠지만, 검색해서 단순하게 결과가 나오는 방법을 이용했습니다.

 1 import json
 2 from elasticsearch import Elasticsearch
 3
 4 from django.views import View
 5 from django.http  import JsonResponse
 6
 7 from my_settings import ELASTICSEARCH
 8
 9 class SearchView(View):
10     def get(self, request):
11         es = Elasticsearch([{
12             'host':ELASTICSEARCH['host'],
13             'port':ELASTICSEARCH['port']
14         }])
15
16         search_word = request.GET.get('search')
17
18         if not search_word:
19             return JsonResponse({'message':'INVALID_REQUEST'}, status=400)
20
21         products = es.search(
22             index       = 'dictionary',
23             body        = {
24                 "query" : {
25                     "multi_match" : {
26                         "query"  : search_word,
27                         "fields" : [
28                             "product_name",
29                             "sub_category_name",
30                             "creator"
31                         ]
32                     }
33                 }
34             }
35         )
36         product_list = []
37         for product in products['hits']['hits']:
38             product_list.append(product.get('_source'))
39
40         return JsonResponse({'data': product_list}, status=200)

 

<검색 결과>

운동이라는 단어를 검색한 경우, 강의 이름(product_name)에 들어간 경우와 강의 카테고리(sub_category_name)가 함께 검색되는 것을 볼 수 있습니다.

 

10. elasticsearch를 적용해보며

정리해보니 정말 간단한 내용인 것처럼 느껴집니다. 물론 혼자서 직접 해보는 저는 많은 시행착오 끝에 완성시켰습니다. 그럼에도 elasticsearch가 빠르고 적용하기가 비교적 간단하기 때문에 다음에도 또 사용할 것입니다.

아쉬운 것은 1. nested 형태를 적용하지 못한 것과 2. 사용자 사전(고유명사, 신조어 등에 활용)을 해보지 못 한 것입니다.

ELK 스택으로의 확장해서 더 다양한 것을 해보고 싶네요.
읽어주셔서 감사합니다!

반응형