SharePoint Portal Server 2003 および Microsoft Search ベースのその他の製品が使用するフィルタを作成する方法

David Lee
Microsoft Corporation

April 2004

対象 :
Microsoft® Office SharePoint™ Portal Server 2003
Microsoft SQL Server™ 2000
Microsoft Exchange Server 2003
Microsoft Exchange Server 2000
Microsoft Windows Server™ 2003

概要 : Microsoft Office SharePoint Portal Server 2003 およびその他の Microsoft Search ベースの製品 (Microsoft SQL Server、Microsoft Exchange Server、Microsoft インデックス サービスなど) が、フィルタをどのように使用してファイルのコンテンツおよびプロパティを抽出し、フルテキスト インデックスに取り込むかを、フィルタのサンプルの実装から学びます。

目次

はじめに
IFilter および IPersist 系のインターフェイス
IFilter を使用してフィルタを作成する
実装に関するメモ
フィルタ DLL のテスト
まとめ

はじめに

Microsoft Search ベースの製品、つまり Microsoft Search (MSSearch) サービスを使用する Microsoft® Office SharePoint™ Portal Server 2003、Microsoft SQL Server、Microsoft Exchange Server、Microsoft インデックス サービスなどの製品は、フィルタを使ってファイルのコンテンツおよびプロパティを抽出し、フルテキスト インデックスに取り込みます。この記事では、これらのアプリケーションが使用できるフィルタの作成方法について解説し、以下の話題を取り上げます。

  • COM インターフェイスでのフィルタのサポート
  • フィルタの実装
  • フィルタのインストールとテスト

MSDN ライブラリの Microsoft Windows Platform SDK には、SmpFilt (シンプル フィルタ) というテキスト ファイル フィルタが含まれており、\mssdk\Samples\WinBase\Index\SmpFilt ディレクトリにインストールされています。SmpFilt が処理の対象とするのは、.smp という拡張子のプレーン テキスト ファイルです。この記事では、このサンプル フィルタの実装を吟味し、参照用として使用できるコードについて解説します。

IFilter および IPersist 系のインターフェイス

フィルタとは、IFilter インターフェイスと、IPersist 系の 1 つまたは複数のインターフェイスを実装した COM オブジェクトです。インデックス サービスなどのアプリケーションは、フィルタ オブジェクトを作成してこれらのインターフェイスのメソッドを呼び出すことで、ドキュメントのテキストとプロパティを取得します。独自のファイル形式をインデックスに確実に取り込むには、IFilter を実装したオブジェクトを作成するのが唯一の方法です。

フィルタに一般的に実装するインターフェイスには次の 4 つがあります。

  • IFilter。ファイルからテキストとプロパティを取得するためのメソッドが含まれています。
  • IPersistFile。絶対パスでファイルを読み込むためのメソッドが含まれています。
  • IPersistStreamIStream からファイルを読み込むためのメソッドが含まれています。
  • IPersistStorage。絶対パスでファイルを読み込むためのメソッドが含まれています。

これらのインターフェイスに関する一般的な情報については、『Platform SDK』を参照してください。

メモ この記事では、これら 4 つのインターフェイスに的を絞るため、標準の IUnknown インターフェイスのメソッドについては解説しません。

IFilter インターフェイス

ファイルのコンテンツとプロパティを抽出してフルテキスト インデックスに取り込む目的で、MSSearch ベースの製品と組み合わせて使用できる COM フィルタを作成するためには、IFilter インターフェイスを実装する必要があります。このインターフェイスは、MSSearch ベースの製品がファイルからテキストとプロパティを取得するために使用するメソッドを公開しています。

次の表に IFilter インターフェイスのメソッドを示します。

表 1 IFilter インターフェイスのメソッド

メソッド 説明
Init(ULONG fFlags, ULONG cAttrib, FULLPROPSPEC * aAttrib, ULONG *pFlags)
フィルタリングのセッションを初期化します。
GetChunk(STAT_CHUNK *pStat)
フィルタの位置を最初のチャンクまたは次のチャンクの先頭に移動し、記述子を返します。
GetText(ULONG *pcwcBuf, WCHAR *pwcBuf)
現在のチャンクからテキストを取得します。
GetValue(PROPVARIANT **ppProp)
現在のチャンクからプロパティ値を取得します。
BindRegion(FILTERREGION origPos, REFIID riid, void *ppUnk)
オブジェクトの特定の部分を表すインターフェイスを取得します。現在は内部的な使用のために予約されています。実装しないでください。このメソッドは E_NOTIMPL を返します。

IPersist インターフェイス

IPersist インターフェイスには、GetClassID というメソッド 1 つだけが定義されています。システムに持続的に格納できるオブジェクトの CLSID を返すメソッドです。このメソッドが呼び出されることにより、オブジェクトは、クライアント プロセスがどのオブジェクト ハンドラを使用すればよいかを指定することができます。OLE のマーシャリングの既定の実装での使用と同様です。

IPersist のこのメソッドの実装は、他の持続性インターフェイス、つまり、IPersistStorageIPersistStream、または IPersistFile のいずれかのインターフェイスの実装により行う必要があります。たとえば、埋め込みオブジェクトでは IPersistStorage、リンク オブジェクトでは IPersistFile、新しいモニカ クラスでは IPersistStream を実装するのが一般的です。ただし、これらインターフェイスの使用は、こうしたオブジェクトに限定されるものではありません。また、マーシャリングでの使用と同様に、持続的なオブジェクトの CLSID を取得することだけが必要という状況では、IPersist を実装することも可能です。

IPersistFile インターフェイス、IPersistStream インターフェイス、および IPersistStorage インターフェイスはすべて、IPersist インターフェイスを継承しています。

次の表に IPersist インターフェイスのメソッドを示します。

表 2 IPersist インターフェイスのメソッド

メソッド 説明
GetClassID(CLSID * pClassID) コンポーネント オブジェクトのクラス識別子 (CLSID) を返します。フィルタの場合は、これはファイルの種類の識別子です。新規のフィルタの場合は、Platform SDK に用意されている uuidgen.exe を使用して、一意な CLSID を生成します。GetClassID メソッドは必ず実装する必要があります。

IPersistFile インターフェイス

