TeamWox SDK: Поиск и фильтрация - Часть 2

 

Введение

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

  Фильтрация
  Пример реализации фильтрации для пользовательского модуля
    1. Менеджер списка языков программирования
    2. Параметры фильтрации
    3. Получение данных для фильтрации
  HTTP API
  Пользовательский интерфейс
  Демонстрация работы

 

Фильтрация

Фильтрация - это выбор подмножества данных и отображение их в виде, в котором данные представлены в модуле. Необходимость фильтрации становится ощутимой при навигации по большому списку записей. При работе с большими объемами данных поиск и фильтрация (как подмножество поиска) средствами SQL неэффективны или вообще невозможны. Для реализации полнотекстового поиска применяются расширения для СУБД.

Система TeamWox создавалась с ориентиром на повышенную отказоустойчивость и максимальное быстродействие. Эти принципы стали приоритетными при реализации модуля "Поиска".

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

 

Пример реализации фильтрации для пользовательского модуля

В учебном модуле HelloWorld, который входит в состав TeamWox SDK, реализован пример фильтрации данных на странице Список записей. Вы можете загрузить и установить обновленный TeamWox SDK. Исходные коды модуля Hello World также доступны в виде прикрепленного к статье файла.

Опишем основные ключевые моменты реализации.

 

1. Менеджер списка языков программирования

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

1.1. Код полнотекстового реиндексирования практически не изменился. Добавился управляющий критерий - общий тип поиска:

//+------------------------------------------------------------------+
//| Реиндексация всех заданий                                        |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::SearchReindex(ISearchManager *search_manager,int search_type)
  {
..............................
      //--- прочитаем описание
      if(search_type==SEARCH_GENERIC)
         ReadDescription(&record,&description,&description_len,&description_allocated);
      //--- добавим/обновим запись в поиске
      SearchRecordAdd(m_search_manager,indexer,search_type,&record,description,description_len);
      //--- проиндексируем комментарии
      if(search_type==SEARCH_GENERIC)
         m_comments_manager->SearchReindexRecord(sql.Interface(),search_manager,indexer,0,NULL,0,
                                                 HELLOWORLD_COMMENT_TYPE_COMMENTS,0,record.id,HELLOWORLD_COMMENT_SEARCH_ID);
..............................
  }

1.2. Объявим две константы пользовательских типов поиска. Перечисление дополнительных параметров поиска должно быть больше константы SEARCH_USER.

enum EnHelloWorldSearchId
  {
   HELLOWORLD_BODY_SEARCH_ID   =0,
   HELLOWORLD_COMMENT_SEARCH_ID=1,
//---
   HELLOWORLD_SEARCH_FILTER         =SEARCH_USER,
   HELLOWORLD_SEARCH_FILTER_CATEGORY=SEARCH_USER+1
  };

1.3. Метод индексирования отдельной записи также претерпел минимальные изменения. Как и для полного реиндексирования, вторым этапом происходит индексирование данных для фильтрации.

//+------------------------------------------------------------------+
//| Индексирование отдельной записи                                  |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::SearchReindexRecord(const Context *context,HelloWorldRecord *record)
  {
.....................................
//--- добавим/обновим запись в поиске
   SearchRecordAdd(m_search_manager,indexer,SEARCH_GENERIC,record,description,description_len);
   m_comments_manager->SearchReindexRecord(context->sql,m_search_manager,indexer,0,NULL,0,
                                           HELLOWORLD_COMMENT_TYPE_COMMENTS,0,record->id,HELLOWORLD_COMMENT_SEARCH_ID);
//--- проиндексируем для фильтра
   SearchRecordAdd(m_search_manager,indexer,HELLOWORLD_SEARCH_FILTER,record,description,description_len);
.....................................
  }

1.4. Для пользовательского типа поиска будем индексировать только поле категории языка программирования.

