— ...Для практических целей теорию поисков надо модифицировать. Больше того, надо коренным образом изменить главную посылку и вернуться к первоначальной концепции Потерянного и Найденного.

Р. Шекли. «Обмен разумов»

Предлагаемая статья адресована администраторам и разработчикам баз данных, чьи задачи связаны с управлением и программированием доступа к разнородным источникам информации, в том числе нереляционной природы. В качестве примера источника слабоструктурированных данных мною выбран Microsoft Index Server (начиная с Windows 2000, Microsoft Indexing Service).

Алексей Шуленин системный инженер отдела бизнес-приложений российского представительства Microsoft. Имеет сертификаты MCSE, MCDA, MSS. С ним можно связаться по адресу: rusdev@microsoft.com.
Впервые службы контекстной индексации появились в составе Windows NT в августе 1996 года как этап развития объектной файловой системы. Учитывая важность полнотекстового поиска в системах управления знаниями, в частности при построении интеллектуальных порталов, Microsoft начиная с октября 1997 года включила Index Server в комплект поставки вместе с Internet Information Server (IIS). Index Server версий 1.0 и 1.1 предназначался для IIS 3.0, впервые обеспечившего поддержку Active Server Pages (ASP) — технологии сценариев на стороне сервера. Index Server 2.0 был встроен в IIS 4.0 и, в основном, распространялся в пакете приложений NT Option Pack 4.0, к которому помимо Web-сервера IIS 4.0 относятся монитор транзакций — брокер объектных запросов Microsoft Transaction Server (MTS) 2.0 и сервер очередей сообщений Microsoft Message Queue Server (MSMQ) 1.0.

Можно было бы сказать, что Windows 2000 будет поставляться вместе с Index Server 3.0 (во всяком случае, MajorVersion в реестре устанавливается в 3), но, как известно, все перечисленные серверы приложений вошли в нее по умолчанию на правах служб. В дальнейшем под термином «Index Server» мы будем также подразумевать и Indexing Service в составе Windows 2000.

В качестве объектов поиска Index Server выступают отдельные слова, словоформы и фразы в содержании документа, а также его свойства (автор, название, тема и др.). Области поиска могут располагаться как на локальном компьютере, так и в сети, в том числе на машинах без установленной службы поиска. Index Server работает на Windows NT Server 4.0 как компонент Internet Information Server либо на Windows NT Workstation 4.0 c Peer Web Services. В документации не содержится каких-либо ограничений на количество или объем индексируемых файлов. Во всяком случае решения на его основе, оперирующие миллионами документов общим объемом несколько десятков гигабайт, не являются чем-то исключительным. Если по соображениям масштабируемости систему предстоит развертывать не на одном, а на нескольких Web-серверах, следует рассмотреть поисковые возможности Microsoft Site Server.

Существенным этапом в истории Index Server стал выход в декабре 1998 г. Microsoft SQL Server 7.0. Во-первых, в этой версии появились собственные механизмы полнотекстового поиска по строковым и текстовым полям, во многом основанные на принципах работы Index Server. Во-вторых, OLE DB-провайдер для Index Server (MSIDXS, известный также под названием Monarch) распространил возможности универсального доступа к данным на область полнотекстового поиска. Ранее приложения, работающие с документами, и приложения, работающие с данными, представляли собой два довольно различных класса задач. С выходом OLE DB-провайдера для Index Server появилась возможность на практике объединить документы и данные, используя стандартные технологии доступа и инструменты разработки независимо от типа ресурса.

Index Server глазами DBA

Полнотекстовый каталог можно смело уподобить базе данных, каталоги — таблицам, файлы — записям. Полнотекстовый каталог является высшим уровнем иерархии объектов внутри Index Server. Каталог состоит из подкаталогов. В качестве каталога может выступать виртуальный или физический каталог: локальная папка файловой системы либо сетевой ресурс, такой, как папка общего доступа (shared folder). Сразу после установки Index Server в нем будет содержаться системный каталог System и, если на данной машине задействована служба WWW, каталог Web. К последнему относятся все виртуальные каталоги IIS, в свойствах которых отмечен флажок Index this resource. В случае Windows 2000 каталог System находится в системном разделе диска. Каталог Web по умолчанию находится в Inetpub. Вновь создаваемым каталогам администратор сам назначает место на диске. В файловой системе каталог выглядит как папка Catalog.wci, которая хранит всю индексную информацию о подкаталогах, относящихся к данному каталогу, и содержании находящихся в них файлов. Соответствующая информация хранится в разделе реестра HKEY_LOCAL_MACHINESYSTEM CurrentControlSetControlContentIndex. Каталоги перечислены в подразделе Catalogs, подкаталоги для каждого каталога — в подразделе Scopes. Эта информация полезна тем, что некоторыми настройками работы Index Server можно управлять только через реестр. Но в большинстве случаев этого практически не требуется.

Признак каталога представляет собой двухбитную величину. Младший бит равен 0, если каталог не индексируется (Include in index = No в его свойствах при просмотре с консоли Index Server), и 1 — в противном случае. Старший бит равен 0, если это виртуальный каталог, и 1, если физический. Например, параметр 3 соответствует виртуальному каталогу, участвующему в индексе.

