Эта статья продолжает цикл рассказов (раз, два) об основных способах/сценариях использования iKnow — инструмента Natural Language Processing'а из стека технологий InterSystems.
Предыдущие посты на эту тему были в основном посвящены работе с данными уже после того, как те были помещены в домен (место, в котором и проходит весь анализ текста). Эта же статья будет о том, как правильно и удобно загрузить информацию в iKnow. В качестве примера рассмотрим загрузку информации о пользователях Вконтакте: их личных данных, постах и т.д.
Статья подразумевает некий базовый бэкграунд в области технологий InterSystems (в частности, Caché ObjectScript).
Долгая дорога в домен
Если верить официальной документации, есть два сценария загрузки данных в существующий домен:
- Создается инстанс класса
%iKnow.Source.Loader
. Он привязан к конкретному домену (тому, id которого был передан в конструктор). Создается инстанс класса, реализующего интерфейс листера. У этого инстанса вызывается методAddListToBatch
с некоторыми аргументами, специфицирующими загружаемую информацию. Таким образом к текущему батчу домена добавляется новый список информации для загрузки. Это может быть проделано несколько раз. Для того, чтобы загрузить текущий батч в домен, у лоадера нужно вызвать методProcessBatch
. Этот вариант лучше подходит для загрузок больших объемов. - Создается инстанc класса, реализующего интерфейс листера, у этого инстанса вызывается метод
ProcessList
с некоторыми аргументами, специфицирующими загружаемую информацию, и загрузка происходит сразу в домен напрямую. Этот варинт лучше подходит для загрузок малых объемов.
Кастомизация листинга
Стандартная библиотека предлагает множество готовых реализаций листера (RSS-листер, файловый листер, листер глобалов). Однако же у конечного программиста имеется возможность написать свою реализацию, подходящую для его собственных нужд.
Перед тем, как писать листер для постов Вконтакте, я написал обертку для некоторых методов Вконтакте API на COS, оперирующих данными в открытом доступе. Весь код доступен на github в пакете VKReader
.
Я решил, что было бы интересно, если бы листер мог загружать последние посты по какому-нибудь ключевому слову, ну, и каким-нибудь еще параметрам. Оказалось, что этого совсем не сложно добиться. Глава документации, посвященная кастомизации, говорит, что для создания своего листера нужно пронаследоваться от системного класса и переопределить несколько методов.
Итак, все в том же пакете я создал класс VKReader.Lister
, наследующийся от класса %iKnow.Source.Lister
. Если вы пишете свой листер, он тоже должен наследоваться от этого класса.
Каждому листеру должно быть присвоено уникальное короткое имя (alias), по которому к нему будут обращаться системные методы iKnow. Если это имя не будет указано, вместо него будет использоваться полное имя класса этого листера.
Чтобы указать alias, просто переопределите в вашем классе класс-метод GetAlias
. Для нашего листера Вконтакте я сделал это так:
ClassMethod GetAlias() As %String
{
Quit "VKAPI"
}
Все источники данных, представленные для загрузки, имеют external id, который должен содержать короткое имя листера и full reference, который, в свою очередь, состоит из имени группы источников и local reference.
Для работы листера нужно переопределить класс-методы BuildFullRef
и SplitFullRef
, соответственно, собирающий full reference из groupname и local reference и разбивающий его на эти две части.
Extrenal id в нашем случае получился такой:
VKAPI:searchQuery:::vkPostId
Здесь VKAPI — короткое имя нашего листера, поисковой запрос играет роль имени группы источников, а id записи Вконтакте — local reference.
Код методов BuildFullRef
и SplitFullRef
:
ClassMethod SplitFullRef(domainId As %Integer, fullRef As %String, Output groupName As %String,
Output localRef As %String) As %Status [ Private ]
{
set delim = ":::"
set localRef = $piece(fullRef, delim, $l(fullRef, delim))
set groupName = $e(fullRef, 1, *-$l(localRef)-$l(delim))
Quit $$$OK
}
ClassMethod BuildFullRef(domainId As %Integer, groupName As %String, localRef As %String) As %String [ Private ] Также нужно указать, какой
{
quit groupName_":::"_localRef
}
Processor
будет стандартным для этого листера. В iKnow Processor
— это объект, который занимается непосредственной обработкой загружаемых данных. Есть несколько типов различных обработчиков (Processor
-ов), но, поскольку в нашем случае данные будут храниться только непосредственно в памяти, я решил использовать обработчик для временного хранилища. Обработчик также указывается через переопределение.
{
Quit "%iKnow.Source.Temp.Processor"
}
Вся основная загрузочная деятельность происходит в еще одном переопределяемом методе с красноречивым названием ExpandList
. Этот метод расширяет список для загрузки в домен. Аргументы методов ProcessList и AddListToBatch будут такими же, какими вы определите их в ExpandList
.
Приведем сначала весь код метода для нашего случая.
Аргументы у нас будут следующие (по порядку): слово-запрос, по которому хотим искать записи; число записей; булевское значение, соответствующее тому, хотим ли мы проверять список для загрузки на существование источника с такими же local reference; ограничения на время публикации записи.
{
set query = $li(listparams, 1)
set count = $li(listparams, 2)
set checkExists = +$lg(listparams, 3, 1)
set startDate = $lg(listparams, 4)
set startTime = $lg(listparams, 5)
set endDate = $lg(listparams, 6)
set endTime = $lg(listparams, 7)
#dim response As %ListOfObjects
set tSC = ##class(VKReader.Requests.APIPublicMethodsCaller).NewsfeedSearch(.response, query,
count,,,startDate, startTime, endDate, endTime)
quit:$$$ISERR(tSC) tSC
do ..RegisterMetadataKeys($lb("PostDate", "PostTime", "AuthorID", "AuthorCity", "AuthorCountry",
"AuthorDOB", "AuthorSex"))
set userIds = "1"
set groupIds = "1"
for i = 1: 1: response.Count() {
if (response.GetAt(i).FromID < 0) {
set groupIds = groupIds _ "," _ (-(response.GetAt(i).FromID))
} else {
set userIds = userIds _ "," _ response.GetAt(i).FromID
}
}
set tSC = ##class(VKReader.Requests.APIPublicMethodsCaller).UsersGet(.responseUsers, userIds,
"sex,city,bdate,country")
quit:$$$ISERR(tSC) tSC
set tSC = ##class(VKReader.Requests.APIPublicMethodsCaller).GroupsGetById(.responseGroups, groupIds,
"city,country")
quit:$$$ISERR(tSC) tSC
for i = 1: 1: response.Count() {
set tPostDate = response.GetAt(i).Date
set tPostTime = response.GetAt(i).Time
set tOwnerID = response.GetAt(i).OwnerID
set tFromID = response.GetAt(i).FromID
set tID = response.GetAt(i).ID
#dim tTextStream as %GlobalCharacterStream
set tTextStream = response.GetAt(i).Text
if (tFromID < 0) {
set tAuthorCity = responseGroups.GetAt(-tFromID).City
set tAuthorCountry = responseGroups.GetAt(-tFromID).Country
set tAuthorDOB = ""
set tAuthorSex = ""
} else {
set tAuthorCity = responseUsers.GetAt(tFromID).City
set tAuthorCountry = responseUsers.GetAt(tFromID).Country
set tAuthorDOB = responseUsers.GetAt(tFromID).DOB
set tAuthorSex = responseUsers.GetAt(tFromID).Sex
}
set tLocalRef = tOwnerID _ "#" _ tFromID _ "#" _ tID
if (checkExists) {
continue:..RefExists(query, tLocalRef, checkExists - 1)
}
set tRef = $lb(i%ListerClassId, ..AddGroup(query), tLocalRef)
do tTextStream.Rewind()
if (tTextStream.Size = 0) {
continue
}
set len = 32000
while (len = 32000) {
do ..StoreTemp(tRef, tTextStream.Read(.len))
}
do ..SetMetadataValues(tRef, $lb(tPostDate, tPostTime, tFromID, tAuthorCity, tAuthorCountry,
tAuthorDOB, tAuthorSex))
}
}
Пройдемся по коду более подробно.
Сначала выделим аргументы.
set query = $li(listparams, 1)
set count = $li(listparams, 2)
set checkExists = +$lg(listparams, 3, 1)
set startDate = $lg(listparams, 4)
set startTime = $lg(listparams, 5)
set endDate = $lg(listparams, 6)
set endTime = $lg(listparams, 7)
Сделаем запрос к API Вконтакте через наш метод-обертку. Результатом работы этого метода является список объектов класса VKReader.Data.Post
, который содержит некоторые характерные для записи Вконтакте поля.
#dim response As %ListOfObjects
set tSC = ##class(VKReader.Requests.APIPublicMethodsCaller).NewsfeedSearch(.response, query,
count,,,startDate, startTime, endDate, endTime)
quit:$$$ISERR(tSC) tSC
Зарегистрируем ключи метаданных для дальнейшего легкого сохранения метаинформации. В метаданных мы хотим хранить дату и время публикации записи, а также id, город, страну и дату рождения автора.
do ..RegisterMetadataKeys($lb("PostDate", "PostTime", "AuthorID", "AuthorCity", "AuthorCountry",
"AuthorDOB", "AuthorSex"))
Сохраним comma-separated-list'ы id пользователей и групп, являющихся авторами найденных нами записей. Id групп, как и в API Вконтакте, являются отрицательными целыми числами, а id пользователей — положительными.
set userIds = "1"
set groupIds = "1"
for i = 1: 1: response.Count() {
if (response.GetAt(i).FromID < 0) {
set groupIds = groupIds _ "," _ (-(response.GetAt(i).FromID))
} else {
set userIds = userIds _ "," _ response.GetAt(i).FromID
}
}
Получим информацию об этих пользователях и группах при помощи методов-оберток. Они возвращают списки объектов типов VKReader.Data.User
и VKReader.Data.Group
, содержащих поля, характерные для пользователей и групп Вконтакте (вроде города, страны и всего прочего).
set tSC = ##class(VKReader.Requests.APIPublicMethodsCaller).UsersGet(.responseUsers, userIds,
"sex,city,bdate,country")
quit:$$$ISERR(tSC) tSC
set tSC = ##class(VKReader.Requests.APIPublicMethodsCaller).GroupsGetById(.responseGroups, groupIds,
"city,country")
quit:$$$ISERR(tSC) tSC
В цикле обработаем все найденные посты. Сначала выделим всю полученную метаинформацию в локальные переменные.
set tPostDate = response.GetAt(i).Date
set tPostTime = response.GetAt(i).Time
set tOwnerID = response.GetAt(i).OwnerID
set tFromID = response.GetAt(i).FromID
set tID = response.GetAt(i).ID
#dim tTextStream as %GlobalCharacterStream
set tTextStream = response.GetAt(i).Text
if (tFromID < 0) {
set tAuthorCity = responseGroups.GetAt(-tFromID).City
set tAuthorCountry = responseGroups.GetAt(-tFromID).Country
set tAuthorDOB = ""
set tAuthorSex = ""
} else {
set tAuthorCity = responseUsers.GetAt(tFromID).City
set tAuthorCountry = responseUsers.GetAt(tFromID).Country
set tAuthorDOB = responseUsers.GetAt(tFromID).DOB
set tAuthorSex = responseUsers.GetAt(tFromID).Sex
}
Local reference — id хозяина стены, id отправителя и id записи, разделенные решеткой.
set tLocalRef = tOwnerID _ "#" _ tFromID _ "#" _ tID
Если необходимо, проверим, есть ли источники с таким же local reference.
if (checkExists) {
continue:..RefExists(query, tLocalRef, checkExists - 1)
}
Следующий код мог бы быть и другим, если бы был выбран другой обработчик источников. Я использую обработчик для временного хранилища, так что мне нужно расширять список при помощи метода StoreTemp
(более подробно для каждого обработчика можно посмотреть на странице с его документацией). Также мне нужно установить полученные значения для полей метаданных.
set tRef = $lb(i%ListerClassId, ..AddGroup(query), tLocalRef)
do tTextStream.Rewind()
if (tTextStream.Size = 0) {
continue
}
set len = 32000
while (len = 32000) {
do ..StoreTemp(tRef, tTextStream.Read(.len))
}
do ..SetMetadataValues(tRef, $lb(tPostDate, tPostTime, tFromID, tAuthorCity, tAuthorCountry,
tAuthorDOB, tAuthorSex))
Все. Листер написан!
Протестируем его работу.
Тестируем листер
Я написал небольшое веб-приложение, которое, используя реализованный нами листер, позволяет просматривать, искать похожие, добавлять по запросу и удалять записи из домена. Вот несколько скриншотов:
Изначально пустой домен.
Нажимаем на плюсик, чтобы добавить новые посты.
У появившейся формы заполняем поля и жмем на кнопку, чтоб добавить записи.
Ждем некоторое время и записи добавляются.
Для тех пользователей или групп, которые предоставили данные о себе в открытый доступ, наш листер сохраняет их в поля метаинформации, а это небольшое демо отображает их в виде не слишком элегантной таблицы.
Из коробки iKnow может показывать похожие записи: нажмем на кнопку с мишенью возле какого-нибудь поста и убедимся, что это работает.
Резюме
По ходу статьи мы разобрались в том, как работает загрузка данных в домен, подробно обсудили, как работает среднестатистический листер и как написать свой листер, который тоже будет работать. Написали свой листер для работы с данными Вконтакте, а также убедились в том, что он действительно работает по модулю того, что домен и конфигурация были созданы где-то за кулисами.
В случае, если имеется желание заглянуть за эти кулисы, весь код, который был изложен, использовался или упоминался в статье, может быть найден на страничке проекта на github.
Автор: deadpadre