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

LIFULL Creators Blog

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

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

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

solrにおいて、複数フィールド値を組み合わせたソートを 実現する方法について紹介します。

実現方法としては、

  1. function query を組み合わせて実現
  2. 独自のfunction query を作成して実現
  3. 独自のsearch component を作成して実現

という三つの方法があり、上から下に

  • 実装方法: 簡単 → 大変
  • 実行速度: 遅い → 早い
  • 応用範囲: 狭い → 広い

という特徴があります。

昨年リリースした、「HOME'S へやくる!」では、 2の方法で、たとえ指定した条件にすべて合致しなくても、指定した条件に、 近い順に物件リストを返すということを実現しています。


今回は、まず、1. function query を組み合わせによる実現方法を 紹介したいと思います。

以下、solr4.6.1 をベースに説明しますが、他のバージョンでも 特に問題ないと思います。

準備


solr 環境を作成

wget http://archive.apache.org/dist/lucene/solr/4.6.1/solr-4.6.1.tgz
tar xvzf solr-4.6.1

テスト用スキーマ作成

./solr-4.6.1/example/solr/collection1/conf/schema.xml を以下の内容に書き換えます。

<?xml version="1.0" encoding="UTF-8" ?>
<schema name="nextblog" version="1.5">
  <fields>
    <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
    <field name="x" type="float" indexed="true" stored="true" required="false" multiValued="false" />
    <field name="y" type="float" indexed="true" stored="true" required="false" multiValued="false" />
    <field name="_version_" type="long" indexed="true" stored="true"/>
    <field name="text" type="text_general" indexed="true" stored="false" multiValued="true"/>
  </fields>
  <copyField source="id" dest="text"/>
  <uniqueKey>id</uniqueKey>
  <solrQueryParser defaultOperator="AND"/>
  <types>
    <fieldType name="string" class="solr.StrField" sortMissingLast="true" />
    <fieldType name="float" class="solr.TrieFloatField" precisionStep="0" positionIncrementGap="0"/>
    <fieldType name="long" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/>
    <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
      <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
      <analyzer type="query">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
        <filter class="solr.LowerCaseFilterFactory"/>
      </analyzer>
    </fieldType>
  </types>
</schema>

solr 起動

cd ./solr-4.6.1/example
java -jar start.jar &

ブラウザを起動して、http://localhost:8983/solr/ で管理画面が開くことを確認します。

データインポート

sample01.csv という名前で以下の内容のファイルを作成します。

id,x,y
homes1,1,4
moneymo1,2,5
lococom1,3,6

curl コマンドでcsvファイルをsolrにインポートします。

curl 'http://localhost:8983/solr/update/csv'  --data-binary @sample01.csv -H 'Content-type:text/plain; charset=utf-8'
curl 'http://localhost:8983/solr/update?commit=true'

function query 実行


さて、ここからが本番です。solr では、こちら にあるように様々なfunction query がデフォルトで実装されています。 例えば、x * y の値をフィールド値を取得したい場合には、

http://localhost:8983/solr/select/?q=*:*&fl=id,x,y,product(x,y)

のように実行すると、スキーマに存在しないx * yの値を取得することができます。

<result name="response" numFound="3" start="0">
  <doc>
    <str name="id">homes1</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="product(x,y)">4.0</float>
  </doc>
  <doc>
    <str name="id">moneymo1</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="product(x,y)">10.0</float>
  </doc>
  <doc>
    <str name="id">lococom1</str>
    <float name="x">3.0</float>
    <float name="y">6.0</float>
    <float name="product(x,y)">18.0</float>
  </doc>
</result>

さらに、function queryを、ソート引数に指定することで、 関数値に従ったソート順でドキュメントを取得できます。

http://localhost:8983/solr/select/?q=*:*&fl=id,x,y,product(x,y)&sort=product(x,y) decs
<result name="response" numFound="3" start="0">
  <doc>
    <str name="id">lococom1</str>
    <float name="x">3.0</float>
    <float name="y">6.0</float>
    <float name="product(x,y)">18.0</float>
  </doc>
  <doc>
    <str name="id">moneymo1</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="product(x,y)">10.0</float>
  </doc>
  <doc>
    <str name="id">homes1</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="product(x,y)">4.0</float>
  </doc>
</result>

function query 同士を組み合わせて、デフォルトには存在しない 関数を実現することができます。

http://172.20.12.206:8983/solr/select/?q=*:*&fl=id,x,y,if(sub(x,2),if(sub(x,1),5,1),10)