フィルタを使用してフルテキスト インデックスに取り込む対象が、ファイル システム上のスタンドアロン ファイルの場合には、IPersistFile インターフェイスを実装する必要があります。このインターフェイスにはファイルを絶対パスで読み込むためのメソッドが含まれているからです。サンプル フィルタには、IFilter インターフェイスと IPersistFile インターフェイスのみが実装されています。

MSSearch は、IUnknown::QueryInterface() メソッドを使用して、フィルタが実装しているインターフェイスを検出します。IPersist 系のインターフェイスを他に実装しない場合には、IPersistFile インターフェイスを実装しなくてはなりません。ほとんどのバージョンの MSSearch は、フィルタが IPersistStreamIPersistStorage を実装していない場合には、一時ファイルを作成して IPersistFile を使用します。

フィルタは読み取り専用モードのみで動作するので、IPersistFile のメソッドのうちで必ず実装する必要があるのは、GetClassID()Load()、および GetCurFile() だけです。フィルタ クライアントはファイルへの書き込みを行わないため、その他のメソッドは呼び出されず、E_NOTIMPL を返します。Load() メソッドはフィルタする対象のファイルのパスを引数に受け取ります。サンプル フィルタでは、このパスを保存して、後でファイルを開くときにそのパスを使えるようにしています。GetCurFile() は単に、フィルタしているファイルのパス、つまり先ほど Load() に渡されたパスを返します。IPersistFileIPersist を継承しています。

次の表に IPersistFile インターフェイスのメソッドを示します。

表 3 IPersistFile インターフェイスのメソッド

メソッド 説明
IsDirty(void) オブジェクトをチェックして、現在のファイルへの最後の保存の後で加えた変更があるかどうか調べます。フィルタでは、このメソッドは E_NOTIMPL を返します。
Load(LPCOLESTR pszPath, DWORD dwMode) 指定したファイルを開き、そのコンテンツでオブジェクトを初期化します。ファイルを開く処理は、そのファイルが必要となる時点まで待たせることもできます。
Save(LPCOLESTR pszPath, BOOL fRemember) 指定したファイルにオブジェクトを保存します。フィルタでは、このメソッドは E_NOTIMPL を返します。
SaveCompleted(LPCOLESTR pszPath) NoScribble モードから Normal モードに戻せるということをオブジェクトに通知します。フィルタでは、このメソッドは E_NOTIMPL を返します。
GetCurFile(LPOLESTR *ppszPath) オブジェクトに関連付けられているファイルの現在の名前を取得します。Load() で指定したパスが返されます。

IPersistStream インターフェイス

IPersistStream インターフェイスは、フルテキスト インデックスに取り込む対象のファイルが他のドキュメント内に埋め込まれている場合によく使用されます。IPersistStream には、IStream からファイルを読み込むためのメソッドが含まれています。また、IPersistStream は、SQL Server データベースなど、ファイル システム以外に格納されているドキュメントをフィルタする目的でも使用されます。フィルタに IPersistStream を実装すべき主な理由には、次の 2 つがあります。

  • 将来の互換性とパフォーマンスを確実に持たせるため。ファイル システム以外の格納場所に置かれたデータをストリームとして直接処理できる場合には、それをフルテキスト インデックスに取り込む方が、データをいったんディスクにコピーしてから IPersistFile インターフェイスを使うよりも効率がよくなります。
  • セキュリティを高められるようにするため。将来のバージョンの MSSearch では、IPersistStream のみがサポートされ、IPersistFileIPersistStorage はサポートされない、ということになる可能性があります。このため、フィルタに IPersistStream を実装しておけば、システムのセキュリティを高められる可能性があります。フィルタが動作するコンテキストで、ディスク上やネットワーク上のファイルを開く権限が必要ないからです。

フィルタは読み取り専用モードで動作するので、このインターフェイスで実装が必要なのは Load() メソッドと GetClassID() メソッドだけです。その他のメソッドは呼び出されないため、E_NOTIMPL を返して問題ありません。Load() メソッドでは、IStream のポインタを待避しておくようにします。後で、ストリームのコンテンツを取得する目的で IFilter のメソッドが呼び出されたときに使用するためです。GetClassID() は、先ほど IPersistFile で解説したとおりです。IPersistStreamIPersist を継承しています。

次の表に IPersistStream インターフェイスのメソッドを示します。

表 4 IPersistStream インターフェイスのメソッド

メソッド 説明
IsDirty(void) オブジェクトをチェックして、最後の保存の後で加えた変更があるかどうか調べます。フィルタでは、このメソッドは E_NOTIMPL を返します。
Load(IStream *pStm) もともとの保存場所のストリームでオブジェクトを初期化します。
Save(IStream *pStm, BOOL fClearDirty) 指定したストリームへオブジェクトを保存します。オブジェクトのダーティ フラグをリセットするかどうかも指定します。フィルタでは、このメソッドは E_NOTIMPL を返します。
GetSizeMax(ULARGE_INTEGER *pcbSize) オブジェクトの保存に必要なストリームのサイズをバイト単位で返します。フィルタでは、このメソッドは E_NOTIMPL を返します。

IPersistStorage インターフェイス

ファイル形式が構造化ストレージ形式の場合は、IPersistStorage インターフェイスを実装します。一般に、IPersistStorage は、他の構造化ストレージ ファイル内に埋め込まれている、構造化ストレージ埋め込みに対して使用します。たとえば、Microsoft Office System のファイル用のフィルタは、埋め込まれているデータ用のフィルタの読み込みに IPersistStorage を使用します。

次の表に IPersistStorage インターフェイスのメソッドを示します。

表 5 IPersistStorage インターフェイスのメソッド

メソッド 説明
IsDirty(void) 変更があったかどうかをチェックします。フィルタでは、このメソッドは E_NOTIMPL を返します。
InitNew(IStorage * pStg) 新しいストレージを作成します。フィルタでは、このメソッドは E_NOTIMPL を返します。
Load(IStorage * pStg) ストレージを保存します。フィルタでは、このメソッドは E_NOTIMPL を返します。
Save(IStorage * pStg, BOOL fSameAsLoad) オブジェクトの保存に必要なストリームのサイズをバイト単位で返します。フィルタでは、このメソッドは E_NOTIMPL を返します。
SaveCompleted(IStorage * pStg) 内部的な使用のために予約されています。フィルタでは、このメソッドは E_NOTIMPL を返します。
HandsOffStorage(void) 内部的な使用のために予約されています。フィルタでは、このメソッドは E_NOTIMPL を返します。

