昨日、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で使う場合は、どういう使い方になるんだろう…?