SolrのFunctionQueryには沢山便利なものが用意されていますが、
上記の関数だけでは要件を満たせないこともあると思います。
今回は、あるフィールドに登録された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クラスは以下のシグネチャをもっています。
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クラスの初期化ブロックでは
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の定義は
に定義されています。
...
/** 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メソッドが呼び出されてMath.abs関数が呼び出され、最終的に絶対値を返しています。
上記ディレクトリに定義されている幾つかのクラスを見ても同じように
- FunctionValuesクラスを継承したクラスのインスタンスを返す
- floatValやboolValなどの名前のメソッド内で抽象メソッドのfuncを実行して結果を返す
のように実装されています。
また、
https://wiki.apache.org/solr/SolrPlugins#ValueSourceParser
上記のドキュメントを見るとsolrconfig.xml
で設定すれば自分でクラスを作成して関数名をつけることができます。
以上より、必要な作業は
- ValueSourceParserクラスを継承したクラスを作成する
- SimpleFloatFunctionクラスのような引数を処理するためのクラスを作成して最終的に返したい値の型に合ったクラス(FloatDocValues等)のインスタンスを作成して返すようにfuncメソッドをオーバーライドする
今回は文字列を引数に受け取ってBool型を返すFunctionQueryを作成したいので
このSimpleBoolFunctionが使えそうです。
/**
* {@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) + ')';
}
};
}
必要な作業を書き直すと
- ValueSourceParserクラスを継承したオリジナルのクラスを作る
- SimpleBoolFunctionを継承したクラスを作ってfuncメソッドをオーバーライドする
になります。上記作業を行って独自のFunctionQueryを作成します。
ValueSourceParserクラスを継承したオリジナルのクラスを作る
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メソッドをオーバーライドする
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を編集します。
コンパイル
$ 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ファイル作成
$ 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を編集
<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)があれば成功です。