インデントがないので分かりづらいですが、javascript で書くと、 以下のような関数を実行していることになります。

function (x) {
    if (x == 2) return 10;
    else if (x==1) return 5;
    else return 1;
}

検索結果のフィールド値をみると、確かに期待した結果を得られています。

<result name="response" numFound="3" start="0">
  <doc>
    <str name="id">homes1</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <long name="if(sub(x,2),if(sub(x,1),5,1),10)">1</long>
  </doc>
  <doc>
    <str name="id">moneymo1</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <long name="if(sub(x,2),if(sub(x,1),5,1),10)">10</long>
  </doc>
  <doc>
    <str name="id">lococom1</str>
    <float name="x">3.0</float>
    <float name="y">6.0</float>
    <long name="if(sub(x,2),if(sub(x,1),5,1),10)">5</long>
  </doc>
</result>

注意


function query では、フィールド値が存在しない場合、そのフィールド値は、 0が入っているものとして計算されてしまいます。 欠損値のあるデータを追加して試してみます。

sample02.csv

id,x,y
homes2,1,4
moneymo2,2,5
lococom2,,6
curl 'http://localhost:8983/solr/update/csv'  --data-binary @sample02.csv -H 'Content-type:text/plain; charset=utf-8'
curl 'http://localhost:8983/solr/update?commit=true'
http://localhost:8983/solr/select/?q=*:*&fl=id,x,y,product(x,y)&sort=product(x,y) asc
<result name="response" numFound="6" start="0">
  <doc>
    <str name="id">lococom2</str>
    <float name="y">6.0</float>
    <float name="product(x,y)">0.0</float>
  </doc>
  <doc>
    <str name="id">homes1</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="product(x,y)">4.0</float>
  </doc>
  <doc>
    <str name="id">homes2</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="product(x,y)">4.0</float>
  </doc>
  <doc>
    <str name="id">moneymo1</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="product(x,y)">10.0</float>
  </doc>
  <doc>
    <str name="id">moneymo2</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="product(x,y)">10.0</float>
  </doc>
  <doc>
    <str name="id">lococom1</str>
    <float name="x">3.0</float>
    <float name="y">6.0</float>
    <float name="product(x,y)">18.0</float>
  </doc>
</result>

id=lococom2 の product(x,y) の値が、0.0 になってしまっています。

<doc>
  <str name="id">lococom2</str>
  <float name="y">6.0</float>
  <float name="product(x,y)">0.0</float>
</doc>

フィールドが空の場合を考慮した処理を行うには、exists関数を 組み合わせてやる必要があります。

例えば、以下の例では、フィールドxの値が空の場合はデフォルト値100を 採用して、ソート順位を最下位にしています。

http://localhost:8983/solr/select/?q=*:*&fl=id,x,y,if(exists(x),product(x,y),100)&sort=if(exists(x),product(x,y),100) asc
<result name="response" numFound="6" start="0">
  <doc>
    <str name="id">homes1</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="if(exists(x),product(x,y),100)">4.0</float>
  </doc>
  <doc>
    <str name="id">homes2</str>
    <float name="x">1.0</float>
    <float name="y">4.0</float>
    <float name="if(exists(x),product(x,y),100)">4.0</float>
  </doc>
  <doc>
    <str name="id">moneymo1</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="if(exists(x),product(x,y),100)">10.0</float>
  </doc>
  <doc>
    <str name="id">moneymo2</str>
    <float name="x">2.0</float>
    <float name="y">5.0</float>
    <float name="if(exists(x),product(x,y),100)">10.0</float>
  </doc>
  <doc>
    <str name="id">lococom1</str>
    <float name="x">3.0</float>
    <float name="y">6.0</float>
    <float name="if(exists(x),product(x,y),100)">18.0</float>
  </doc>
  <doc>
    <str name="id">lococom2</str>
    <float name="y">6.0</float>
    <long name="if(exists(x),product(x,y),100)">100</long>
  </doc>
</result>


まとめ

function query を組み合わせて、新たなfunction query を作成する 方法を紹介しました。結構、柔軟に独自のスコア計算を実現できることが 伝わったのではないかと思います。

この手法では、データアクセスに非効率な部分があり、対象となる ドキュメント数が増加したり、関数が複雑になってくると、速度的な 問題が発生してしまいます。

次回は、それを回避する方法として、「独自のfunction query を作成して実現」 を紹介したいと思います。