ネクストでレコメンドエンジン開発をしてる古川です。
前回は、solr で独自基準ソートを実現する方法として、「1.既存のfunction query を組み合わせで実現する方法」を紹介しましたので、今回は、「2. 独自のfunction query 作成して実現する方法」を紹介したいと思います。
solr のソースコード確認
まずは、solrのfunction queryがどのように実装されているか、recip関数を対象に見てみます。 solr のソースコードを取得して、適当なディレクトリに展開します。
wget http://ftp.yz.yamagata-u.ac.jp/pub/network/apache/lucene/solr/4.6.1/solr-4.6.1-src.tgz tar xvzf solr-4.6.1-src.tgz
recip関数 の説明を読むと、recip(x,m,a,b) のようにクエリで指定し、xはドキュメントのフィールド名か、もしくは数値を返す関数、その他の、m、a、bは、数値定数であると記述されています。
solr-4.6.1/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
のrecip関数に関連する部分
151 addParser("recip", new ValueSourceParser() { 152 @Override 153 public ValueSource parse(FunctionQParser fp) throws SyntaxError { 154 ValueSource source = fp.parseValueSource(); 155 float m = fp.parseFloat(); 156 float a = fp.parseFloat(); 157 float b = fp.parseFloat(); 158 return new ReciprocalFloatFunction(source, m, a, b); 159 } 160 });
を見ると、ドキュメントに依存するフィールド値をValueSourceクラスの変数 sourceとして、ドキュメントに依存しないその他は、float型の変数m、a、b として解釈し、それを引数にReciproacalFloatfunction クラスを生成して返していることが分かります。
ReciproacalFloatfunctionの中身をみると、意外と簡単なコードであることが分かります。
solr-4.6.1/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/ReciprocalFloatFunction.java
これらをベースにして作成していけばよさそうです。
plugin jar ファイル作成
ReciprocalFloatFunction.java をまねして、myfunc(x,y,a,b,c,d) という、ドキュメントフィールド値x, y とその他、4つの数値定数を入力とし、axx + bxy + cyy + dx + ey + f の計算結果を返す独自クラスを実装してみたのが、以下のファイルです。
MyFloatFunction.java
package jp.co.homes.functionquery; import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.queries.function.FunctionValues; import org.apache.lucene.queries.function.ValueSource; import org.apache.lucene.queries.function.docvalues.FloatDocValues; import org.apache.lucene.search.IndexSearcher; import java.io.IOException; import java.util.Map; public class MyFloatFunction extends ValueSource { protected final ValueSource x; protected final ValueSource y; protected final float a; protected final float b; protected final float c; protected final float d; protected final float e; protected final float f; /** * f(x,y,a,b,c,d,e,f) = a*x*x + b*x*y + c*y*y + d*x + e*y + f */ public MyFloatFunction(ValueSource x, ValueSource y, float a, float b, float c, float d, float e, float f) { this.x = x; this.y = y; this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; } @Override public FunctionValues getValues(Map context, AtomicReaderContext readerContext) throws IOException { final FunctionValues xVals = x.getValues(context, readerContext); final FunctionValues yVals = y.getValues(context, readerContext); return new FloatDocValues(this) { @Override public float floatVal(int doc) { float x = xVals.floatVal(doc); float y = yVals.floatVal(doc); return a*x*x + b*x*y + c*y*y + d*x + e*y + f; } @Override public String toString(int doc) { String xd = xVals.toString(doc); String yd = yVals.toString(doc); return Float.toString(a) + "*" + xd + "*" + xd + '+' + Float.toString(b) + "*" + xd + "*" + yd + '+' + Float.toString(c) + "*" + yd + "*" + yd + '+' + Float.toString(d) + "*" + xd + '+' + Float.toString(e) + "*" + yd + '+' + Float.toString(f); } }; } @Override public int hashCode() { int h = Float.floatToIntBits(a) + Float.floatToIntBits(b) + Float.floatToIntBits(c) + Float.floatToIntBits(d) + Float.floatToIntBits(e) + Float.floatToIntBits(f); h ^= (h << 13) | (h >>> 20); return h + (Float.floatToIntBits(b)) + x.hashCode() + y.hashCode(); } @Override public boolean equals(Object o) { if (MyFloatFunction.class != o.getClass()) return false; MyFloatFunction other = (MyFloatFunction)o; return this.a == other.a && this.b == other.b && this.c == other.c && this.d == other.d && this.e == other.e && this.f == other.f && this.x.equals(other.x) && this.y.equals(other.y); } }
次に、検索クエリにmyfunc(x,y,a,b,c,d,e,f)という文字列(x,yは任意の数値フィールド名、a-f は数値定数)が 与えられた場合に、引数を解釈してMyFloatFunctionクラスを作成して返すクラスを実装します。
HomesValueSourceParser.java
package jp.co.homes.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 HomesValueSourceParser extends ValueSourceParser { @Override public void init(NamedList namedList) { } @Override public ValueSource parse(FunctionQParser fp) throws SyntaxError { ValueSource x = fp.parseValueSource(); ValueSource y = fp.parseValueSource(); float a = fp.parseFloat(); float b = fp.parseFloat(); float c = fp.parseFloat(); float d = fp.parseFloat(); float e = fp.parseFloat(); float f = fp.parseFloat(); return new MyFloatFunction(x, y, a, b, c, d, e, f); } }
この二つのファイルを、前回インストールしたsolrのディレクトリ、以下のjarファイル
solr-4.6.1/example/solr-webapp/webapp/WEB-INF/lucene-core-4.6.1.jar solr-4.6.1/example/solr-webapp/webapp/WEB-INF/lucene-queries-4.6.1.jar solr-4.6.1/example/solr-webapp/webapp/WEB-INF/lucene-queryparser-4.6.1.jar solr-4.6.1/example/solr-webapp/webapp/WEB-INF/solr-core-4.6.1.jar solr-4.6.1/example/solr-webapp/webapp/WEB-INF/solr-solrj-4.6.1.jar
にpathに通してコンパイルして、homes-function-query.jar というjar ファイルを作成します。
設定
前回設定したsolrで、このプラグインを使えるようにします。 まず、collection1フォルダの下にlibディレクトリを作成し、その下に、作成したjar ファイルをコピーします。
mkdir solr-4.6.1/example/solr/collection1/lib cp homes-function-query-plugin.jar solr-4.6.1/example/solr/collection1/lib
次に、 solr-4.6.1/example/solr/collection1/conf/solrconfig.xml の60行目に以下のような二行を追加します。
<lib dir="./lib" /> <valueSourceParser name="myfunc" class="jp.co.homes.functionquery.HomesValueSourceParser" />
動作確認
solr を起動します。
cd ./solr-4.6.1/example java -jar start.jar &
新しく作成したmyfunc関数を指定したクエリを実行してみます。
http://localhost:8983/solr/collection1/select?q=*:*&fl=x,y,myfunc(x,y,1,2,3,4,5,6)
実行結果
<result name="response" numFound="6" start="0"> <doc> <float name="x">1.0</float> <float name="y">4.0</float> <float name="myfunc(x,y,1,2,3,4,5,6)">87.0</float> </doc> <doc> <float name="x">2.0</float> <float name="y">5.0</float> <float name="myfunc(x,y,1,2,3,4,5,6)">138.0</float> </doc> <doc> <float name="x">3.0</float> <float name="y">6.0</float> <float name="myfunc(x,y,1,2,3,4,5,6)">201.0</float> </doc> <doc> <float name="x">1.0</float> <float name="y">4.0</float> <float name="myfunc(x,y,1,2,3,4,5,6)">87.0</float> </doc> <doc> <float name="x">2.0</float> <float name="y">5.0</float> <float name="myfunc(x,y,1,2,3,4,5,6)">138.0</float> </doc> <doc> <float name="y">6.0</float> <float name="myfunc(x,y,1,2,3,4,5,6)">144.0</float> </doc> </result>
正しく計算できているようです。
最後のx値は欠損しているため、xが0として扱われています。フィールド値が欠損している場合に0と扱いたくない場合には、
if (xVals.exists(doc) { ... }
のようにして欠損値の場合の処理を追加する必要があります。
速度比較
function query の組み合わせと、独自 function query でどの程度速度に差が出てくるか、簡単な検証をしてみました。
データ量が少ないと差が出てこないので、100万件のデータを追加後、キャッシュが影響しないよう、数値定数を変更しながら、以下クエリのQTimeの5回平均を計算してみました。
既存function queryの組み合わせ
http://localhost:8983/solr/collection1/select?q=*:*&fl=x,y&sort=sum(product(pow(x,2),1),product(product(x,y),2), product(pow(y,2),3),product(x,4),product(y,5),6) desc
独自function query
http://localhost:8983/solr/collection1/select?q=*:*&sort=myfunc(x,y,1,2,3,4,5,6) desc
結果
function query 組み合わせ 平均QTime 201ms 独自 function query 平均QTime 19ms
myfuncの方が、相当高速であることが分かります。function queryの組み合わせでは、同じフィールドに 何度もアクセスが発生してしまうのに対して、独自 function query の方は1回しか発生しないなど、 データアクセスが効率化されているのが原因と思われます。
まとめ
思ったよりも簡単に、独自のfunction query を作成することができました。 この方法の場合、solrのqueryに、そのまま埋め込んで使えるので応用範囲が広いのがポイントで、 大抵の用途には、これで十分ではないかと思います。
次回、「独自のsearch component を作成して実現」 に続きます。