IFilter を使用してフィルタを作成する

MSSearch の一部のバージョンは、COM の CoCreateInstance() 関数または同様の関数を使用してフィルタを作成します。また、その他のバージョンは、レジストリを調べ、LoadLibrary()GetProcAddress() といった関数を使用してフィルタを読み込みます。

フィルタのクライアントは、まず最初に、IPersistFileIPersistStream、または IPersistStorage のいずれかの Load() メソッドを呼び出します。次に、IFilter インターフェイスの Init() メソッドを呼び出します。そして、GetChunk() を呼び出してから、GetText() または GetValue() のどちらかを必要な回数だけ何度も呼び出して、チャンクに関連付けられているテキストまたはプロパティ値をすべて取得します。GetChunk() の戻り値が、ドキュメント内の末尾のチャンクに達したことを示す値になるまで、この処理を繰り返します。BindRegion() は呼び出されないため、単純に E_NOTIMPL を返します。

次のサンプル コードは、一般的なフィルタの使用法を示したものです。この中では、ヘルパー関数 LoadIFilter() を使用しています。LoadIFilter 内では、CoCreateInstance() などの方法でフィルタを作成してから、指定されたファイル名を使用して IPersistFile::Load を呼び出しています。

メモ このサンプルは実際のコードではありません。一般的なフィルタの使用法を示すためだけのものです。

IFilter *pFilt;
HRESULT hr = LoadIFilter( L"c:\\file.smp", 0, &pFilt );
if ( FAILED( hr ) )
    return hr;

ULONG flags;
hr = pFilt->Init( IFILTER_INIT_APPLY_INDEX_ATTRIBUTES, 0, 0, &flags );
if ( FAILED( hr ) )
    return hr;

STAT_CHUNK stat;
while ( SUCCEEDED( hr = pFilt->GetChunk( &chunk ) ) )
{
    if ( CHUNK_TEXT == chunk.flags )
    {
        WCHAR awc[100];
        ULONG cwc = 100;
        while ( SUCCEEDED( hr = pFilt->GetText( &cwc, awc ) ) )
        {
// テキスト バッファを処理する. . .
        }
    }
    else // CHUNK_VALUE
    {
        PROPVARIANT *pVar;
        while ( SUCCEEDED( hr = pFilt->GetValue( &pVar ) ) )
        {
// プロパティ値を処理する. . .

            PropVariantClear( pVar );
            CoTaskMemFree( pVar );
        }
    }

    if ( FAILED( hr ) )
        return hr;
}

return hr;

Init() メソッドは、ファイル内のデータの取り出しを準備するようフィルタに伝えるメソッドです。通常フィルタは、このメソッドが呼び出されたときに、IPersistFile::Load() で指定されたファイルを開きます。Init() へ渡す引数では、ドキュメント内の何をどのようにフィルタするかを指定します。Init() にプロパティの配列を渡した場合は、ファイルのフィルタ時にはそれらのプロパティだけが取り出されます。IFILTER_INIT_APPLY_INDEX_ATTRIBUTES フラグを指定した場合は、フィルタは、処理対象のドキュメントから、必要なプロパティおよびテキストをどれでも取り出すことができます。プロパティを渡さず、かつ IFILTER_INIT_APPLY_INDEX_ATTRIBUTES フラグも指定しなかった場合は、ファイルのテキスト コンテンツだけが取り出されます。Init() のその他のフラグでは、段落、改行、およびハイフンの処理方法について指定します。サンプル フィルタでは、処理の対象は生テキストであり、これらのフラグは使用しません。

インデックス サービスの Webhits.dll ISAPI Hit-Highlighting 機能は、Init() の引数に属性を渡します。フィルタは、これに対応する機能を実装するか、属性のパラメータを適宜無視するか、どちらかとする必要があります。現在出荷されている MSSearch ベースのその他の製品はいずれも、Init() に属性の配列は渡しません。

IFilter オブジェクトの Init() メソッドは複数回呼び出される可能性があるため、既存の状態をリセットして処理対象のファイルの開始位置まで戻せるようにフィルタを準備しておく必要があります。Init() は、フィルタリングの場合は 1 ファイルにつき 1 回呼び出され、Hit-Highlighting 機能 (Webhits.dll) を使用してドキュメント内のクエリのヒット箇所を表示しているアプリケーションからは 1 ファイルにつき 2 回呼び出されるのが普通です。1 回目の呼び出しではファイル内でクエリに合致する箇所を特定し、2 回目の呼び出しではファイルを HTML に変換します。

サンプル フィルタの Init() メソッドの実装では、IPersistFileLoad() メソッドであらかじめ指定されていたファイルを開いています。また、IFILTER_FLAGS_OLE_PROPERTIES フラグを呼び出し元に返すようになっています。ファイルの OLE 構造化ストレージ プロパティの列挙および抽出を行うよう呼び出し元に伝えるフラグです。こうしないと、フィルタ自身が OLE の IPropertySetStorage インターフェイスを使用してファイルの OLE プロパティを取得しなくてはならなくなってしまいます。このフラグを返すようにすることで、フィルタの作成が楽になります。Microsoft Windows® 2000 以降のバージョンの NTFS ファイル システムでは、OLE 構造化ストレージ ファイルだけでなく、すべてのファイルが、任意のプロパティを持てるようになっています。呼び出し側のプロセスでプロパティがきちんとフィルタされるようにするためには、フィルタは IFILTER_FLAGS_OLE_PROPERTIES フラグを返さなくてはなりません。一方、IPropertySetStorage で得られるプロパティのうちの一部のみをフィルタリングするようにフィルタを作成したい場合には、IFILTER_FLAGS_OLE_PROPERTIES は返さないようにします。代わりに、チャンクおよび値を必要に応じて取り出すようにします。

GetChunk() メソッドは、フィルタ対象のファイルから、情報の論理ブロックについての情報を、先頭または次のブロックから取得するメソッドです。チャンクには、テキスト チャンクとプロパティ値チャンクの 2 種類があります。GetChunk() は、テキストやプロパティ値自体は返しません。チャンクの中身は、それに続いて呼び出す GetText() および GetValue() で取り出します。テキスト チャンクは Unicode 文字列です。プロパティ値チャンクは PROPVARIANT 値で、さまざまなデータ型を保持できます。

