CLOVER🍀

That was when it all began.

Lucene 5.0.0のKuromojiに、mecab-ipadic-neologdの辞書を組み込むと実行時にエラーになる話

追記)
LuceneにIssueとパッチを出してみましたが、取り込まれたようです。次のバージョンで解消されるでしょう。

ここ最近、LuceneのKuromojiにmecab-ipadic-neologdの辞書を組み込んでみているのですが、3/19を境にneologdの辞書を組み込むとエラーになるようになりました。

辞書を使ったビルドはできるのですが、実際に使おうとするとこのようなエラーを見ることになります。

java.lang.ExceptionInInitializerError
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary.getInstance(TokenInfoDictionary.java:65)
	at org.apache.lucene.analysis.ja.JapaneseTokenizer.<init>(JapaneseTokenizer.java:212)
	at org.apache.lucene.analysis.ja.JapaneseTokenizer.<init>(JapaneseTokenizer.java:198)
	at org.apache.lucene.analysis.ja.JapaneseAnalyzer.createComponents(JapaneseAnalyzer.java:88)
	at org.apache.lucene.analysis.Analyzer.tokenStream(Analyzer.java:179)
〜省略〜
Caused by: java.lang.RuntimeException: Cannot load TokenInfoDictionary.
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary$SingletonHolder.<clinit>(TokenInfoDictionary.java:74)
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary.getInstance(TokenInfoDictionary.java:65)
	at org.apache.lucene.analysis.ja.JapaneseTokenizer.<init>(JapaneseTokenizer.java:212)
	at org.apache.lucene.analysis.ja.JapaneseTokenizer.<init>(JapaneseTokenizer.java:198)
	at org.apache.lucene.analysis.ja.JapaneseAnalyzer.createComponents(JapaneseAnalyzer.java:88)
	at org.apache.lucene.analysis.Analyzer.tokenStream(Analyzer.java:179)
〜省略〜
Caused by: java.io.EOFException
	at org.apache.lucene.store.InputStreamDataInput.readBytes(InputStreamDataInput.java:47)
	at org.apache.lucene.util.fst.BytesStore.<init>(BytesStore.java:71)
	at org.apache.lucene.util.fst.FST.<init>(FST.java:387)
	at org.apache.lucene.util.fst.FST.<init>(FST.java:322)
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary.<init>(TokenInfoDictionary.java:47)
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary.<init>(TokenInfoDictionary.java:33)
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary$SingletonHolder.<clinit>(TokenInfoDictionary.java:72)
	at org.apache.lucene.analysis.ja.dict.TokenInfoDictionary.getInstance(TokenInfoDictionary.java:65)
	at org.apache.lucene.analysis.ja.JapaneseTokenizer.<init>(JapaneseTokenizer.java:212)
	at org.apache.lucene.analysis.ja.JapaneseTokenizer.<init>(JapaneseTokenizer.java:198)
	at org.apache.lucene.analysis.ja.JapaneseAnalyzer.createComponents(JapaneseAnalyzer.java:88)
	at org.apache.lucene.analysis.Analyzer.tokenStream(Analyzer.java:179)
〜省略〜

問題になっているのは、このあたり。

https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/core/src/java/org/apache/lucene/util/fst/FST.java#L387
https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/core/src/java/org/apache/lucene/util/fst/BytesStore.java#L71
https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/core/src/java/org/apache/lucene/store/InputStreamDataInput.java#L47

@moco_betaさんもこちらを見てくださったのですが、まさにこの通りです。

もう少し具体的に書くと、「読みとるべき辞書データのトータルバイト数」を辞書ファイルの冒頭に書いておき、実行時はそれを信用して辞書のロードを行うのですが、ファイルに書いてあるサイズと実際に読み取れるサイズがずれた状態になっているようです。

http://mocobeta-backup.tumblr.com/post/114198209897/lucene-kuromoji-5-0-0

ここでいう「読み取るべき辞書データのトータルバイト数」というのは、以下のreadVLongしたnumBytesのことを言っていて

    long numBytes = in.readVLong();
    bytes = new BytesStore(in, numBytes, 1<<maxBlockBits);

