Сообщений 2    Оценка 30 [+0/-1]         Оценить  
Система Orphus

Добавление полнотекстового поиска в Windows-приложения

Автор: Вадим Станкевич
БГУ

Источник: RSDN Magazine #3-2009
Опубликовано: 23.02.2010
Исправлено: 10.12.2016
Версия текста: 1.0
Муки выбора
МЛ Следопыт SDK
Solarix SDK
SearchInform SDK
Борьба с SearchInform SDK, раунд первый: поиск
Борьба с SearchInform SDK, раунд второй: поисковые индексы
Резюме

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

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

Муки выбора

Первый вопрос, который встал перед нами во весь свой немалый рост, заключался в следующем: писать поиск самостоятельно или же приобрести какой-то готовый поисковый движок и просто «прикрутить» его к нашей разработке? С одной стороны, если всё писать самостоятельно, можно быть на все 100% уверенным в соответствии получившегося поискового движка нашим потребностям. Но реализовать всё, что нужно, добиться нужной производительности, отладить – на всё это нужно немало времени. В общем, извечная дилемма, возникающая при создании любой более-менее серьёзной функциональности продукта. В итоге после нескольких «мозговых штурмов» было решено посмотреть, какие поисковые решения (по возможности российские – требование заказчика, опасающегося проблем с русскоязычным текстом) существуют в природе. А затем уже, если не найдётся ничего подходящего, можно было подумать над написанием поиска своими силами.

После почти целого дня гугления и обсуждения того, что было в итоге найдено, мы остановились на трёх вариантах: МЛ Следопыт SDK, Solarix SDK и SearchInform SDK. Выяснилось, что каждый из этих SDK имеет свои особенности, свои плюсы и минусы, и выбирать нужно было именно исходя из них.

МЛ Следопыт SDK

Плюсы

