Русский
Русский
English
Статистика
Реклама

Нормализация

Iresine, нормализация данных на клиенте

17.03.2021 14:12:14 | Автор: admin

Нормализация. От нее мы или страдаем или пишем собственное решение с множеством проверок на существование сущности в общем хранилище. Попробуем разобраться и решить эту проблему!

Описание проблемы

Представим себе такую последовательность:

  1. Клиентское приложение запрашивает список пользователей запросом к /users и получается пользователей с id от 1 до 10

  2. Пользователь с id 3 меняет свое имя

  3. Клиентское приложение запрашивает пользователя с id 3 с помощью запроса к /user/3

Вопрос:Какое имя пользователя с id 3 будет в приложении?
Ответ:Зависит от компонента, который запросил данные. В компоненте, который использует данные из запроса к /users, будет отображаться старое имя. В компоненте, который использует данные из запроса к /user/3, будет отображаться новое имя.

Вывод: В таком случае в системе существует несколько одинаковых по смыслу сущностей с разным набором данных.

Вопрос:Почему это плохо?
Ответ:В лучшем случае пользователь увидит разные имена одного человека в разных разделах сайта, в худшем переведет деньги на старые банковские реквизиты.

Варианты решения

В настоящее время существуют следующие варианты решения этой проблемы:

  • Не обращать внимание

  • Нормализовать данные собственноручно

  • Использовать клиент graphql (apollo или relay)

Не обращать внимание

Это самый очевидный и заманчивый вариант. В некоторых случаях клиентское приложение действительно может позволить себе иметь одинаковые сущности с разными данными. Но что делать со случаями, когда это недопустимое поведение? Как быть с разработчиками, которые не хотят создавать приложение с такими дефектами?

Нормализовать данные собственноручно

Примером собственноручной реализации может послужить код для mobx:

class Store {  users = new Map();  async getUsers() {    const users = await fetch(`/users`);    users.forEach((user) => this.users.set(user.id, user));  }  async getUser(id) {    const user = await fetch(`/user/${id}`);    this.users.set(user.id, user);  }}

И если пример с mobx выглядит приемлемо, то нормализация в redux простоужасает. Работать с таким кодом становится сложнее по мере его увеличения и совсем неинтересно

Использовать клиент graphql (apollo или relay)

Apollo и relay это библиотеки, которые из коробки умеют нормализовать данные. Однако такое решение заставляет нас использовать graphql и apollo, которые, по моему мнению, имеют множество недостатков.

Нормализация

Что такое нормализация и как она позволяет graphql клиентам бороться с указанной проблемой? Разберемся на примере apollo! Так apollo описывает свои действия с данными:

...normalizesquery response objects before it saves them to its internal data store.

Что включает в себя указанноеnormalize?

Normalization involves the following steps:

1. The cache generates a unique ID for every identifiable object included in the response.
2. The cache stores the objects by ID in a flat lookup table.

То есть apollo формирует уникальный идентификатор для каждой сущности, для которой возможно его сформировать. Apollo использует его как ключ в хранилище всех сущностей. Вот как примерно выглядит формирование идентификатора и его хранение:

const store = new Map();const user = {  id: '0',  type: 'user',  name: 'alex',  age: 24,};const id = `${user.type}:${user.id}`;store.set(id, user);

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

Получение уникального идентификатора

Apollo достигает указанного эффекта, запрашивая при каждом запросе внутреннее поле __typename, а как достигнуть похожего эффекта без graphql?

Поскольку мы не имеем внутренних полей с типами, то должны полагаться только на поля данных. Вот несколько решений:

  • сделать поле id или аналогичное поле глобально уникальным

  • добавить информацию о типах сущности в данные

    • добавить типы на сервере

    • добавить типы на клиенте

Сделать поле глобально уникальным

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

const store = new Map();const user = {  id: '0',};const comment = {  id: '1',};store.set(user.id, user);store.set(comment.id, comment);// ...store.get('0'); // userstore.get('1'); // comment

Решение выглядит достаточно удобным в использовании, однако реализация глобально уникальных полей id будет затруднительна. Как правило, сущности хранятся в базе данных и имеют id уникальный только внутри коллекции/таблицы (или другими словами какого-то типа). А значит, чтобы сделать id глобально уникальным, нужно приложить много усилий.

