CLOVER🍀

That was when it all began.

LuceneのAnalyzer/Tokenizerを、フィールドごとに切り替える

久々のLuceneネタです。

Luceneを使ってプログラムを書く時に必ずAnalyzerが登場するわけですが、例えば

Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);

// インデックス作成
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_CURRENT, analyzer);
IndexWriter iwriter = new IndexWriter(directory, config);

// QueryParserを作成
QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "fieldname", analyzer);

みたいな感じで、インデキシング、クエリの発行にはAnalyzerを使うことになります。

が、例えばKuromojiを使って形態素解析をするのはいいのですが、単純にJapaneseAnalyzerを使うとDocumentのフィールドから形態素解析する/しないを選んでいくことになりますよね。

一部N-Gramにする、なんてことはできないわけです。

ところで、Solrを使った場合はフィールドごとにTokenizerやFilterを指定できますね。なおかつ、インデキシングとクエリで分けられるわけですが…。

というわけで、同じようなことができるように頑張ってみたいと思います。
Lucene使いの方には、超当たり前のネタだったらすみません…

下準備

ビルドの設定。
build.sbt

name := "lucene-analyzer-per-field"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "littlewings"

libraryDependencies += "org.apache.lucene" % "lucene-analyzers-kuromoji" % "4.5.1"

いろいろトライしましたが、最終的にKuromojiだけあればいいことにしました。

で、雛形としてこんなソースを用意。
src/main/scala/LuceneAnalyzerPerField.scala

import scala.collection.JavaConverters._
import scala.collection._

import java.io.Reader

import org.apache.lucene.analysis.{Analyzer, AnalyzerWrapper, Tokenizer, TokenStream}
import org.apache.lucene.analysis.cjk._
import org.apache.lucene.analysis.core._
import org.apache.lucene.analysis.ja._
import org.apache.lucene.analysis.ngram._
import org.apache.lucene.analysis.standard._
import org.apache.lucene.analysis.tokenattributes._
import org.apache.lucene.analysis.util._
import org.apache.lucene.util.Version

object LuceneAnalyzerPerField {
  def main(args: Array[String]): Unit = {
    val luceneVersion = Version.LUCENE_45
    val analyzer = createAnalyzer(luceneVersion)

    for {
      text <- Array("すもももももももものうち。",
                    "メガネは顔の一部です。",
                    "日本経済新聞でモバゲーの記事を読んだ。",
                    "Java, Scala, Groovy, Clojure",
                    "LUCENE、SOLR、Lucene, Solr",
                    "アイウエオカキクケコさしすせそABCXYZ123456",
                    "Lucene is a full-featured text search engine library written in Java.")
      fieldName <- Array("text-cjk", "text-ngram", "text-japanese-search")
    } {
      println(s">$fieldName:")
      println(s"  Original[$text]")

      val tokenStream = analyzer.tokenStream(fieldName, text)
      val charTermAttribute = tokenStream.addAttribute(classOf[CharTermAttribute])

      tokenStream.reset()

      println {
        Iterator
          .continually(tokenStream)
          .takeWhile(_.incrementToken())
          .map(t => charTermAttribute.toString)
            .mkString("  Tokenize[", " ", "]")
      }

      tokenStream.close()

      println()
    }
  }

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    // ここをカスタマイズ
  }
}

いくつかのテキストを、フィールドごとに変えていくイメージのループを書いています。

    for {
      text <- Array("すもももももももものうち。",
                    "メガネは顔の一部です。",
                    "日本経済新聞でモバゲーの記事を読んだ。",
                    "Java, Scala, Groovy, Clojure",
                    "LUCENE、SOLR、Lucene, Solr",
                    "アイウエオカキクケコさしすせそABCXYZ123456",
                    "Lucene is a full-featured text search engine library written in Java.")
      fieldName <- Array("text-cjk", "text-ngram", "text-japanese-search")

見ての通り、CJKAnalyzer、N-Gram、Kuromoji Seachモードでトークン化できればいいなぁという思いが出ています(笑)。

で、以後はこの部分を順次変えて試していきます。

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    // ここをカスタマイズ
  }

普通にKuromojiを使う

最初は、普通にKuromojiのJapaneseAnalyzerを使ってみましょう。

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    new JapaneseAnalyzer(luceneVersion)
  }

実行。

> run
[info] Running LuceneAnalyzerPerField 
>text-cjk:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

>text-ngram:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

>text-japanese-search:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