Таким образом, в случае Index Server индексы создаются не на уровне «таблиц», а на уровне «базы данных», причем наша полнотекстовая «база данных» может иметь всего один индекс. В свойствах каталога (закладка General) видны текущие размеры индекса и кэша свойств. Конкретные «таблицы» указываются впоследствии в тексте запроса. В отличие от обычных индексов, присущих СУБД, операции модификации «записей» (создание, удаление и изменение файла в индексированном каталоге) не вызывают немедленного изменения индекса, так как, учитывая специфику его структуры и возможные размеры, это сильно замедляло бы работу остальных файловых служб. Существует два способа обновления индекса: сканирование и обновление на основе уведомлений. Первый способ состоит в том, что Index Server обходит все подкаталоги данного каталога, помеченные к индексированию, и выбирает из них файлы, удовлетворяющие подключенным фильтрам. Для каждого подкаталога операция сканирования может быть вызвана явно из административного интерфейса (контекстное меню каталога -> All Tasks -> Rescan ...). В случае полного сканирования (full rescan) все содержащиеся в нем документы переиндексируются. Эта операция выполняется автоматически для нового подкаталога, добавленного в каталог. Очевидно, что при большом количестве документов процесс полного сканирования может занять довольно много времени, поэтому к нему стоит прибегать только при крайней необходимости (например, при восстановлении после сбоя). Дифференциальное сканирование (incremental rescan) отличается от полного тем, что выбираются документы, изменившиеся с момента последней индексации. Дифференциальное сканирование будет проведено, например, если на какое-то время остановить службу контекстного индексирования CiSvc.exe, а затем вновь запустить ее. Для томов формата NTFS 5.0 Index Server определяет модифицированные файлы на основе Update Sequence Number (USN) из журнала изменений файловой системы, поэтому затраты на сканирование в этом случае минимальны. В процессе нормальной работы службы преимущественно используется второй способ обновления индекса. При этом Index Server полагается на уведомления операционной системы, направляемые ему всякий раз при модификации файла, находящегося в его зоне ответственности. Очевидно, что этот способ требует наименьших временных затрат, но, к сожалению, он работает, только если каталог находится под управлением NT. Иначе (для Windows 9x, Novell NetWare, Unix) сканирования не избежать. Если от каталогов уведомлений не приходит, периодичностью обхода можно управлять с помощью параметра ForcedNetPathScanInterval (по умолчанию, 2 часа) вышеуказанного раздела реестра. Если же обновления документов производятся слишком часто, буфер уведомлений переполняется, и новые уведомления попросту теряются. В этом случае при следующем обходе Index Server обнаружит изменившиеся документы, не отраженные в индексе, и проиндексирует их. Т. е. вместо механизма уведомлений здесь будет работать дифференциальное сканирование, соответственно, обновление индекса произойдет с некоторым запозданием.

Документы, помеченные к (пере)индексированию, выбираются из очереди и подвергаются воздействию фильтра. Фильтром в терминологии Index Server называется динамическая библиотека, которая извлекает из документа его содержимое (в виде потока текста) и свойства (автор, размер, дата создания и т. д.). Нужный фильтр определяется по расширению документа. Например, для расширения .doc параметр HKEY_CLASSES_ ROOT.docPersistentHandler имеет параметр {98de59a0-d175-11cd-a7bd-00006b827d94}. HKEY_CLASSES_ ROOTCLSID{98de59a0-d175-11cd-a7bd-00006b827d94}PersistentAddinsRegistered{89BCB740-6119-101A-BCB7-00DD010655AF} равно {f07f3920-7b8c-11cf-9be8-00aa004b9986}, откуда из HKEY_ CLASSES_ROOTCLSID {f07f3920-7b8c-11cf-9be8-00aa004b9986}InprocServer32 получаем название библиотеки фильтра — C:WINNTSystem32offfilt.dll. По умолчанию, Index Server содержит фильтры для документов формата HTML (nlhtml.dll), Microsoft Office (offilt.dll) и MIME (mimefilt.dll), а также для бинарных и простых текстовых файлов (непосредственно в query.dll). Фильтры можно рассматривать как подключаемые модули, и никто не мешает написать фильтр для нужного типа файлов. Например, если у нас есть книготорговый сайт, где каждый документ — аннотация книги, то в свойства можно включить ее стоимость. Для этого необходимо реализовать интерфейс IFilter, описанный в http://msdn.microsoft.com/isapi/ msdnlib.idc?theURL=/library/psdk/ indexsrv/ixufilt_8msl.htm. Параметр DLLsToRegister упомянутого выше раздела ContentIndex содержит список библиотек, автоматически регистрируемых при старте службы индексации, в том числе фильтры. На самом деле фильтры выполняются в контексте дочернего процесса CiDaemon, который порождается CiSvc. Это сделано для того, чтобы неправильно написанный фильтр не остановил основной процесс Index Server. Если фильтр не задействуется дольше значения, заданного параметром FilterIdleTimeout, то для экономии памяти он выгружается. Другая ситуация — когда самодельный фильтр работает, но «спотыкается» или, наоборот, зацикливается на каком-нибудь документе. Параметры FilterRetries и FilterRetryInterval регулируют количество попыток проиндексировать документ и интервалы между ними. Параметр MaxFilesizeMultiplier = n ограничивает размер текстового потока на выходе фильтра: если он превышает размер файла в n раз, файл считается поврежденным и отбрасывается. Кстати, для файлов, чей размер больше MaxFilesizeFiltered, фильтр возвращает только свойства, но не содержимое.