TWRESULT CHelloWorldManager::SearchRecordAdd(ISearchManager *search_manager,ISearchIndexer *indexer,int search_type,
                                             HelloWorldRecord *record,const wchar_t *description,size_t description_len)
...............................
//---
   if(search_type==SEARCH_GENERIC)
     {
      if(description!=NULL && description_len>0)
         indexer->AddText(SEARCH_PLAIN_TEXT,description,description_len,1.0f);
     }
   else
     {
      indexer->AddUserItem(HELLOWORLD_SEARCH_FILTER_CATEGORY,record->category, 1.0);
     }
...............................

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

//+------------------------------------------------------------------+
//| Удаление записи из поискового индекса                            |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::SearchRemoveRecord(const Context *context,INT64 record_id)
  {
//--- проверки
   if(m_search_manager==NULL)                    return(RES_S_OK);
   if(context==NULL || m_comments_manager==NULL) ReturnError(RES_E_INVALID_ARGS);
//---
   m_search_manager->Remove(HELLOWORLD_MODULE_ID,SEARCH_GENERIC,HELLOWORLD_BODY_SEARCH_ID,record_id);
   m_comments_manager->SearchRemove(context,m_search_manager,HELLOWORLD_COMMENT_TYPE_COMMENTS,record_id,HELLOWORLD_COMMENT_SEARCH_ID);
//---
   m_search_manager->Remove(HELLOWORLD_MODULE_ID,HELLOWORLD_SEARCH_FILTER,HELLOWORLD_BODY_SEARCH_ID,record_id);
//---
   return(RES_S_OK);
  }

 

2. Параметры фильтрации

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

//+------------------------------------------------------------------+
//| Параметры для фильтрации                                         |
//+------------------------------------------------------------------+
struct HelloWorldFilter
  {
   const wchar_t    *keyword;
   int               category;
  };

2.2. В классе менеджера списка языков программирования (CHelloWorldManager) объявим метод InfoFilter, с помощью которого мы будем получать проиндексированные данные для отображения на странице.

//----
TWRESULT          InfoFilter(const Context *context,HelloWorldFilter *filter,
                             HelloWorldRecord *records,size_t start,int *count,int *total);

 

3. Получение данных для фильтрации

По своему назначению этот этап аналогичен получению всех данных для отображения (метод CHelloWorldManager::InfoGet) - по запросу заполняется список записей. Однако реализация отличается - запрос выполняется не сразу в СУБД, а сначала в модуль "Поиск", который возвращает идентификаторы записей, удовлетворяющих критерию поиска.

Более подробно рассмотрим реализацию метода CHelloWorldManager::InfoFilter.

3.1. Подготовка/Привязка параметров запроса.

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

Зададим параметры поиска. Минимум - это идентификаторы модулей, данные которых нужно отфильтровать, количество модулей, тип поиска (в нашем случае - пользовательский, который мы перечислили ранее в п. 1.2.) и ключевое слово поиска.

//---
   SearchFilter  search_filter={0};
   SearchResul   results[40]  ={0};
...............................
//--- заполняем параметры фильтрации/поиска
   int module_id              =HELLOWORLD_MODULE_ID;
   search_filter.modules      =&module_id;
   search_filter.modules_count=1;
   search_filter.type_id      =HELLOWORLD_SEARCH_FILTER;
   search_filter.keyword      =(filter->keyword!=NULL ? filter->keyword : L"");

В нашем примере мы также укажем пользовательский критерий поиска - категорию языка программирования (см. п. 1.2.).

//---
   SearchFilter::UserItem  extra[1]     ={0};
   size_t                  extra_index  =0;
...............................
//---
   if(filter->category>0 && extra_index<_countof(extra))
     {
      extra[extra_index].type_id=HELLOWORLD_SEARCH_FILTER_CATEGORY;
      extra[extra_index].id     =filter->category;
      extra_index++;
     }
//---
   if(extra_index>0)
     {
      search_filter.extra      =extra;
      search_filter.extra_count=extra_index;
     }