>text-cjk:
  Original[メガネは顔の一部です。]
  Tokenize[メガネ 顔 一部]

>text-ngram:
  Original[メガネは顔の一部です。]
  Tokenize[メガネ 顔 一部]

>text-japanese-search:
  Original[メガネは顔の一部です。]
  Tokenize[メガネ 顔 一部]

〜省略〜

当然ですが、すべてのフィールドに対して同じ結果になります。

JapaneseAnalyzerに少し細工をしてみる

Analyzerクラスを作成する時には、少なくとも以下のメソッドを実装する必要があります。

protected TokenStreamComponents createComponents(String fieldName, Reader reader) {

これ、フィールド名ごとに呼ばれそうな雰囲気があるので、JapaneseAnalyzerを拡張してちょっと実験。

class MyJapaneseAnalyzer(luceneVersion: Version) extends JapaneseAnalyzer(luceneVersion) {
  override protected def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents = {
    println(s"fieldName = $fieldName")
    super.createComponents(fieldName, reader)
  }
}

Analyzerをこう変更します。

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    new MyJapaneseAnalyzer(luceneVersion)
  }

実行。

> run
[info] Running LuceneAnalyzerPerField 
>text-cjk:
  Original[すもももももももものうち。]
fieldName = text-cjk
  Tokenize[すもも もも もも]

>text-ngram:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

>text-japanese-search:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

〜省略〜

全フィールドの名前が出力されるかと思いきや、これしか出力されていません。

fieldName = text-cjk

どういうことか?理由は、Analyzerのコンストラクタで指定してある、これだと思います。

  /**
   * Create a new Analyzer, reusing the same set of components per-thread
   * across calls to {@link #tokenStream(String, Reader)}. 
   */
  public Analyzer() {
    this(GLOBAL_REUSE_STRATEGY);
  }

Analyzerで作成するTokenStreamComponentsを、どの単位で再利用するかをここで決めています。

Strategy defining how TokenStreamComponents are reused per call to Analyzer.tokenStream(String, java.io.Reader).

http://lucene.apache.org/core/4_5_1/core/org/apache/lucene/analysis/Analyzer.ReuseStrategy.html

デフォルトは「GLOBAL_REUSE_STRATEGY」なので、フィールド単位ではなく、ひとつのフィールド定義で作成したTokenStreamComponentsがAnalyzer全体で使い回されるというわけですね。

これが、フィールドがひとつしか呼ばれなかった理由でしょう。フィールドごとに使い回すようにしたいなら、「PER_FIELD_REUSE_STRATEGY」を指定すればよさそうです。

Analyzer.PER_FIELD_REUSE_STRATEGYを使う

タネが分かったところで、それなら今度はAnalyzerを拡張して、AnalyzerにAnalyzer.PER_FIELD_REUSE_STRATEGYを指定するようにして実装します。

class AnalyzerPerField(matchVersion: Version) extends Analyzer(Analyzer.PER_FIELD_REUSE_STRATEGY) {
  override protected def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents =
    fieldName match {
      case "text-cjk" =>
        val tokenizer = new StandardTokenizer(matchVersion, reader)
        var stream: TokenStream = new CJKWidthFilter(tokenizer)
        stream = new LowerCaseFilter(matchVersion, stream)
        stream = new CJKBigramFilter(stream)
        stream = new StopFilter(matchVersion, stream, CJKAnalyzer.getDefaultStopSet)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
      case "text-ngram" =>
        val tokenizer = new WhitespaceTokenizer(matchVersion, reader)
        var stream: TokenStream = new CJKWidthFilter(tokenizer)
        stream = new NGramTokenFilter(matchVersion, stream, 2, 2)
        stream = new LowerCaseFilter(matchVersion, stream)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
      case "text-japanese-search" =>
        val tokenizer = new JapaneseTokenizer(reader, null, true, JapaneseTokenizer.Mode.SEARCH)
        var stream: TokenStream = new JapaneseBaseFormFilter(tokenizer)
        stream = new JapanesePartOfSpeechStopFilter(matchVersion, stream, JapaneseAnalyzer.getDefaultStopTags)
        stream = new CJKWidthFilter(stream)
        stream = new StopFilter(matchVersion, stream, JapaneseAnalyzer.getDefaultStopSet)
        stream = new JapaneseKatakanaStemFilter(stream)
        stream = new LowerCaseFilter(matchVersion, stream)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
  }
}

