Lucene8:Tokenizerの選択が元文書のオフセット位置取得を左右する

2019年8月11日

ここで取り上げるのはLucene 8.2.0だ。

さんざんLuceneを使ってきて、今回初めてHighlighterの機能を試そうとしてうまく行かず、重要な事実に初めて気がついた。全く知らなかったのだが、表題の通りだ。

Luceneでは元文書を一つのTokenizer、複数のフィルターで処理してTermを取得するのだが、このTokenizerの選択が元文書のオフセット位置に影響してしまう。

次の二つのテストを見てみる。両者ともに入力は「吾輩は猫である」だ。

前者では、NGramTokenizerのみを使用している。後者ではWhitespaceTokenizerの後にNGramFilterを使用している。これだけの違いだ。

  @Test
  public void tokenizer1() throws Exception {
    try (Analyzer an = new Analyzer() {
      protected TokenStreamComponents createComponents(String fieldName) {                  
        NGramTokenizer tokenizer = new NGramTokenizer(1, 2);        
        return new TokenStreamComponents(tokenizer, tokenizer);
      }
    }) {
      TokenStream stream = an.tokenStream(null,  "吾輩は猫である");
      stream.reset();    
      CharTermAttribute t = stream.getAttribute(CharTermAttribute.class);
      OffsetAttribute o = stream.getAttribute(OffsetAttribute.class);
      System.out.println("\nNGramTokenizer");
      while (stream.incrementToken()) {
        System.out.println(t + ",offset " + o.startOffset() + ":" + o.endOffset());
      }  
    }
  }

  @Test
  public void tokenizer2() throws Exception {
    try (Analyzer an = new Analyzer() {
      protected TokenStreamComponents createComponents(String fieldName) {  
        Tokenizer tokenizer = new WhitespaceTokenizer();  
        NGramTokenFilter filter = new NGramTokenFilter(tokenizer, 1, 2, false);       
        return new TokenStreamComponents(tokenizer, filter);
      }
    }) {
      TokenStream stream = an.tokenStream(null,  "吾輩は猫である");
      stream.reset();    
      CharTermAttribute t = stream.getAttribute(CharTermAttribute.class);
      OffsetAttribute o = stream.getAttribute(OffsetAttribute.class);
      System.out.println("\nWhitespaceTokenizer");
      while (stream.incrementToken()) {
        System.out.println(t + ",offset " + o.startOffset() + ":" + o.endOffset());
      }  
    }
  }

入力文には空白が無いので、どちらも同じ結果かと思いきや、結果は以下だ。

NGramTokenizer
吾,offset 0:1
吾輩,offset 0:2
輩,offset 1:2
輩は,offset 1:3
は,offset 2:3
は猫,offset 2:4
猫,offset 3:4
猫で,offset 3:5
で,offset 4:5
であ,offset 4:6
あ,offset 5:6
ある,offset 5:7
る,offset 6:7

WhitespaceTokenizer
吾,offset 0:7
吾輩,offset 0:7
輩,offset 0:7
輩は,offset 0:7
は,offset 0:7
は猫,offset 0:7
猫,offset 0:7
猫で,offset 0:7
で,offset 0:7
であ,offset 0:7
あ,offset 0:7
ある,offset 0:7
る,offset 0:7

後者では、常に取得位置が0,7になってしまっている。WhitespaceTokenizerが0,7位置しか返していないせいだ。この状態でHighlighterを使うと、当然ながら常に0.7の範囲をハイライトしてしまう。

TokenizerとTokenFilterの違いがなぜあるのか理解できなかったのだが、ここに来て重大な違いのあることがわかった。