Возвращаемый IFilter::GetText поток текста подается на вход делителя (Word Breaker), который разбивает его на слова. Здесь в действие вступают библиотеки локализации. Они разбирают грамматические конструкции, присущие языку. Например, для США и западноевропейских языков — это infosoft.dll, для японского языка — msir2jp.dll и т. д. Сведения об установленной языковой поддержке можно найти в подразделе Language раздела ContentIndex. Так, ...Language <язык>WBreakerClass содержит CLSID словоделителя, а в HKEY_ CLASSES_ROOTCLSID{<данный CLSID>}InprocServer32 — имя соответствующего файла. По умолчанию, для определения языка документа, Index Server использует системную информацию о региональных установках или значения, указанные в метатеге MS.Locale (для HTML-файлов). Написание собственного модуля выделения слов — задача более нетривиальная, чем добавление фильтра. Тем не менее уже сегодня несколько отечественных производителей программного обеспечения предлагают свои расширения для полнотекстового поиска в Index Server, SQL Server и Site Server с учетом специфики русского, украинского и других языков стран Содружества. После словоделения происходит «подавление помех»: из текста выбрасываются незначащие слова (noise words), такие, как предлоги, артикли, междометия и т. д. Список незначащих слов для каждого языка хранится в файле, указанном в ...Language<язык>NoiseFile. Например, для английского языка файл называется noise.eng, для немецкого — noise.deu и т. п. По умолчанию, они лежат в system32. Это обычные текстовые файлы, которые можно редактировать в зависимости от специфики конкретного приложения. К зависящим от языка компонентам относится также дифференциатор — модуль варьирования лексической формы слова (stemming), генерирующий производные слова от исходной основы. Например, от drink порождаются drinking, drank, drunk и т. д. Он напрямую не относится к процессу построения индекса, но задействуется при обработке запроса. Полнотекстовый запрос к Index Server точно так же подвергается разбиению на слова и отбраковке незначащих слов; кроме того, те слова, по которым не требуется точного текстового совпадения, проходят через дифференциатор для генерации списка словоформ. CLSID дифференциатора указан в ...ContentIndexLanguage<язык>StemmerClass.

После выделения значащих слов процесс построения индекса сохраняет их в памяти в виде списков слов (word lists). Списки слов можно рассматривать как временные оперативные индексы по кэшированным данным. Очевидно, что в зависимости от объема памяти они возможны только для ограниченного числа документов. Максимальное количество списков в памяти контролируется параметром MaxWordLists раздела реестра ...ContentIndex (по умолчанию, 20). Списки начинают создаваться, если объем свободной памяти превышает MinWordlistMemory (по умолчанию 5 Мбайт). Максимальный объем памяти, которая может отводиться под список, задается в MaxWordlistSize (измеряется в порциях по 128 Кбайт, по умолчанию 2,5 Мбайт). Как водится, чем больше размер кэша, тем быстрее (в общем случае) выполняются запросы. Если реально списки начинают занимать больше памяти, чем MinSizeMergeWordlists (по умолчанию 1 Мбайт) или их количество превышает MaxWordLists, они выгружаются на диск в так называемые «теневые индексы» (shadow indexes). Несмотря на то, что теневые индексы лежат на диске, они не сохраняются после перезапуска Index Server. Когда теневых индексов становится больше, чем MaxIndexes (по умолчанию 25), некоторые из них объединяются друг с другом. Главный индекс (master index) также хранится на диске, но он имеет гораздо более эффективную структуру хранения по сравнению с теневыми индексами как с точки зрения сжатия информации, так и по скорости ее выборки при обработке запросов. Обычно теневые индексы добавляются в главный индекс по расписанию. MasterMergeTime указывает количество минут после полуночи, когда должен инициироваться процесс построения главного индекса. Кроме того, он может вызываться, если совокупный размер теневых индексов превышает MaxShadowIndexSize, если свободного места на диске, где хранится каталог, остается меньше MinDiskFreeForceMerge и если количество файлов, относящихся к каталогу, изменившихся с момента последнего обновления главного индекса, превысило MaxFreshCount. Наконец, его можно вызвать принудительно, выбрав All Tasks -> Merge из контекстного меню каталога. Процесс построения главного индекса требует довольно больших затрат времени и ресурсов, поэтому он устроен так, что если Index Server в какой-то момент будет остановлен, то в следующий раз процесс возобновится не сначала, а с того места, на котором был прерван. Итак, содержание документов хранится в списках слов, теневых индексах и главном индексе. Свойства документов сразу после этапа фильтрации попадают в кэш свойств. Это дисковая структура, в которой Index Server также сохраняет некоторую служебную информацию и те атрибуты документов, которые он может установить, не применяя фильтра, например путь, размер и т. п. Для ускорения выборки кэш свойств может 64-килобайтными страницами загружаться в память. Максимальное число страниц в памяти контролируется PropertyStoreMappedCache. Кэш свойств, списки слов, теневые индексы и главный индекс составляют содержимое полнотекстового каталога.

Index Server API

Традиционным способом подготовки статических запросов к службе полнотекстового поиска является использование .ida/.idq-файлов. Idq-файл определяет полнотекстовый запрос. Он состоит из секций, в которых указываются имена полей и свойств, а также сам запрос. Этот файл затем может вызываться из обычной HTML-страницы или ASP-сценария:

. Для задания параметров выполнения запроса и форматирования результатов используются файлы .htx (html extension). Аналогично, с помощью ida-файлов осуществляется администрирование Index Server.

В Windows 2000 реализовано более гибкое решение, не требующее наличия Web-сервера. Оно основано на использовании Indexing Service API. Администрирование Index Server может быть выполнено с помощью библиотеки Admin Helper (ciodm.dll), а работа с запросами — с помощью Query Helper (ixsso.dll). Поскольку указанные объекты обладают IDispatch-интерфейсами, в качестве инструмента программирования можно выбрать любое средство разработки, поддерживающее технологию ActiveX. Ниже в качестве примеров приводятся простейшие сценарии на Visual Basic, использующие эти библиотеки. В Листинге 1 иллюстрируется работа с ciodm.dll: выполняются простейшие задачи администрирования Index Server, такие, как остановка/запуск поисковой службы, удаление/создание полнотекстового каталога и разделение его на области поиска (scopes), создание главного индекса, а также вывод всех определенных на сервере каталогов с некоторыми их свойствами. Обратите внимание, что имя класса IxAdm не совпадает с его ProgID.