3.2. Выполнение запроса. Методом ISearchManager::Search в модуле Поиск запрашиваем идентификаторы интересующих записей.

//---
   int      cnt      =0;
   TWRESULT res      =RES_S_OK;
...............................
//---
   cnt=_countof(results);
...............................
   if((res=m_search_manager->Search(context, SEARCH_ORDER_BY_RELEVANCE, &search_filter, results, 
                                    (start>0 ? start-1 : start), &cnt, total))==RES_E_NOT_FOUND)
     {
      *count=0;
      return(RES_S_OK);
     }
   else
     {
      if(RES_FAILED(res)) ReturnError(res);
     }

3.3. Обработка результатов запроса. По составленному ранее списку идентификаторов записей получаем эти записи из СУБД.

//---
   size_t   max_count=*count,index=0;
...............................
   for(size_t i=0;i<cnt && i<max_count; i++)
     {
      //--- на всякий случай проверим тип результата
      if(results[i].record1!=HELLOWORLD_BODY_SEARCH_ID) continue;
      //--- получим информацию о записи
      if(RES_FAILED(res=Get(context,results[i].record2,&records[index])))
        {
         ExtLogger(context,LOG_STATUS_ERROR) << "failed to get record #" << results[i].record2 << " [" << res << "]";
        }
      else
        {
         index++;
        }
     }
//---
   *count=index;

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

 

HTTP API

Теперь рассмотрим каким образом проиндексированные для фильтрации данные запрашиваются на странице просмотра. Напомним, что в нашем примере данные для просмотра на странице PageNumberTwo формируются с помощью метода WriteJson.

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

1. В нашем примере при формировании HTTP запроса будет использоваться либо ключевое слово (запрашивается строка), либо одно из возможных значений поля Category (запрашивается число). Со стороны сервера изменения минимальны - необходимо получить из запроса параметры фильтрации и вызвать метод получения отфильтрованных данных CHelloWorldManager::InfoFiler.

