Hatena::ブログ(Diary)

<s>gnarl,</s>技術メモ”’<marquee><textarea>¥ このページをアンテナに追加 RSSフィード Twitter

2008-09-12

Lucene/JapaneseAnalyser/Sen、辞書にすごく長い単語が含まれてるとその単語を含んだドキュメントを追加する際にIndexOutOfBoundsExceptionで失敗する

環境:sen 1.2.2.1

IndexOutOfBoundsExceptionって明らかにバグやんけ。

エラーメッセージ

java.lang.RuntimeException: java.lang.IndexOutOfBoundsException
	at net.java.sen.Dictionary.getPosInfo(Dictionary.java:149)
	at net.java.sen.Viterbi.analyze(Viterbi.java:134)
	at net.java.sen.StringTagger.analyze(StringTagger.java:180)
	at net.java.sen.StreamTagger.hasNext(StreamTagger.java:109)
	at org.apache.lucene.analysis.ja.sen.SenTokenizer.next(SenTokenizer.java:45)
	at org.apache.lucene.analysis.ja.POSFilter.next(POSFilter.java:73)
	(略)
	at org.apache.lucene.analysis.LowerCaseFilter.next(LowerCaseFilter.java:33)
	at org.apache.lucene.analysis.StopFilter.next(StopFilter.java:107)
	at org.apache.lucene.index.DocumentWriter.invertDocument(DocumentWriter.java:219)
	at org.apache.lucene.index.DocumentWriter.addDocument(DocumentWriter.java:95)
	at org.apache.lucene.index.IndexWriter.buildSingleDocSegment(IndexWriter.java:1013)
	at org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:1001)
	at org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:983)
	(以下略)

対処法

net.java.sen.Dictionary#getPosInfo内のループを、

	while (ffd.read(b, cnt, 1) != -1 && b[cnt] != (byte) '\0')
		cnt++;

から

	while (ffd.read(b, cnt, 1) != -1 && b[cnt] != (byte) '\0') {
		cnt++;
		if(b.length <= cnt) {
			byte new_b[]=new byte[b.length*2];
			for(int i=0;i<b.length;i++)
				new_b[i]=b[i];
			b=new_b;
		}
	}

に変更

問題解決の経緯

一見Dictionary#getPosInfo内での配列操作が原因の例外に見えるがそれは罠、

    } catch (Exception e) {
      throw new RuntimeException(e.toString());
    }

なるコードがスタックトレースを握りつぶしてる(IOExceptionをRuntimeExceptionに変換したかったんだろう、意図としては)。

実際のところは、

 	at java.nio.Buffer.checkBounds(Unknown Source)
 	at java.nio.ByteBuffer.get(Unknown Source)
 	at java.nio.DirectByteBuffer.get(Unknown Source)
 	at net.java.sen.io.MappedBufferedReader.read(MappedBufferedReader.java:73)

が例外の元。java.nio.DirectByteBuffer#getに渡すパラメータがおかしいようだ。

問題のコードは、

	//Dictionary#get
	int cnt = 0;
	byte b[] = new byte[256];
	ffd.seek(f);
	while (ffd.read(b, cnt, 1) != -1 && b[cnt] != (byte) '\0')
		cnt++;

このへん。配列の境界をチェックしてないように見えるが、例外が投げられてるのはffd.read()内部。

ffd.readの帰り値を-1と比較してるところ、一見正しそうに見えるが

	//net.java.sen.io.MappedBufferedReader
	public int read(byte b[], int start, int length) {
		map.get(b, start, length);
		return length; // !!!
	}

インターフェース定義部にはコメントの類が一切書かれておらず、何が正しい仕様なのかは謎。このへん明らかに怪しい……だがこれはファイル終端の判定っぽいので別件くさい。

さて、DirectByteBuffer#getがIndexOutOfBoundsExceptionを投げるのは

BufferUnderflowException - If there are fewer than length bytes remaining in this buffer

IndexOutOfBoundsException - If the preconditions on the offset and length parameters do not hold

http://java.sun.com/j2se/1.5.0/docs/api/java/nio/ByteBuffer.html#get%28byte[], int, int%29]

バッファ終端を過ぎて読もうとしてたら別の例外だよなー、あれ、ひょっとして

	//net.java.sen.io.MappedBufferedReader
	public int read(byte b[], int start, int length) {
		if(b.length <= start) throw new IllegalArgumentException("you are an idiot: b.length <= start");
		map.get(b, start, length);
		return length;
	}
java.lang.IllegalArgumentException: you are an idiot: b.length <= start

YES!!

原因は確定したのでバッファ再確保処理を追加:

	while (ffd.read(b, cnt, 1) != -1 && b[cnt] != (byte) '\0') {
		cnt++;
		if(b.length <= cnt) {
			byte new_b[]=new byte[b.length*2];
			for(int i=0;i<b.length;i++)
				new_b[i]=b[i];
			b=new_b;
		}
	}

落ちなくなった^^

まとめ

あちこちにマジックナンバー256が頻出・ほとんどテスト書いてない・オフィシャルサイトがSPAMに乗っ取られてると、有名なプロダクトな割にかなり危険な状況。こわい。

GoSenなる派生プロジェクトがある。

Furigana processing support

Source upgraded to Java 5

Improved GPL compatibility through removal of the dependency on commons-logging

Pure Java dictionary compilation with no dependency on Perl

Greatly reduced heap usage during dictionary compilation, allowing compilation with the default Java heap settings

EUC-JISX0213 character set support allowing correct compilation of the Ipadic dictionary

Significantly improved text analysis speed

Support for morphemes within Ipadic with multiple alternative readings

Full Javadoc class documentation

JUnit test suite

ほとんど読んでないけど、かなりがっつり書き直してある印象。こっちをためす価値はある、かも。

ShimojimojiShimojimoji 2008/12/31 15:40 GoSenも結構バグがありますね。
あとソースのコメント見る限り、実装者が仕様を理解しているとは思えない

gnarlgnarl 2009/01/23 18:16 javaにまともな日本語形態素解析エンジンはないのでしょうか……
大人しくMeCabでも呼んでろってことなのかねー。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/gnarl/20080912/1221189985