というわけで、case式ではありますが、「text-cjk」「text-ngram」「text-japanese-search」でそれぞれTokenizerとFilterを使って定義しました。

もちろん、親クラスのAnalyzerのコンストラクタ引数には「PER_FIELD_REUSE_STRATEGY」を指定してあります。

class AnalyzerPerField(matchVersion: Version) extends Analyzer(Analyzer.PER_FIELD_REUSE_STRATEGY) {

それでは、このAnalyzerを使ってみます。

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    new AnalyzerPerField(luceneVersion)
  }

実行。

> run
[info] Running LuceneAnalyzerPerField 
>text-cjk:
  Original[すもももももももものうち。]
  Tokenize[すも もも もも もも もも もも もも もも もの のう うち]

>text-ngram:
  Original[すもももももももものうち。]
  Tokenize[すも もも もも もも もも もも もも もも もの のう うち ち。]

>text-japanese-search:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

>text-cjk:
  Original[メガネは顔の一部です。]
  Tokenize[メガ ガネ ネは は顔 顔の の一 一部 部で です]

>text-ngram:
  Original[メガネは顔の一部です。]
  Tokenize[メガ ガネ ネは は顔 顔の の一 一部 部で です す。]

>text-japanese-search:
  Original[メガネは顔の一部です。]
  Tokenize[メガネ 顔 一部]

>text-cjk:
  Original[日本経済新聞でモバゲーの記事を読んだ。]
  Tokenize[日本 本経 経済 済新 新聞 聞で でモ モバ バゲ ゲー ーの の記 記事 事を を読 読ん んだ]

>text-ngram:
  Original[日本経済新聞でモバゲーの記事を読んだ。]
  Tokenize[日本 本経 経済 済新 新聞 聞で でモ モバ バゲ ゲー ーの の記 記事 事を を読 読ん んだ だ。]

>text-japanese-search:
  Original[日本経済新聞でモバゲーの記事を読んだ。]
  Tokenize[日本 日本経済新聞 経済 新聞 モバゲ 記事 読む]

>text-cjk:
  Original[Java, Scala, Groovy, Clojure]
  Tokenize[java scala groovy clojure]

>text-ngram:
  Original[Java, Scala, Groovy, Clojure]
  Tokenize[ja av va a, sc ca al la a, gr ro oo ov vy y, cl lo oj ju ur re]

>text-japanese-search:
  Original[Java, Scala, Groovy, Clojure]
  Tokenize[java scala groovy clojure]

>text-cjk:
  Original[LUCENE、SOLR、Lucene, Solr]
  Tokenize[lucene solr lucene solr]

>text-ngram:
  Original[LUCENE、SOLR、Lucene, Solr]
  Tokenize[lu uc ce en ne e、 、s so ol lr r、 、l lu uc ce en ne e, so ol lr]

>text-japanese-search:
  Original[LUCENE、SOLR、Lucene, Solr]
  Tokenize[lucene solr lucene solr]

>text-cjk:
  Original[アイウエオカキクケコさしすせそABCXYZ123456]
  Tokenize[アイ イウ ウエ エオ オカ カキ キク クケ ケコ コさ さし しす すせ せそ abcxyz123456]

>text-ngram:
  Original[アイウエオカキクケコさしすせそABCXYZ123456]
  Tokenize[アイ イウ ウエ エオ オカ カキ キク クケ ケコ コさ さし しす すせ せそ そa ab bc cx xy yz z1 12 23 34 45 56]

>text-japanese-search:
  Original[アイウエオカキクケコさしすせそABCXYZ123456]
  Tokenize[アイウエオカキクケコ しす そ abcxyz 123456]

>text-cjk:
  Original[Lucene is a full-featured text search engine library written in Java.]
  Tokenize[lucene full featured text search engine library written java]

>text-ngram:
  Original[Lucene is a full-featured text search engine library written in Java.]
  Tokenize[lu uc ce en ne is fu ul ll l- -f fe ea at tu ur re ed te ex xt se ea ar rc ch en ng gi in ne li ib br ra ar ry wr ri it tt te en in ja av va a.]

>text-japanese-search:
  Original[Lucene is a full-featured text search engine library written in Java.]
  Tokenize[lucene is a full featured text search engine library written in java]

[success] Total time: 1 s, completed 2013/11/14 23:32:10

できましたー!!無事、それぞれのフィールドをCJK、N-Gram、Kuromoji Searchモードでトークン化できています。