https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/core/src/java/org/apache/lucene/util/fst/FST.java#L386
この期待するバイト数、もしくはブロック長サイズとずれた状態を検出すると、EOFExceptionが飛びます。

      final int cnt = is.read(b, offset, len);
      if (cnt < 0) {
          // Partially read the input, but no more data available in the stream.
          throw new EOFException();
      }

https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/core/src/java/org/apache/lucene/store/InputStreamDataInput.java#L47

とまあ、そこまではわかるのですが、なんでこういうことになったのか?という話ですね。

さらに、前述の@moco_betaさんのエントリによると、Lucene 4.10.xで辞書を作り、Lucene 5.0.0で読ませると動くという…。

そもそも「自分はずっとneologdの辞書をLucene 5.0.0で使っていて、それで動いていた」ということを考えると、辞書の更新によりLuceneの何らかの潜在的な問題を踏んだと考えるのが自然だと思います。
※@overlastさん、neologd側も確認していただいてありがとうございました

で、ちょっとデバッグしてみたのですが、FSTに書き込んだトータルバイト数がきちんと読み出せていることは確認できました。となると、ホントに後続する書き出しの部分が怪しいです。

Kuromojiの辞書構築部分で、FSTの書き出し指示をしているのはここになります。

  protected void writeFST(String filename) throws IOException {
    Path p = Paths.get(filename);
    Files.createDirectories(p.getParent());
    fst.save(p);
  }

https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/analysis/kuromoji/src/tools/java/org/apache/lucene/analysis/ja/util/TokenInfoDictionaryWriter.java#L48

このFST#saveの実装はこうなっています。

  public void save(final Path path) throws IOException {
    try (OutputStream os = Files.newOutputStream(path)) {
      save(new OutputStreamDataOutput(new BufferedOutputStream(os)));
    }
  }

https://github.com/apache/lucene-solr/blob/lucene_solr_5_0_0/lucene/core/src/java/org/apache/lucene/util/fst/FST.java#L606

あれ…?

ここで、4.10.4のFST#saveの実装を見てみましょう。

  public void save(final File file) throws IOException {
    boolean success = false;
    OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
    try {
      save(new OutputStreamDataOutput(os));
      success = true;
    } finally { 
      if (success) { 
        IOUtils.close(os);
      } else {
        IOUtils.closeWhileHandlingException(os); 
      }
    }
  }

https://github.com/apache/lucene-solr/blob/lucene_solr_4_10_4/lucene/core/src/java/org/apache/lucene/util/fst/FST.java#L588

そういえば、Lucene 5からはNIO.2を使うように変更されたんでしたね。

[翻訳] Apache Lucene Migration Guide (Lucene 5)
http://mocobeta-backup.tumblr.com/post/112142588992/apache-lucene-migration-guide-lucene-5

LUCENE-5945 / Full cutover to Path api from java.io.File
https://issues.apache.org/jira/browse/LUCENE-5945

で、Lucene 5.0.0のFST#save、BufferedOutputStreamの扱いが…。

というわけで、こう修正してみました。

  public void save(final Path path) throws IOException {
    try (OutputStream os = Files.newOutputStream(path);
         BufferedOutputStream bos = new BufferedOutputStream(os)) {
      save(new OutputStreamDataOutput(bos));
    }
  }

だって、BufferedOutputStreamをクローズしてないじゃないですか…。となると、バッファリングされたままのデータが書き出されないことが起こりえますよね?Lucene 4.10.4ではクローズしてますからね。

この状態で、Lucene Kuromojiのディレクトリで「ant regenerate」、「ant jar-core」を実行してできあがったJARファイルを使うと、問題は解消されました。

というわけで、たぶんLuceneのバグだと思います。3/19のneologd側の辞書更新で、たまたまバッファリングされていたデータが書き出されない状態になって発現したのだと。

@overlastさんには、ご迷惑をおかけしました…スミマセン…。