ネクストでレコメンドエンジン開発をしてる古川です。 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 を作成して実現 」 を紹介したいと思います。