+ Базируется на технологии COM (важно для нашего многоязычного проекта, где движок написан на C++, а UI – на Delphi и C#)

+ Поддержка запросов на естественном языке, поиск с учётом морфологии – строгий или нечёткий поиск, поддержка формальных запросов

+ Достаточно удобная и безопасная модель транзакций и сессий

Минусы

- Малое число индексируемых форматов: обработка многих форматов документов ложится на плечи самого разработчика

- Синтаксис формальных запросов мало похож на синтаксисе Google и других поисковиков

- Невысокая скорость индексации документов

Solarix SDK

Плюсы

+ Открытые исходные тексты поискового движка (C++)

+ Поддержка нечётких запросов и регулярных выражений, поиск с учётом морфологии

+ Большое количество поддерживаемых при индексации форматов файлов, возможность реализации поддержки других форматов своими силами

+ Возможность поиска внутри архивов

+ Плагины для взаимодействия с Google Desktop, оптического распознавания текста и т.п.

Минусы

- Ориентация на использование XML при обмене данными (лишний трафик + время на парсинг XML – в нашей разработке преимущества XML практически не использовались)

- Опять-таки, скорость

SearchInform SDK

Плюсы:

+ Базируется на технологии COM

+ Поддерживает большое количество форматов, есть поддержка индексации баз данных (через ADO и ODBC)

+ Высокая скорость индексации документов

+ Поиск с учётом морфологии, семантический поиск похожих документов

+ Цена вопроса

Минусы:

- Практически полностью отсутствующая справка по SDK (что, впрочем, искупается готовностью SoftInform’а оперативно отвечать даже на самые идиотские вопросы со стороны того, кто их SDK использует)

- Формальные запросы практически полностью ложатся на плечи клиента (хотя сложно сказать, плюс это или минус)

В общем и целом, как видите, все SDK более-менее схожи по своим возможностям, и каждый имеет несколько существенных плюсов и несколько не менее существенных минусов. После общения с компаниями, их разработавшими, обсуждения политики лицензирования и его стоимости, а также неоднократного обсуждения всего изложенного выше плюс более мелких достоинств и недостатков, мы, наконец, остановили свой выбор на SearchInform SDK. Не скрою, одним из решающих факторов стала скорость, вторым – цена вопроса. Как показал опыт дальнейшей состыковки поискового движка с нашей системой, выбор решения на основе COM оказался очень даже удачной идеей. Не менее удачной идеей была покупка готового поискового движка – создание собственного решения такого уровня обошлось бы нам и заказчику в разы дороже, и потребовало бы просто астрономического по сравнению со всем остальным проектом количества человеко-часов.

Борьба с SearchInform SDK, раунд первый: поиск

Поскольку документация по SearchInform SDK практически полностью исчерпывалась ёмкими фразами вроде «The Count property specify count of elements in the array» (а сколько вы заметили грамматических ошибок в этом предложении?), основным способом изучения была разведка боем. Поэтому рассказать о практике использования этого SDK, думаю, будет совсем не лишним. Примеры кода по работе с SDK я буду давать на Delphi, так как именно он чаще всего использовался в нашем проекте.

Во-первых, сразу после установки SDK (кстати, на сайте компании-разработчика http://www.searchinform.ru можно обнаружить его trial-версию) имеет смысл заново сгенерировать модули из поставляемых в комплекте SDK библиотек типов, поскольку те, которые лежат в папке с самим SDK, видимо, просто давно не обновлялись теми, кто готовил дистрибутив. Впрочем, это мелочь, ведь сделать новые модули – дело пары минут.

Основной интерфейс, который обеспечивает доступ ко всему SearchInform API, называется ISearchInformAPI. Давайте рассмотрим применение основных интерфейсов SDK на примере кода, который позволит нам подключиться к серверу и посмотреть, какие нам доступны индексы и базы синонимов.

      procedure ConnectToSIServer;
var
  SiAPI: ISearchInformAPI;
  SiSrv: ISIServer2;
  SiReqGen: ISISearchRequestGenerator3;
  SiIndex: ISIIndex4;
  SiSynDb: ISISynonymDatabases;
  SiDBs: ISIStrings;
  Msg1, Msg2: string;
  i: integer;
begin
  // Создаём экземпляр коннектора для SearchInform API
  SiAPI := CoSearchInformAPI.Create;
  // Открываем удалённый сервер - естественно, вместо localhost'а может быть 
  // remote-адрес. Локальный сервер можно также открыть методом OpenLocalServer
  SiSrv := API.OpenRemoteServer('localhost') as ISIServer2;
  // Создаём генератор запросов
  SiReqGen := API.SearchRequestGenerator as ISISearchRequestGenerator3;
  // Получаем список доступных индексов, чтобы потом показать его в сообщении
  Msg := '';

  for i:= 0 to SiSrv.Indexes.Count - 1dobegin
    Msg1 := Msg1 + SiSrv.Indexes.IndexName[i] + #13#10;
  end;

  // Загружаем первый попавшийся индекс
  Randomize;
  SiIndex := SiSrv.OpenIndex(SiSrv.Indexes.IndexName[Random(SiSrv.Indexes.Count)]) as ISIIndex4;

  // Получаем список доступных баз синонимов (тоже потом его показываем)
  SiSynDb := srv.SynonymDatabases;
  Msg2 := '';
  SiDBs := SiSynDb.GetDatabaseList;

  for i := 0 to SiDBs.Count - 1dobegin
    Msg2 := Msg2 + SiDBs.Items[I] + #13#10;
  end;

  ShowMessage('Индексы:'#13#10 + Msg1 + 'Базы синонимов:'#13#10 + Msg2);
end;

Как видите, процесс подключения к серверу не слишком сложен. В реальных приложениях, конечно, лучше это всё обернуть блоком try…except; переменные, отвечающие за взаимодействие с сервером лучше сделать глобальными, а ещё лучше написать для взаимодействия с сервером отдельный класс. Но сейчас это всё нюансы, а потому давайте дальше знакомиться с тем, как управлять SearchInform SDK.

После того, как клиент подключен к серверу, можно начинать поиск. Следующий фрагмент кода демонстрирует, как именно искать с помощью сервера SearchInform. Здесь будет рассмотрен только простейший вариант формального запроса, не содержащего сведений о дополнительных ограничениях по поиску (какие слова исключать, какие должны обязательно встречаться в словосочетании и т.д.). Часть переменных, ответственных за взаимодействие с сервером, в приведенном ниже листинге не объявляется заново, и если вы будете копировать и запускать данный код, вам нужно будет перенести их из первого листинга.

      procedure PerformSearchRequest(Query: string);
var
  SiTextRequest: ISITextSearchRequestEx;
  SiDocLst: ISIDocumentsList;
  Msg: string;
  i: Integer;
begin// Создаём текстовый запрос к серверу
  SiTextRequest := SiReqGen.CreateTextSearchRequestEx;
  SiTextRequest.Query := Query;
  // Точное соответствие запросу
  SiTextRequest.WordOption := siwoExact;
  // Подключаем случайную базу синонимов
  Randomize;
  SiTextRequest.SynonymDatabase := SiSynDb.GetDatabase(
   SiDBs.GetDatabaseList.Items[
    Random(SiDBs.GetDatabaseList.Items.Count)], true);
  // Собственно поиск
  SiDocLst := SiIndex.ComplexSearch(SiTextRequest, nil);

  // Показываем результаты поискаif SiDocLst.Count > 0 thenbegin
    Msg := 'Найдено документов: ' + IntToStr(SiDocLst.Count) + #13#10;

    for i := 0 to SiDocLst.Count -1 dobegin
      Msg := Msg +'Документ ' + IntToStr(i) + ': ' 
        + SiDocLst.DocumentID[i].FormatID(sidfFullFormat) + #13#10;
    end;
  endelsebegin
    Msg := 'Ничего не найдено.';
  end;
end;

К сожалению, если при работе с SearchInform SDK вам потребуется реализовать формальные запросы хотя бы в виде исключения результатов с определёнными словами или с обязательным нахождением пары слов, встречающихся в тексте именно рядом друг с другом, весь процесс работы с такими запросами от начала и до победного конца на сервер переложить не удастся. Конечно, механизмы для подобного поиска в SearchInform SDK есть, но первоначально запрос нужно обрабатывать на стороне клиента. Впрочем, здесь есть и плюс: можно гибко менять синтаксис формальных запросов, делая его более простым для пользователя. В следующем листинге демонстрируется применение отрицания в формальном запросе. Для простоты восприятия основные моменты, показанные в предыдущем листинге, опущены.

      i := Pos('[NOT]', Query);
      // Проверяем наличие необходимой лексемы в запросеif i > 0 thenbegin// Отбрасываем [NOT] и формируем запрос по оставшемуся тексту
        s := Copy(Query, i + 5, MaxInt);
        SiUnaryRequest := SiReqGen.CreateComplementSearchRequest;
        // Искомый текст будет содержаться в под-запросе
        SiTextRequest := SiReqGen.CreateTextSearchRequestEx;
        SiTextRequest.Query := s;
        SiTextRequest.WordOption := siwoExact;
        SiUnaryRequest.SubRequest := SiTextRequest;
      end;

В последнем листинге переменная SiUnaryRequest имеет тип ISIUnarySearchRequest. Однако если нам потребуется реализовать формальный запрос с двумя подзапросами (т.е., скажем, для поиска текста, в котором обязательно должны присутствовать обе фразы из разных подзапросов) этот интерфейс уже не подходит, так как в нём есть возможность работы только с одним подзапросом. Здесь нам потребуется использовать ISIBinarySearchRequest, а пример его использования вы можете увидеть ниже (опять-таки, для простоты восприятия уже продемонстрированные моменты опущены). Переменная SiBinaryRequest должна иметь тип ISIBinarySearchRequest.

  i := Pos('[AND]', Query);
  // Снова проверяем наличие необходимой лексемыif i > 0 thenbegin// Разделяем наш запрос на два подзапроса
    SubQuery1 := Copy(Query, 1, i - 1);
    SubQuery2 := Copy(Query, i + 5, MaxInt);

    SiBinaryRequest := SiReqGen.CreateIntersectionSearchRequest;

    SiTextRequest := SiReqGen.CreateTextSearchRequestEx;
    SiTextRequest.Query := SubQuery1;
    SiTextRequest.WordOption := siwoExact;

    SiBinaryRequest.SubRequestA := SiTextRequest;

    SiTextRequest := SiReqGen.CreateTextSearchRequestEx;
    SiTextRequest.Query := SubQuery2;
    SiTextRequest.WordOption := siwoExact;

    SiBinaryRequest.SubRequestB := SiTextRequest;
  end;

Ещё раз обращу ваше внимание на то, что разделение на подзапросы на стороне клиента упрощает жизнь, если вам нужно изменить синтаксис формального поискового запроса (скажем, вместо [NOT] написать {не}), хотя это и требует немного большего количества кода, чем в случае, когда обработка такого рода запросов целиком ложится на плечи сервера. Чем считать данную особенность SearchInform SDK – минусом или плюсом, остается на ваше усмотрение.

Борьба с SearchInform SDK, раунд второй: поисковые индексы

Не менее важным аспектом, чем сам поиск, является работа с поисковыми индексами. Поэтому мы с вами перейдём ко второй части функций SearchInform SDK, которая отвечает за работу с поисковыми индексами. Здесь главным интерфейсом, с которым наиболее часто будет иметь дело разработчик, является ISIIndex.

Новый индекс можно создавать после подключения к локальному или удалённому серверу (см. первый в статье листинг). Это делается с помощью вызова метода CreateIndex экземпляра класса, реализующего интерфейс ISILocalServer. У этого метода один параметр – имя файла, в котором будет храниться индекс. В приведённом ниже листинге можно увидеть, как происходит создание индекса.

      procedure CreateIndex;
var
  SiIndex: ISIIndex;
  SiLocalServer: ISILocalServer;
begin// Здесь должен быть код подключения к серверу// ...// Создаём индекс SearchInformIndexExamlpe.siidx в той же папке, 
  // где находится само приложение
  SiIndex := SiLocalServer.CreateIndex(
   IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))
   + 'SearchInformIndexExamlpe.siidx');

  // Проверяем, создалось что-нибудь, или нетif SiIndex = nilthenbegin
    ShowMessage('Индекс создать не удалось.');
  endelsebegin// Если всё-таки что-нибудь создалось, осуществляем начальную настройку. Handle – дескриптор приложения, нужен для уникальности конфигурации каждого клиента
    SiIndex.ClearIndexData;
    SiIndex.Configurate(Handle, 0);
  end;
end;

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

      procedure OpenRemoteIndex;
var
  SiIndex: ISIIndex;
  SiServer: ISIServer;
begin// Здесь должен быть код открытия удалённого сервера// ...// Проверяем, есть ли индексы на сервереif SiServer.Indexes.Count = 0 thenbegin
    ShowMessage('Данный сервер не содержит индексов.');
  endelsebegin// Выбираем случайный индекс и загружаем его
    Randomize;
    SiIndex := AServer.OpenIndex(SiServer.Indexes.IndexName[
      Random(SiServer.Indexes.Count)]);

    if SiIndex = nilthen
      ShowMessage('Индекс открыть не удалось.');
  end;
end;

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

      procedure OpenRemoteIndex;
var
  SiProcess: ISIIndexProcess;
begin// Создаём процесс
  SiProcess := SiIndex.CreateUpdateProcess;
  // Если удалось создать процесс, то мы его запускаемif SiProcess <> nilthen
    SiProcess.Start;
end;

Резюме

Что ж, как видите, добавить полнотекстовый поиск в разрабатываемое Windows-приложение – не такая трудная задача, как может показаться сначала. Конечно, если вы решите не прибегать к каким-то сторонним разработкам, а создать свой поисковый движок, возможно, вы приобретёте бесценный опыт, но зато точно проиграете во времени, так что пользоваться продуктами наподобие SearchInform SDK представляется мне весьма удачной мыслью. Благодарю всех за внимание и надеюсь, что этот рассказ о нашем опыте выбора поискового SDK и об основных моментах его использования оказался познавательным и интересным.


Эта статья опубликована в журнале RSDN Magazine #3-2009. Информацию о журнале можно найти здесь
    Сообщений 2    Оценка 30 [+0/-1]         Оценить