ЛИСТИНГ 1.

Dim IxAdm As CIODMLib.AdminIndexServer
Set IxAdm = CreateObject(?Microsoft.ISAdm?)
IxAdm.MachineName = ?alexeysh-desk?

Dim IxCat As CIODMLib.CatAdm
If IxAdm.IsRunning Then IxAdm.Stop

On Error Resume Next
Set IxCat = IxAdm.GetCatalogByName
(?MyCatalog?)
On Error GoTo 0

If Err.Number &kt;> 0 Then ?Каталог 
существует
	IxAdm.RemoveCatalog 
bstrCatName:=?MyCatalog?, 
fDelDirectory:=True
End If

Set IxCat = IxAdm.AddCatalog
(bstrCatName:=?MyCatalog?, 
bstrcatlocation:=?d:Temp?)
IxCat.AddScope bstrScopeName:=?d:FTS?, 
fExclude:=False
IxCat.AddScope bstrScopeName:=?d:FTS
Test?, fExclude:=True

IxAdm.Start
While Not IxAdm.IsRunning
Wend
IxCat.ForceMasterMerge

Dim lFound As Boolean
lFound = IxAdm.FindFirstCatalog()
Do While True
	If Not lFound Then Exit Do
	Set IxCat = IxAdm.GetCatalog()
	If IxCat.IsCatalogStopped Then 
IxCat.StartCatalog
	Debug.Print IxCat.CatalogName, 
IxCat.CatalogLocation, _
	IxCat.FilteredDocumentCount, 
IxCat.IndexSize, IxCat.WordListCount
	lFound = IxAdm.FindNextCatalog()
Loop

Для полнотекстовых запросов используется два языка. Базовый внутренний язык Index Server — поддерживается для всех версий, начиная с 1.0 (Dialect 1). В версии 3.0 он был расширен дополнительными возможностями (Dialect 2). Подробное описание этого языка и примеры можно найти по адресу: http://msdn.microsoft.com/library/psdk/ indexsrv/ixqlang_6i79.htm. В Листинге 2 показано, как составить и обработать запрос к Index Server с использованием библиотеки ixsso.dll.

ЛИСТИНГ 2.

Dim IxQuery As Cisso.CissoQuery
Set IxQuery = CreateObject(«IXSSO.Query.2»)

IxQuery.Catalog = «MyCatalog»
IxQuery.Columns = «FileName, 
Path, VPath, DocAuthor, DocTitle, 
Size, DocLastSavedTm»
IxQuery.Dialect = «2»
IxQuery.Query = «{prop name=Contents} 
SQL Server {/prop}»
IxQuery.SortBy = «Rank [d], Size [a]»

Dim IxUtil As Cisso.CissoUtil
Set IxUtil = CreateObject(«IXSSO.Util.2»)
IxUtil.AddScopeToQuery pdisp:=IxQuery, 
pwszScope:=»d:FTS», pwszDepth:=»Shallow»

Dim adoRS As ADODB.Recordset
Set adoRS = IxQuery.CreateRecordset
(pwszSequential:=»sequential»)
Dim adoFld As ADODB.Field, s As String
While Not adoRS.EOF
 s = «»
 For Each adoFld In adoRS.Fields
 s = s & adoFld.Value & « «
 Next
 Debug.Print s
 adoRS.MoveNext
Wend
По умолчанию, в качестве области поиска выступает весь каталог, указанный в IxQuery. Catalog. Ее можно изменить при помощи объекта Util. Первый после создания объекта Query вызов метода AddScopeToQuery заменяет область по умолчанию на pwszScope. Все последующие добавляют к ней дополнительные области поиска. Этот метод позволяет также изменять глубину поиска в области (параметр pwszDepth): Shallow — только в каталоги, указанные в pwszScope, Deep — плюс во всех подкаталогах (параметр по умолчанию).

Результаты запроса возвращаются в виде обычного курсора, представленного объектом Recordset библиотеки Microsoft ActiveX Data Objects. Тип курсора зависит от параметра метода CreateRecordset: sequential означает forward only-курсор, nonsequential — скроллируемый.

Index Server как OLE DB-ресурс

API Query Helper поддерживает только внутренний язык запросов Index Server. В гетерогенных приложениях, работающих одновременно с различными источниками информации, такими, как базы данных, файловая система, службы каталогов, электронная почта и т. д., использование всякий раз частного API в зависимости от того ресурса, к которому приходится обращаться, приводит к существенному повышению стоимости процесса разработки. При появлении нового источника данных доступ к нему приходится программировать заново. Возникает резонный вопрос: коль скоро для приема результатов Query Helper в предыдущем примере мы все равно использовали ADO, нельзя ли уменьшить число сущностей и целиком положиться на технологии универсального доступа? OLE DB-провайдер к Index Server позволяет ответить утвердительно. Еще одним доводом в пользу этой технологии служит тот факт, что сам индексный «движок» взаимодействует со своими данными через OLE DB. Следовательно, использование OLE DB/ADO позволит клиентским приложениям добиться наибольшей скорости работы с Index Server.

Второй язык запросов, поддерживаемый в Index Server, начиная с версии 2.0, — это SQL. В приведенном ниже примере сначала устанавливается соединение ADODB.Connection с Index Server, затем ему передается SQL-запрос select FileName from scope(). Результат получается в виде объекта ADODB.Recordset и отображается в элементе управления adoDataGrid1.

Как видно из Листинга 3, для доступа к Index Server через OLE DB-провайдер (MSIDXS) требуется только библиотека Microsoft ActiveX Data Objects (все примеры в тексте статьи были протестированы на третьей бета-версии MDAC 2.5).

ЛИСТИНГ 3.

