본문 바로가기

DRF

Django full text search (Gin index)

Django 검색 쿼리 개선(Gin index)

django에서 Q객체를 이용하여 검색 기능을 구현하였다.

검색어가 게시물의 제목(title)과 내용(description)에 있다면 검색이 되어야 한다.

# views.py
def get_queryset(self):
        q = self.request.query_params.get("search")

        queryset = (
            CrawlingData.objects.annotate(
                fast_count=(
                    Count(
                        "emotion",
                        filter=Q(
                            emotion__emotion_type="F",
                            emotion__is_deleted=False,
                        ),
                    )
                ),
                is_bookmark=(
                    Exists(
                        self.request.user.bookmark_set.filter(
                            crawling_data_id=OuterRef("id"),
                            user=self.request.user,
                            is_deleted=False,
                        )
                    )
                ),
            )
            .filter(
                Q(is_private=True)
                & (Q(title__contains=q.lower()) | Q(description__contains=q.lower()))
            )
            .prefetch_related(
                Prefetch(
                    "emotion_set", queryset=Emotion.objects.all().select_related("user")
                ),
                Prefetch(
                    "bookmark_set",
                    queryset=Bookmark.objects.all().select_related("user"),
                ),
            )
        )

        return queryset

초기에는 위의 로직으로 이루어져 있었다. 데이터가 적을 땐 영향이 없었지만 데이터가 점점 쌓이면서 150만개 이상이 되었을 때 검색 시 30초 이상이 걸렸다. 방법을 찾다가 Gin Index를 알게 되었다.

또한 Pagination을 사용하고 있었는데 쿼리에 count를 포함하여 반환하고 있었다. Django 의 CursorPagination 으로 변경하니 쿼리 속도가 크게 개선되었다.(30초 → 15초)

Gin Index란 Full Text Search에 굉장히 효과적인 인덱싱 방법이다. 인덱스 타입에는 GIST와 GIN 인덱스 두 가지 종류가 있다. (https://www.postgresql.org/docs/9.4/textsearch-indexes.html)

일단 full test search 는 LIKE 쿼리를 이용하는데 LIKE를 이용한 쿼리는 굉장히 느리기 때문에 INDEX를 생성해야한다. 그냥 INDEX를 생성하는 경우 btree기반 INDEX가 생성된다.

  • {search}% 를 쿼리하는 것은 빠르다.
    • btree의 상단에서부터 분기점을 고를 수 있기 때문에
    • O(log(n))
  • %{search}% 를 쿼리하는 것은 느리다.
    • btree의 상단에서부터 분기점을 고를 수 없으므로 결국 full table search와 같다.
    • O(n)

Postgresql에서는 텍스트 검색을 위한 GIN, GIST 인덱싱을 제공한다.

GIN, GIST 비교

  • GIN index lookups are about three times faster than GiST
  • GIN indexes take about three times longer to build than GiST
  • GIN indexes are moderately slower to update than GiST indexes, but about 10 times slower if fast-update support was disabled (docs)
  • GIN indexes are two-to-three times larger than GiST indexes

GIN INDEX 구현 설명

  • gabcgaga 문자열에서 {'ga': [(0,1), (4,5), (6,7)]}과 같은 형태로 Index를 미리 생성

GIN 적용 방법

# models.py
from django.contrib.postgres.indexes import GinIndex

indexes = [
        GinIndex(Lower("title"), name="title_lower_gin_idx"),
    GinIndex(Lower("description"), name="description_lower_gin_idx"),
        ]

title과 description필드를 소문자로 변환하여 GIN 인덱스를 설정해주고 쿼리에서도 소문자로 변환된 검색어로 쿼리를 조회하니 2초대까지 줄어들었다.

'DRF' 카테고리의 다른 글

Django csv파일 다운로드  (0) 2022.12.08