ネクストでレコメンドエンジン開発をしてる古川です。
solrにおいて、複数フィールド値を組み合わせたソートを 実現する方法について紹介します。
実現方法としては、
- function query を組み合わせて実現
- 独自のfunction query を作成して実現
- 独自の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 を作成して実現」 を紹介したいと思います。