Робота з повнотекстовим пошуком (FTS) #

Загальна інформація #

У платформу UnityBase вбудовано сервіс повнотекстового пошуку на базі розширення SQLite FTS3, з підтримкою української, російської та англійської морфології (токенізатор stemka). Починаючи з версії UB@5.25.26 підтримуються також розширення SQLite FTS5 з токенізаторами unicode61, porter та trigram.

Сервіс може бути використаний як в «ручному» (програміст самостійно повинен написати CRUID операції для оновлення повнотекстового індексу при зміні даних сутності), так і в «автоматичному» режимі - в цьому випадку для сутностей, які необхідно індексувати повнотекстовим пошуком, необхідно увімкнути та налаштувати міксин `fts.

При роботі через міксин оновлення повнотекстового індексу проводиться «на льоту», тобто відразу ж після оновлення даних сутності (після коміту транзакцій), або в «асинхронному» режимі (application.fts.async=true в конфігурації додатка).

Існує можливість як використовувати загальний індекс для декількох сутностей, так і рознести сутності по різних БД повнотекстового пошуку (вказанням різних mixins.fts.connectionName в метафайлах сутності).

Підтримується пошук з обмеженнями за ДАТОЮ і за списками контролю доступу (mixin aclRLS).

Увімкнення FTS для додатка #

Нижче наведено приклади для додатка autotest

Увімкнемо FTS на рівні додатка, додавши в конфіг (ubConfig.json) опцію fts.enabled: true:

"application": {
        ......
        "fts": {
            "enabled": true
        },
}

Додаємо коннекшин для SQLite БД повнотекстового пошуку. Ім'я з'єднання за замовчуванням ftsDefault:

"application": {
       ......
       "fts": {
           "enabled": true,
       },
       .....
       "connections": [{
            "name": "ftsDefault", 
                "driver": "SQLite3",
                "dialect": "SQLite3",
                "databaseName": "./fts/autotestFTS.ftsdb",
                "supportLang": ["uk"],
                "advSettings": "Synchronous=Off,Tokenizer=stemka,TokenizerParams=\"stem=yes\""
            },
           ......
       ]
}

При необхідності можна визначити кілька повнотекстових БД (конекшинів), при цьому в описі міксинів сутності необхідно вказувати імена відповідних з'єднань.

Приклад додавання другого конекшену:

"application": {
        ......
        "fts": {
            "enabled": true,
        },
        .....
        "connections": [{
            "name": "ftsDefault",
            "driver": "SQLite3",
            "dialect": "SQLite3",
            "databaseName": "./fts/autotestFTS.ftsdb",
            "supportLang": ["uk"],
            "advSettings": "Synchronous=Off,Tokenizer=stemka,TokenizerParams=\"stem=yes\""
        }, {
            "name": "ftsSubjectSearch",
            "driver": "SQLite3",
            "dialect": "SQLite3",
            "databaseName": "./fts/autotestFTSSubjectSearch.ftsdb",
            "supportLang": ["uk"],
            "advSettings": "Synchronous=Off,Tokenizer=stemka,TokenizerParams=\"stem=yes\""
        },
        .....
        ]
}

У міксині сутності відповідне з'єднання другого конекшену явно вказуємо через «connectionName»: «ftsSubjectSearch»

**Важливо: **

  • БД повнотексту повинні розташовуватися на локальній до сервера додатків файловій системі (бажано SSD)
  • Рекомендується називати файл БД, починаючи з імені додатка
  • обов'язково необхідно задати список підтримуваних мов
  • директорія, в якій знаходиться БД, повинна існувати. На етапі додавання FTS БД повинна бути відсутня
  • SQLIte база переключена в режим WAL. BusyTimeout=10sec. Не робіть довгих транзакцій
  • параметр «advSettings»: «Synchronous=Off» в 20 разів прискорює зміни в повнотекстовий індекс, але при цьому ваш сервер повинен бути захищений від перепадів живлення

Використання та налаштування токенізатора в повнотекстовому пошуку #

Токенізатор — це програма, яка розбиває рядки на лексеми.

Платформа UnityBase підтримує токенізатор «stemka», який може не тільки коректно розбивати рядки на українські та російські лексеми, а також містить у собі імовірнісний морфологічний аналізатор російської та української мов.

Для вказання цього токенізатора існує параметр з назвою «Tokenizer» в “advSettings”. Якщо значення «stemka» не вказати, то використовується токенізатор «simple» зі стандартної поставки SQLite. Токенайзер «stemka» може приймати на вхід кілька аргументів, які прописуються в параметрі «TokenizerParams»:

  • «stem=yes|no» - Включає/виключає морфологічний аналізатор російської та української мов. За замовчуванням значення «yes».
  • «lang=uk|ru» - Мова морфологічного аналізатора. За замовчуванням значення «uk», але значення цього параметра контролюється платформою UnityBase при створенні FTS-таблиці сутності і на даний момент не рекомендується використання цього параметра в «TokenizerParams».

Починаючи з версії UB@5.25.26 підтримуються також токенізатори unicode61, porter та trigram. Див. токенізатори FTS3 У випадку використання цих токенізаторів буде використано розширення SQLite FTS5

Важливо:

після зміни значення параметра «Tokenizer» або «TokenizerParams» потрібно ОБОВ'ЯЗКОВО перестворити й потім переіндексувати SQLte БД повнотекстового пошуку, тому що токенізер і його аргументи прописані в заголовку кожної FTS-таблиці. Тобто, БД повнотекстового пошуку буде працювати, але використовувати токенерайзер і аргументи, які були на момент створення цієї БД.

Додавання сутностей до повнотекстового пошуку #

Для додавання сутності до повнотекстового пошуку необхідно в meta файлі сутності налаштувати міксин fts.
myEntity.meta:

"mixins": {
    ....
    "fts": {
        "dataProvider": "Mixin",
        "scope": "Connection",
        "indexedAttributes": ["code", "description"],
        "dateAttribute": "docDate"
    }

У прикладі вище ми для сутності myEntity:

  • використовували з'єднання ftsDefault (за замовчуванням, оскільки пропущено параметр “connectionName”) для зберігання повнотекстового індексу
  • вказали необхідність індексувати два атрибути: code і description
  • додатково в індекс додали хеш атрибута docDate, завдяки чому можна виконувати повнотекстовий пошук з урахуванням обмеження за датами

Тепер при будь-яких CRUD операціях з сутністю myEntity повнотекстовий індекс буде оновлюватися синхронно.

Початкове побудова повнотекстового індексу / переіндексація #

У штатному режимі роботи, коли додавання/видалення/модифікація даних сутності проводиться через методи сутності insert/delete/update перебудова повнотекстового індексу проводиться автоматично.

Проте бувають ситуації, коли потрібно перебудувати/побудувати повнотекстовий індекс заново. Наприклад:

  • міксин fts додається до сутності, яка вже має дані
  • fts був тимчасово відключений на рівні сервера додатків
  • база з повнотекстовим індексом втрачена

У такому випадку необхідно скористатися утилітою командного рядка cmd/ftsReindex. Приклад перебудови всього індексу додатка для autotest:

ubcli -app autotest ftsReindex -c ftsDefault -u admin -p admin

Детальніше див. довідку по команді:

ubcli ftsReindex -help

Налаштування асинхронної реіндексації #

Оновлення (update) одного запису в повнотекстовому індексі - дуже швидка операція (порядку мікросекунд). У зв'язку з цим повнотекстова БД не підтримує довгих транзакцій. У поточній реалізації максимальний час транзакції становить 10 секунд.

Якщо бізнес-логіка додатка працює довго, наприклад після оновлення документа (відповідно - запустилася транзакція в повнотекстовій БД) виконуються ще якісь тривалі обчислення, а в цей момент інший користувач теж зажадає модифікації індексу

  • отримаємо помилку SQLITE_BUSY (5) - «database is locked».

Вихід із ситуації - налаштування fts для запуску в асинхронному режимі. У такому режимі при зміні сутностей з міксином fts не проводиться миттєве оновлення повнотекстового індексу, але в чергу повідомлень ubq_messages записується команда на оновлення індексу за модифікованим екземпляром.

Безпосередньо оновлення індексу виконується планувальником завдань шляхом періодичного запуску завдання UB.UBQ.FTSReindexFromQueue.

Включаємо асинхронне оновлення повнотексту #

На рівні додатка, в конфігурації (ubConfig.json) додаємо опцію fts.async: true:

"application": {
    ......
    "fts": {
        "enabled": true,
        "async": true
    },
    .....
    "connections": {
        ......
    }
}

У переліку моделей domainConfigs має бути додана модель UBQ

Налаштовуємо планувальник #

Додаємо нове завдання планувальника для періодичного оновлення індексу, наприклад раз на 10 хвилин у конфігураційний файл планувальника \Autotest\schedulers\schedulers.json:

{
    ....,
    "fts":{
         "enabled": true,
         "ownerUser": "admin",
         "runcmd": "UB.UBQ.FTSReindexFromQueue",
         "useDaysOf": "Month",
         "daysOfMonth": [],
         "allMonthDays": true,
         "lastMonthDay": true,
         "daysOfWeek": [],
         "timePeriodic": "Periodic",
         "timeList": [],
         "timePeriodicHour": 0,
         "timePeriodicMinute": 10,
         "name": "fts"
    }
}

Вмикаємо планувальник на рівні додатка:
UB_USE_SCHEDULERS=true в env файлі або явно в конфізі

{
  "application": {
    "schedulers": {
      "enabled": true
    }
  }
}

Програмне використання повнотекстового пошуку #

Пошук за конкретною сутністю #

Пошук за конкретною сутністю - використовуємо умову match в where:

UB.Repository('myEntity').attrs(["ID", "code"])
    .where('', 'match', 'республіка')
    .selectAsObject()
    .then(UB.logDebug);

З додаванням умов на інші атрибути сутності (у даному прикладі - docDate):

UB.Repository('tst_document').attrs("ID")
    .where('', 'match', 'Україна')
    .where('docDate', '<', new Date(2015, 02, 13))
    .selectAsArray()
    .then(UB.logDebug);

Обидва приклади вище виконуються в 2 етапи:

  • пошук за повнотекстовим індексом ідентифікаторів записів, що задовольняють умові match
  • вибір із сутності записів за знайденими ідентифікаторами з накладенням додаткових умов (якщо вони є)

Безсумнівний плюс такого підходу - можливість вибрати будь-який набір атрибутів сутності, а не тільки ті атрибути, які включені в повнотекстовий індекс, а також - побудова фінальної вибірки з урахуванням обмежень, що накладаються на сутність іншими міксинами, наприклад RLS.

Пошук по всіх сутностях одного індексу #

При підключенні fts в сутностях і вказівці атрибутів для індексування створюється таблиця в файлі повнотекстового пошуку, яка містить індексовані дані FTSIndexedData

Пошук по всіх сутностях в даному конекшині - використовуємо метод fts_ftsDefault.fts:

$App.connection.run({
    entity: "fts_ftsDefault",
    method: "fts",
    fieldList: ["ID", "entity", "entitydescr", "snippet"],
    whereList: {match: {condition: "match", values: {"any": "Україна"}}},
    options: {limit: 100, start: 0}
})
.then(function(result){...});

Але також можна вказувати конкретну сутність tst_incDocument, використовуючи метод fts.

UI-елементи повнотекстового пошуку #

Комбобокс із підтримкою повнотекстового пошуку #

Віджет для верхньої панелі #

Віджет повнотекстового пошуку packages/adminui-vue/components/navbarSlotDefault/UNavbarSearchButton.vue для верхнього тулбара дозволяє користувачеві шукати по всіх сутностях обраного коннекшину. На програмному рівні робить запит, аналогічний описаному в «Пошук по всіх сутностях одного індексу»

В результаті отримаємо наступний функціонал FTSToolbarWidget

Подвійний клік на рядку грида з результатами пошуку відкриє форму за замовчуванням відповідної сутності.

Див. документацію та приклади використання

Створення власного пошуку на формі #

Приклад створення власного поля пошуку з використанням fts за аналогією з віджетом UB.view.FullTextSearchWidget:

  • Створюємо рядок для введення даних:

    var me = this
    me.textBox = Ext.create('Ext.form.field.Text', {
        enableKeyEvents: true,
        fieldLabel: UB.i18n('myFieldLabel'),
        style: "color: black; border-width: 5px;",
        fieldStyle: "border-width: 0px;",
        listeners: {
            keyup: function(sender, e){
                if (e.getKey() === e.ENTER){
                    me.buttonClick();
                }
            },
            scope: me
        }
    });
    
  • якщо необхідно, створюємо кнопку-іконку пошуку:

    me.button = Ext.create('Ext.button.Button',{
        border: false,
        margin: 3,
        padding: 1,
        style: {backgroundColor: 'white'},
        iconCls: 'u-icon-search',
        handler: me.buttonClick,
        scope: me
        });
    
  • додаємо в потрібне місце на формі:

    {
       xtype: 'panel',
       layout: 'hbox',
       style: {
           background: "white"
       },
       items: [
           me.textBox,
           me.button
       ]
    }
    
  • Використовуємо пошук за всіма сутностями або конкретно вказаною (див. Пошук за всіма сутностями одного індексу):

    buttonClick: function () {
        ...
        $App.connection.run({
            entity: "fts_ftsDefault",
            method: "fts",
            fieldList: ["ID", "entity", "entitydescr", "snippet"],
            whereList: {match: {condition: "match", values: {"any": "Україна"}}},
            options: {limit: 100, start: 0}
        })
        .then(function(result){...});
        ...
    }
    

Попередня фільтрація довідника (автофільтр) #

При передачі на рівні команди showList параметра autoFilter (приклад ярлика):

{
    "cmdType": "showList",
    "autoFilter": true,
    "cmdData": {
        "params": [{
            "entity": "tst_document",
            "method": "select",
            "fieldList": ["favorites.code", "docDate", "code", "description", "fileStoreSimple"]
        }]
    }
}

для сутностей з міксином fts додається окрема вкладка «Повнотекстовий пошук»: FTSAutofilter

Для грідів, відфільтрованих повнотекстовим пошуком - відповідна індикація: FTSAutofilterResult

На програмному рівні формується запит, аналогічний описаному в «Пошук за конкретною сутністю».

Див. додатково документацію з конфігурації автофільтра

Правила побудови запитів #

Службові символи, які беруть участь у побудові запиту #

Службові символи
*
#
(
)
[
]

Символи-роздільники, пошук за якими не буде здійснюватися #

Символи з таблиці нижче є незначущими для повнотекстового індексу (грубо кажучи - це все одно, що пробіл)

Символи-роздільники
-
,
.
/
\
:
{
}
=
+
&
^
%
$
@
#
!
?
~
`
'
;