AnalyzerWrapperを使う

とはいえ、こうやって毎度TokenizerとFilterをひとつひとつ定義していくの?という話もあろうかと。既存のAnalyzerがあるなら、それを使い回したいところです。

そこで、AnalyzerWrapperを使います。AnalyzerWrapperを使って、getWrapperdAnalyzerメソッドをオーバーライドしてAnalyzerを返してあげればいいみたいです。
※なのですが、実は後述のPerFieldAnalyzerWrapperを使用すると、さらにストレートに書けます

というわけで、先ほどのAnalyzerをAnalyzerWrapperを使って書き直してみます。ここでも、コンストラクタ引数に「PER_FIELD_REUSE_STRATEGY」を渡すことをお忘れなく。

class AnalyzerWrapperPerField(matchVersion: Version) extends AnalyzerWrapper(Analyzer.PER_FIELD_REUSE_STRATEGY) {
  override def getWrappedAnalyzer(fieldName: String): Analyzer =
    fieldName match {
      case "text-cjk" => new CJKAnalyzer(matchVersion)
      case "text-ngram" =>
        new Analyzer {
          override protected def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents = {
            val tokenizer = new WhitespaceTokenizer(matchVersion, reader)
            var stream: TokenStream = new CJKWidthFilter(tokenizer)
            stream = new NGramTokenFilter(matchVersion, stream, 2, 2)
            stream = new LowerCaseFilter(matchVersion, stream)
            new Analyzer.TokenStreamComponents(tokenizer, stream)
          }
        }
      case "text-japanese-search" => new JapaneseAnalyzer(matchVersion)
    }
}

CJKAnalyzerとJapaneseAnalyzerは使い回せるので、だいぶスッキリしました。N-GramはAnalyzerがないので、そのままです…。

というわけで、このAnalyzerを使うように修正します。

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    new AnalyzerWrapperPerField(luceneVersion)
  }

実行結果。こちらも、フィールドごとにトークン化を切り替えられています。

> run
[info] Running LuceneAnalyzerPerField 
>text-cjk:
  Original[すもももももももものうち。]
  Tokenize[すも もも もも もも もも もも もも もも もの のう うち]

>text-ngram:
  Original[すもももももももものうち。]
  Tokenize[すも もも もも もも もも もも もも もも もの のう うち ち。]

>text-japanese-search:
  Original[すもももももももものうち。]
  Tokenize[すもも もも もも]

>text-cjk:
  Original[メガネは顔の一部です。]
  Tokenize[メガ ガネ ネは は顔 顔の の一 一部 部で です]

>text-ngram:
  Original[メガネは顔の一部です。]
  Tokenize[メガ ガネ ネは は顔 顔の の一 一部 部で です す。]

>text-japanese-search:
  Original[メガネは顔の一部です。]
  Tokenize[メガネ 顔 一部]

>text-cjk:
  Original[日本経済新聞でモバゲーの記事を読んだ。]
  Tokenize[日本 本経 経済 済新 新聞 聞で でモ モバ バゲ ゲー ーの の記 記事 事を を読 読ん んだ]

>text-ngram:
  Original[日本経済新聞でモバゲーの記事を読んだ。]
  Tokenize[日本 本経 経済 済新 新聞 聞で でモ モバ バゲ ゲー ーの の記 記事 事を を読 読ん んだ だ。]

>text-japanese-search:
  Original[日本経済新聞でモバゲーの記事を読んだ。]
  Tokenize[日本 日本経済新聞 経済 新聞 モバゲ 記事 読む]

>text-cjk:
  Original[Java, Scala, Groovy, Clojure]
  Tokenize[java scala groovy clojure]

>text-ngram:
  Original[Java, Scala, Groovy, Clojure]
  Tokenize[ja av va a, sc ca al la a, gr ro oo ov vy y, cl lo oj ju ur re]

>text-japanese-search:
  Original[Java, Scala, Groovy, Clojure]
  Tokenize[java scala groovy clojure]

>text-cjk:
  Original[LUCENE、SOLR、Lucene, Solr]
  Tokenize[lucene solr lucene solr]

>text-ngram:
  Original[LUCENE、SOLR、Lucene, Solr]
  Tokenize[lu uc ce en ne e、 、s so ol lr r、 、l lu uc ce en ne e, so ol lr]

>text-japanese-search:
  Original[LUCENE、SOLR、Lucene, Solr]
  Tokenize[lucene solr lucene solr]

>text-cjk:
  Original[アイウエオカキクケコさしすせそABCXYZ123456]
  Tokenize[アイ イウ ウエ エオ オカ カキ キク クケ ケコ コさ さし しす すせ せそ abcxyz123456]

>text-ngram:
  Original[アイウエオカキクケコさしすせそABCXYZ123456]
  Tokenize[アイ イウ ウエ エオ オカ カキ キク クケ ケコ コさ さし しす すせ せそ そa ab bc cx xy yz z1 12 23 34 45 56]

>text-japanese-search:
  Original[アイウエオカキクケコさしすせそABCXYZ123456]
  Tokenize[アイウエオカキクケコ しす そ abcxyz 123456]

>text-cjk:
  Original[Lucene is a full-featured text search engine library written in Java.]
  Tokenize[lucene full featured text search engine library written java]

>text-ngram:
  Original[Lucene is a full-featured text search engine library written in Java.]
  Tokenize[lu uc ce en ne is fu ul ll l- -f fe ea at tu ur re ed te ex xt se ea ar rc ch en ng gi in ne li ib br ra ar ry wr ri it tt te en in ja av va a.]

>text-japanese-search:
  Original[Lucene is a full-featured text search engine library written in Java.]
  Tokenize[lucene is a full featured text search engine library written in java]

[success] Total time: 1 s, completed 2013/11/14 23:37:40

PerFieldAnalyzerWrapperを使う

さらにもっとストレートな方法としては、PerFieldAnalyzerWrapperを使用します。
PerFieldAnalyzerWrapperを使用すると、フィールド名とAnalyzerのペアをMapとして定義して設定することができます。

https://lucene.apache.org/core/4_5_1/analyzers-common/org/apache/lucene/analysis/miscellaneous/PerFieldAnalyzerWrapper.html

AnalyzerWrapperを拡張してやるよりも、さらに簡易に実現することができます。

Solrをヒントに

最後は、オマケです。もともと、以前にSolrのソースを読んでみて「こうやるのかぁ」というのを軽く知ったはずなのですが、すっかり忘れてしまっていて、この機に読み直してみようと思ったのがこのエントリを書いたきっかけです。

参考にしたのは、この辺のソースですね。

solr/core/src/java/org/apache/solr/schema/IndexSchema.java
solr/core/src/java/org/apache/solr/schema/FieldType.java
solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java

また、Solrのスキーマ定義でXMLで組み立てているAnalyzerの定義は、TokenizerChainを使ってこんな感じで表されています。

         new TokenizerChain(
          null,
          new JapaneseTokenizerFactory(mutable.Map("luceneMatchVersion" -> matchVersion.toString,
                                                   "mode" -> "SEARCH").asJava),
          Array[TokenFilterFactory](
            new JapaneseBaseFormFilterFactory(mutable.Map("luceneMatchVersion" -> matchVersion.toString).asJava),
            new JapanesePartOfSpeechStopFilterFactory(mutable.Map("luceneMatchVersion" -> matchVersion.toString,
                                                                  "tags" -> "stopTags.txt").asJava),
            new CJKWidthFilterFactory(mutable.Map.empty[String, String].asJava),
            new StopFilterFactory(mutable.Map("luceneMatchVersion" -> matchVersion.toString,
                                              "words" -> "stopwords.txt").asJava),
            new JapaneseKatakanaStemFilterFactory(mutable.Map.empty[String, String].asJava),
            new LowerCaseFilterFactory(mutable.Map("luceneMatchVersion" -> matchVersion.toString).asJava)
          )
        )

TokenizerChainを使った実装にトライしようと思ったのですが、ストップワードとかの設定を整えるのが面倒でやめました。まあ、いっかぁと。

それにしても、時間はかかりましたが、マジメにやったおかげでよい勉強になりました!

最後は、ソース全体を掲載します。
src/main/scala/LuceneAnalyzerPerField.scala

import scala.collection.JavaConverters._
import scala.collection._

import java.io.Reader

import org.apache.lucene.analysis.{Analyzer, AnalyzerWrapper, Tokenizer, TokenStream}
import org.apache.lucene.analysis.cjk._
import org.apache.lucene.analysis.core._
import org.apache.lucene.analysis.ja._
import org.apache.lucene.analysis.ngram._
import org.apache.lucene.analysis.standard._
import org.apache.lucene.analysis.tokenattributes._
import org.apache.lucene.analysis.util._
import org.apache.lucene.util.Version