Добавить информацию о типах

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

const store = new Map();const user = {  id: '0',  type: 'user', // <-- new field};const comment = {  id: '1',  type: 'comment', // <-- new field};function getStoreId(entity) {  return `${entity.type}:${entity.id}`;}store.set(getStoreId(user), user);store.set(getStoreId(comment), comment);// ...store.get('user:0'); // userstore.get('comment:1'); // comment

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

Где добавлять типы в данные?

Проблема нормализации данных особенно характерна для клиентских приложений. Поэтому рассмотрим вопрос - в какой момент добавлять информацию о типах в данные. Мы можем выбрать один из указанных вариантов для добавления типов.

  • На сервере, при отдаче данных:

app.get('/users', (req, res) => {  const users = db.get('users');  const typedUsers = users.map((user) => ({    ...user,    type: 'user',  }));  res.json(typedUsers);});
  • На клиенте, при получении данных:

function getUsers() {  const users = fetch('/users');  const typedUsers = users.map((user) => ({    ...user,    type: 'user',  }));  return typedUsers;}

Как мне кажется вариант добавления данных на сервере является предпочтительным. Api, которое отдает данные, знает о том какие данные и какого типа отдает. Однако в некоторых случаях нет возможности изменить код сервера для отдачи типа, в таких случаях можно добавить типы на клиенте.

Теперь разберемся как все это автоматизировать.

iresine

iresineэто библиотека созданная для нормализации данных и оповещении об их изменении.

В данный момент iresine состоит из следующих модулей:

Так iresine работает с react-query:

@iresine/core

Основной модуль библиотеки, именно он отвечает за парсинг данных, их нормализацию и оповещении подписчиков об изменении конкретной сущности.

const iresine = new Iresine();const oldRequest = {  users: [oldUser],  comments: {    0: oldComment,  },};// new request data have new structure, but it is OK to iresineconst newRequest = {  users: {    0: newUser,  },  comments: [newComment],};iresine.parse(oldRequest);iresine.parse(newRequest);iresine.get('user:0' /*identifier for old and new user*/) === newRequest.users['0']; // trueiresine.get('comment:0' /*identifier for old and new comment*/) === newRequest.comments['0']; // true

Как видим из идентификаторов, по которым мы получаем сущности из хранилища, @iresine/core использует следующую схему для создания идентификаторов:

entityType + ':' + entityId;

По умолчанию @iresine/core берет тип из поляtype, а id из поляid. Это поведение можно изменить, передав собственные функции. Например попробуем использовать такой же идентификатор как в apollo:

const iresine = new Iresine({  getId: (entity) => {    if (!entity) {      return null;    }    if (!entity.id) {      return null;    }    if (!entity.__typename) {      return null;    }    return `${entity.__typename}:${entity.id}`;  },});

Так же мы можем обрабатывать и глобально уникальное поле id:

const iresine = new Iresine({  getId: (entity) => {    if (!entity) {      return null;    }    if (!entity.id) {      return null;    }    return entity.id;  },});

А что @iresine/core делает с сущностями, где идентификатор не обнаружен? Например такими:

const user = {  id: '0',  type: 'user',  jobs: [    {      name: 'milkman',      salary: '1$',    },    {      name: 'woodcutter',      salary: '2$',    },  ],};

user имеет своей идентификатор в хранилище, а как быть с jobs? У них нет ни поля type ни поля id! @iresine/core следует простому правилу: если у сущности нет идентификатора, то она становится частью ближайшей родительской сущности с идентификатором.

@iresine/core являет универсальной библиотекой, которая знает о том как распарсить данные и точечно уведомлять подписчиков. Но использовать ее напрямую довольно нудно и утомительно! Посмотрим как сделать этот процесс удобнее.

@iresine/react-query

react-query это прекрасная библиотека, с которой я бы посоветовал ознакомиться каждому. Но в ней отсутствует нормализация данных, и именно этот факт вдохновил меня на написание iresine.

@iresine/react-query это плагин для react-query. Он позволяет использовать функцию нормализации и обновления данных @iresine/core на данных хранилища react-query. Вся работа по нормализации происходит автоматически и клиент работает с react-query так, как бы работал без iresine.

import Iresine from '@iresine/core';import IresineReactQuery from '@iresone/react-query';import {QueryClient} from 'react-query';const iresineStore = new IresineStore();const queryClient = new QueryClient();new IresineReactQueryWrapper(iresineStore, queryClient);// now any updates in react-query store will be consumbed by @iresine/core

Схема взаимодействия выглядит так(была приведена выше):

Итог

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

Подробнее..

Опрос. Денормализация или нет?

29.11.2020 18:06:30 | Автор: admin

Недавно мы с коллегой по профессии обсуждали в Интернете (точнее в ЛС на Хабре) архитектуру некоторой системы, и у нас возник спор по одному вопросу.


Имеется склад, где хранятся разные материалы, имеются документы прихода и расхода материалов, в документах есть записи с информацией "материал, количество", связь один-ко-многим. В базе данных есть таблица с информацией о текущем наличии материалов на складе, есть таблица для документов и таблица для их позиций. Назовем их "current_stocks", "documents", "document_positions".


Считать ли таблицу "current_stocks" денормализацией данных?


Аргумент коллеги заключается в том, что данные в "current_stocks" можно рассчитать по документам за все время существования системы, поэтому это денормализация.


Мой аргумент в том, что любое текущее состояние можно рассчитать по истории изменений, поэтому это не денормализация. Иначе в термине "нормализация" не было бы смысла. Он относится именно к моделированию текущего состояния. Это разные модели, "current_stocks" моделирует состояние склада, "document_positions" строки текста в документах.

Подробнее..

Как упростить доработки и поддержку хранилища данных?

08.06.2021 12:19:01 | Автор: admin

1. Адаптированная методология Anchor modeling

Архитектура ядра хранилища данных должна соответствовать описанной ниже адаптированной (не оригинальной) методологии Anchor modeling (но не Data Vault).

Тип таблицы

Примеры имени таблицы (в скобках описание)

С таблицами каких типов может быть связана

Обязательный тип поля

Примеры имени поля

Сущности (Anchor, Entity type). Обозначается квадратом

TR_Transaction (полупроводка по дебету или по кредиту), AC_Account (синтетический счет)

Связи, Атрибут сущностей

Суррогатный ключ сущности

TR_ID, AC_ID

Атрибут сущностей (Attribute). Обозначается кругом

TR_TDT_TransactionDate (дата заключения сделки)

Сущности

Суррогатный ключ сущности (является первичным ключом в течение срока действия записи)

TR_ID

Дата и время начала срока действия записи

TR_TDT_FROM

Дата и время окончания срока действия записи (не включительно)

TR_TDT_BEFORE

Атрибут сущностей

TR_TDT

Связи (Tie, Relationship). Обозначается ромбом

TR_AC_DC_Transaction_Account_DrCr (счет главной книги в полупроводке)

Сущности

Суррогатные ключи каждой связанной сущности (вместе с некоторыми атрибутами связей, обозначающими тип связи, образуют составной ключ в течение срока действия записи)

TR_ID, AC_ID

Дата и время начала срока действия записи

TR_AC_DC_FROM

Дата и время окончания срока действия записи (не включительно)

TR_AC_DC_BEFORE

Опциональный атрибут или несколько атрибутов связей

DC (дебет/кредит)

Схема данных примераСхема данных примера

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

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

В ядре хранилища данных не должны использоваться значения NULL, за исключением тех атрибутов связей, которые не входят в составной ключ (обычно это наименования, обозначения, коды, ссылки, выбранные значения, флаги). Если неизвестны начало и/или окончание срока действия записи, то должны указываться принятые условные даты (например, '0001-01-01', '-infinity', '9999-12-31', 'infinity').

Для облегчения создания полиморфных связей двухсимвольный код в именах должен совпадать с двумя последними символами в соответствующих суррогатных ключах, которыми обозначается тип сущностей или атрибут связей (см. ниже). Поэтому в нем необходимо использовать символы алфавита Crockford's base32.

Таблицы типа узел (knot) исключены из адаптированной методологии Anchor modeling. Однако типовое обозначение узла на схеме в виде квадрата с закругленными углами удобно использовать для обозначения атрибутов связей.

Набросок БД может быть сделан (в том числе, офлайн) с помощью наглядных и удобных веб-инструментов Online Modeler или Online Modeler (test version), но сгенерированный ими SQL-код непригоден для использования. Для генерации SQL-кода (включая SQL-запросы) по методологии Anchor modeling все известные компании используют самостоятельно разработанные ими инструменты на основе языка программирования Python и Microsoft Excel.

2. Суррогатные ключи в адаптированном формате ULID

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

В качестве суррогатного ключа должна использоваться адаптированная (не оригинальная) версия ULID (но не UUID), имеющая любой из двух форматов:

  • ttttttttttrrrrrrrrrrrrrrxx (пример: 01F5B023PBG3C48TSBDQQ3V9TR)

  • ttttttttttsssrrrrrrrrrrrxx (пример: 01F5B023PB00448TSBDQQ3V5TR)

где

t дата и время генерации с точностью до миллисекунды (Timestamp) (10 символов или 48 бит), UNIX-time в миллисекундах (UTC)

s счетчик от 0 до 32768, сбрасываемый каждую миллисекунду, (Sequence) (3 символа или 15 бит)

r случайное число (Randomness) (14/11 символов или 65/55 бит)

x тип сущностей (Entity type) (2 символа или 10 бит)

Должна использоваться кодировка и алфавит Crockford's base32.

Генератор ULIDов должен удовлетворять следующим требованиям:

  1. Соблюдение требуемого формата ULIDов

  2. Однократное использование каждого генерируемого ULIDа в качестве суррогатного ключа сущности

  3. Использование (достаточно производительного) криптографически стойкого генератора псевдослучайных чисел или генератора истинно случайных чисел

  4. Монотонное возрастание ULIDов в интервале менее миллисекунды (за счет инкремента случайного числа для формата без счетчика, или за счет счетчика для формата со счетчиком)

  5. Генерация ULIDов в формате (текстовый, бинарный, UUID или целочисленный), наиболее производительном для операций поиска в применяемых СУБД и носителе данных (HDD или SSD)

  6. Пиковая (в течение 5 мс) производительность генерации ULIDов должна быть выше максимальной производительности записи в применяемых СУБД и носителе данных (HDD или SSD) (например, за счет буферизации заранее вычисленных частей ULIDа)

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

3. Указание начала и окончания срока действия записи

Для сохранения историчности данных должен применяться второй тип медленно меняющегося измерения SCD2 с добавлением двух унифицированных полей (столбцов): Дата и время начала срока действия записи (имя столбца с суффиксом _FROM) и Дата и время окончания срока действия записи (не включительно) (имя столбца с суффиксом _BEFORE).

Единственным условием связи записей в таблицах, помимо суррогатного ключа, должно быть одновременное действие (valid time) связанных записей, определяемое унифицированными полями начала и окончания срока действия записи, но не датой загрузки в систему или датой создания записи (transaction time). Данные не должны храниться в форме срезов на отчетные или на текущую дату или за календарный период (например, месяц).

4. Указание даты и времени создания записи

Если нужно знать не только изменение состояния реального мира во времени, но и изменение состояния самой учетной системы (когда в учетной системе изменяются сведения об одном и том же моменте в реальном мире), то во все таблицы необходимо добавить поле с датой и временем создания записи (transaction time), являющееся частью составного ключа. Чтобы точно воспроизвести отчет, сформированный в определенный момент в прошлом, достаточно игнорировать записи, чьи дата и время создания позднее этого момента в прошлом.

Примеры имени поля: TR_TIMESTAMP, TR_TDT_TIMESTAMP, TR_AC_DC_TIMESTAMP.

5. Только внешние источники пунктов классификаторов

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

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

6. Фасетная классификация

Необходимо по возможности использовать фасетную классификацию вместо иерархической классификации.

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

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

  • признак счета активный/пассивный,

  • глава,

  • раздел,

  • счет первого порядка,

  • тип контрагента,

  • срок.

7. Теги

Если есть большое количество атрибутов с логическими значениями true и false, то эти атрибуты удобнее заменить соответствующими тегами, которые можно хранить в одном поле типа array, типа hstore или типа jsonb.

8. Полиморфные связи

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

Однако использование таблиц связей в таких случаях нежелательно, так как их будет слишком много, и на их поддержание будет затрачиваться слишком много ресурсов IT-подразделения.

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

Если полиморфные связи необходимы одновременно с несколькими таблицами, то суррогатные ключи записей в этих связанных таблицах можно хранить в основной таблице в одном поле типа array, типа hstore или типа jsonb.

9. Устранение витрин данных

Каждый отчет должен формироваться непосредственно из реплики ядра хранилища данных.

Нежелательно использовать витрины данных, так как они сводят на нет все достоинства методологии Anchor modeling. Особенно большими недостатками обладают универсальные витрины данных, предназначенные для формирования любых видов отчетности.

10. Типовые SQL-запросы и материализованные представления

Разработка SQL-запросов к базе данных, соответствующей методологии Anchor modeling, трудоемка. Поэтому для облегчения работы системных аналитиков и SQL-программистов могут быть созданы типовые SQL-запросы или материализованные представления, соединяющие сущности с их атрибутами на задаваемую дату. Но использование таких SQL-запросов и материализованных представлений может привести к усложнению БД и снижению производительности. Поэтому для рабочей системы вместо них необходимо использовать автоматическую генерацию SQL-запросов (с использованием языка программирования Python и Microsoft Excel).

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

Сильная нормализация данных в Anchor modeling не позволяет производить многие автоматические проверки целостности на уровне таблиц. Поэтому для проверки целостности и качества данных нужны соответствующие SQL-запросы, автоматически запускаемые перед вставкой данных в таблицы.

11. Вынесение логики из программного кода в таблицы решений

Логику необходимо по возможности выносить из программного кода на языках программирования, SQL и различных ORM в таблицы решений. Например, в таблице решений может быть закодирован выбор дебетуемого синтетического счета в зависимости от типа операции, категории контрагента и срока погашения финансового инструмента. Таблица решений может быть реализована двумя способами:

  • таблица сущностей правил, к которой привязаны входные атрибуты и один выходной атрибут,

  • таблица связей, соединяющая входные сущности и атрибуты с выходной сущностью либо с выходным атрибутом.

Первый способ очевидно более гибкий и упорядоченный.

Подробнее..

Как мы запустили агрегатор удаленных вакансий и зачем в нем ML

26.05.2021 18:20:11 | Автор: admin

В один прекрасный день в конце 2020 года мы решили сделать еще один агрегатор удаленных вакансий, начав с исключительно IT-позиций. Логично спросить: зачем? Мол, на рынке их уже достаточно. Ответ на этот вопрос звучит очень просто: мы понимали, как улучшить текущие решения как минимум по пяти параметрам.

  • Количество (агрегировать больше всех в мире);

  • Реальная удаленка (а не позиции в стиле "remote until COVID-19");

  • Актуальность (часто на схожих сайтах можно найти большое количество неактуальных вакансий);

  • Хороший поиск (по нашему мнению поиск на текущих сайтах с удаленными вакансиями находится на уровне 2005 года);

  • Фильтр по гражданству.

О последнем параметре я и хочу сегодня рассказать.

Прежде, чем вы начнете читать. Сегодня мы запустились с Bergamot на ProductHunt. И, если у вас вдруг возникло желание поддержать наш продукт, будет круто. Ищите нас тут.

Проблема

Иногда компании устанавливают ограничения для граждан некоторых стран (например, компания готова нанимать только ребят с гражданством США / или конкретным типом визы ЕС). Как правило, на страницах с описанием вакансий нет отдельного поля где выводились бы подобные ограничения. И поиска/фильтра, соответственно, тоже нет. Поэтому соискателю приходится внимательно читать текст каждой вакансии, чтобы понять, есть ли вообще смысл откликаться на эту позицию.

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

Анализ

Сначала мы думали решить эту задачу простыми алгоритмическими методами. Базовая идея заключалась в следующем:

Шаг 1

Ищем определенные ключевые слова в тексте, например: only, remote in, authorized to work in и так далее.

Шаг 2

Ищем рядом с ключевыми словами локацию. Как правило, это слово, которое начинается с заглавной буквы. Если такое слово-локация находится, то оно и является ограничением.

Окей, если в вакансии написано USA only, то такая логика работает идеально. Но если капнуть глубже, становится понятно, что ограничения могут быть обозначены самыми разными, например:

  • This role is remote and you can be based anywhere across the UK

  • Living in Europe is a must

  • This opportunity is only open to candidates within Canada at this time

  • Location: Argentina (any part of the country its great for us!)

  • и еще сотни других описаний.

Очевидно, алгоритмами задачу не решить и мы попробовали использовать силу ML-a.

Задача

На всякий случай ещё раз сформулирую задачу. На входе текст с описанием вакансии, в котором, как правило, содержится описание компании, стек технологий, требования, условия, бенефиты и так далее. На выходе у нас должны быть параметры:

restriction: 0 (no) / 1 (yes)

если restriction = 1, то тогда необходимо выделять еще и страну, по которой есть ограничение

Решение

Структура решения

Итак: на входе у нас текст, в котором обычно содержится куча всего. Задача была несколько сложнее, чем просто написать обычный классификатор. Сначала нужно было найти, что именно классифицировать.

Так как мы пытаемся решить проблему с локациями, для начала нам нужно найти их в тексте. Потом выделить все предложения, в которых содержались локации и уже для них написать классификатор.

Нахождение локаций

Сначала мы также хотели решить задачу в лоб: найти где-нибудь список всех стран и городов и просто искать их вхождение в тексте. Но опять же, задача оказалась не такой очевидной.

Во-первых, ограничения касались не только стран и столиц мира, а также небольших городов и штатов. Например Can work full time in Eugene, OR / Hammond, IN. А сделать список локаций всех уровней уже сложнее.

Во-вторых, написания локаций в вакансиях часто отличались от стандартного (например 100% Remote in LATAM).

Поэтому для выделения локаций мы приняли решение использовать NER. Пробовали разные готовые варианты:

В итоге выбор пал на spaCy, потому что из готовых и бесплатных вариантов spaCy EntityRecognizer показал наилучший результат.

Итого: нам удалось выделить в тексте локации.

Разделение на предложения

Для разделения на предложения, где есть локации, мы тоже использовали spaCy.

На выходе мы получили список предложений из текста, где содержатся локации. Вот примеры таких предложений:

  • The position is remote so the only thing is they have to be in the US and be able to work Eastern or Central time.

  • This job is located out of our Chicago office, but remote, US-based applicants are still encouraged to apply.

  • This is a remote role, but we're looking for candidates based in Montreal, Canada.

Классификатор

Эти предложения и должна была разметить модель. Важно у нас не было возможности сделать датасет с десятками тысяч таких предложений (на это нужно очень много времени), поэтому при подборе модели нам надо было учесть эти ограничения.

Решили попробовать несколько моделей, среди которых как более простые CNN и LSTM, так и более современные transformers. Последние предсказуемо оказались лучше, обучение которых сводилось по сути к fine-tunning это нам точно подходило, ведь датасет, как я уже сказал выше, был невелик.

Среди transformers наилучший результат показала архитектура RoBERTa (roberta-base) с показателем точности 94% для нашего датасета.

Нормализация локаций

На основе классификатора и NER-a для каждой вакансии мы получили вот такие дополнительные поля:

restriction: 1 (yes); location: London

Restriction отдавал классификатор. А вот Location выдавал NER. Из-за того что в поле Location могли быть разные написания городов и стран, мы еще сделали дополнительную нормализацию через Google API. Остановились на том, чтобы сделать ограничения по странам.

То есть на выходе получалось:

restriction: 1 (yes); location: United Kingdom

Итог

В итоге мы теперь умеем это делать и кандидаты могут фильтровать неподходящие для них вакансии. Mission accomplished (вроде бы! вы можете сами потестить Bergamot и написать, что думаете).

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru