CLOVER🍀

That was when it all began.

LuceneのAnalyzer、KuromojiのModeごとの挙動を確認する

昨日、LuceneのAnalyzerをいくつか触ってみたわけですが、そのうちのJananeseAnalyzer(形態素解析器Kuromojiを使っているやつ)のEXTENDEDモードの挙動が納得いかなくて、もう少し追ってみることにしました。

なにせ、ロンウィットのページ
http://www.rondhuit.com/solr%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%AF%BE%E5%BF%9C.html
によれば、

モード 説明
NORMAL 形態素解析による通常の単語分割を行う
SEARCH 複合語で構成された単語を細かく分割する。例えば「関西国際空港」という固有名詞を「関西」「国際」「空港」に分割する。このため、「国際」や「空港」で「関西国際空港」をヒットすることができるようになる。
EXTENDED SEARCHモードの処理をしつつ、追加で、辞書にない未知語をuni-gramに分割する。例えば「ディジカメ」という未知語をデ/ィ/ジ/カ/メに分割する。これにより、未知語の検索漏れを防ぐ

というような感じだったのですが、EXTENDEDモードでJapaneseAnalyzerを使うと、辞書にないような文字がごそっと消えました。

この挙動をもうちょっと確認するために、JapaneseAnalyzerではなくて、まずはJapaneseTokenizerを使ってみることにしました。

src/main/scala/JapaneseTokenizerTest.scala

import java.io.StringReader

import org.apache.lucene.analysis.{Tokenizer, TokenStream}
import org.apache.lucene.analysis.ja.JapaneseTokenizer
import org.apache.lucene.analysis.ja.dict.UserDictionary
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute

object JapaneseTokenizerTest {
  def main(args: Array[String]): Unit = {
    val texts =
      List(
        "すもももももももものうち。",
        "メガネは顔の一部です。",
        "日本経済新聞でモバゲーの記事を読んだ。",
        "Java, Scala, Groovy, Clojure",
        "LUCENE、SOLR、Lucene, Solr",
        "アイウエオカキクケコさしすせそABCXYZ123456",
        "Lucene is a full-featured text search engine library written in Java."
      )

    val modes =
      List(
        JapaneseTokenizer.Mode.SEARCH,
        JapaneseTokenizer.Mode.NORMAL,
        JapaneseTokenizer.Mode.EXTENDED
      )

    for {
      text <- texts
      mode <- modes
    } withJapaneseTokenizer(text, mode)(displayTokens)
  }

  def withJapaneseTokenizer(text: String, mode: JapaneseTokenizer.Mode)(body: (String, TokenStream) => Unit): Unit = {
    val tokenizer = new JapaneseTokenizer(new StringReader(text),
                                          null,
                                          true,
                                          mode)

    println(s"Mode => $mode Start")

    try {
      body(text, tokenizer)
    } finally {
      tokenizer.close()
    }

    println(s"Mode => $mode End")
    println()
  }

  def displayTokens(text: String, tokenStream: TokenStream): Unit = {
    val charTermAttr = tokenStream.addAttribute(classOf[CharTermAttribute])

    println("<<==========================================")
    println(s"input text => $text")
    println("============================================")

    tokenStream.reset()

    while (tokenStream.incrementToken()) {
      val token = charTermAttr.toString
      println(s"token: $token")
    }

    tokenStream.close()
  }
}

各文書に対して、デフォルトのSEARCHモード、NORMALモード、EXTENDEDモードの順に形態素解析を行っていくサンプルです。

では、これを実行してみます。

> run-main JapaneseTokenizerTest
[info] Running JapaneseTokenizerTest 
Mode => SEARCH Start
<<==========================================
input text => すもももももももものうち。
============================================
token: すもも
token: も
token: もも
token: も
token: もも
token: の
token: うち
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => すもももももももものうち。
============================================
token: すもも
token: も
token: もも
token: も
token: もも
token: の
token: うち
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => すもももももももものうち。
============================================
token: すもも
token: も
token: もも
token: も
token: もも
token: の
token: うち
Mode => EXTENDED End

Mode => SEARCH Start
<<==========================================
input text => メガネは顔の一部です。
============================================
token: メガネ
token: は
token: 顔
token: の
token: 一部
token: です
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => メガネは顔の一部です。
============================================
token: メガネ
token: は
token: 顔
token: の
token: 一部
token: です
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => メガネは顔の一部です。
============================================
token: メガネ
token: は
token: 顔
token: の
token: 一部
token: です
Mode => EXTENDED End

Mode => SEARCH Start
<<==========================================
input text => 日本経済新聞でモバゲーの記事を読んだ。
============================================
token: 日本
token: 日本経済新聞
token: 経済
token: 新聞
token: で
token: モバゲー
token: の
token: 記事
token: を
token: 読ん
token: だ
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => 日本経済新聞でモバゲーの記事を読んだ。
============================================
token: 日本経済新聞
token: で
token: モバゲー
token: の
token: 記事
token: を
token: 読ん
token: だ
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => 日本経済新聞でモバゲーの記事を読んだ。
============================================
token: 日本
token: 経済
token: 新聞
token: で
token: モ
token: バ
token: ゲ
token: ー
token: の
token: 記事
token: を
token: 読ん
token: だ
Mode => EXTENDED End

Mode => SEARCH Start
<<==========================================
input text => Java, Scala, Groovy, Clojure
============================================
token: Java
token: Scala
token: Groovy
token: Clojure
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => Java, Scala, Groovy, Clojure
============================================
token: Java
token: Scala
token: Groovy
token: Clojure
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => Java, Scala, Groovy, Clojure
============================================
token: J
token: a
token: v
token: a
token: S
token: c
token: a
token: l
token: a
token: G
token: r
token: o
token: o
token: v
token: y
token: C
token: l
token: o
token: j
token: u
token: r
token: e
Mode => EXTENDED End

Mode => SEARCH Start
<<==========================================
input text => LUCENE、SOLR、Lucene, Solr
============================================
token: LUCENE
token: SOLR
token: Lucene
token: Solr
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => LUCENE、SOLR、Lucene, Solr
============================================
token: LUCENE
token: SOLR
token: Lucene
token: Solr
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => LUCENE、SOLR、Lucene, Solr
============================================
token: L
token: U
token: C
token: E
token: N
token: E
token: S
token: O
token: L
token: R
token: L
token: u
token: c
token: e
token: n
token: e
token: S
token: o
token: l
token: r
Mode => EXTENDED End

Mode => SEARCH Start
<<==========================================
input text => アイウエオカキクケコさしすせそABCXYZ123456
============================================
token: アイウエオカキクケコ
token: さ
token: しす
token: せ
token: そ
token: ABCXYZ
token: 123456
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => アイウエオカキクケコさしすせそABCXYZ123456
============================================
token: アイウエオカキクケコ
token: さ
token: しす
token: せ
token: そ
token: ABCXYZ
token: 123456
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => アイウエオカキクケコさしすせそABCXYZ123456
============================================
token: ア
token: イ
token: ウ
token: エ
token: オ
token: カ
token: キ
token: ク
token: ケ
token: コ
token: さ
token: しす
token: せ
token: そ
token: A
token: B
token: C
token: X
token: Y
token: Z
token: 1
token: 2
token: 3
token: 4
token: 5
token: 6
Mode => EXTENDED End

Mode => SEARCH Start
<<==========================================
input text => Lucene is a full-featured text search engine library written in Java.
============================================
token: Lucene
token: is
token: a
token: full
token: featured
token: text
token: search
token: engine
token: library
token: written
token: in
token: Java
Mode => SEARCH End

Mode => NORMAL Start
<<==========================================
input text => Lucene is a full-featured text search engine library written in Java.
============================================
token: Lucene
token: is
token: a
token: full
token: featured
token: text
token: search
token: engine
token: library
token: written
token: in
token: Java
Mode => NORMAL End

Mode => EXTENDED Start
<<==========================================
input text => Lucene is a full-featured text search engine library written in Java.
============================================
token: L
token: u
token: c
token: e
token: n
token: e
token: i
token: s
token: a
token: f
token: u
token: l
token: l
token: f
token: e
token: a
token: t
token: u
token: r
token: e
token: d
token: t
token: e
token: x
token: t
token: s
token: e
token: a
token: r
token: c
token: h
token: e
token: n
token: g
token: i
token: n
token: e
token: l
token: i
token: b
token: r
token: a
token: r
token: y
token: w
token: r
token: i
token: t
token: t
token: e
token: n
token: i
token: n
token: J
token: a
token: v
token: a
Mode => EXTENDED End

[success] Total time: 2 s, completed 2013/06/02 19:51:53

EXTENDEDモードの、

Mode => EXTENDED Start
<<==========================================
input text => 日本経済新聞でモバゲーの記事を読んだ。
============================================
token: 日本
token: 経済
token: 新聞
token: で
token: モ
token: バ
token: ゲ
token: ー
token: の
token: 記事
token: を
token: 読ん
token: だ
Mode => EXTENDED End

みたいなところは、ある種予想通りです。

が、英単語とかはキレイにuni-gramで分割されちゃってます…。

Mode => EXTENDED Start
<<==========================================
input text => Java, Scala, Groovy, Clojure
============================================
token: J
token: a
token: v
token: a
token: S
token: c
token: a
token: l
token: a
token: G
token: r
token: o
token: o
token: v
token: y
token: C
token: l
token: o
token: j
token: u
token: r
token: e
Mode => EXTENDED End

この結果を見て、なんとなく昨日の挙動を見ることになった理由が分かった気がします。

要は、Analyzerに登録されてるFilterに捨てられちゃったんだろうなぁ、と。

これを確認するために、JapaneseAnalyzerのソースを参考にしつつ、再びAnalyzerを使ったサンプルを書いてみました。
src/main/scala/JapaneseAnalyzerTest.scala

import java.io.{Reader, StringReader}

import org.apache.lucene.analysis.{Analyzer, Tokenizer, TokenStream}
import org.apache.lucene.analysis.cjk.CJKWidthFilter
import org.apache.lucene.analysis.core.{LowerCaseFilter, StopFilter}
import org.apache.lucene.analysis.ja._
import org.apache.lucene.analysis.ja.dict.UserDictionary
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute
import org.apache.lucene.util.Version

object JapaneseAnalyzerTest {
  def main(args: Array[String]): Unit = {
    val texts =
      List(
        "すもももももももものうち。",
        "メガネは顔の一部です。",
        "日本経済新聞でモバゲーの記事を読んだ。",
        "Java, Scala, Groovy, Clojure",
        "LUCENE、SOLR、Lucene, Solr",
        "アイウエオカキクケコさしすせそABCXYZ123456",
        "Lucene is a full-featured text search engine library written in Java."
      )

    val modes =
      List(
        JapaneseTokenizer.Mode.SEARCH,
        JapaneseTokenizer.Mode.NORMAL,
        JapaneseTokenizer.Mode.EXTENDED
      )

    for {
      text <- texts
      mode <- modes
    } withJapaneseAnalyzer(text, mode)(displayTokens)
  }

  def withJapaneseAnalyzer(text: String, mode: JapaneseTokenizer.Mode)(body: (String, TokenStream) => Unit): Unit = {
    val japaneseAnalyzer = createJapaneseAnalyzer(mode)
    val tokenStream = japaneseAnalyzer.tokenStream("", new StringReader(text))

    println(s"Mode => $mode Start")

    try {
      body(text, tokenStream)
    } finally {
      tokenStream.close()
    }

    println(s"Mode => $mode End")
    println()
  }

  def createJapaneseAnalyzer(mode: JapaneseTokenizer.Mode): JapaneseAnalyzer = {
    val userDict: UserDictionary = null
    val stopwords = JapaneseAnalyzer.getDefaultStopSet
    val stoptags = JapaneseAnalyzer.getDefaultStopTags

    new JapaneseAnalyzer(Version.LUCENE_43,
                         null,
                         mode,
                         stopwords,
                         stoptags) {
      override def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents = {
        val tokenizer = new JapaneseTokenizer(reader, userDict, true, mode)
        var stream: TokenStream = new JapaneseBaseFormFilter(tokenizer)
        stream = new JapanesePartOfSpeechStopFilter(true, stream, stoptags)
        stream = new CJKWidthFilter(stream)
        stream = new StopFilter(matchVersion, stream, stopwords)
        stream = new JapaneseKatakanaStemFilter(stream)
        stream = new LowerCaseFilter(matchVersion, stream)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
      }
    }
  }

  def displayTokens(text: String, tokenStream: TokenStream): Unit = {
    val charTermAttr = tokenStream.addAttribute(classOf[CharTermAttribute])

    println("<<==========================================")
    println(s"input text => $text")
    println("============================================")

    tokenStream.reset()

    while (tokenStream.incrementToken()) {
      val token = charTermAttr.toString
      println(s"token: $token")
    }

    tokenStream.close()
  }
}

今回の確認で調節するのは、ここ。

    new JapaneseAnalyzer(Version.LUCENE_43,
                         null,
                         mode,
                         stopwords,
                         stoptags) {
      override def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents = {
        val tokenizer = new JapaneseTokenizer(reader, userDict, true, mode)
        var stream: TokenStream = new JapaneseBaseFormFilter(tokenizer)
        stream = new JapanesePartOfSpeechStopFilter(true, stream, stoptags)
        stream = new CJKWidthFilter(stream)
        stream = new StopFilter(matchVersion, stream, stopwords)
        stream = new JapaneseKatakanaStemFilter(stream)
        stream = new LowerCaseFilter(matchVersion, stream)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
      }
    }

JapaneseAnalyzerのサブクラスを作っているところですね。この状態だと、ほぼデフォルトのJapaneseAnalyzerの実装となります。

これをいくつか付けたり外したりして、JapanesePartOfSpeechStopFilterクラスにuni-gramになってしまった単語が捨てられていることが分かりました。

EXTENDEDモードを使う時には、Filterに注意した方がいい感じでしょうか。Solrで使う場合は、どういう使い方になるんだろう…?