[Django, PostgreSQL] Сравнение разных django filter на примере демо базы PostgreSQL

Автор Сообщение
news_bot ®

Стаж: 6 лет 2 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
09-Июл-2020 02:36

Вместо предисловия
Началось всё с того, что мне предложили в рамках предмета "Основы веб-программирования" поучаствовать в проекте, вместо проделывания лабораторных работ и курсовой, поскольку я заявил о том, что хотел быть делать нечто отдалённое от общего курса (и так уже достаточно знаний было по связке DRF + Vue, хотелось чего-то нового). И вот в одном из своих PR на github я решил использовать полнотекстовый поиск (задание намекало на это) для фильтрации контента, что заставило меня обратиться к документации Django в поисках того, каким же образом лучше это дело реализовать. Думаю, вы знаете большую часть из тех методов, что были там предложены (contains, icontains, trigram_similar). Все они подходят для каких-то конкретных задач, но не слишком хороши в, именно, полнотекстовом поиске. Пролистав чуть ниже, я наткнулся на раздел, в котором говорилось о взаимодействии Django и Pgsql для реализации document-based поиска, что меня привлекло, поскольку в постгре встроен инструмент для реализации этого самого [полнотекстового] поиска. И я решил, что скорее всего, django просто предоставляет API к этому поиску, исходя из чего такое решение должно работать и точнее и быстрее, чем любые другие варианты. Преподаватель мне не слишком поверил, мы с ним поспорили, и он предложил провести исследование на эту тему. И вот я здесь.
Начало работы
Первая проблема, которая передо мной встала — поиск мокапа БД, чтобы не придумать каких-нибудь непонятных штук самому и я отправился гуглить и читать вики постгреса. В итоге остановился на их демо базе о полётах по России.
Хорошо, база найдена. Теперь нужно определиться в том, какие способы фильтрации будут использоваться для сравнения. Первое, что я бы хотел использовать, разумеется, стандартный метод search из django.contrib.postgres.search. Второе — contains (ищет слово в строке) и icontains (предоставляет данные, игнорируя акценты, например: по запросу "Helen" будет результат: <Author: Helen Mirren>, <Author: Helena Bonham Carter>, <Author: Hélène Joy>), которые предоставляет сам django. Все эти способы фильтрации я так же хочу сравнить со встроенным поиском внутри postgresql. Искать я решил по таблице tickets в версии small она содержит 366733 записей. Поиск будет происходить по полю passenger_name, где, как нетрудно догадаться, содержится имя пассажира. Написано оно транслитом.
Дать django возможность работать с уже существующей БД
Вторая проблема — разрешить django только чтение данных из нашей демонстрационной БД. Покопавшись ещё в документации django я нашёл каким же образом, можно составить модельки по существующей БД, чтобы не перепечатывать ручками всё:
$ python manage.py inspectdb > models.py

При этом, разумеется, сама БД должна быть обозначена в settings.py. Всего пару ошибочек мне пришлось поправить и всё заработало как следует. Сразу после этого я решил написать простенькую вьюшку, которая сможет нам эти данные вернуть. Браузер, разумеется очень напрягся (что и не мудрено), когда я пытался открыть адрес, по которому должно было вернуться 300к+ записей, поэтому я ограничил их число для 10, чтобы удостовериться, что они там хотя бы есть. А вообще, совершенно точно понятно, что запрос лучше отправлять через curl. Это явно скушает в разы меньше оперативной памяти.
Выбор метрик
Изначально я подумал, что считать время фильтрации в питоне получится, используя таймер для получения времени исполнения скрипта, и дополнительной метрикой должно было стать время исполнения запроса через curl, поскольку это показывает приблизительное время, за которое отфильтрованные данные дойдут до конечного пользователя. Кроме этого, следует сравнивать это время с эталонным (прямым исполнением соответствующих запросов в БД).
Фильтруем в django
Но поскольку я уже снял 600 измерений времени выполнения скрипта — в таблице финальной решил оставить, просто чтобы было сразу понятно, что это время вообще мало что реально отражает.

Итоговая view для contains

SPL
class TicketListView(g.ListAPIView):
    serializer_class = TicketSerializer
    def get_queryset(self):
                queryset = ''
        params = self.request.query_params
        name = params.get('name', None)
        if name:
            start_time = d.datetime.now()
            queryset = queryset.filter(passenger_name__contains=name)
            end_time = d.datetime.now()
            time_diff = (end_time - start_time)
            execution_time = time_diff.total_seconds() * 1000
            print("Filter execution time {} ms".format(execution_time))
        return queryset

Contains
Начнём с contains, по сути, он работает как WHERE LIKE.

Запрос в Django ORM/Запрос в sql для contains

SPL
queryset = queryset.filter(passenger_name__contains=name)