Dim cnn As ADODB.Connection, 
rst As ADODB.Recordset
Dim qry As String

Set cnn = CreateObject(«ADODB
.Connection.2.5»)
cnn.Provider = «MSIDXS.1»
cnn.Properties(«Integrated 
Security .») = «»
cnn.Properties(«Data Source») 
= «MyCatalog»
cnn.Properties(«Location») = «.»
cnn.Open

Set rst = CreateObject(«ADODB
.Recordset.2.5»)
rst.CursorLocation = adUseClient
qry = «select FileName from scope()»
rst.Open qry, cnn, adOpenStatic, 
adLockReadOnly, -1
adoDataGrid1.AllowUpdate = False
adoDataGrid1.DataSource = rst
При этом можно использовать все стандартные компоненты простой объектной модели ADO: Connection, Command, Parameter, Recordset, Record, Field, Stream, Error и т. д. Data Source в свойствах соединения обозначает полнотекстовый каталог, а Location — имя компьютера. Если их не задавать явно, по умолчанию, будут выбраны каталог Web и локальный компьютер. Location = «.» также обозначает локальную машину.

Список выводимых полей (select list) в SQL-запросе логически эквивалентен IxQuery.Columns из примера предыдущей главы и представляет собой «дружественные» имена (friendly names) рассматривавшихся нами выше свойств документа. Для каждого каталога они хранятся в папке Properties административной консоли Index Server. Напомним, что все свойства, кроме общих и OLE-свойств, генерируются фильтром, следовательно, их состав будет частично определяться типом документа. Разные документы обладают разными свойствами. Например, свойство DocWordCount имеет смысл для текстовых документов, но, очевидно, бесполезно и не будет определено, скажем, для бинарных файлов. Так как набор полей может меняться от записи к записи (), выражение типа select * ... недопустимо (как и для всякого правила, здесь существует исключение, но о нем позже). Команда SET PROPERTYNAME позволяет присвоить/изменить «дружественное» имя свойства. Например,

cnn.Execute ?set propertyname 
?b725f130-47ef-101a-a5f1-02608c9eebac? 
propid 0xa as NameOfFile?,

где первый параметр команды — GUID группы свойств (Property Set), к которой относится данное свойство, а второй — идентификатор свойства внутри группы. Эта информация также хранится в папке Properties, либо ее можно получить через реестр. В результате вместо FileName теперь можно использовать имя NameOfFile:

rst.Open ?select NameOfFile 
from scope()?, cnn

Переопределение действует до закрытия сессии. MSIDXS «не понимает» выражений над полями в списке выводимых значений, поэтому не удастся, например, вывести размер файла сразу в килобайтах: qry = ?select FileName, Size / 1000 from scope()?.

Логические операции над полями допускаются в предикате WHERE. Например, запрос

qry = ?select Path from scope() 
where DocAuthor = ?Leshik? and 
(DocWordCount > 3000 or Size > 500000) 
and Create >=?2000/01/01??

выводит все документы, созданные конкретным автором с 2000 года, у которых число слов или размер в байтах превышают оговоренные величины. К сожалению, одним из операндов должна быть константа, поэтому, если мы хотим узнать, к каким документам обращались с момента их последнего редактирования, «сказать» where Access > Write напрямую Index Server нельзя.

Любопытные расширения операций сравнения предоставляет векторная функция Array. В WHERE поддерживается стандартный оператор LIKE: where FileName like ?%.do[ct]? and DocAuthor like ?Sm_th%? and Contents like ?Once upon a time%?. Он ищет все документы MS Word и их шаблоны (файлы с расширениями doc или dot), чье содержание начинается со слов Once upon a time, авторами которых могут быть Smith, Smyth (_ означает одну произвольную букву), а также Smythe, Smithson и иже с ними (% означает любую последовательность символов, в том числе пустую). Функция Matches приблизительно эквивалентна оператору LIKE, однако возможности поиска по образцу у нее несколько шире. С помощью LIKE и Matches можно осуществлять поиск слова или фразы в тексте документа: where Contents like ?%SQL Server%?, где свойство Contents означает содержание документа. Однако для полнотекстового поиска по содержанию проще воспользоваться встроенной функцией Contains(): qry = ?select Path from scope() where contains(Contents, ???SQL Server???)?.

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

qry = ?select Path from scope() 
where contains(DocSubject, ???SQL???) and
contains(???Server???)?

Условие contains(??SQL Server??) означает, что в содержании должна присутствовать точная фраза SQL Server. Условие contains(??SQL? and ?Server??) требует, чтобы в документе обязательно встречались слова и SQL и Server, при этом они могут находиться где угодно в содержании. Третий вариант:

qry = ?select Path from scope() 
where contains(???SQL?? NEAR ??Server???)? — 

означает искать все документы, в которых слова SQL и Server находятся поблизости друг от друга. «Поблизости» в данном случае означает на расстоянии порядка 50 слов, но эта оценка очень приблизительная, потому что между словами в разных предложениях расстояние считается большим, чем если они находятся на таком же расстоянии друг от друга в пределах одного предложения, в разных параграфах — еще больше и т. д. Есть и другие нюансы. Возможно, в будущем у NEAR появится числовой аргумент, позволяющий точно задавать расстояние между словами. Существует полезное свойство Rank, вычисляемое Index Server, которое отражает степень соответствия данного документа условию запроса в неких условных единицах. Его абсолютная величина может принимать значения от 0 до 1000. Если в предыдущий запрос вставить рейтинг: select Path, Rank .., то при прочих равных параметр Rank будет тем больше, чем ближе друг к другу стоят слова SQL и Server. В простейшем случае, например, select Rank from scope() where contains(??SQL??) на величину Rank влияет то, сколько раз слово SQL встретилось в документе. В Index Server поддерживаются 5 алгоритмов расчета рейтинга, которые можно выбирать с помощью оператора SET RANKMETHOD. По умолчанию, используется коэффициент Жаккарда. Функция ISABOUT позволяет задавать весовые коэффициенты значимости слов или выражений в диапазоне от 0 до 1 с точностью до 3-го знака. Например, если каталог c:Resume содержит анкеты сотрудников, и мы хотим отыскать всех, кто владеет английским или французским языком, но при этом английский нас интересует больше, можно составить следующий запрос:

qry = ?select Path, Rank from 
scope(???c:Resume???) where contains
(?ISABOUT(??French?? WEIGHT(0.1), 
??English?? WEIGHT(1.0))?)?

В этом случае документы, где упоминается English или он упоминается чаще, чем French, получат более высокий Rank. Аналогом % в LIKE в функции Contains является *: where contains(??skat*??). Однако для поиска словоформ проще воспользоваться функцией FORMSOF: where contains(?FORMSOF(INFLECTIONAL, ?skat*?)?). Последнему условию будут удовлетворять документы, содержащие skate, skates, skated, skating... Первый параметр этой функции определяет лингвистический принцип генерации словоформ. Пока поддерживается только INFLECTIONAL — единственное и множественное числа + глагольные формы. В дальнейшем, возможно, появятся генерация всех производных слов, близких по звучанию слов, а также синонимов. Частным случаем функции Contains c комбинацией FORMSOF и ISABOUT можно считать функцию FreeText, которая позволяет задавать вопросы в свободной текстовой форме, подобно тому, как это делается в Microsoft Office Answer Wizard или в Microsoft English Query. Например,

qry = ?select Path, Rank from scope
(???c:Resume???) where FreeText (?Which of 
employees speak English or French?)?

При обработке подобного запроса фраза, являющаяся аргументом FreeText, разбивается на слова, словам назначаются эвристические веса, от слов генерируются производные, после чего происходит поиск всех таких комбинаций. Очевидно, что поиск словоформ и свободный текстовый поиск невозможно использовать при работе с языком, у которого модуль вариации основы в Index Server отсутствует. (В запросах на точные совпадения, если язык не найден, задействуются стандартные в ситуации по умолчанию делитель и список незначащих слов.) Несмотря на то, что поддержка русского языка в Index Server не включена, существуют, как уже отмечалось выше, готовые решения, обеспечивающие работу функций FORMSOF и FreeText с русским языком. В качестве примера можно привести разработки московской фирмы «Алеста».

Провайдер MSIDXS позволяет сортировать результаты запроса. В предикате ORDER BY могут употребляться любые из доступных полей документов. Пример:

qry = ?select Path, Rank, Size 
from scope() where contains(?FORMSOF
(INFLECTIONAL, ??compete??)?) and DocAuthor 
IS NULL order by Rank DESC, Size? 

В этом запросе результаты упорядочиваются по убыванию релевантности. Документы с одинаковым рейтингом располагаются по возрастанию размера файла.

По аналогии с таблицами в базе данных возникает соблазн написать что-нибудь вроде select FileName from d:MyDocuments. Однако для этого сначала требуется преобразовать иерархическую структуру, свойственную файловой системе, в некий реляционный вид. Это преобразование реализуется с помощью функции Scope. Scope() без параметров означает все проиндексированное Index Server содержимое всех виртуальных каталогов для выбранного каталога. Если требуется сузить область поиска до одного или нескольких каталогов, их нужно передать функции Scope в качестве параметров, например:

qry = ?select Directory, FileName, 
Path from scope(???D:My Documents»», 
??d:FTSTest???)?

(Cвойство Path = Directory FileName.) Если один из каталогов в параметрах Scope() помечен в каталоге Index Server как неиндексируемый (Include in index = No), то для него будет возвращено пустое множество результатов. Если подкаталог в данном каталоге вообще отсутствует, то для него можно запрашивать свойства, не требующие применения фильтра (Directory, FileName, Path, Size ...). В противном случае (например, если в списке полей присутствуют DocTitle, DocAuthor, DocWordCount, HitCount ...) результат также будет пустой. Index Server поддерживает уточняющую нотацию имени объекта вида <сервер>.<каталог>.<схема>.<таблица>, например:

qry = ?select ShortFileName from
??alexeysh-desk??.Web..scope()?

Если имена сервера и каталога не указаны явно, действуют значения свойств соединения. Имена, содержащие служебные символы, заключаются в кавычки. Аналогично параметру pwszDepth метода AddScopeToQuery из примера предыдущей главы ключевые слова DEEP TRAVERSAL OF и SHALLOW TRAVERSAL OF позволяют указать функции Scope степень глубины поиска. Они аналогичны значениям Deep и Shallow. По умолчанию, выбирается первое, т. е. рассматривать содержимое указанного каталога и подкаталогов всех уровней вложенности тоже. В примере

qry = ?select FileName, DocAuthor, 
DocLastAuthor, DocTitle, DocSubject from 
scope(?SHALLOW TRAVERSAL OF ??d:FTS???)?

поиск осуществляется только по файлам, лежащим в корне d:FTS, но не в подкаталогах. (DocAuthor — первоначальный автор документа, DocLastAuthor — тот, кто последним вносил в него изменения.) Из реляционных операций над объектами MSIDXS поддерживает UNION ALL:

qry = ?select Directory, FileName, 
Size from (TABLE MyCatalog..scope
(???d:FTS???) UNION ALL TABLE 
??alexeysh-desk??...scope(?SHALLOW 
TRAVERSAL OF ??c:Inetpubwwwroot???))?

Из операций DDL в Index Server поддерживается только CREATE VIEW:

cnn.Execute ?create view #MyView 
as select FileName, DocAuthor, 
DocLastAuthor, DocTitle, DocSubject 
from MyCatalog..scope()?
qry = ?select FileName, DocTitle 
from #MyView?

Поскольку набор полей в этом случае задан, вместо явного перечисления всех полей допускается употребление *:

qry = ?select * from #MyView?

Как и положено приличным представлениям, они действуют в пределах того каталога, где были определены. Команда DROP VIEW отсутствует, поэтому созданное представление сохраняется до конца сессии. Наряду со временными (пользовательскими) представлениями Index Server имеет ряд служебных представлений (predefined views), доступных всегда. К ним относятся FILEINFO (содержит поля Path, Filename, Size, Write, Attrib); FILEINFO_ABSTRACT (то же, что и для FILEINFO + Characterization); EXTENDED_FILEINFO (то же, что и для FILEINFO_ABSTRACT + DocTitle, DocAuthor, DocSubject, DocKeywords); WEBINFO (то же, что и для FILEINFO_ABSTRACT + VPath, DocTitle) и EXTENDED_ WEBINFO (то же, что и для WEBINFO + DocAuthor, DocSubject, DocKeywords — Attrib). В качестве области поиска в них всегда задается Scope(), т. е. выбранный сервер и каталог. Примеры:

qry = ?select VPath, FileName from WEBINFO?
qry = ?select * from ??alexeysh-desk??
.MyCatalog..FILEINFO?

Удаленный доступ к Index Server

Чтобы работать с OLE DB-провайдером для Index Server 2.0, необходимо на том же компьютере иметь установленный Index Server (см. Microsoft Knowledge Base, Q178849, Q216822). В большинстве случаев это ограничение вполне согласуется с требованиями архитектуры. Типичный пример — intranet-приложения, чья бизнес-логика сосредоточена в ASP-сценариях, выполняющихся на IIS, в состав которого входит Index Server. Тем не менее то, что клиент и сервер должны обязательно находиться в пределах одной машины, создает некоторый дискомфорт. Remote Data Services (RDS) помогают вновь обрести душевное равновесие и избавить клиентскую машину от навязчивого присутствия Index Server. Пример:

cnn.Provider = ?MS Remote?
cnn.Properties(?Remote Provider?) = ?MSIDXS?
cnn.Properties(?Remote Server?) = ?http://bsgrus?
cnn.Properties(?Data Source?) = ?Web?
cnn.Open
qry = ?select FileName from scope()?
...

Таким образом, клиент может легко обойтись даже без провайдера MSIDXS. Microsoft OLE DB Remoting Provider позволяет вызывать провайдеры данных, установленные на удаленных машинах. Провайдеры данных могут располагаться на самих серверах — источниках данных — или на серверах приложений в промежуточном слое. В любом случае конечному клиенту необходимо иметь только провайдер MS Remote для доступа к любому виду ресурсов, например к SQL Server:

cnn.Provider = ?MS Remote?
cnn.Properties(?Remote Provider?) = ?SQLOLEDB?
cnn.Properties(?Remote Server?) = ?http://bsgrus?
cnn.Properties(?User ID?) = ?sa?
cnn.Properties(?Password?) = ??
cnn.Open
qry = ?select * from authors?

Для работы с OLE DB Remoting Provider по протоколу HTTP необходимо убедиться, что служба RDS установлена и правильно сконфигурирована на удаленном сервере. В частности, на IIS должен существовать виртуальный каталог MSADC (ему соответствует путь ...Program Files Common FilesSystemmsadc), содержащий файл Msadcs.dll. Проверить корректность работы RDS можно, вызвав тестовый сценарий Adctest.asp (обычно — в ...msadc Samples). Типичные ошибки, как правило, связаны с неправильными установками прав доступа. В первую очередь необходимо убедиться, что клиент обладает достаточными правами для доступа к каталогу. В свойствах (закладка Directory Security) в секции IP Address and Domain name restrictions необходимо убедиться, что с IP-адреса клиента можно зайти на этот каталог. В секции Anonymous access and authentication control следует проверить метод аутентификации, используемый Web-cервером. Если доступ анонимному пользователю не разрешен, но разрешена базовая аутентификация, должны быть созданы соответствующие учетные записи пользователей. Полномочия, проверяемые IIS для всех пользователей, обращающихся к виртуальному каталогу, должны предусматривать выполнение сценариев. Наконец, необходимо проверить разрешения NTFS на файлы и сам каталог, на который отображается виртуальный каталог MSADC. Кроме того, поскольку при базовой аутентификации IIS выступает от имени пользователя, следует убедиться, что соответствующие учетные записи обладают правом Log on locally.

Второй протокол, по которому может работать RDS, — это DCOM. Выбор протокола определяется свойством Remote Server при инициализации соединения MS Remote. Отсутствие префикса http:// предполагает, что соединение будет установлено через DCOM: cnn.Properties(?Remote Server?) = ?bsgrus?. В этом случае на удаленном сервере не требуется устанавливать IIS, а динамическая библиотека RDS.DataFactory, реализующая базовую бизнес-логику на стороне сервера, такую, как перенаправление клиентских запросов на сервер данных и возвращение клиенту объектов Recordset, должна вызываться из другого процесса. Для этого ее можно «завернуть» в пакет MTS/приложение СОМ+, тогда она будет выполняться в контексте mtx.exe либо использовать для этой цели традиционный dllhost.exe.

Index Server и SQL Server

