SolrのFunctionQueryを自作する

SolrのFunctionQueryには沢山便利なものが用意されていますが、

https://wiki.apache.org/solr/FunctionQuery#Available_Functions

上記の関数だけでは要件を満たせないこともあると思います。

今回は、あるフィールドに登録されたJSON形式文字列をパースして何らかの処理をした後に結果を返すことを想定してFunctionQueryを自作します。

こちらの記事
http://www.lifull.blog/entry/2014/02/25/145220
を参考に作成しました。
上記の記事だけでも十分参考になるので自作できる方もいらっしゃると思います。
本記事ではSolrのソースコードをもう少し詳しく見てどのようなクラスの構成になっているかなどの解説も含めて作成して行きます。

#### デフォルトの関数はどのように実装されているか

まず、デフォルトで用意されている関数の実装を参考にします。

https://github.com/apache/lucene-solr/blob/master/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
用意されているFunctionQueryは上記ソースでValueSourceParserクラスのオブジェクトとして実装されています。

ValueSourceParserクラスは以下のシグネチャをもっています。

```java
public abstract class ValueSourceParser implements NamedListInitializedPlugin {
  /**
   * Initialize the plugin.
   */
  @Override
  public void init(NamedList args) {}

  /**
   * Parse the user input into a ValueSource.
   */
  public abstract ValueSource parse(FunctionQParser fp) throws SyntaxError;

  ...
```

また、ValueSourceParserクラスの<span id="initialize">初期化ブロック</span>では

```java
addParser("abs", new ValueSourceParser() {
  @Override
  public ValueSource parse(FunctionQParser fp) throws SyntaxError {
    ValueSource source = fp.parseValueSource();
    return new SimpleFloatFunction(source) {
      @Override
      protected String name() {
        return "abs";
      }

      @Override
      protected float func(int doc, FunctionValues vals) {
        return Math.abs(vals.floatVal(doc));
      }
    };
  }
});

```

のようにデフォルトの関数が作成されています。

注意点は
* `abs(a_value)`の`a_value`ように引数に渡されたフィールドの値は`fp.parseValueSource()`で`ValueSource`型として取得できる
* SimpleFloatFunctionというValueSourceクラス（の子クラス）のインスタンスを作成してparseメソッドの返り値としている

です。

SimpleFloatFunctionの定義は

https://github.com/apache/lucene-solr/blob/master/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/SimpleFloatFunction.java

に定義されています。

```java
...
/** A simple float function with a single argument
 */
 public abstract class SimpleFloatFunction extends SingleFunction {
  public SimpleFloatFunction(ValueSource source) {
    super(source);
  }

  protected abstract float func(int doc, FunctionValues vals) throws IOException;

  @Override
  public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
    final FunctionValues vals =  source.getValues(context, readerContext);
    return new FloatDocValues(this) {
      @Override
      public float floatVal(int doc) throws IOException {
        return func(doc, vals);
      }
      @Override
      public String toString(int doc) throws IOException {
        return name() + '(' + vals.toString(doc) + ')';
      }
    };
  }
...
```

