InterSystems iKnow. Загружаем данные из Вконтакте

в 7:06, , рубрики: cache, data mining, iKnow, intersystems cache, natural language processing, nlp, Блог компании InterSystems, Вконтакте API, разработка, социальные сети

Эта статья продолжает цикл рассказов (раз, два) об основных способах/сценариях использования iKnow — инструмента Natural Language Processing'а из стека технологий InterSystems.
Предыдущие посты на эту тему были в основном посвящены работе с данными уже после того, как те были помещены в домен (место, в котором и проходит весь анализ текста). Эта же статья будет о том, как правильно и удобно загрузить информацию в iKnow. В качестве примера рассмотрим загрузку информации о пользователях Вконтакте: их личных данных, постах и т.д.
Статья подразумевает некий базовый бэкграунд в области технологий InterSystems (в частности, Caché ObjectScript).

Долгая дорога в домен

alt text

Если верить официальной документации, есть два сценария загрузки данных в существующий домен:

  1. Создается инстанс класса %iKnow.Source.Loader. Он привязан к конкретному домену (тому, id которого был передан в конструктор). Создается инстанс класса, реализующего интерфейс листера. У этого инстанса вызывается метод AddListToBatch с некоторыми аргументами, специфицирующими загружаемую информацию. Таким образом к текущему батчу домена добавляется новый список информации для загрузки. Это может быть проделано несколько раз. Для того, чтобы загрузить текущий батч в домен, у лоадера нужно вызвать метод ProcessBatch. Этот вариант лучше подходит для загрузок больших объемов.
  2. Создается инстан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 %IntegerfullRef As %StringOutput groupName As %String
Output localRef As %StringAs %Status Private ]
{
    
set delim ":::"
    
set localRef $piece(fullRefdelim$l(fullRefdelim))
    
set groupName $e(fullRef, 1, *-$l(localRef)-$l(delim))
    
Quit $$$OK
}

ClassMethod BuildFullRef(domainId As %IntegergroupName As %StringlocalRef As %StringAs %String Private ]
{
    
quit groupName_":::"_localRef
}

Также нужно указать, какой Processor будет стандартным для этого листера. В iKnow Processor — это объект, который занимается непосредственной обработкой загружаемых данных. Есть несколько типов различных обработчиков (Processor-ов), но, поскольку в нашем случае данные будут храниться только непосредственно в памяти, я решил использовать обработчик для временного хранилища. Обработчик также указывается через переопределение.

ClassMethod DefaultProcessor() As %String
{
    
Quit "%iKnow.Source.Temp.Processor"
}

Вся основная загрузочная деятельность происходит в еще одном переопределяемом методе с красноречивым названием ExpandList. Этот метод расширяет список для загрузки в домен. Аргументы методов ProcessList и AddListToBatch будут такими же, какими вы определите их в ExpandList.
Приведем сначала весь код метода для нашего случая.
Аргументы у нас будут следующие (по порядку): слово-запрос, по которому хотим искать записи; число записей; булевское значение, соответствующее тому, хотим ли мы проверять список для загрузки на существование источника с такими же local reference; ограничения на время публикации записи.

Много кода под спойлером

Method ExpandList(listparams As %ListAs %Status
{
    
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(.responsequery,
 count,,,startDatestartTimeendDateendTime)
    
quit:$$$ISERR(tSCtSC
    
    
do ..RegisterMetadataKeys($lb("PostDate""PostTime""AuthorID""AuthorCity""AuthorCountry",
 "AuthorDOB""AuthorSex"))
    
    
set userIds "1"
    
set groupIds "1"
    
    
for = 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(.responseUsersuserIds,
 "sex,city,bdate,country")
    
quit:$$$ISERR(tSCtSC
    
set tSC ##class(VKReader.Requests.APIPublicMethodsCaller).GroupsGetById(.responseGroupsgroupIds,
 "city,country")
    
quit:$$$ISERR(tSCtSC
    
    
for = 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(querytLocalRefcheckExists - 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(tReftTextStream.Read(.len))
        
}
        
        
do ..SetMetadataValues(tRef$lb(tPostDatetPostTimetFromIDtAuthorCitytAuthorCountry,
 tAuthorDOBtAuthorSex))
    
}
}

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

    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(.responsequery,
 count,,,startDatestartTimeendDateendTime)
    
quit:$$$ISERR(tSCtSC

Зарегистрируем ключи метаданных для дальнейшего легкого сохранения метаинформации. В метаданных мы хотим хранить дату и время публикации записи, а также 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 = 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(.responseUsersuserIds,
 "sex,city,bdate,country")
    
quit:$$$ISERR(tSCtSC
    
set tSC ##class(VKReader.Requests.APIPublicMethodsCaller).GroupsGetById(.responseGroupsgroupIds,
 "city,country")
    
quit:$$$ISERR(tSCtSC

В цикле обработаем все найденные посты. Сначала выделим всю полученную метаинформацию в локальные переменные.

        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(querytLocalRefcheckExists - 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(tReftTextStream.Read(.len))
        
}
        
        
do ..SetMetadataValues(tRef$lb(tPostDatetPostTimetFromIDtAuthorCitytAuthorCountry,
 tAuthorDOBtAuthorSex))

Все. Листер написан!
Протестируем его работу.

Тестируем листер

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

Изначально пустой домен.

alt text
Нажимаем на плюсик, чтобы добавить новые посты.
У появившейся формы заполняем поля и жмем на кнопку, чтоб добавить записи.

alt text
Ждем некоторое время и записи добавляются.

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

alt text

Резюме

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

Автор: deadpadre

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js