-
Notifications
You must be signed in to change notification settings - Fork 14
Разработка Lingvodoc
Система Лингводок предназначена для хранения и работы с лингвистическими данными, и можно сказать, состоит из трех частей, собственно данных, которые хранятся в базе данных, бэкенда, в котором данные обрабатываются, и фронтенда, в котором данные и результаты их обработки отображаются.
База данных самая простая часть, реализована в виде БД на PostgreSQL, на данный момент версии 13. При разработке используются локальные копии БД которые поднимаются из дампа, перед работой необходимо один раз создать БД, например через консоль с помощью команды psql
как
drop database if exists lingvodoc;
create database lingvodoc with owner postgres encoding 'UTF8' LC_COLLATE = 'ru_RU.UTF-8' LC_CTYPE = 'ru_RU.UTF-8' template template0;
и после это можно когда нужно неограниченное число раз восстанавливать конкретное состояние БД из любого имеющегося дампа как
xzcat lingvodoc-2024-12-24.06-51.sql.xz | sudo -u postgres psql lingvodoc
Дампы БД делаются ежедневно, их можно брать на одной из тестовых ВМ, как конкретно можно узнать у более опытных разработчиков.
Заходить в БД, чтобы например посмотреть общую схему или какие-то конкретные данные, также можно через консоль с psql
, или любым другим удобным способом, например через pgAdmin.
В системе следующая иерархия сущностей (в скобках названия таблиц БД): язык (language) -> ... -> язык -> словарь (dictionary) -> перспектива (dictionaryperspective) -> лексический вход (lexicalentry) -> сущность данных в узком смысле (entity). Сущности идентифицируются айдишниками, сущности разных уровней связаны друг с другом через ссылки по айдишнику родительской сущности, начинается иерархия с корневых языков у которых родителей нет и айдишники родительского языка не выставлены.
Посмотреть структуру таблиц БД можно в psql
-консоли через команду \d+
, \d+ language
например. По идее в pgAdmin тоже должно быть можно структуру таблиц смотреть.
Айдишники составные, из двух значений, client_id и object_id, соответственно ссылки на родительские сущности идут через составной внешний ключ parent_client_id и parent_object_id, к примеру у дочернего языка в parent_client_id и parent_object_id хранятся значения client_id и object_id родительского языка. У корневых языков parent_client_id и parent_object_id — null.
При этом client_id — айдишник (простой bigint, не составной) записи в БД-таблице client, в которой навая запись появляется при каждом новом логине любого пользователя, а object_id — значения простого последовательный счетчика, привязанного к этой client-записи как поле counter. Одно из авторизационных значений которые появляются в куках в браузере при логине это именно client_id логина пользователя.
Если интересно, всю иерархию языков от корневых к дочерним можно посмотреть на https://lingvodoc.ispras.ru/languages. У языков системы также есть еще одно явное дополнительное свойство-пометка, некоторые языки помечены как такие так сказать мощные/крупные, а другие, не помеченные, интерпретируются как менее крупные, скорее диалекты, эти два типа языков могут обрабатываться по-разному в зависимости от используемых инструментов системы.
В языках могут быть словари (таблица БД dictionary), словарь привязывается к языку через свой внешний ключ parent_client_id и parent_object_id. У словарей есть название и другие свойства, важно что собственно словарные данные хранятся не в словарях а в так называемых перспективах (таблица БД dictionaryperspective) этих словарей, на случай если в словаре может быть больше одного типа словарных записей, возможно связанных с друг другом. И да, по тому же принципу перспектива привязывается к словарю через parent_client_id и parent_object_id.
Самый распространенный пример, когда больше одной перспективы — когда словарь создается на основе языкового корпуса, где данные в виде предложений с разделением на отдельные слова, предложения идут в перспективу парадигм, слова (без повторов одинаковых) — в перспективу лексических входов, каждое предложение в перспективе парадигм связано со словами в перспективе лексических входов, которые в этом предложении употреблены, и каждое слово в перспективе лексических входов связано со всеми предложениями в перспективе парадигм, в которых это слово употреблено.
Тем не менее в системе достаточно много словарей где просто одна перспектива.
В перспективах данные хранятся в виде записей, называемых лексическими входами (таблица БД lexicalentry), опять-таки лексический вход привязывается к перспективе через внешний ключ parent_client_id, parent_object_id. Кстати в примере выше для распространненого типа словарей с двумя перспективами хотя одна перспектива называется "Парадигмы", а другая "Лексические входы", на уровне сущностей системы и в той и другой хранятся лексическое входы, одно с другим не связано.
В самих объектах БД лексических входов данные не хранятся, лексические входы как бы являются единицами хранения, обозначающими одну запись в перспективе. Данные входов хранятся в сущностях (таблица БД entity), привязанных к своим входам так же через parent_client_id, parent_object_id. Тут имеются в виду сущности в узком смысле, сущности данных, в противоположность когда говорим о сущностях системы в широком смысле, языках, словарях и т.д.
Сущности могут быть разных типов, текстовые, звукозаписи, разметки звукозаписей, ссылки на файлы, ссылки на лексические входы в других перспективах, и т.д. Есть сущности так называемого типа группировочных тэгов, которые используются для группировки лексических входов в разных словарях, например для объединения входов в этимологические группы, группы слов из разных языков/диалектов со сходным значением, связанные происхождением.
Тип сущности опеределяется ее полем, записью в таблице БД field на которую сущность ссылается через внешний ключ field_client_id, field_object_id. Какие вообще поля у сущностей могут быть в перспективе определяется набором записей в таблице БД dictionaryperspectivetofield, привязанных к перспективе через свои parent_client_id, parent_object_id.
Бэкенд https://github.com/ispras/lingvodoc/tree/heavy_refactor реализован на Python с использованием фреймворка Pyramid в качестве базы и для предоставления REST API, с использованием библиотеки SQLAlchemy для работой с базой данных и с использованием библиотеки Graphene для предоставления GraphQL API.
Версии используемых библиотек не самые последние, текущая политика насчет версий зависимостей — жесткая привязка к конкретным версиям, выбранные версии зависимостей см. в файле server-requirements-1.txt.
См. как запускать https://github.com/ispras/lingvodoc?tab=readme-ov-file#running-the-project-for-development, или в README.md в склонированном репозитории.
REST API сейчас большей частью не используется и не поддерживается, некоторое время назад даже было принято решениe от неиспользованных частей постепенно избавляться, по пока неиспользуемые части еще в коде есть. Используется из REST API часть, относящаяся к авторизации пользователей, которая в будущем должна быть заменена, и один-единственный API-интерфейс через который идет доступ к GraphQL API.
GraphQL API определяется в файле lingvodoc/schema/query.py, состоит как обычно из запросов и мутаций, набор доступных в API запросов задается в классе Query
в этом файле, набор доступных в API мутаций — в классе MyMutations
. С общими принципа реализации запросов и мутаций можно ознакомиться в документации библиотеки Graphene, конкретику их применения можно посмотреть на примере реализаций в существующем коде.
В целом вообще в ряде случаев может быть удобнее и быстрее разбираться в используемых концепциях на примерах из существующего кода; при необходимости после просьбы к более опытным разработчикам подсказать куда смотреть.
При собственно реализации GraphQL запросов, мутаций и типов доступ к данным сущностей системы, хранящимся в БД, осуществляется средствами SQLAlchemy; таблицы БД в большинстве своем отражаются классами в файле lingvodoc/models.py через ORM-средства SQLAlchemy, см. ее документацию.
Для большинства сущностей системы имеет место быть отношение отражения вида: Таблица БД <-> ORM-класс в models.py <-> класс GraphQL-тип. Например, таблица БД language, в которой хранятся данные языков, отражается в ORM-класс Language
в models.py и в соответствующий GraphQL-тип, представляемый классом Language
из файла lingvodoc/schema/gql_language.py.
При необходимости использования одновременно соответствующих ORM-классов и классов GraphQL-типов для дизамбигуации используется соглашение об импорте, в соответствии с которым ORM-классы импортируются с префиксом db
, например:
from lingvodoc.models import Language as dbLangauge
from lingvodoc.schema.gql_language import Language
Реализация полей GraphQL-типов, соответствующих сущностям системы, требует доступа к соответствующим объектам ORM-классов, и для этого каждый объект класса GraphQL-типа должен при создании получить либо идентификатор соответствующей БД-сущности, либо сам объект ORM-класса. Для того чтобы гарантировать наличие доступа к соответствующему объекту ORM-класса, используется декоратор fetch_object()
.
К примеру, GraphQL-запрос верхнего уровня language(id: LingvodocID)
реализуется в файле query.py как
def resolve_language(self, info, id):
return Language(id=id)
и если в этом запросе для выбираемого языка выбирается поле locale_exists
, например при запросе
language(id: $id) {
locale_exists
}
то его реализация обеспечит себе доступ к ORM-объекту языка для вычисления значения поля на основе БД-данных языка следущем образом, см. файл gql_language.py:
@fetch_object()
def resolve_locale_exists(self):
return self.dbObject.locale
Пример инициализации GraphQL-объекта прямо через ORM-объект, см. файл gql_language.py, реализация мутации create_language
:
return (
CreateLanguage(
language = Language(db_language),
triumph = True))
В коде также встречаются устаревшие варианты с явным внешним заданием ORM-объекта при создании GraphQL-объекта, например в реализации поля languages
дочерних языков, в методе resolve_languages()
в файле gql_language.py:
gql_language = Language(id=[language.client_id, language.object_id])
gql_language.dbObject = language
Такой способ допустим, но лучше пользоваться более новым, более коротким и понятным способом:
gql_language = Language(language)
Декоратор fetch_object()
также имеет дополнительное применение, он может использоваться для оптимизации вычисления значений GraphQL-полей. Технические подробности и детали см. в реализации этого декоратора в файле gql_holders.py, но общий принцип его использования для такой оптимизации следующий:
При создании GraphQL-объекта мы явно задаем ему значение поля, и при присутствии декоратора fetch_object()
с дополнительным аргументом вычисление значения это поля будет заключатся просто в возвращении этого явно заданного значения.
Делать это — явно задать предвычисленное значение поля — стоит только если это действительно оптимизация, например когда при некоторых обстоятельствах мы можем вычислить значение поля с меньшими вычислительными затратами, чем если бы оно было вычислено стандартным способом.
Например, при оптимизированном получении данных сразу большого количества языков вместе с их словарями с помощью класса Language_Resolver
в файле gql_language.py происходит оптимизированные вычисление числа словарей в языках, и эти значения явно сохраняются в GraphQL-объектах языков при их создании:
gql_language = (
Language(id = language_id))
gql_language.dbObject = language
...
if dictionary_count_f:
gql_language.dictionary_count = (
dictionary_count_f(language_id))
После этого, если в обрабатываемом GraphQL-запросе будет присутствовать извлечение поля dictionary_count
, при получении значения этого поля для нашего GraphQL-объекта с явно сохраненным значением в атрибуте dictionary_count
, с помощью стандартного resolve-метода
@fetch_object('dictionary_count')
def resolve_dictionary_count(
self,
info,
recursive = False,
category = None,
published = None):
...
с соответствующим вызовом декоратора @fetch_object('dictionary_count')
, см. файл gql_language.py, сам метод resolve_dictionary_count()
даже не будет запущен, функционал декоратора просто вернет значение которое было явно сохранено в объекте присваиванием gql_language.dictionary_count = ...
из кода выше.
Для добавления мутации в GraphQL API бэкенда нужно в файле lingvodoc/schema/query.py определить класс мутации и добавить его в главный класс мутаций MyMutations
, см. для аналогии класс мутации BidirectionalLinks
. В классе мутации должен быть метод mutate()
, обычно еще бывает объявление аргументов в виде только для этого и используемого внутреннего класса Arguments
, но в случаях когда у мутации аргментов нет можно без него.
Если явно нет причин так не делать, все что внутри метода mutate()
лучше оборачивать в try
/except
, см. по аналогии с mutate()
в BidirectionalLinks
, естественно с соответствующими изменениями в сообщениях.
В процессе отладки при разработке GraphQL API практически всегда бывает необходимость логиниться в Лингводок, под стандартным пользователем или под админом. Обычно в качестве стандартного пользователя при отладке используется логин julianor, админский логин — modis.
При незнании реального паролей можно их подменить апдейтом в БД. В БД можно заходить как удобно, прямо из консоли через psql
, или через pgAdmin, или вообще любым другим подходящим способом. Если под Linux и с глобальной системной установкой БД, как оно на продакшен-сервере и как в целом может быть удобнее для разработки, то из консоли заходится как sudo -u postgres psql lingvodoc
. Если БД в докере, то там нужно еще указывать дополнительно хост, порт и пароль, может еще что-то, нужно разбираться. Если под Windows то можно проконсультироваться с Владимиром Монаховым.
В БД для замены паролей пользователя julianor и админа можно использовать стандартный апдейт-запрос
update passhash set hash='$2a$12$tNxWXqi4Puyc5RX77Vqtpu31h1riDWpTtqCjN1hZFtXalKGzPv3fa' where user_id=5 or user_id=1 or user_id=141;
он используется на тестовых ВМ в дженкинсе ровно для этого. После выполнения этого запроса пароль у админа, у пользователя julianor и еще у деактивированного пользователя test становится G59TVXu2unvbtg.
Для отладки частей бэка в изоляции от фронта можно прямо делать соответствующие GraphQL-запросы к запущенному бэку. Можно это делать из консоли через curl
или другими удобными способами, например есть такая достаточно удобная штука Insomnia. Пример curl-запроса к бэку, запущенному стандартным образом через pserve development.ini
, см. также в BidirectionalLinks
.
Если запрос для выполнения требует авторизации из-под пользователя, обычного или админа, нужны будут авторизационные куки. Простейший способ их получить это запустить в дополнение к бэку еще и фронт, залогиниться в браузере и посмотреть в соответствующей вкладке development tools. В зависимости от предпочтений может еще даже проще будет зайти на допустим подстраницу фронта /corpora_all и во вкладке network браузерных development tools на первый же graphql-запрос (подадрес graphql, тип json) кликнуть правой кнопкой мыши и выбрать copy as curl, после чего вставить в допустим текстовый редактор и скопировать оттуда всю необходимую инфу.
Для прямой отладки запросов есть как минимум один способ. В нужном месте в коде запроса добавляется import pdb; pdb.set_trace()
, бэк запускается как обычно из консоли через pserve development.ini
, запрос делается через curl
или другим удобным способом, и в консоли где запущен бэк попадаем в том месте где добавили pdb.set_trace()
в питоновский стандартный дебаггер pdb, он хоть и командной строки но все что надо там есть.
Фронтенд https://github.com/ispras/lingvodoc-react/tree/staging реализован как React-приложение.
Для запуска нужна 14-я версия Node.js, разными версиями Node.js можно консольно управлять скриптом nvm
, см. в Google.
Для запуска локально для разработки в директории исходников (важно что с текущей главной ветки staging) нужно один раз выполнить
npm install
после чего при запущенном бэке можно запускать фронт как:
PROD=http://localhost:6543 npm start
Фронтенд поддерживает возможность перевода интерфейса на различные языки и возможность переключения между переводами.