AttributeFactory (New TokenStream API)

今回もNew TokenStream APIに関連した機能を見ていきます。前回はサンプルを通して同API全体的に見ていきましたが、今回は少し詳細な内容で、Attributeを生成する役割を持つAttributeFactoryについて紹介します。なお、今回紹介するLuceneのコードのバージョンも3.5です。

AttributeFactoryについて

さて、このAttributeFactoryですが、通常直接使う事はありません。同APIの内部処理としてAttributeを継承したインターフェースを受け取り、対応するAttributeImplのサブクラスのインスタンスを生成する役割を持っています。デフォルトのAttributeFactoryの実装も提供されており、通常はこれを使い(未指定で自動的に使われ)ます。デフォルトの実装では(前回も紹介しましたが)、インターフェースと同じパッケージで、インターフェース名に接尾辞'Impl'が付いたクラスをLookupしてインスタンスを生成します。

デフォルト実装のDefaultAttributeFactoryのコードは<こちら>です。やる事の割に少々長いコードになっていますが、その多くが例外処理と、一度Lookupしたクラスをキャッシュする処理で、それ以外をのぞくと以下の2つの処理が見えてきます。

  1. AttributeのサブインターフェースのClassインスタンス(Class)からインターフェース名+ImplのClassインスタンスを返す
    ただし、TermAttribute.classが渡された場合は例外的にCharTermAttributeImpl.classを返す
  2. 1で返されたClassのインスタンスを作成する

コードにもコメントが書かれていますが、1のTermAttributeは現在deprecatedなので、特別に上記のような対応が行われています。既存のコードでエラーにならないように、今の所CharTermAttributeImplはTermAttributeも実装しています。インターフェースのCharTermAttributeがTermAttributeを継承しているわけではない事に注意して下さい。(CharTermAttributeだとして取得した場合にはTermAttributeで定義されているメソッドは呼び出せません)4.0のtrunkからはTermAttribute及び上記対応は削除されています。
参考Issue:LUCENE-2372

デフォルト実装の紹介は以上で、次はAttributeFactoryの実装を別途作る事で、上記のデフォルトの振る舞いを変更してみます。
今回もgithubにコードを置いていますので、出来れば動かしながら見てみて下さい。sbtで動作しますが、Scala IDEとivyDEを入れたEclipse 3.6.2でも動作するようになっているので(テストは、、sbtがいいかも)、お好きな方で試してみて下さい。
<study-attributeFactory>

CustomAttributeFactoryを作ってみよう

最初に、デフォルト実装を変えたい時の要求を考えてみます。新たなAttributeを追加したいという事だけだったら、任意のパッケージにAttributeとAttributeImplのセットを作れば良いだけですので、カスタム実装を作る必要はありません。思いつくのは以下の2つぐらいです。(他にもあったら教えて下さい・・・)

  1. 既存のAttributeインターフェースに対して別の実装を割り当てたい
    (既存のTokenizerやTokenFilterのコードを変更せずに振る舞いを変えたい)
  2. あるAttributeインターフェースに対する複数の実装を、状況に応じて切り替えられるようにしたい
    (直接ハンドリングする為の情報は渡せないので、ThreadLocalを使って情報を受け渡して切り替えるイメージです)

2はサンプルとしては複雑になりそうだし、効果的な使い道を思いつかないので、今回は1を実現する実装を作ってみます。
次にAttributeFactoryを拡張するポイントを考えてみましょう。

No 変更点
1 マッチ方法を変える 接頭辞'Custom'+インターフェース名のクラスにマッチするクラスを探す
2 インターフェースと実装のパッケージを変えられるようにする 指定したパッケージからクラスを探す
3 マッチしなかった時の振る舞い エラーとする or デフォルト実装に委譲する

パフォーマンスや動的変更など他にもポイントはあると思いますが、機能的には大体こんなものでしょうか。1は分かりやすいですが、個人的にLuceneのパッケージ(org.apache.lucene.analysis.tokenattributes)に独自のコードを入れるのには少々抵抗がありますので、今回は2のやり方を選択します。また、3のマッチしなかった時の振る舞いも重要です。エラーとする場合、既存のインターフェースに対応する実装を全て2のパッケージに用意する必要があり、あまりよろしくありません。(全て変えたいなら別ですが・・・)なので、エラーとなった場合はデフォルト実装に委譲するというのが良さそうです。実装したコードはこちらです。→<CustomAttributeFactory>

まずコンストラクタでlookupするパッケージ名を受け取ります。そしてcreateAttributeInstanceメソッドで受け取ったパッケージ名とメソッドパラメータのクラスの単純名を使って実装クラスをlookupして、インスタンスの生成を試みます。エラーなくインスタンスの生成が出来ればそのまま返しますし、クラスが見つからない場合はClassNotFoundExceptionがthrowされますので、その場合はデフォルト実装を実行した結果を返しています。またコメントにも書いていますが、実際にこのような実装を使う場合は、デフォルト実装と同じように、毎回lookupせずにインターフェースに対応する実装クラスのClassインスタンスをキャッシュしておくのがいいでしょう。また、実際に使う際は、ほぼキャッシュが必須になるため、インスタンスをsingletonにする必要もあります。CustomAttributeFactory自体をobjectで書けば良かったのですが、コンストラクタでパッケージ名を受け取れるようにしたかったので、インスタンスを保持するクラスは別途作りました。→<Factory>
実際使う場合もデフォルト実装と同じようにstatic変数に入れておくなりすると良いでしょう。

Factoryが出来たので、早速使ってみます。次の2つを作りました。

  1. 動作確認用Attributeのインターフェースと実装
    <EmptyAttribute>
  2. CharTermAttributeの別実装
    <CharTermAttributeImpl>
    ※当初元のクラスを継承してちゃちゃっと終わりにする予定だったのですが、変更したいメソッドがことごとくfinalだったので、、インターフェースを全て実装して委譲する事にしました(次回以降で利用する予定)

内容はどちらも大したことはしてません。

さて、準備が整いましたので、早速テストコードで動かしてみましょう。→<CustomAttributeFactoryTest>

study-AttributeFactoryのディレクトリ直下でsbtとして、testでテストが動作します。無事に動きましたか?[success]と出れば正常にテストが完了しています。
このテストではインターフェースと実装クラスがペアになってるケースと、既存インターフェースに対する別実装を提供するケースと、対応する実装がパッケージにない場合にデフォルト実装が使われることを確認しています。

無事にNew TokenStream APIのデフォルト実装を切り替える事が出来ましたね!
今回はやりませんが、実際にSolrとかで使う場合は、Tokenizerのコンストラクタに今回のAttributeFactoryのインスタンスを渡す、TokenizerFactoryを作ってschema.xmlでそれを指定すればOKです。
という事で今回はAttributeFactoryについてでした。次回はTokenizerの紹介に(ようやく)戻る予定です。