ネクストでレコメンドエンジン開発をしてる古川です。 前回 は、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つの数値定数を入力とし、a x x + b x y + c y y + d x + e y + 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 を作成して実現 」 に続きます。