GetChunk() は、現在のチャンクに関する情報を STAT_CHUNK 構造体で返します。たとえば、値が次々と大きくなっていくチャンク ID、当該チャンクと前のチャンクとの関係についてのステータス情報、チャンクがテキストと値のどちらを含むかを示すフラグ、チャンクのロケール、チャンクのプロパティ仕様などです。プロパティ仕様は、CLSID と、整数または文字列プロパティ識別子で構成されます。

チャンクのロケール識別子は、適切なワード ブレーカを選択するために使用されるもので、正しく識別することがきわめて重要です。フィルタでは、テキストのロケールを特定できない場合は、GetSystemDefaultLCID() で得られる既定のシステム ロケールであるものと想定するようにします。自分で管理する独自のファイル形式で、かつ現在はロケール情報が含まれていない場合には、ユーザーが適切にロケール情報を指定できる機能を追加しましょう。不適切なワード ブレーカを使用していると、ユーザーがクエリの結果に不満を持つことにもなりかねません。

サンプル フィルタの GetChunk() メソッドでは、ファイルの決まった部分をバッファに読み込んで、Unicode に変換し、その後の GetText() 呼び出しに備えてそのテキストを保持しています。

GetText() は現在の CHUNK_TEXT チャンクから取得したテキストを返すメソッドです。このメソッドが成功した場合のリターン コードには次の 3 つがあります。

  • GetText() に渡されたバッファに、チャンクから取得したテキスト全体が収まりきらない場合には、バッファに収まる最大限のテキストを格納し、リターン コードを S_OK に設定します。GetText() は何度も呼び出され、チャンクのテキストすべてを取得するために必要な回数だけ繰り返されます。
  • 残っているテキストすべてがバッファに収まる場合には、メソッドのリターン コードを FILTER_S_LAST_TEXT に設定します。
  • 取得できるテキストが残っていない場合には、メソッドのリターン コードを FILTER_E_NO_MORE_TEXT に設定します。

FILTER_S_LAST_TEXT は、使用すれば申し分ありませんが、使用しないことも可能です。言い換えると、S_OKFILTER_E_NO_MORE_TEXT だけでも、フィルタの処理をまかなうことができます。

サンプル フィルタの GetText() メソッドでは、ファイル バッファの中から最大限の量を出力バッファにコピーします。また、次の GetText() 呼び出しに備えるために、コピー済みの分量を把握しています。

GetValue() は現在の CHUNK_VALUE チャンクのプロパティ値を返すメソッドです。プロパティ値を設定したうえで、S_OK を返します。取得できるプロパティ値が残っていない場合は、FILTER_E_NO_VALUES を返します。

プロパティ値は、多種多様なデータ型を保持できる PROPVARIANT 構造体で返されます。PROPVARIANT 構造体自身の領域の確保は、GetValue() 内で CoTaskMemAlloc() を使用して行わなくてはなりません。一方、その解放は GetValue() を呼び出す側の役目です。PROPVARIANT 構造体が指すメモリの解放には PropVariantClear() を使用し、構造体自身の解放には CoTaskMemFree() を使用します。

サンプル フィルタはプロパティ値を取り出しません。このため、GetValue() では単に FILTER_E_NO_VALUES が返ります。

フィルタは、OLE 構造化ストレージ プロパティとして格納されていないプロパティ値、つまり対象のファイル形式から暗黙的に派生したプロパティ値を返すことができます。たとえば、HTML フィルタであれば、メタタグの値をプロパティとして取り出すことができます。あるいは、C++ のソース ファイル用のフィルタであれば、ソース内で見つかったクラス名とメソッド名をプロパティ値として返すことができます。独自のプロパティを返すフィルタを作成する場合には、それらにプロパティ仕様を割り当て、GetChunk()STAT_CHUNK パラメータで返さなくてはなりません。新しいプロパティ セットの GUID を uuidgen.exe を使って生成してから、そのセットの各プロパティの PROPID または文字列識別子を定義します。また、これらのプロパティをクエリ用として使用している場合は、当該の製品のドキュメントで解説されているとおりにプロパティを定義することも必要です。

MSSearch は、GetText() で返されたテキストをインデックス化して、コンテンツ クエリを実行できるようにします。GetValue() で返されたプロパティも同様にフルテキスト インデックスに取り込まれます。コンテンツ クエリでは、テキスト プロパティ値 (VT_LPWSTR などの PROPVARIANT 型の値) も使用できます。プロパティ値クエリでは、クエリの実行時にプロパティ値が利用可能とされれば、すべてのプロパティ型が使用できます。こうしたプロパティを利用可能とするのに最も便利なのは、それらプロパティをプロパティ キャッシュに格納する方法です。

実装に関するメモ

サンプル フィルタでは、IFilterIPersistFile を継承および実装したクラスである CSmpFilter が定義されています。このクラスの各インスタンスは、それぞれ別個のフィルタ オブジェクトに対応します。たとえば、Microsoft インデックス サービスの Webhits.dll が、.smp ドキュメントの Hit-Highlighting 処理を 3 人のユーザーに対して同時に行っている場合、3 つの IFilter オブジェクトが作成されており、内部では CSmpFilter のインスタンスが 3 つ作成されています。

他の COM オブジェクトと同様に、フィルタ DLL は、フィルタのインスタンスを作成するためにクラス ファクトリを必要とします。サンプル フィルタのクラス ファクトリ (CSmpFilterCF) は、CreateInstance() が呼び出されたときに、CSmpFilter のインスタンスを作成します。

サンプル フィルタに実装されている DllGetClassObject()DllCanUnloadNow()、および DllMain() は、COM DLL では一般的なものであり、フィルタ固有の処理はまったく含まれていません。

フィルタに関する高度な話題

フィルタはマルチスレッド対応でなくてはなりません。フィルタがグローバル データを使用する場合は、クリティカル セクションなどの同期プリミティブを使用してそのデータを保護することが必要です。フィルタがフリースレッドまたはアパートメントと指定されている場合、フィルタ オブジェクトの複数のインスタンスが同時に作成および使用されます。フィルタがフリースレッドまたはアパートメントとなっていると、複数のインスタンスを同時に実行できるため、フィルタリングや Hit-Highlighting の処理効率が大きく向上します。後ほど、「フィルタのインストール」の節で、フィルタのスレッド モデルをレジストリに指定する方法について説明します。

フィルタをフリースレッドにできない場合は、クラッシュを防ぐために、ThreadingModel は必ず Single と指定します。パフォーマンス上の理由から、これは窮余の策としてのみ行うべきです。レジストリでスレッド モデルを指定しない場合は、Single であるものとみなされます。

MSSearch 製品には、複数のスレッドが 1 つのフィルタ インスタンスを同時に使うものはありません。各スレッドはそれぞれ別個のフィルタ インスタンスを持ちます。

フィルタをマルチスレッドにするために、すべてのグローバル変数をフィルタ オブジェクト クラスに置くことができます。こうすることで、状態に関する競合が決して起きないようにし、また各インスタンスがそれぞれ独立して確実に動作するようにします。

MSSearch ベースの製品は、フィルタの読み込みを、フィルタ デーモンと呼ばれる特別なプロセスで実行します。フィルタをデバッグするには、次の表に示すように、いずれかのプロセスに対してデバッガを使用します。

表 6 各アプリケーションが使用するフィルタ デーモン プロセス

アプリケーション 使用するフィルタ デーモン プロセス
SharePoint Portal Server、Exchange Server、および SQL Server Mssdmn.exe
インデックス サービス Cidaemon.exe

通常、フィルタ デーモン プロセスが数分間応答なしになった場合、フィルタのバグにより問題が発生したものとみなされ、プロセスは強制終了されます。フィルタ デーモン プロセスのデバッグ中は、このアイドル チェックは自動的に無効化されます。ただし、古いバージョンのインデックス サービスの一部には、このデバッガ チェックがないものもあり、その場合はレジストリ キーを HKLM\System\CurrentControlSetControl\ContentIndex\FilterIdleTimeout REG_DWORD 6000000 (10 進) と設定する必要があります。これにより、タイムアウト値が 100 分に延長されます。

フィルタを読み込んでいるプロセスを確実に突き止めるには、デバッガ ツール tlist を使用できます。「tlist -m yourfilter.dll」のように使用します。

フィルタは、締めくくりとなる Release() がいつ呼び出されてもいいように準備しておかなくてはなりません。フィルタが破棄されるのはファイルのコンテンツすべてが取得された後だと決めてかからないようにしましょう。Release() は、エラー発生により前倒しで呼び出される場合があるほか、その他のさまざまな状況でも早目に呼び出される可能性があります。たとえば、アプリケーションがシャットダウンしたときや、大きなドキュメントがフルテキスト インデックスに取り込まれたときなどです。

フィルタは、適切なエラー処理につながる有効な HRESULT を返さなくてはなりません。メモリ不足などのエラーに遭遇した場合には、その障害が発生したメソッドがエラーを返す必要があります。手抜きをすると、ユーザーが混乱することになります。インデックスが完全に最新になったと思ったのに、クエリで目的の結果が得られないからです。また、エラー コードを E_FAIL などの汎用的なエラー コードに設定しないよう注意しましょう。本当のエラー状態がはっきりしなくなってしまうからです。

フィルタでは、ファイルを書き込みアクセス用としてオープンしてはなりません。CreateFile() の呼び出しで書き込みアクセスを要求するフィルタは、フィルタ デーモンでデッドロックを引き起こすことになります。フィルタ デーモンは、フィルタを起動する前に、ファイルに対して便宜的ロック (oplock) を行うからです。他の何らかのアプリケーションがそのファイルを書き込みアクセス用に開こうとすると、oplock は解除されます。つまり、フィルタ デーモンはそのハンドルを閉じ、ファイルのインデックス化を後で実行するように再スケジュールします。フィルタがファイルを書き込みアクセス用にオープンするとデッドロックが起きる理由はこうです。つまり、oplock の解除が行われるまでは、フィルタによるオープン処理はブロックされ、一方でこのスレッドは、オープン処理の完了待ちでビジー状態にあるため、oplock の解除を実行できないからです。filtdump.exe などのスタンドアロン プログラムではフィルタがきちんと動作するのに、フィルタ デーモンでは停止してしまうという場合には、おそらくこれが原因です。

埋め込みドキュメントを持つことができるファイル用のフィルタでは、埋め込みドキュメント用のフィルタ DLL の読み込みと呼び出しを行う必要があります。Platform SDK のドキュメントで解説されている BindIFilterFromStream() 関数を使用すると、この処理を簡潔に実行できます。クエリに関しては、埋め込みドキュメントから取得したテキストはすべて、ホスト ドキュメントに入っていたテキストと同様に扱われます。

LoadIFilter()BindIFilterFromStream() など、Platform SDK のドキュメントで解説されているフィルタ API は、すべてのバージョンの MSSearch で動作します。SharePoint Portal Server などの製品では、これらの関数の既定の実装がオーバーライドされていますが、これはフィルタを作成する側からは意識できないことです。

IPersist 系インターフェイスの Load() メソッドは、フィルタの 1 つのインスタンスに対して複数回呼び出すことができます。フィルタはこれに対処できるよう、先にフィルタしたファイル用に確保した既存のリソースを解放して、次のファイルのフィルタに備える必要があります。

メモリ不足の状況に適切に対処できるようにフィルタをデザインしておきましょう。MSSearch 製品は大量のメモリを消費する場合があり、メモリ不足の可能性がある状態でプロセスが動作することになります。フィルタは、こうした状況を予期して、適切なエラー コードを返す必要があります。メモリ不足のためにドキュメントをインデックス化できない場合には、何らかの方法で管理者にその旨が伝わるようになっていなくてはなりません。管理者が適切な対応を取れるようにするためです。

メモ 作成したフィルタ DLL をデジタル署名することを検討しましょう。将来のバージョンの MSSearch ベースの製品では、これが既定で必須となるようなセキュリティ機能が追加されるかもしれません。システム管理者が信頼する署名付きのバイナリだけを読み込むようになれば、製品のセキュリティが向上します。現時点で DLL に署名しておけば、将来の検討事項に対する準備は万端です。

フィルタのインストール

フィルタには、ここまでに解説してきたインターフェイスに加えて、インストール用の自己登録処理を実装する必要があります。COM では、インストールおよびアンインストール用として、DLL が DllRegisterServer 関数と DllUnRegisterServer 関数をエクスポートするよう定められています。フィルタの登録方法は COM オブジェクトの登録方法と同様で、それに加えてファイル形式とフィルタを関連付けるための処理が必要となります。インストール情報はすべてシステム レジストリに書き込まれます。

ここで解説する登録情報は、インデックス サービス用のものです。他の MSSearch ベースの製品は、フィルタ用の登録モデルをそれぞれ独自に持っています。ただし、大半の製品では、自らの構成で対応していないファイル拡張子については、インデックス サービスの登録情報を利用するようになっています。また製品によっては、そのように明示的に構成することが必要なものもあります。作成するフィルタが、MSSearch ベースのある特定の製品 1 つだけを対象としている場合には、その製品のドキュメントを参照して、登録方法を確認しましょう。その他の場合は、すべてのアプリケーションが利用できるよう、インデックス サービス用としてフィルタを登録しましょう。

メモ SharePoint Portal Server 2003 は、インデックス サービスと同じフィルタは使用しません。SharePoint Portal Server は、Windows と共に提供されている独自のバージョンのフィルタを、レジストリの次の場所にインストールします。

HKLM\software\Microsoft\SPSSearch\ContentIndexCommon\Filters

SharePoint Portal Server はレジストリのこの場所をまず参照したうえで、インデックス サービス 3.0 のレジストリ モデルを参照します。これにより、SharePoint Portal Server は、独立ソフトウェア ベンダがインデックス サービス用に作成したフィルタを読み込むことができます。SharePoint Portal Server 用のレジストリのレイアウトは、『SharePoint 製品とテクノロジ 2003 ソフトウェア開発キット (SDK)』で解説されており、この記事では取り上げません。独立ソフトウェア ベンダはインデックス サービス 3.0 のモデル向けにコーディングするだけで済みます。

フィルタは特定の形式のファイルに対して処理を行います。ファイル形式は基本的にファイル拡張子で定義されます。インデックス サービスなどのアプリケーションがファイルをフィルタしようとするときには、レジストリでファイルの拡張子が登録されている部分を参照して、読み込むフィルタを判断します。そして、レジストリの一連のリンクをたどって、フィルタ DLL の名前を突き止めます。フィルタのインストールでは、ファイル形式とフィルタ DLL の関連付けをレジストリに書き込むことが必要です。

各フィルタは次の 3 つの CLSID を持ちます (自分で作成するフィルタ用にこれらの値を生成するには uuidgen.exe を使用します)。

  • 1 つ目は、当該ファイル形式に対応する CLSID で、前述の IPersistFile::GetClassID で返すものです。サンプル フィルタでは、これは CLSID_CSmpFilter で、{8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4} という値です。

  • 2 つ目は、当該ファイル形式に対応する持続的ハンドラを表す CLSID です。サンプル フィルタでは {8B0E5E73-3C30-11d1-8C0D-00AA00C26CD4} という値を使用しています。持続的ハンドラのキーには、そのクラスに登録されている持続的ハンドラの一覧が含まれています。持続的ハンドラは、持続的なデータに対して処理を行うオブジェクトの作成を、アプリケーション全体を読み込むことなく実現できるようにするものです。たとえば、Microsoft Office Word ドキュメントをフィルタするときに、Word を起動させなくてはならないとしたら、許容できないパフォーマンスになってしまうことでしょう。代わりに、Word ドキュメントのファイル構造を理解できる小さな DLL を読み込むだけで済むようになっています。ここで重要な持続的ハンドラは具体的には IID_IFilter で、その値は {89BCB740-6119-101A-BCB7-00DD010655AF} です。この CLSID はすべてのフィルタで同じ値です。どのフィルタも IFilter を実装しているからです。

  • 3 つ目の CLSID は、IID_IFilter キーの値です。この CLSID は、当該ファイル形式用の IFilter を実装するオブジェクトを表し、ここでは {8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4} です。このキーには、InprocServer32 値が含まれており、これが DLL 名とスレッド モデルを表しています。フィルタが system32 ディレクトリなどのシステム パスに置かれている場合には、この値はファイル名だけで十分です。フィルタがシステム パス以外に置かれている場合には、この値はフルパスで指定する必要があります。

    メモ 将来のバージョンの Microsoft オペレーティング システムでは、system32 ディレクトリにファイルをインストールするのが難しくなる可能性があります。このため、フィルタは Program Files 配下にインストールして、そのフルパスをレジストリに指定するのがベストです。また、セキュリティ上の理由からも、DLL のフルパスをレジストリに指定するのはよいことです。フルパスを指定しなかった場合、"トロイの木馬" バージョンの DLL がたまたま本物のバージョンより先にプロセス パスにあったときに、トロイの木馬バージョンの方が読み込まれてしまう可能性があります。

Index Server 2.0 のフィルタ登録モデル (現在は使われていない)

アプリケーションは、.smp ファイル用のフィルタを読み込むときに、.smp キーの値を参照し、SmpFilt.Document を見つけます。続いて、このキーへ行って CLSID 値を読み込んでから、その値に一致するキーを開き、PersistentHandler 値を見つけます。次に、この値に一致するキーを開いて、PersistentAddinsRegistered の下の値をスキャンし、IID_IFilter を探します。これが見つかった場合は、その値に一致するキーを開いて、InprocServer32 を探し、フィルタ DLL の名前とスレッド モデルを見つけます。フィルタをこの方式で登録する必要があるのは、Microsoft Windows NT® 4.0 をサポートする場合に限られます。新しいフィルタは、この記事で解説する新しい方法で登録するか、または Windows バージョン番号に基づいて動的に登録します。

Windows NT 4.0 上でのサンプル フィルタのレジストリ エントリを、HKEY_CLASSES_ROOT からの相対位置で表すと、次のようになります。

.smp <No Name>: REG_SZ: SmpFilt.Document
SmpFilt.Document <No Name>: REG_SZ: Sample FilterDocument
   CLSID <No Name>: REG_SZ: {8B0E5E72-3C30-11d1-8C0D-00AA00C26CD4}
CLSID
   {8B0E5E72-3C30-11d1-8C0D-00AA00C26CD4}
   PersistentHandler <No Name>: REG_SZ: {8B0E5E73-3C30-11d1-8C0D-00AA00C26CD4}
{8B0E5E73-3C30-11d1-8C0D-00AA00C26CD4} <No Name>: REG_SZ: SmpFilt Persistent Handler
   PersistentAddinsRegistered
      {89BCB740-6119-101A-BCB7-00DD010655AF} <No Name>: REG_SZ: 
{8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4}
{8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4} <No Name>: REG_SZ: Sample Filter
   InprocServer32 <No Name>: REG_SZ smpfilt.dll
                ThreadingModel: REG_SZ Both

インデックス サービス 3.0 以降およびその他の MSSearch ベースの製品のフィルタ登録モデル

Windows 2000 に組み込まれているインデックス サービス 3.0 では、フィルタの登録は少し異なります。現在の MSSearch ベースの製品はすべてこの新しいフォーマットに準拠しています。この方式では、フィルタが必要とする一意の CLSID は 2 つだけです。

Windows 2000 にサンプル フィルタを登録した場合のレジストリ エントリを、HKEY_CLASSES_ROOT からの相対位置で表すと、次のようになります。

.smp
   PersistentHandler <No Name>: REG_SZ: {8B0E5E73-3C30-11d1-8C0D-00AA00C26CD4}
CLSID
   {8B0E5E73-3C30-11d1-8C0D-00AA00C26CD4} <No Name>: REG_SZ: SmpFilt Persistent Handler
      PersistentAddinsRegistered
         {89BCB740-6119-101A-BCB7-00DD010655AF} <No Name>: REG_SZ: {8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4}
{8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4} <No Name>: REG_SZ: Sample Filter
   InprocServer32 <No Name>: REG_SZ smpfilt.dll
                ThreadingModel: REG_SZ Both

このフォーマットは、以前の Index Server 2.0 のものと似ていますが、より単刀直入になっています。この新しいレイアウトでは、ファイル形式に対応する CLSID は不要です。ただし、下位互換性のために、もしあった場合でも引き続きサポートされるようになっています。インデックス サービスが .smp という拡張子のファイル用の IFilter を読み込むときには、まず .smp キーへ行き、PersistentHandler の値を参照します。この値から、PersistentAddinsRegistered をスキャンし、IID_IFilter を探します。IID_IFilter が見つかった場合は、その値に一致するキーを開いて、InprocServer32 を探し、フィルタ DLL の名前とスレッド モデルを見つけます。

サンプル フィルタでは、DEFINE_DLLREGISTERFILTER という C マクロが filtreg.hxx ファイルに含まれています。このマクロを使うと、フィルタの登録が楽になります。このマクロは、DllRegisterServer 関数と DllUnregisterServer 関数を生成します。また、前述の新旧 2 つのフォーマットのどちらかで適切にインストールされるよう、フィルタが動作するオペレーティング システムのバージョンを検出します。

次に示すのは、サンプル フィルタ内でのマクロの使われ方です。

SClassEntry const asmpClasses[] =
{
  { L".smp",
   L"SmpFilt.Document",
   L"Sample Filter Document",
   L"{8B0E5E72-3C30-11d1-8C0D-00AA00C26CD4}",
   L"Sample Filter Document"
  }
};

SHandlerEntry const smpHandler =
{
  L"{8B0E5E73-3C30-11d1-8C0D-00AA00C26CD4}",
  L"SmpFilt Persistent Handler",
  L"{8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4}"
};

SFilterEntry const smpFilter =
{
  L"{8B0E5E70-3C30-11d1-8C0D-00AA00C26CD4}",
  L"Sample Filter",
  L"smpfilt.dll",
  L"Both"
};

DEFINE_DLLREGISTERFILTER( smpHandler, smpFilter, asmpClasses )

自作のフィルタでこのマクロを使用するには、このサンプルに登場する名前と CLSID をフィルタに合致するものに変えます。このサンプルで "Both" と指定している箇所は、フィルタのスレッド モデルを表します。つまり、このフィルタは、アパートメント モデルとフリースレッド モデルの両方で使用できるということです。

メモ フィルタがスレッドセーフでない場合には、必ず "Apartment" を指定します。そうしないと、検索インデックス作成のプロセスがクラッシュに至ることになります。アパートメント モデルのフィルタは Microsoft Windows XP では読み込まれません。また、SQL Server などの製品でアパートメント モデルのフィルタを使用すると、クロールにかかる時間が桁違いに長くなる可能性があります。システムの堅牢性とパフォーマンスのためには、フィルタをフリースレッドとして作成し、そのように指定することを強くお勧めします。

フィルタ DLL の登録には「regsvr32 myfilter.dll」コマンドを使用し、アンインストールには「regsvr32 -u myfilter.dll」コマンドを使用します。フィルタの登録と登録解除のテストはデバッガで行いましょう。特に、登録処理に独自のコードを追加した場合はそうすべきです。regsvr32 では、登録に問題があった場合でも、その旨は必ずしも通知されません。

インデックス サービスのプロセスは、毎回の起動時に、レジストリの HKLM\System\CurrentControlSet\Control\ContentIndex\DllsToRegister の値を参照します。ここには複数の文字列値があり、その中身は DLL のリストです。これらの DLL に対して、DllRegisterServer が呼び出されます。この機能は、フィルタとワード ブレーカが登録された状態を確実に維持するために追加されたものです。アプリケーションの中には、インストール時に、処理対象のファイル形式に対応する IFilters をうっかり削除してしまうものがあります。これは一般に、複数のアプリケーションが同じファイル拡張子を使う場合にのみ生じる問題です。フィルタのインストール時に、フィルタをこのリストに追加しておくと、フィルタがインストールされた状態を確実に維持するのに役立ちます。

リソース

Microsoft Platform SDK には、IFilter の概要およびリファレンス資料に加えて、2 つのサンプル フィルタが含まれています。そのうちの 1 つが、この記事で解説したシンプル テキスト フィルタです。

もう 1 つのサンプルは、HTML メタタグ プロパティ フィルタです。このフィルタは、MSSearch ベースの製品に同梱されている HTML フィルタの上位層として動作するもので、Meta プロパティを文字列から変換して、構成可能なデータ型に変えることで、高度な検索、並べ替え、およびプロパティ取得を行えるようにします。このフィルタについては、MSDN の記事『Using HTML Meta Properties with Microsoft Index Server』 で詳しく解説されています。このサンプル フィルタは、IFilter::GetValue() メソッドの使用方法について理解を深めるのに特に役立ちます。この記事で解説したシンプル テキスト フィルタでは、このメソッドはこれといった処理をしていないからです。