SELECT "tickets"."ticket_no", "tickets"."book_ref", "tickets"."passenger_id", "tickets"."passenger_name", "tickets"."contact_data" FROM "tickets" WHERE "tickets"."passenger_name"::text LIKE %IVAN%

Для того, чтобы получить результат из curl я выполнял запрос следующим образом (считается в секундах):
$ curl -w "%{time_total}\n" -o /dev/null -s http://127.0.0.1:8000/api/tickets/?name=IVAN
1,242888

Свёл всё в таблице, на соответствующем листе.
Но если резюмировать — отклонение от скорости фильтрации внутри самого постгреса достаточно большое, и по факту исполнение такого запроса к серверу займёт от 140 до 1400 мс. Не претендую на истину, но работает всё приблизительно так. А время самой фильтрации через ORM займёт от 73 до 600 мс, в то время как такая же фильтрация внутри постгреса выполняется за промежуток от 55 до 100 мс.
Icontains
Icontains работает несколько по-другому (он приводит всё к одному виду, чтобы сравнение было более близким). Код для вьюшки использовался почти аналогичный, только вместо contains — icontains. Вот и вся разница.

Запрос в Django ORM/Запрос в sql для icontains

SPL
queryset = queryset.filter(passenger_name__icontains=name)

SELECT "tickets"."ticket_no", "tickets"."book_ref", "tickets"."passenger_id", "tickets"."passenger_name", "tickets"."contact_data" FROM "tickets" WHERE UPPER("tickets"."passenger_name"::text) LIKE UPPER(%IVAN%)

По итогу, отклонение в данном случае уже меньше, поскольку и сам постгрес начал тратить намного большее времени на исполнение запроса (порядка 300 мс), а исполнение такого запроса к серверу займёт у клиента от 200 до 1500 мс. Фильтрация через ORM — от до 200 до 700 мс.
Full text search (через django.contrib.postgres)
Поскольку индексов никаких создано не было, full text search довольно сильно и вполне ощутимо проигрывает прошлым вариантам. Время исполнения запроса в постгресе колеблется около 1300 мс, а запрос к серверу занимает от 1000 до 1700 мс. При этом, фильтрация через ORM — укладывается в промежуток от 1000 до 1450 мс.

Код

SPL
class TicketListView(g.ListAPIView):
    serializer_class = TicketSerializer
    def get_queryset(self):
        # queryset = Tickets.objects.all()
        queryset = ''
        params = self.request.query_params
        name = params.get('name', None)
        if name:
            start_time = d.datetime.now()
            queryset = Tickets.objects.filter(passenger_name__search=name)
            end_time = d.datetime.now()
            time_diff = (end_time - start_time)
            execution_time = time_diff.total_seconds() * 1000
            print("Filter execution time {} ms".format(execution_time))
            f = open('results.txt', 'a')
            f.write('{}'.format(execution_time))
            f.write('\n')
            f.close()
        return queryset

Full text search (через rest_framework.filters, точнее — SearchFilter)
Если не использвоать именно FTS, то результаты получаются сравнимыми с FTS внутри постгре, но хуже, чем contains и icontains. От 200 до 1710 мс.
А с использованием FTS эффективность повышается, отклонение сводится к минимальному. В среднем, это займёт от 800 до 1120 мс.

Код

SPL
...
from rest_framework import filters as f
class TicketListView(g.ListAPIView):
    queryset = Tickets.objects.all()
    serializer_class = TicketSerializer
    filter_backends = [f.SearchFilter]
    search_fields = ['@passenger_name']

Использование фильтров через модуль django-filter
Результаты почти совпали со стандартными contains и icontains, поэтому смысла детально это рассматривать не вижу. Да и в целом, модуль django-filter не показал какого-то ощутимого преимущества перед стандартными средствами фильтрации Django ORM.
Так что в итоге?
Если у вас есть большой объём данных — нужно прописывать нормальные индексы и использовать полнотекстовый поиск (разумеется, только в том случае, когда соответствует вашим целям) с радостью и счастьем, потому что он решает довольно широкий круг проблем. Но вот всегда ли в нём есть необходимость — уже решать вам. Я усвоил для себя, что в некоторых случаях (когда не стоит задачи именно полнотекстового поиска, а есть поиск по подстроке, который реализуется с помощью contains/icontains) лучше вовсе не использовать полнотекстовый поиск, потому что индексы в определённый момент начинают кушать всё больше и больше памяти вашего сервера, что, скорее всего, негативно скажется на работе вашего сервера.
В целом, моё понимание некоторых внутренних механизмов работы django благодаря этому исследованию устаканилось. И пришло, наконец, осознание разницы между поиском по подстроке и полнотекстовым поиском. Разнице в их реализации через Django ORM.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_django, #_postgresql, #_django, #_djangofilter, #_djangorestframework, #_postgresql, #_issledovanie (исследование), #_curl, #_django_orm, #_django, #_postgresql
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 30-Апр 10:38
Часовой пояс: UTC + 5