Skip to content

SergeyLebidko/Graphite_server

Repository files navigation

Graphite_server

Это серверная часть приложения Graphite. Более подробно о нем я рассказываю в репозитории клиентской части. Здесь я ограничусь кратким описанием особенностей серверной части.

Серверная часть написана на Django. Для обработки запросов к api использован Django Rest Framework.

Для обработки CORS я применил пакет django-cors-headers. В файл settings.py помимо стандартных для django настроек также добавлены и настройки для CORS, требуемые этим пакетом:

CORS_ORIGIN_WHITELIST = ['http://localhost:3000', 'http://127.0.0.1:3000', 'http://127.0.0.1', 'http://localhost']

Естественно, при развертывании проекта для работы на реальном сервере эти настройки должны быть изменены на адрес (или адреса) реального размещения фронтэнд-части проекта.

Также в проекте я не использую стандартную систему работы с пользователями, предлагаемую Django (за исключением, конечно, создания суперпользователя для доступа к админке). Для хранения сведений о клиентах я написал свою модель - Account и дополнительную модель - Token - для отслеживания пользовательских сессий. Работает механизм довольно просто: при регистрации нового пользователя или при выполнении входа ранее уже зарегистрированного генерируется новый токен, который отправлется браузеру пользователя. Браузер сохраняет токен в local storage и в дальнейшем использует его для выполнения запросов к api, требующих обязательной авторизации. При выполнении выхода из аккаунта (через соответствующих хук) текущий токен удаляется. Также предусмотрен хук для выхода одновременно на всех устройствах, на которых был до этого залогинен пользователь (хук для этого производит удаление всех токенов, связанных с данным пользователем в результате чего токены, сохраненные на устройствах станут недействительными и выполоняемые с их помощью запросы будут отклняться со статусом 403 Forbidden).

В файле settings.py предусмотрена настройка для установки длины токена:

ACCOUNT_TOKEN_SIZE = 32

Аутентификация пользователя по токену производится автоматически с помощью простой middleware-функции, которая просматривает заголовок Authorization в запросе, извлекает из него токен и ищет в БД ассоцированного с токеном пользователя. Если пользователь найден, то в объекте запроса (request) создается поле account со ссылкой на найденного пользователя. Если пользователь не найден (например, токен недействителен или отсутствует), то поле account получает значение None.

Так как я запускал и тестировал проект на локальной машине, на которой сетевые запросы выполняются мгновенно, мне хотелось как-то имитировать сетевые задержки, неизбежно возникающие при работе в реальной сети с медленными каналами. Для этого я написал небольшой middleware-класс LagMiddleware. Если проект находится в тестовом режиме (DEBUG = True), то данный класс вносит небольшую задержку в выполнение каждого сетевого запроса. Величина задержки в секундах указывается в settings.py в параметре LAG (я установил 0,5 сек.).

Если параметр DEBUG равен False, то задержка игнорируется.

Также я реализовал самостоятельно простой полнотекстовый поиск. Так как проект, по сути, представляет собой платформу для ведения блогов, то поиск по содержимому постов совершенно необходим. К тому же нужно учитывать, что пользователь должен иметь возможность искать не только по блогам, но и по именам авторов. Большинство СУБД сейчас (например, Postgres) предлагают встроенные средства для полнотекстового поиска, но это небольшой учебный проект, использующий предоставляемую Django по-умолчанию БД sqlite, а она не содержит средств для реализации этой функциональности. Пришлось написать их самому. Алгоритм поиска в проекте реализован в функциях в файле full_text_search.py. Там же в комментариях кратко описана его суть. Здесь расскажу о нем подробней.

В самом простом варианте поиск по полях БД можно проводить с помощью модификатора icontains. Вот пример:

q = request.query_params.get('q')
queryset = Post.objects.filter(text__icontains=q)

где q - это строка для поиска, извлекаемая из объекта запроса. Такой поиск прост, быстр, но не может учитывать множество нюансов. Во-первых, модификатор icontains (поиск без учета регистра) почему-то не учитывает русские буквы. То есть слова 'Stone' и 'stone' будут для него одинаковыми, а 'Камень' и 'камень' - разными. Во-вторых, а что если пользователь ищет словосочетание или целую фразу? Фраза не обязательно должна встретиться в тексте в точности так, как её вводит в строке поиска пользователь (и, зачастую, так оно и будет). Например, пользователь вводит в строке поиска 'создание перчатки бесконечностей', а в тексте эта фраза будет выглядеть так: 'Создание Перчатки Бесконечности'. Или же перестановка слов, не меняющая смысла: 'найти лекарство от его болезни' и 'найти от его болезни лекарство'. В обоих случаях текст, очевидно, подошел бы под строку поиска, но он будет забракован модификатором contains.

Для решения этой проблемы, очевидно, нужно, во-первых, вести поиск без учета регистра символов. Встроенные функции python в отличие от джанговской icontains позволяют делать это без проблем и для русских и для английских букв. Во-вторых, поиск необходимо проводить в несколько этапов, преобразуя исходную строку поиска на каждом из них. В-третьих, очевидно, что чем сильнее будет модифицирована строка поиска, тем менее релевантными будут результаты и тем ближе к концу списка найденных значений они должны находиться.

Итак, первый этап - получение queryset со значениями, среди которых мы будем искать:

queryset = Post.objects.all()

Список итоговых объектов на данном этапе пока пуст.

Второй этап: отбор из кверисета только тех значений, в которых строка поиска содержится как есть, в непреобразованном виде. Эти значения (в данном случае посты авторов) будут, очевидно, самыми релевантными и будут первыми добавленными в список результатов. Второй этап: удаление лишних пробелов из строки поиска и снова поиск по исходному кверисету. Теперь при добавлении в результат учитываем, что итоговый список уже может содержать какие-то объекты и нам нужно избегать создания в нем дубликатов. Третий этап: разбиение строки поиска на отдельные слова и поиск этих слов в текстах постов уже по отдельности. И четвертый этап: учитываем возможность преобразования слов. Например, слово 'каменный' можно преобразовать в 'камен' просто отрезав несколько последних букв. Слова в языке так, по большей части, и образуются - путем добавления суффиксов и окончаний. Последовательно отсекая конечные буквы, мы избавляемся от этих окончаний и ищем уже по корням слов. Таким образом, если пользователь задаст в строке поиска слова 'каменный пляж', он сможет найти статьи со словосочетаниями как 'каменный пляж', так и, например, 'каменистый пляж'. Хотя релевантность таких результатов будет, естественно, немного ниже, чем релевантность точных совпадений.

Таким образом, преобразуя строку поиска этап за этапом, мы выбираем подходящие результаты из исходного кверисета и переносим их в итоговый автоматически располагая их по убыванию релевантности. Конечно же, если в тексте поста не находится совершенно никаких совпадений, то пост совершенно не релевантен строке поиска и в результаты поиска не попадет.

Аналогичный алгоритм применяется и для поиска по именам авторов.

Также необходимо заметить, что полнотекстовый поиск выполняется довольно долго и в реальном, не учебном проекте его результаты надо было бы кэшировать (например, в redis). Это значительно сокращало бы время загрузки и получения второй, третьей и т.д. страниц результатов поиска и экономило бы вычислительные ресурсы сервера. Но я стремился к максимально простому бэкенду (так как Graphite для меня - проект, направленный преимущественно на изучение React) и не реализовал кэширование.

И в заключение. В репозитории находится БД, наполненная тестовыми данными (пользователи, посты, комментарии, лайки и т.д.). Создан суперпользователь. Данные суперпользователя: Логин: admin Пароль: Rd368446

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages