言語判定プラグイン for Apache Nutch


Apache Nutch についてちらほら調べてきたけど、いよいよ 言語判定ライブラリを使って Nutch 用の言語判定プラグインを作ってみた。


プラグイン本体はとても小さいので、言語判定ライブラリに同梱されている。
上のドキュメントの通りに設置すれば、以下のように様々な言語での判定が出来るようになる。



利用方法は上のドキュメントを読んでもらうとして、ここでは言語判定プラグインはどのように作られているか、という話をしよう。

plugin.xml

Apache Nutch のプラグインと言語判別 では、Nuth に標準付属している言語識別プラグインは3つの extension を含むという説明をした。

HTMLLanguageParser
メタデータ(meta タグ、HTTP ヘッダなど)に含まれる言語情報を取得し、ドキュメントのメタデータフィールドに格納する。HtmlParseFilter extension-point にプラグインする。
LanguageIdentifier
メタデータにて指定された言語名をインデックス(に登録されるフィールドを持つ Document オブジェクト)の lang フィールドに言語名を設定する。メタデータにて言語が指定されていない場合、統計情報を元に言語判定を行い、その結果を用いる。IndexingFilter extension-point にプラグインする。
LanguageQueryFilter
検索条件に lang を追加する extension。QueryFilter extension-point にプラグインする。


ここで LanguageIdentifier extension のみ差し替えて、残りの2つは標準のものをそのまま使うことにしたい。プラグインなのでもちろんそういうことができる。plugin.xml を以下のように書けばよい。

<plugin
   id="language-detector"
   name="Language Detection Parser/Filter"
   version="1.0.0"
   provider-name="labs.cybozu.co.jp">

    <runtime>
      <library name="language-identifier.jar">
         <export name="*"/>
      </library>
      <!-- ★ 追加 ★ -->
      <library name="langdetect-nutch.jar">
         <export name="*"/>
      </library>
      <library name="jsonic-1.2.0.jar" />
      <library name="langdetect.jar" />
      <!-- ★ 追加 ★ -->
   </runtime>

   <requires>
      <import plugin="nutch-extensionpoints"/>
   </requires>

   <extension id="org.apache.nutch.analysis.lang.LanguageParser"
              name="Nutch language Parser"
              point="org.apache.nutch.parse.HtmlParseFilter">
      <implementation id="LanguageParser"
                      class="org.apache.nutch.analysis.lang.HTMLLanguageParser"/>
   </extension>

   <!-- ★ 変更 ★ -->
   <extension id="com.cybozu.labs.nutch.plugin.LanguageDetectionFilter"
              name="language detection filter"
              point="org.apache.nutch.indexer.IndexingFilter">
      <implementation id="LanguageDetectionFilter"
                      class="com.cybozu.labs.nutch.plugin.LanguageDetectionFilter"/>
   </extension>
   <!-- ★ 変更 ★ -->

   <extension id="org.apache.nutch.analysis.lang.LanguageQueryFilter"
              name="Nutch Language Query Filter"
              point="org.apache.nutch.searcher.QueryFilter">
      <implementation id="LanguageQueryFilter"
                      class="org.apache.nutch.analysis.lang.LanguageQueryFilter">
        <parameter name="raw-fields" value="lang"/>
      </implementation>
   </extension>

</plugin>


標準の言語識別プラグインの plugin.xml のうち、★の部分について追加や変更を行っている。


最初の追加部は、言語判定ライブラリとプラグイン実装を指定している。extension-points を持つ jar については <export name="*"/> を指定する必要があるようだ*1
プラグインの中から使われるライブラリは <library> のみを指定すれば参照可能になる。おそらく子クラスローダーがプラグインごとにちゃんと作られているようで、参照は「当該プラグインの中からのみ」可能になっている。したがって、他のプラグインで同じライブラリの別バージョンが参照されているような場合も衝突は起きない、のかもしれない。まああまりそういう事態は嬉しくないけれど。


変更部は IndexingFilter extension の差し替えを指定している。id や class に実装した extension のクラス名を書くだけなので、何も難しくないだろう。


これで、HTMLLanguageParser と LanguageQueryFilter は温存しつつ、IndexingFilter extension だけを差し替えることが出来る。

IndexingFilter extension-point

Apache Nutch のプラグインの作り方 にて、IndexingFilter extension-point についてのとても簡単な説明を行っているので、そちらを参照しつつ。
実装しなければならないのは次の2点。

  • setConf() にて extension の初期化
  • filter() にて、実際の処理内容


まずは setConf() にて言語判定モジュールの初期化を行う。
言語判定モジュールには、言語プロファイルを与えなければならない。jar に同梱する手もありそれは簡単なのだが、先述の通り、プロファイルの追加や削除などがとても煩雑になるという問題があるので、外部から与えるようにしたい。
そのためには、設定ファイルから言語プロファイルの在りかをディレクトリパスで指定できるようにしなければならない。


Nutch のプラグインでは、setConf() に渡される Configuration オブジェクトにより、nutch-(default|site).xml に指定された設定パラメータが取得できる

	private static final int TEXTSIZE_UPPER_LIMIT_DEFAULT = 10000;
	private Configuration conf = null;
	private LangDetectException cause = null;
	private int textsize_upper_limit;

	/** 渡された Configuration オブジェクトを使って初期化を行う */
	public void setConf(Configuration conf) {
		if (this.conf == null) {	/* 初期化を1回にするため */
			try {
				// 設定ファイルから言語プロファイルの場所を取得し、モジュールを初期化
				DetectorFactory.loadProfile(conf.get("langdetect.profile.dir"));

				// 設定ファイルから判定に用いるテキストサイズの上限を取得
				textsize_upper_limit = conf.getInt("langdetect.textsize", TEXTSIZE_UPPER_LIMIT_DEFAULT);
			} catch (LangDetectException e) {
				// 例外があれば、filter() 呼び出し時に投げられるように保持
				// (setConf() は例外を投げられないから。RuntimeException を投げるのとどちらがいい?)
				cause = e;
			}
		}
		this.conf = conf;	/* getConf() のために保持 */
	}


上記のコードのポイントは、わざわざ初期化処理を1回しか行わないように書いていること。
Nutch のインデクサ等は Hadoop 上で実装されており、おそらく通常は1つの Mapper プロセスから setConf() は1回しか呼び出されないはずなため、このように書く必要はない。
しかし、Hadoop を standalone 状態で動かしたときに限って、複数の Mapper が擬似的に1つのプロセスの中で動かされるため、1つのプロセスの中で初期化処理が複数回実行されることになり、場合によっては都合の悪いことが起きることが考えられるため、上記のようにしているわけだ。
いわゆる並列処理ではこんなコードでは初期化の実行が1回しか行われない保証は全くないわけだが、そういう理由なのでカジュアルな防止コードで済ませている。


filter() では実際の言語判定コードを実装する。前回の記事にもサンプルとしてあげたが、再掲。

	public NutchDocument filter(NutchDocument doc, Parse parse, Text url,
			CrawlDatum datum, Inlinks inlinks) throws IndexingException {

		// meta タグに language の指定があれば、それを言語とする。
		String lang = parse.getData().getParseMeta().get(Metadata.LANGUAGE);
		if (lang == null) {

			// 指定がなければ Language Detection Library を使って統計情報から言語を推定する
			StringBuilder text = new StringBuilder();
			text.append(parse.getData().getTitle()).append(" ").append(parse.getText());
			try {
				Detector detector = DetectorFactory.create();
				detector.append(text.toString());
				lang = detector.detect();
			} catch (LangDetectException e) {
				throw new IndexingException("Detection failed.", e);
			}
		}
		if (lang == null) lang = "unknown";

		// ドキュメントのインデックスに言語メタデータを追加し、返す
		doc.add("lang", lang);
		return doc;
	}


Nutch 標準の言語識別プラグインでは meta タグだけではなく、HTTP ヘッダーの Language が設定されていればそちらを取るように実装されていた。が、HTTP ヘッダーがページごとにちゃんと正しい言語を教えてくれるんだろうか、と考えるとどうもそういうふうには思えなかったのと、実際の HTTP ヘッダーで 'Language: it-IT' となっていて少し困ったケース(間違ってはいない)があったので、本プラグインではそちらは見ないことにした。
meta や HTTP ヘッダーの指定を事前情報とした推論をするのが一番正解に近い気がしないでもないが、そこらへんは要望があれば追々、という感じで。


この通り、Nutch のプラグインは比較的容易に実装できる。
問題はドキュメントがないこと……。まだ何をする extension-points かわかってないのもそこそこあるんだよなあ。

*1:export@name の値はおそらくクラス名を指定できるのだと思うが、そういうサンプルやドキュメントがないので確たることは不明