フィルタ DLL のテスト

Platform SDK には、フィルタ DLL のテストに役立つツールが 3 つ含まれています。

  • iFilttst.exe は、IFilter のメソッドを呼び出して、その結果が仕様に準拠しているかをチェックするもので、フィルタの検証に役立ちます。たとえば、チャンク ID が一意の値でかつ次々と大きくなっていくかどうか、再初期化の後でも IFilter の動作が一貫しているかどうか、IFilter のメソッドに不正なパラメータを渡したときに予期したとおりのエラー コードが返って来るかどうか、などをチェックします。ifilttst.exe のドキュメントは Platform SDK に含まれています。
  • filtdump.exe は、フィルタする対象のファイルの名前を引数で受け取って、フィルタを読み込み、フィルタ DLL からの出力を表示するというものです。filtdump.exe ファイルは、Platform SDK で解説されている LoadIFilter() API を使用して、コマンド ラインで指定されたファイルに合致する適切なフィルタを読み込みます。たとえば、filtdump myfile.smp というコマンドでは、smpfilt.dll を読み込み、フィルタからすべてのテキストとプロパティを取得して、その結果を表示するよう、filtdump.exe に対して指示することになります。
  • Filtreg.exe は、フィルタのインストール情報をレジストリ内で調べます。フィルタが関連付けられているファイル拡張子をすべて列挙し、ファイル拡張子とフィルタ DLL 名を表示します。フィルタがきちんとインストールされたかどうかを検証するのによい方法です。

フィルタはさまざまな MSSearch 製品で使用されるので、複数の製品でテストしておきましょう。

IFilter のテストで行うとよい項目

  • iFilttst.exe の実行中に CPU 使用率をチェックして、応答なしの状態にならないことを確認します。これは、デッドロックが発生しないことを確認する助けになります。
  • フィルタが複数の言語をサポートする場合は、出力チャンクに正しい LCID が設定されていることを確認します。フィルタがサポートするすべての言語をテストします。
  • 非常に巨大なドキュメントのフィルタリングをテストして、フィルタが予期したとおりに動作することを確認します。
  • 対象のファイル形式がサポートするプロパティからの出力を、1 つ 1 つのプロパティについてすべてテストします。たとえば Word では、ヘッダー、メモ、テキスト ボックスなどについてテストします。
  • 大きなプロパティ値の使用をテストします。たとえば、HTML 文書で大きなメタタグを使用するなどです。
  • フィルタがファイル ハンドルをリークしないことをチェックするために、フィルタから出力を取得した後でファイル ハンドルを編集するか、またはファイルをフィルタする前後に oh.exe などのツールを使用します。oh.exe の詳細については、『Windows 2000 Resource Kit Tool: Open Handles (oh.exe)』を参照してください。
  • 埋め込みドキュメントをサポートするファイル形式の場合は、埋め込みドキュメントからの出力の取得をテストします。異なる種類のネストした埋め込みを持つドキュメントを作成することを検討します。
  • フィルタに関連付けられているすべてのファイル形式をテストします。たとえば HTML フィルタであれば、.htm と .html の両方のファイル形式で動作することをチェックします。Indexing Service SDK の filtreg.exe を使用すると、フィルタが関連付けられているすべてのファイル拡張子の一覧を表示することができます。
  • 壊れているファイルでテストします。フィルタが適切にエラーとなるかを調べます。
  • 暗号化をサポートするアプリケーションの場合は、フィルタが出力するのが暗号化されたテキストでないことをテストします。
  • ファイルのコンテンツで複数の特別な Unicode 文字を使用して、その出力をテストします。次の図は、テストする Unicode 文字の例を示したものです。

Dd583122.odc_sppthowtowriteafilter_fig1(ja-jp,office.11).gif

図 1 テストする Unicode 文字

セットアップのテスト

  • インストールの失敗から復旧できなくてはなりません。たとえば、1 度キャンセルした後でセットアップを再起動した場合などです。
  • アンインストールでは、フィルタに関連するすべてのファイルを削除しなくてはなりません。
  • アンインストールでは、フィルタのインストールに関連しないファイルは削除してはなりません。
  • フィルタに関連するレジストリ キーはアンインストール時に削除しなくてはなりません。
  • インストール ディレクトリからファイルが削除されている場合であってもアンインストールが動作しなくてはなりません。

テストに関するその他のリファレンス

IFilter インターフェイスのテストに関する情報については、『Windows Platform SDK』の「IFilter」のトピックを参照してください。

フィルタのテストに関する情報については、『Platform SDK』の「Testing Filters」を参照してください。

まとめ

情報が爆発的に増加している今日、サイトが抱える情報が増えていく中で、必要なドキュメントをすばやく簡単に見つけ出すために、人々は検索のメカニズムに大きく依存するようになっています。この記事では、サンプル フィルタの実装を吟味し、参照用として使用できるコードについて解説してきました。

フィルタとは、IFilter インターフェイスと、IPersist 系の 1 つまたは複数のインターフェイスを実装した COM オブジェクトです。フィルタ オブジェクトは、これらのインターフェイスのメソッドを起動して、ドキュメントのテキストとプロパティを取得します。独自のファイル形式をフルテキスト インデックスに確実に取り込むには、IFilter を実装したオブジェクトを作成するのが唯一の方法です。ここでは、検索アプリケーションが使用できるフィルタの作成方法と、COM インターフェイスのフィルタ サポート、フィルタの実装、およびフィルタのインストールとテストについて学びました。

Microsoft Office SharePoint Portal Server 2003、Microsoft SQL Server、Microsoft Exchange Server、Microsoft インデックス サービスなどの MSSearch ベースの製品を使用してユーザーが目的のデータを確実に見つけられるようにするためには、対象のファイル形式用の IFilter オブジェクトを作成するのが最適です。Microsoft Search (MSSearch) ベースの製品は、フィルタを使用してファイルのコンテンツとプロパティを抽出し、フルテキスト インデックスに取り込みます。