В заключение рассмотрим Index Server в роли присоединенного сервера Microsoft SQL Server 7.0. Традиционный способ построения гетерогенного запроса начинается с создания присоединенного сервера (linked server). Механизм присоединенных серверов предназначен для описания произвольных внешних OLE DB-ресурсов и выполнения по ним запросов со стороны SQL Server. В роли таких серверов могут выступать практически любые источники данных: серверы БД (другой SQL Server, Oracle, Sybase и др.), настольные и персональные СУБД и менеджеры записей (FoxPro, Access, dBase, Clipper и др.), табличные редакторы (Excel и т. д.), текстовые файлы и др. Непременным условием является наличие соответствующего стыковочного узла в виде OLE DB-провайдера. Создадим присоединенный сервер для Index Server:

exec sp_addlinkedserver 
@server=?MyIDXSrv?,@srvproduct=?Index Server?, 
@provider=?MSIDXS?, @datasrc=?Web?, 
@location=?bsgrus?

Здесь @server — имя присоединенного сервера, а все остальные параметры — суть значения свойств инициализации соединения, хорошо знакомые нам по объекту ADODB.Connection в предыдущих примерах. Пользователь SQL Server должен иметь право доступа к компьютеру, имя которого передается в параметре @location и на котором находится Index Server. В приведенном примере эта машина вообще принадлежит другому домену, с которым нет доверительных отношений. Вызов хранимой процедуры

exec sp_addlinkedsrvlogin 
@rmtsrvname=?MyIDXSrv?, 
@useself=?false?, @locallogin=NULL,
@rmtuser=?AnotherDomainAdministrator?, 
@rmtpassword=?aaa?

дает SQL Server указание обращаться к удаленному серверу @rmtsrvname под именем пользователя @rmtuser с паролем @rmtpassword вне зависимости от того, кто из пользователя SQL Server обращается к данному серверу (@locallogin=NULL). Если бы мы хотели указать какого-то конкретного пользователея SQL Server, его имя надо было указать в явном виде, например @locallogin=?sa?, @useself= ?true? означает, что пользователь будет представлен SQL Server на удаленном сервере тем именем и паролем, с которыми он авторизовался на SQL Server. @rmtuser и @rmtpassword в этом случае игнорируются. Функция OpenQuery выполняет запрос к присоединенному серверу и возвращает результат SQL Server, с которым тот далее может обращаться как с обыкновенной таблицей:

select * from openquery(myidxsrv, 
?select Path, FileName from scope()?)
Подобные задачи выполняет функция OpenRowset:
select * from openrowset(?MSIDXS?, 
?MyCatalog?;??;??, ?select Path, FileName 
from scope()?)

В отличие от OpenQuery для нее не требуется создавать присоединенный сервер, так как вся необходимая информация для инициализации соединения передается в параметрах этой функции. OpenRowset лучше использовать при работе с большим числом различных внешних источников, когда к каждому из них по ходу дела требуется осуществить всего 1-2 запроса. Синтаксис SQL-запросов к Index Server в OpenQuery и OpenRowset полностью идентичен разобранному нами в предыдущих главах, так как в обоих случаях взаимодействие с Index Server происходит через его OLE DB-провайдер, а кто выступает в роли клиента, SQL Server или пользовательское приложение на VB/VBScript, не имеет значения. В наружном операторе SELECT, которым SQL Server производит выборку из результата внутреннего запроса в OpenQuery/OpenRecordset, полученного и переданного ему Index Server, можно использовать любые соответствующие Transact-SQL конструкции. Это удобно, когда требуется дополнительно обработать результат запроса от Index Server, приведя его к виду, который невозможно получить только средствами Index Server в силу ограничений его SQL-диалекта. Например, уже упоминалось, что MSIDXS не допускает выражений над выводимыми полями в списке SELECT или сравнения двух выражений в условии WHERE, когда одно из них — не константа. Это может сделать SQL Server:

select Path, Size / 1000 from 
openquery(MyIDXSrv, ?select Path, 
Size from scope(???d:FTS???)?)
select * from openquery(MyIDXSrv, 
?select Path, Create, Write, Access, 
DocAuthor, DocLastAuthor, DocTitle, 
DocSubject from scope(???d:FTS???)?) 
where Write < Access

По соображениям оптимизации не стоит перекладывать всю работу на SQL Server, если запрос можно целиком выполнить средствами Index Server. Так, следующие запросы

select * from openquery(myidxsrv, 
?select Directory, FileName, Size from 
scope() where FileName like ??%.doc?? 
order by Size?) 
select * from openquery(myidxsrv, 
?select Directory, FileName, Size from 
scope()?) where FileName like ?%.doc? 
order by Size

дают одинаковые результаты, однако в первом запросе фильтрацию и сортировку выполняет Index Server, а во втором — SQL Server, т. е. клиент. Полуфабрикат в виде результата select Directory, FileName, Size from scope(), который во втором случае требуется передать клиенту, может иметь очень внушительные размеры, что способно сильно (а главное, совершенно неоправданно) увеличить трафик и, как следствие, общее время выполнения запроса. Первый запрос лучше использует преимущества клиент-серверного подхода и должен быть признан более эффективным. Результаты обработки запроса к присоединенному серверу можно затем использовать вместе с собственными данными SQL Server, а также других внешних источников. Например, если таблица Articles содержит информацию об опубликованных статьях с указанием издания и даты публикации, а сами тексты статей хранятся в файловой системе, то запрос

select q.DocTitle, q.Size, 
q.DocAuthor, q.Path, a.Pub_Name, 
a.Pub_Date from openquery(MyIDXSrv, 
?select Rank, Path, Size, DocAuthor, 
DocTitle, FileIndex from scope() where 
contains(contents, ???SQL Server???)?) 
as q inner join Articles a on 
q.FileIndex = a.ID order by q.Rank desc

позволяет объединить эти данные. Предполагается, что первичным разделом таблицы Articles является поле ID, тождественное свойству FileIndex — уникальному идентификатору файла в файловой системе.