회원가입

Django ElasticSearch 연동

Beany 2024-11-24

ChatGPT 요약

이 글은 ElasticSearch를 활용하여 Django 웹사이트의 검색 성능을 향상시키는 방법에 대해 다룬다. 기존에 사용하던 %로 검색하는 방식의 한계를 설명하고, ElasticSearch를 이용하면 검색 엔진으로 효과적으로 활용할 수 있다는 내용을 다룬다. ElasticSearch를 Django와 함께 사용하는 방법과 설정하는 방법, 그리고 한국어 검색을 위한 Nori 분석기 사용 방법에 대해 상세히 설명되어 있다. ElasticSearch 설정에서 메모리 사용량 문제를 수정하는 방법도 포함돼 있다.

아래 링크 후속으로 만든 게시글입니다. ElasticSearch 란 무엇인가 확인하고 싶으면 아래 글을 참고하세요~

https://cwbeany.com/tip-dev/68

 

 

활용하게 된 계기


블로그에 대한 전반적인 내용을 확인하고 싶으면 아래 게시판 참고

https://cwbeany.com/board/blog_diary

 

현재 블로그 코드는 icontains 를 이용해서 블로그 글을 조회하고 가져오고 있습니다.

def get_active_filtered_posts(search: str = None,
                              board_urls: List[str] = None,
                              tag_names: List[str] = None) -> QuerySet[Post]:
    q = Q()
    qs = get_active_posts()
    if search:
        q = q & Q(title__icontains=search) | Q(body__icontains=search)
    if board_urls:
        q = q & Q(board__url__in=board_urls)
    if tag_names:
        tag_id_list = Tag.objects.filter(
            tag_name__in=tag_names
        ).values_list(
            'id',
            flat=True
        )
        q = q & Q(tag_set__in=tag_id_list)
        qs = qs.prefetch_related('tag_set')
    return qs.select_related(
        'board',
    ).filter(
        q
    )

위의 코드를 실행하면 대충 아래와 같은 SQL 이 나옵니다.

SELECT *
FROM "board_post"
         INNER JOIN "board_board" ON ("board_post"."board_id" = "board_board"."id")
