SW개발/개발이야기

[Django]검색 퍼포먼스를 향상하기까지의 과정 2

4. Full Text Search 적용 (GIN Index)

검색을 하면서 SearchVector를 활용하면 LIKE가 포함된 SQL에 굉장히 효과적이라는 글을 여럿 찾을 수 있었습니다. Django 공식 문서에서도 나와있는 만큼 Full Text Search를 적용해보기로 합니다.

https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/search/

 

Full text search | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

4-1. SearchVector 란?

예시) 메시지 내용 입니다.

Postgres의 SearchVector란 위와 같은 메시지 content가 존재한다고 할 때, 문자열을 1. 메시지 2. 내용 3. 입니다 와 같이 분리 하여 GIN Index를 적용해 LIKE 검색 시에 조금 더 빠르게 수행되도록 하는 필드라고 할 수 있습니다.

 

Django에서 적용하기 위해 다음과 같이 모델을 수정하였습니다.

from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex

class Message(models.Model):
    content = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
	# SearchVector 필드 추가
    content_search = SearchVectorField(null=True)
    ...

    class Meta:
        # GIN Index 적용
        indexes = [GinIndex(fields=["content_search"])]

마이그레이션을 진행하고나서 기존에 존재하는 데이터에 대해서도 vector 필드의 값을 업데이트 해주어야 합니다.

Message.objects.update(content_search=SearchVector('content'))

다만, 저의 경우에 약 5천만개의 데이터 업데이트를 수행하려다보니 시간이 굉장히 오래 소요되었습니다. 또한, 데이터 크기를 줄여 적용도 해보았지만 속도 개선이 미미한 수준이였기에 해당 방법 역시 제외하게 되었습니다.

 

다수의 데이터 업데이트를 오류 없이 빠르게 수행할 수 있는 방법을 정확히 모릅니다. 관련해서 정보를 아시는 분은 댓글 남겨주시면 감사하겠습니다.

 

5. 쿼리를 유심히 보다가..! (feat. Pagination)

앞선 게시물에서 언급했던 속도가 느린 SQL을 유심히 살펴보다가 한 가지 의문점을 발견하게 됩니다.

Select Count(*) FROM message WHERE ~~~
Between 2017-01-01 and 2021-12-31 LIKE %검색내용%;

대체 Pagination, 검색을 하는데 count(*)이라는 쿼리는 왜 필요한거지?

 

유심히 소스를 살펴보니 Pagination의 방법에 의해 발생되는 쿼리임을 찾을 수 있었고, PageNumberPagintion 방식을 이용해 페이징이 이루어지고 있다는 사실도 알게 됩니다. 30개 단위로 되어있는 검색 결과에서 더보기 버튼을 누를 때마다 count(*) 쿼리도 같이 발생되었습니다. 

 

DRF의 공식 Pagination 문서를 보면서 Count(*)가 필요없는 CursurPagination 방법으로 변경해보기로 합니다.

https://www.django-rest-framework.org/api-guide/pagination/#pagination

 

Pagination - Django REST framework

pagination.py Django provides a few classes that help you manage paginated data – that is, data that’s split across several pages, with “Previous/Next” links. — Django documentation REST framework includes support for customizable pagination styl

www.django-rest-framework.org

CursorPagination의 여러 특징들이 다행히도 서비스에 적합하다고 판단되었고, 변경하자마자 즉각적으로 결과를 얻을 수 있었습니다.

 

Count(*)로 발생되는 쿼리가 사라짐에 따라 가장 오래걸리는 2개의 쿼리중 1개가 제거되어 약 45%이상의 성능 향상을 얻게 되었습니다. 메모리에 캐싱된 이후라면 서비스 전체 기간으로 검색을 실행해도 약 1초 내외로 결과를 얻을 수 있어 꽤나 만족스러운 퍼포먼스라고 생각되었습니다.

 

마치며

오랜 기간동안 해결되지 않았던 문제이었기에 저의 수준에서 해결하는 것이 굉장히 어려웠습니다. 하지만, 문제를 해결하기 위해 수많은 가설을 세워보고 ,검색을 통해 얻어가는 것은 상당히 많았다고 생각합니다. 데이터베이스의 작동 방식 구조, 원리, 페이징 등 많은 것들을 몸소 느껴 보는 부분들이 굉장히 좋았던 것 같습니다.

특히, DRF Nested Serializer의 성능 issue와 관련된 글들을 많이 보았지만 직접 serializer를 변경해보니 실제로는 무의미한 차이 정도 였다는 것도 알게됩니다. (직접 해보는 것이 중요한 이유)

 

여러 시행착오를 통해 45%의 성능 향상을 이루어내기는 했지만, 쌓여만 가는 데이터에 대한 대책 역시 같이 마련해야 될 것 같습니다. 현재는 데이터베이스 파티셔닝 기법을 통해 월 단위로 데이터를 분리시키는 방법, 전문 검색 서버인 ElasticSearch를 적용해보려고 준비중입니다.

 

긴 글 읽어 주셔서 감사합니다. 궁금한 점은 댓글로 남겨주시면 최대한 답변 드리겠습니다.

 

728x90