object LuceneAnalyzerPerField {
  def main(args: Array[String]): Unit = {
    val luceneVersion = Version.LUCENE_45
    val analyzer = createAnalyzer(luceneVersion)

    for {
      text <- Array("すもももももももものうち。",
                    "メガネは顔の一部です。",
                    "日本経済新聞でモバゲーの記事を読んだ。",
                    "Java, Scala, Groovy, Clojure",
                    "LUCENE、SOLR、Lucene, Solr",
                    "アイウエオカキクケコさしすせそABCXYZ123456",
                    "Lucene is a full-featured text search engine library written in Java.")
      fieldName <- Array("text-cjk", "text-ngram", "text-japanese-search")
    } {
      println(s">$fieldName:")
      println(s"  Original[$text]")

      val tokenStream = analyzer.tokenStream(fieldName, text)
      val charTermAttribute = tokenStream.addAttribute(classOf[CharTermAttribute])

      tokenStream.reset()

      println {
        Iterator
          .continually(tokenStream)
          .takeWhile(_.incrementToken())
          .map(t => charTermAttribute.toString)
            .mkString("  Tokenize[", " ", "]")
      }

      tokenStream.close()

      println()
    }
  }

  private def createAnalyzer(luceneVersion: Version): Analyzer = {
    //new JapaneseAnalyzer(luceneVersion)
    //new MyJapaneseAnalyzer(luceneVersion)
    //new AnalyzerPerField(luceneVersion)
    new AnalyzerWrapperPerField(luceneVersion)
  }
}


class MyJapaneseAnalyzer(luceneVersion: Version) extends JapaneseAnalyzer(luceneVersion) {
  override protected def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents = {
    println(s"fieldName = $fieldName")
    super.createComponents(fieldName, reader)
  }
}

class AnalyzerPerField(matchVersion: Version) extends Analyzer(Analyzer.PER_FIELD_REUSE_STRATEGY) {
  override protected def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents =
    fieldName match {
      case "text-cjk" =>
        val tokenizer = new StandardTokenizer(matchVersion, reader)
        var stream: TokenStream = new CJKWidthFilter(tokenizer)
        stream = new LowerCaseFilter(matchVersion, stream)
        stream = new CJKBigramFilter(stream)
        stream = new StopFilter(matchVersion, stream, CJKAnalyzer.getDefaultStopSet)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
      case "text-ngram" =>
        val tokenizer = new WhitespaceTokenizer(matchVersion, reader)
        var stream: TokenStream = new CJKWidthFilter(tokenizer)
        stream = new NGramTokenFilter(matchVersion, stream, 2, 2)
        stream = new LowerCaseFilter(matchVersion, stream)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
      case "text-japanese-search" =>
        val tokenizer = new JapaneseTokenizer(reader, null, true, JapaneseTokenizer.Mode.SEARCH)
        var stream: TokenStream = new JapaneseBaseFormFilter(tokenizer)
        stream = new JapanesePartOfSpeechStopFilter(matchVersion, stream, JapaneseAnalyzer.getDefaultStopTags)
        stream = new CJKWidthFilter(stream)
        stream = new StopFilter(matchVersion, stream, JapaneseAnalyzer.getDefaultStopSet)
        stream = new JapaneseKatakanaStemFilter(stream)
        stream = new LowerCaseFilter(matchVersion, stream)
        new Analyzer.TokenStreamComponents(tokenizer, stream)
  }
}

class AnalyzerWrapperPerField(matchVersion: Version) extends AnalyzerWrapper(Analyzer.PER_FIELD_REUSE_STRATEGY) {
  override def getWrappedAnalyzer(fieldName: String): Analyzer =
    fieldName match {
      case "text-cjk" => new CJKAnalyzer(matchVersion)
      case "text-ngram" =>
        new Analyzer {
          override protected def createComponents(fieldName: String, reader: Reader): Analyzer.TokenStreamComponents = {
            val tokenizer = new WhitespaceTokenizer(matchVersion, reader)
            var stream: TokenStream = new CJKWidthFilter(tokenizer)
            stream = new NGramTokenFilter(matchVersion, stream, 2, 2)
            stream = new LowerCaseFilter(matchVersion, stream)
            new Analyzer.TokenStreamComponents(tokenizer, stream)
          }
        }
      case "text-japanese-search" => new JapaneseAnalyzer(matchVersion)
    }
}