//+------------------------------------------------------------------+
//| Вывод списка записей в формате JSON                              |
//+------------------------------------------------------------------+
TWRESULT CPageNumberTwo::WriteRecords(const Context *context)
  {
.......................................
//---
   if(context->request->Exist(IRequest::GET,L"k") || context->request->Exist(IRequest::GET,L"category"))
     {
      HelloWorldFilter filter={0};
      //---
      if(context->request->Exist(IRequest::GET,L"k"))
         filter.keyword=context->request->GetString(IRequest::GET,L"k");
      //---
      if(context->request->GetInt32(IRequest::GET,L"category")>0)
         filter.category=context->request->GetInt32(IRequest::GET,L"category");
      //---
      if(RES_FAILED(res=m_manager->InfoFilter(context,&filter,m_info,m_start,&m_info_count,&m_info_total)))
         ReturnErrorExt(res,context,"failed get info records");
     }
.............................................

2. В противном случае будем запрашивать все данные.

.............................................
   else
     {
      //---
      if(RES_FAILED(res=m_manager->InfoCount(context,&m_info_total)))
         ReturnErrorExt(res,context,"failed get count of records");
      //---
      m_start=max(1,context->request->GetInt32(IRequest::GET,L"from")+1);
      int max_page   =max(0,int((m_info_total+_countof(m_info))/_countof(m_info))-1);
      //---
      if(m_start>max_page*_countof(m_info)+1)
         m_start=max_page*_countof(m_info)+1;
      //---
      if(RES_FAILED(res=m_manager->InfoGet(context,m_info,m_start,&m_info_count)))
         ReturnErrorExt(res,context,"failed get info records");
     }
................................................

Т.е. фактически, при HTTP запросе, который включает критерии фильтрации, в фоновом режиме происходит обращение к модулю Поиск, который выдает список проиндексированных записей, удовлетворяющих заданному критерию (см. реализацию метода CHelloWorldManager::InfoFilter).

Формат вывода в обоих случаях остается одинаковым.

 

Пользовательский интерфейс

Рассмотрим компоновку пользовательского интерфейса, использующую реализованный выше функционал.

1. В шапке страницы "Список записей" (List of Records) добавим кнопку фильтрации рядом со строкой поиска. Это позволит выполнять фильтрацию по ключевому слову - вводить название категории языка программирования в поле поиска.

Элемент управления для фильтрации по ключевому слову

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

//+----------------------------------------------+
//| Элементы управления                          |
//+----------------------------------------------+
var header = TeamWox.Control("PageHeader","#41633C")
                .Command("<lngj:MENU_HELLOWORLD_LIST>","/helloworld/index","<lngj:MENU_HELLOWORLD_LIST>")
                .Command("<lngj:MENU_HELLOWORLD_NEW>","/helloworld/number_two/edit/","<lngj:MENU_HELLOWORLD_NEW_DESCR>")
                .Help("helloworld/number2")
                .Search(65536)
                .Filter("/helloworld/number_two/");

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

Элемент управления для фильтрации по категориям (выпадающий список)

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

//---
var filter_category;
//--- Выбор категории
TeamWox.Control("Layout",{
    type: "table",
    items: [[
        //--- Название выпадающего списка
        TeamWox.Control("Label",'<lngj:HELLOWORLD_INFO_LIST_CAPTION />'),
        //--- Выпадающий список
        filter_category=TeamWox.Control("Input","combobox","category",0,{options:[[0,"All"],[1,"Compiled"],[2,"Interpreted"]]})
                           .Style({width:"180px"}),
        //--- Заполнитель оставшегося пространства
        TeamWox.Control("Label",'').Style({width:"100%"})
    ]]
}).Style({padding:"0px 0px 16px 10px"});
filter_category.Append("onchange",LoadData);

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

//---
function LoadData(startFrom,perPage) {
    var params={};
    //---
    if(startFrom==undefined) startFrom=pages.Item();
    //---
    if(startFrom>0) params.from=startFrom;
    //---
    var keyword=header.SearchControl().Input().Value();
    if(keyword!="") params.k=keyword;
    //---
    var category=filter_category.Value();
    if(category>0) params.category=category;
    //---
    TeamWox.Ajax.get('/helloworld/number_two/',params,
    {
        onready: function(text) {
            Update(TeamWox.Ajax.json(text));
        },
        onerror: function(status) {
            alert(status);
        }
    });
}

Подробности реализации других служебных методов работы с данными см. в шаблоне страницы helloworld\templates\number2.tpl.

 

Демонстрация работы

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

Для демонстрации реализованного функционала на странице "Список записей" необходимо наполнить ее данными. Мы добавили несколько языков программирования различных категорий.

Таблица со списком всех записей

1. Сначала продемонстрируем фильтрацию по ключевому слову:

Фильтрация по ключевому слову: компилируемый язык

Фильтрация по ключевому слову: интерпретируемый язык

2. Теперь отфильтруем записи по категориям из выпадающего списка.

Сначала компилируемые языки:

Фильтрация по категориям: компилируемые языки

затем интерпретируемые:

Фильтрация по категориям: интерпретируемые языки

3. Оба вида реализованной фильтрации могут работать совместно, позволяя сужать критерии до нужного уровня.

Фильтрация одновременно и по ключевому слову, и по категориям

 

Заключение

Для этой и последующих статей учебный модуль Hello World был переработан и адаптирован под текущие возможности сервера TeamWox. Теперь все страницы содержат пояснения к демонстрируемым возможностям. Все тексты вынесены из шаблонов страниц в языковой файл модуля helloworld.lng и доступны для перевода на любой поддерживаемый в TeamWox язык.

В следующих статьях планируется осветить возможности импорта и экспорта данных в пользовательские модули.


helloworld-search-part2-ru.zip (260.32 KB)

2011.12.29