getValuesというメソッドの中でabs()の引数を読み込んで、FloatDocValuesクラス(FunctionValuesクラスを継承している)のインスタンスを返しています。FloatDocValuesクラスのfloatValメソッドから[オーバーライドされたfuncメソッド](#initialize)が呼び出されてMath.abs関数が呼び出され、最終的に絶対値を返しています。

https://github.com/apache/lucene-solr/blob/master/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/

上記ディレクトリに定義されている幾つかのクラスを見ても同じように

* FunctionValuesクラスを継承したクラスのインスタンスを返す
* floatValやboolValなどの名前のメソッド内で抽象メソッドのfuncを実行して結果を返す

のように実装されています。

また、
https://wiki.apache.org/solr/SolrPlugins#ValueSourceParser

上記のドキュメントを見ると`solrconfig.xml`で設定すれば自分でクラスを作成して関数名をつけることができます。

以上より、必要な作業は

1. ValueSourceParserクラスを継承したクラスを作成する
1. SimpleFloatFunctionクラスのような引数を処理するためのクラスを作成して最終的に返したい値の型に合ったクラス(FloatDocValues等)のインスタンスを作成して返すようにfuncメソッドをオーバーライドする

今回は文字列を引数に受け取ってBool型を返すFunctionQueryを作成したいので

https://github.com/apache/lucene-solr/blob/master/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/SimpleBoolFunction.java

このSimpleBoolFunctionが使えそうです。

```java
/**
 * {@link BoolFunction} implementation which applies an extendible boolean
 * function to the values of a single wrapped {@link ValueSource}.
 *
 * Functions this can be used for include whether a field has a value or not,
 * or inverting the boolean value of the wrapped ValueSource.
 */
public abstract class SimpleBoolFunction extends BoolFunction {
  protected final ValueSource source;

  public SimpleBoolFunction(ValueSource source) {
    this.source = source;
  }

  protected abstract String name();

  protected abstract boolean func(int doc, FunctionValues vals) throws IOException;

  @Override
  public BoolDocValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
    final FunctionValues vals =  source.getValues(context, readerContext);
    return new BoolDocValues(this) {
      @Override
      public boolean boolVal(int doc) throws IOException {
        return func(doc, vals);
      }
      @Override
      public String toString(int doc) throws IOException {
        return name() + '(' + vals.toString(doc) + ')';
      }
    };
  }
```

必要な作業を書き直すと

1. ValueSourceParserクラスを継承したオリジナルのクラスを作る
1. SimpleBoolFunctionを継承したクラスを作ってfuncメソッドをオーバーライドする

になります。上記作業を行って独自のFunctionQueryを作成します。

## ValueSourceParserクラスを継承したオリジナルのクラスを作る

```java
package com.overseas_lifull.solr.functionquery;

import org.apache.lucene.queries.function.ValueSource;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.search.FunctionQParser;
import org.apache.solr.search.ValueSourceParser;

public class PremiumParser extends ValueSourceParser {
    @Override
    public void init(NamedList namedList) {
    }

    @Override
    public ValueSource parse(FunctionQParser fp) throws SyntaxError {
        ValueSource premiumInfo = fp.parseValueSource();

        return new PremiumFunction(premiumInfo);
    }
}
```

引数に受け取ったpremiumInfoという値をこの後に定義するPremiumFunctionという関数に渡しています。
このPremiumFunctionが、SimpleBoolFunctionを継承したクラスです。

## SimpleBoolFunctionを継承したクラスを作ってfuncメソッドをオーバーライドする

```java
package com.overseas_lifull.solr.functionquery;

import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.valuesource.SimpleBoolFunction;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.Date;

public class PremiumFunction extends SimpleBoolFunction {

    public PremiumFunction(ValueSource source) {
        super(source);
    }

    protected String name() {
        return "isPremium";
    }

    protected boolean func(int doc, FunctionValues vals) {
        boolean result = false;
        String jsonStr = vals.strVal(doc); // isPremium関数の引数を文字列として取得する
        /* 文字列を処理してtrue, falseを返す
         * ... いろいろな処理 */
        if (/* 判定条件 */) {
          return true;
        } else {
          return false;
        }
    }
}
```

コンパイルしてSolrが想定しているディレクトリに配置し、solrconfig.xmlを編集します。


## コンパイル

```bash
$ javac -classpath "./lib/*:/opt/solr-5.5.3/server/solr-webapp/webapp/WEB-INF/lib/lucene-core-5.5.3.jar:/opt/solr-5.5.3/server/solr-webapp/webapp/WEB-INF/lib/lucene-queries-5.5.3.jar:/opt/solr-5.5.3/server/solr-webapp/webapp/WEB-INF/lib/lucene-queryparser-5.5.3.jar:/opt/solr-5.5.3/server/solr-webapp/webapp/WEB-INF/lib/solr-core-5.5.3.jar:/opt/solr-5.5.3/server/solr-webapp/webapp/WEB-INF/lib/solr-solrj-5.5.3.jar" com/overseas_lifull/solr/functionquery/PremiumFunction.java com/overseas_lifull/solr/functionquery/PremiumParser.java com/overseas_lifull/solr/functionquery/PremiumData.java
```

指定するclasspathは使用するクラスやソースの場所によって変わってきます。

## JARファイル作成

```bash
$ jar cvf premiumQueryFunction.jar ./com/overseas_lifull/solr/functionquery/PremiumData.class ./com/overseas_lifull/solr/functionquery/PremiumData.java ./com/overseas_lifull/solr/functionquery/PremiumFunction.class ./com/overseas_lifull/solr/functionquery/PremiumFunction.java ./com/overseas_lifull/solr/functionquery/PremiumParser.class ./com/overseas_lifull/solr/functionquery/PremiumParser.java
```

## JARファイルの配置

Solrコアのディレクトリに`lib`ディレクトリを作成してそこに配置します。

下記のようなディレクトリ構成になるはずです。

```
conf/
  admin-extra.html
  admin-extra.menu-bottom.html
  admin-extra.menu-top.html
  data-config.xml
  dataimport.properties
  elevate.xml
  schema.xml
  solrconfig.xml
lib/
  premiumQueryFunction.jar
core.properties
```

## solrconfig.xmlを編集

```xml
<valueSourceParser name="isPremium" class="com.overseas_lifull.solr.functionquery.PremiumParser" />
```

## Solrを再起動

編集したsolrconfig.xmlと配置したjarファイルを読み込ませるためにSolrを再起動して下さい。

## クエリを投げてみる

```
http://localhost:8983/solr/my_core/select?q=*:*&fl=*,isPremium(flat_premium_data)&wt=json&indent=true
```

レスポンスとして返ってきたドキュメントの中に
`isPremium(flat_premium_data): true`のようにフィールドと値(trueまたはfalse)があれば成功です。