読者です 読者をやめる 読者になる 読者になる

LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

solr で独自基準ソート(function query plugin)

ネクストでレコメンドエンジン開発をしてる古川です。

前回は、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 を作成して実現」 に続きます。