Lucene の CJKAnalyzer で カタカナだけ Bigram じゃなくする。

## 成果物

https://github.com/YmBIgo/CJKAnalyzer_mini_patch

## CJKAnalyzer とは？
Lucene で 日本語韓国語中国語 を Bi-gram にしてくれるやつ。
ただ、日本語だとカタカナはバイグラムじゃなくても良かったりする。
CJKAnalyzer を使う簡単なソースコードだけ、先にあげときます。

```java:Sample.java
import java.util.ArrayList;
import java.util.List;
import java.io.IOException;

import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;

public class Sample {
	public static void main(String[] args) {
		String text = "岸田文雄首相は衆院本会議で所信表明演説し、個人のリスキリング（学び直し）の支援に5年で1兆円を投じると表明した。";
		CJKAnalyzer analyzer = new CJKAnalyzer();
		// 英語でよく使う StandardAnalyzer では、
		// StandardAnalyzer analyzer = new StandardAnalyzer();
		TokenStream tokenStream = analyzer.tokenStream("testField", text);
		try {
			tokenStream.reset();
			CharTermAttribute termAtt = tokenStream.addAttribute(CharTermAttribute.class);
			// OffsetAttribute offsetAtt = tokenStream.addAttribute(OffsetAttribute.class);
			while (tokenStream.incrementToken()) {
				System.out.println(termAtt.toString());
			}
			tokenStream.end();
		} catch (IOException e) {
			System.out.println(e);
		}
	}
}
```

`lucene/lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk` にある cjk と util を `/lucene/lucene/core/src/java/org/apache/lucene/analysis` に移動させて、上の Sample.java を `lucene/lucene/core/src/java` に置けば動きます。

結果は、Bi-gram された結果が出てきたのではないでしょうか？

## カタカナだけ Bigram でなくする。
日本語では特に Bigram にする必要もない カタカナだけ、連続して表示させてみましょう。
まずは、[CJKAnalyzer.java のソースコード](https://github.com/apache/lucene/blob/cc342ea7407c729a743123d8f7957aff6c6f9792/lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk/CJKAnalyzer.java) を見ます。一番下の方です。

```java:CJKAnalyzer.java
  @Override
  protected TokenStreamComponents createComponents(String fieldName) {
    final Tokenizer source = new StandardTokenizer();
    // run the widthfilter first before bigramming, it sometimes combines characters.
    TokenStream result = new CJKWidthFilter(source);
    result = new LowerCaseFilter(result);
    result = new CJKBigramFilter(result);
    return new TokenStreamComponents(source, new StopFilter(result, stopwords));
  }
```

ここの StandardTokenizer で 日本語を １文字区切りにしていますが、これをカタカナだけ一まとまりにしてくれたら、カタカナだけ Bi-gram にしないようにできそうじゃないでしょうか？
なので、この StandardTokenizer の中身を見てみましょう。
詳細は、[この記事](https://qiita.com/YmBIgo/items/b64b7b7b4209d31e7019) に譲るとして、今回は この JFlex ファイルをいじれば、StandardTokenizer ([元ファイル](https://github.com/apache/lucene/blob/44b4602776a63d93896d887e5000c9e3ca7c3119/lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizerImpl.jflex)) の結果だけでは、カタカナを一まとまりにすることができます。それが下のコードです。

```java:StandardTokenizerImpl.jflex の 90行目
KatakanaEx          = [\p{WB:Katakana}]
```

`[` と `]` をつけただけですが、これで StandardTokenizer はカタカナを１まとまりにしてはき出してくれます。
ですが、このまま CJKAnalyzer を使っても、カタカナは Bi-gram のまま表示されます。ここで見ておく必要が出てくるのが、[CJKBigramFilter](https://github.com/apache/lucene/blob/cc342ea7407c729a743123d8f7957aff6c6f9792/lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk/CJKBigramFilter.java) です。

今回は、結果だけ載せますが、

```java:CJKBigramFilter.java
public static final String EXTRA_TYPE = "<EXTRA>";
// 省略
  private void flushExtragram(int extra_length) {
    //
    refill();
    // 【 や 】でズレた時 用の patch
    char bufferChar = (char)buffer[index + 1];
    int bufferOffset = 0;
    if (bufferChar != originalBuffer[0]) {
      int[] checkBufferRange = Arrays.copyOfRange(buffer, index + 1, index + extraLength + 1);
      char[] checkOriginalBufferRange = Arrays.copyOfRange(originalBuffer, index, index + extraLength);
      for (int i = 0; i < checkOriginalBufferRange.length; i++) {
        if ( checkOriginalBufferRange[i] == (char)checkBufferRange[0] ) {
          bufferOffset = i;
          break;
        }
      }
    }
    int originalIndex = 1;
    int originalExtraLength = extraLength + 1;
    originalIndex -= bufferOffset;
    originalExtraLength -= bufferOffset;
    //
    clearAttributes();
    char[] termBuffer = termAtt.resizeBuffer(extraLength*2);
    int len_extra = 0;
    for (int i = originalIndex; i < originalExtraLength; i++){
      int len_i = Character.toChars(buffer[index + i], termBuffer, len_extra);
      len_extra += len_i;
    }
    termAtt.setLength(len_extra);
    offsetAtt.setOffset(startOffset[originalIndex], endOffset[originalIndex + originalExtraLength - bufferOffset]);
    typeAtt.setType(EXTRA_TYPE);
    index += extraLength + 1 - bufferOffset;
  }
```

の定数と関数が新しく追加した部分で、この関数を

```diff_java:CJKBigramFilter.java
      } else if (doNext()) {

        // case 2: look at the token type. should we form any n-grams?

        String type = typeAtt.type();
        if (type == doHan || type == doHiragana || type == doKatakana || type == doHangul) {
+          if (termAtt.length() > 1) {
+            //
+            flushExtragram(termAtt.length());
+            return true;
+          }
```

188行目あたりで呼び出せば、カタカナだけ連続して表示されるはずです。
簡単に説明すると、`doNext()` で StandardTokenizer で分割された 次の トークン を読み出す時に、その長さが 1 より大きい場合は カタカナになるので、その場合だけ flushExtragram という関数で独自に termAtt に一まとまりのカタカナを登録する流れになります。
最後の `index += extra_length + 1 - bufferOffset` で トークンにあるカタカナの数だけ index を飛ばしています。

StandardTokenizer で出来たから試したけど、意外とできた。
修正点などあったら、ご指摘お願いします。