WHERE ("board_post"."is_active" AND ("board_post"."title" LIKE %Java Python% ESCAPE '\' OR "board_post"."body" LIKE %Java Python% ESCAPE '\'))

LIKE 절을 %% 로 이용하고 있습니다.

이런 경우 조회할 때, Index 를 타지 못하고 오랜 시간이 걸립니다. 물론 많은 게시글이 없으면 문제는 없겠지만 언제든지 문제가 일어날 수 있는 상황입니다.

 

또한 검색에서 원하는 것을 의도하지 못할 수 있습니다. 예를 들어 제 게시글에 "안녕하세요 오늘은 Java 와 Python 에 대해 알아봅시다!" 라는 문구가 있다고 합시다.

만약 Java Python 을 검색창에 입력하면 저 내용이 있는 게시판을 찾지 못할 것입니다.

 

정리하자면

  • LIKE %% 절 Index Scan 되지 않아 성능 이슈가 있다
  • 원하는 검색이 이뤄지지 않을 수 있다.

 

이런 경우 ElasticSearch 를 이용하면 검색엔진으로서 아주 좋게 활용할 수 있습니다!

 

 

ElasticSearch 활용


Django 와 ElasticSearch 를 활용하는 방법은 4가지 정도 있는 것 같습니다.

  1. request 로 요쳥
  2. elasticsearch 라이브러리 이용
  3. elasticsearch-dsl 라이브러리 이용
  4. django-elasticsearch-dsl 라이브러리 이용

4번은 이용하려고 합니다.

 

 

ElasticSearch 설치


설치 링크https://www.elastic.co/guide/en/elasticsearch/reference/8.16/deb.html#deb-repo

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
sudo apt-get install apt-transport-https
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list
sudo apt-get update && sudo apt-get install elasticsearch

# 한국어 조회 가능 플러그인 설치
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-nori
# -> Installing analysis-nori
# -> Downloading analysis-nori from elastic
# [=================================================] 100%
# -> Installed analysis-nori
# -> Please restart Elasticsearch to activate any plugins installed

 

sudo systemctl start elasticsearch
sudo systemctl enable elasticsearch

sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable elasticsearch.service
# 비밀번호 설정
/usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -i

 

https://localhost:9200/ 에 들어가면 설정한 비밀번호로 접속하면 잘 접속됩니다!

 

 

elasticsearch-dsl 사용


elasticsearch-dsl 을 사용하기 위해서 설치를 합시다.

pip install django-elasticsearch-dsl

 

Django 프로젝트의 settings.pydjango_elasticsearch_dsl을 추가합니다.

INSTALLED_APPS = [
    # 기타 앱
    'django_elasticsearch_dsl',
]

 

Django 설정 파일 (settings.py)에 다음을 추가합니다:

ElasticSearch 설치 시, http_ca.crt 는 설치 할 때 받을 수 있습니다. 해당 파일을 프로젝트 루트에 놓습니다.

cp /etc/elasticsearch/certs/http_ca.crt 프로젝트_루트/http_ca.crt

cd 프로젝트_루트
# 실행 권한 추가
chmod 644 http_ca.crt
# settings.py
ELASTICSEARCH_DSL = {
    'default': {
        'hosts': [
            {
                'host': 'localhost',
                'port': 9200,
                'use_ssl': True,
                'ca_certs': os.path.join(BASE_DIR, 'http_ca.crt'),
            }
        ],
        'http_auth': (ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD),
    },
}

 

 

[ Index 생성 ]

이제 Elasticsearch Index 를 생성합시다.

django-elasticsearch-dsl에서 Elasticsearch 인덱스와 Django 모델의 매핑을 정의하려면 Document 클래스를 사용합니다.

한국어 검색이 잘 되지 않는 이유는 Elasticsearch의 기본 분석기(standard analyzer)가 영어와 같은 공백 기반 언어에 최적화되어 있기 때문입니다. 한국어와 같은 공백이 아닌 형태소 기반 언어는 적합한 분석기(Analyzer)를 사용해야 검색이 제대로 동작합니다.

Nori 분석기 사용: Elasticsearch에서 한국어 텍스트 처리를 위한 기본 제공 분석기인 Nori를 사용합니다.

from django_elasticsearch_dsl import (
    Document,
    fields,
)
from django_elasticsearch_dsl.registries import registry
from board.models import Post


@registry.register_document
class PostDocument(Document):
    title = fields.TextField(analyzer="korean_analyzer")
    body = fields.TextField(analyzer="korean_analyzer")
    post_img = fields.KeywordField()
    board = fields.ObjectField(
        properties={
            "url": fields.TextField(),
        }
    )
    author = fields.ObjectField(
        enabled=False,  # 색인하지 않음, 조회에서만 사용
        properties={
            "id": fields.IntegerField(),
            "username": fields.KeywordField(),
            "nickname": fields.KeywordField(),
        }
    )
    tag_set = fields.NestedField(
        properties={
            "id": fields.IntegerField(),
            "tag_name": fields.TextField(analyzer="korean_analyzer"),
        }
    )

    class Index:
        # Define the Elasticsearch index name
        name = 'posts'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
            "analysis": {
                "analyzer": {
                    "korean_analyzer": {
                        "type": "custom",
                        "tokenizer": "nori_tokenizer",
                        "filter": ["lowercase"]
                    }
                }
            }
        }

    class Django:
        model = Post  # Django 모델과 연결
        fields = [
            'id',
            'like_count',
            'reply_count',
            'rereply_count',
            'is_active',
            'created_at',
            'updated_at',
        ]

    def prepare_post_img(self, instance):
        """
        이미지 URL을 Elasticsearch에 저장
        """
        return instance.post_img.url if instance.post_img else None

인덱스를 생성합니다. (참고로 저는 ckeditor 를 이용하고 있어서 body 를 위쪽에 뺏습니다.)

python manage.py search_index --rebuild

--rebuild 는 기존 인덱스를 삭제하고 새로 만듭니다.

Are you sure you want to delete the 'posts' indices? [y/N]: y
Deleting index 'posts'
Creating index 'posts'
Indexing 11 'Post' objects 

 

 

[ 조회 ]

searchs.py 파일을 만들어서 검색하는 로직을 추가합니다.

def search_posts(
        query: str = None,
        board_urls: List[str] = None,
        tag_ids: List[int] = None,
        sort_fields: List[str] = None,
) -> Search:
    """
    Elasticsearch에서 게시글 검색 및 Django Paginator와 연동
    """
    # 기본 정렬 설정
    if sort_fields is None:
        sort_fields = ["-created_at"]

    # 기본 검색 쿼리 설정
    if query:
        q = Q(
            "bool",
            should=[
                Q("match", title=query),
                Q("match", body=query),
            ],
            minimum_should_match=1
        )
    else:
        q = Q("match_all")  # query가 없을 경우 전체 검색

    # 필터 조건 설정
    filters = [Q("term", is_active=True)]  # 활성화된 게시글만 필터링

    if board_urls:
        filters.append(Q("terms", board__url=board_urls))

    if tag_ids:
        filters.append(
            Q(
                "nested",
                path="tag_set",
                query=Q("terms", tag_set__id=tag_ids)
            )
        )

    # 필터를 bool 쿼리에 추가
    if filters:
        q = Q("bool", must=[q], filter=filters)

    return PostDocument.search().query(q).sort(*sort_fields)
from board.searchs import search_posts

search_posts('테스트')[0]
<Response: [PostDocument(index='posts', id='3')]>

search_posts('조커')[0]
<Response: [PostDocument(index='posts', id='12')]>

search_posts('신')[0]
<Response: [PostDocument(index='posts', id='12')]>

아주 한국어 잘 작동합니다~!

이제 기존 코드를 전부 이렇게 수정합니다.

 

 

추가


Elastic Search 를 이용하고 메모리를 너무 많이 쓰고 있는 것 같다.

기본 2G 로 설정하자.

sudo vi /etc/elasticsearch/jvm.options

# 안에 들어가서 4g 를 2g 로 수정합니다.
-Xms4g
-Xmx4g

# 수정
-Xms2g
-Xmx2g

# 재실행!
sudo systemctl restart elasticsearch

 

수정 완료!!

0 0
블로그 일기
제 블로그의 고도화 과정을 설명합니다. 이는 코드 리팩토링과 추가된 기능들에 대해 기록하기 위한 게시판입니다. 어떤 기능이 추가되었는지, 무엇이 개선되었는지 등 고도화되는 과정을 자세히 다룰 예정입니다.
Yesterday: 456
Today: 55