最近用到solr排序的復雜排序,系統最開始的排序時重寫了文本相關分計算部分,增加新的排序邏輯后性能下降十分明顯,考慮到用reRank和自定義函數的方法來解決,實際操作中碰到一些問題,自定義函數參考了http://blog.sina.com.cn/s/blog_65ae97720102vujw.html
然后再rerank測試通過,確實只會對頭部指定的docs進行重新計算分數。
首先創建一個maven項目,pom依賴項如下,solr core使用6.6版本
<dependencies> <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-core</artifactId> <version>6.6.0</version> <exclusions> <exclusion> <groupId>org.restlet.jee</groupId> <artifactId>org.restlet.ext.servlet</artifactId> </exclusion> <exclusion> <groupId>org.restlet.jee</groupId> <artifactId>org.restlet</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>6.6.0</version> </dependency> </dependencies>
項目中增加自己的package,增加兩個類文件,分別實現 ValueSourceParser和ValueSource
項目文件結構如下圖
首先是繼承ValueSourceParser的MyValueSourceParser 類,重寫parse方法,return MyScoreSource類的實例。
MyValueSourceParser 類代碼如下:
package com.wmf.customfunc; /** * Created by wangmf on 2019/7/19. */ import org.apache.lucene.queries.function.ValueSource; import org.apache.solr.schema.SchemaField; import org.apache.solr.search.FunctionQParser; import org.apache.solr.search.SyntaxError; import org.apache.solr.search.ValueSourceParser; //需要繼承ValueSourceParser,重寫parse方法 public class MyValueSourceParser extends ValueSourceParser { public MyValueSourceParser() { super(); } @Override public ValueSource parse(FunctionQParser fp) throws SyntaxError { // String sortType = fp.parseArg(); //這里是從FunctionQParser取到第一個參數,也就是我們自定義函數里的第一個參數 sortType // String latitudeStr = fp.parseArg();//第二個參數latitude // String longitudeStr = fp.parseArg();//第三個參數longitude //本例沒用到參數
//獲取三個字段的信息 ValueSource intval1 = getValueSource(fp,"intval1"); ValueSource intval2 = getValueSource(fp,"intval2"); ValueSource floatval1 = getValueSource(fp,"floatval1"); //將參數及需要的文檔的值傳給自定義的ValueSource方法,打分規則在自定義的ValueSource中定制 MyScoreSource stringFieldSource = new MyScoreSource(intval1,intval2,floatval1); return stringFieldSource; } //該方法是根據字段名,從FunctionQParser得到文檔該字段的相關信息 public ValueSource getValueSource(FunctionQParser fp, String arg) { if (arg == null) return null; SchemaField f = fp.getReq().getSchema().getField(arg); return f.getType().getValueSource(f, fp); } }
MyScoreSource類實現評分計算,代碼如下
package com.wmf.customfunc; import java.io.IOException; import java.util.Map; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.queries.function.FunctionValues; import org.apache.lucene.queries.function.ValueSource; import org.apache.lucene.queries.function.docvalues.FloatDocValues; /** * Created by wangmf on 2019/7/19. */ public class MyScoreSource extends ValueSource {//需要繼承ValueSource ,重寫getValues方法 private ValueSource intval1; private ValueSource intval2; private ValueSource floatval1; //通過構造方法的參數傳遞取得filed中的值 public MyScoreSource(ValueSource intVal1, ValueSource intVal2, ValueSource floatVal1) { this.intval1 = intVal1; this.intval2 = intVal2; this.floatval1 = floatVal1; } @Override public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException { final FunctionValues IntValue1 = intval1.getValues(context,readerContext); final FunctionValues IntValue2 = intval2.getValues(context,readerContext); final FunctionValues FloatValue1 = floatval1.getValues(context,readerContext); return new FloatDocValues(this) { //重寫floatVal方法,此處為打分排序規則 @Override public float floatVal(int doc) { String strInt1 = IntValue1.strVal(doc); String strInt2 = IntValue2.strVal(doc); String strFloat1 = FloatValue1.strVal(doc); float sc1 = Float.parseFloat(strInt1); float sc2 = Float.parseFloat(strInt2); float sc3 = Float.parseFloat(strFloat1); System.out.println("Int1:" + strInt1 + "," + "Int2:" + strInt2 + "," + "Float1:" + strFloat1); return sc1 + sc2 + sc3; //返回自己計算出的得分值 } @Override public String toString(int doc) { return name() + '(' + IntValue1.strVal(doc) + ',' + IntValue2.strVal(doc) + ',' + FloatValue1.strVal(doc) + ')'; } }; } @Override public boolean equals(Object o) { return true; } @Override public int hashCode() { return 0; } @Override public String description() { return name(); } public String name() { return "Myfunction"; } }
測試用的邏輯,實際很簡單,將三個字段:intval1,intval2,floatval1的值相加作為新的分數,實際業務中邏輯會更復雜,但邏輯都可以在實例new FloatDocValues(this)寫,根據不同的返回,重寫floatVal方法,或者用其他繼承了FloatDocValues類或者DoubleDocValues、StrDocValues等類實現邏輯。
代碼完成后打包,將生成的包復制到solr中,如果是在使用solr自帶的jetty作為web容器,則將打好的jar包復制到 server/solr-webapp/webapp/WEB-INF/lib/目錄下,然后修改對應對應core的solrconfig.xml
修改內容如下:
<valueSourceParser name="myfunc" class="com.wmf.customfunc.MyValueSourceParser" />
name就是我們再調用時需要使用的函數名,通常我們在查詢時想要得到函數計算值,可以直接在fl中增加該函數
http://solrserver/solr/core_demo/select?fl=*,_val_:myfunc()&indent=on&q=*:*&wt=json
得到結果如下
結合solr的reRankQuery可以實現在文本相關的頭部docs中進行排序,注意,在rerank查詢時{}中的內容大小寫敏感
http://solrserver/solr/core_demo/select?q=*:*&wt=json&sort=intval1 desc&rq={!rerank reRankQuery=$rqq reRankDocs=2 reRankWeight=1.0}&rqq={!func}myfunc()&
fl=*,score,_val_:myfunc()&indent=on
該查詢實現:查詢所有文檔,並按照intval1字段進行倒序排序,再在前面的兩個doc,按照我們自定義的函數myfunc計算的分數,重新進行年排序,返回結果如下圖
這里的邏輯是在reRankQuery中計算的分數按照reRankWeight的權重和第一次查詢的score相加后計算新的分數,計算式只針對reRankDocs的指定的頭部數量的doc進行,得到新的分數score。從上圖可以看到前面兩個doc的score是函數計算的分數*1(權重是1.0)+1(原有的分數)。從第三條開始score只有第一次查詢的1.0。從而在進行第二次查詢是實現自定義的邏輯。
另外如果要按照計算的分數排序,使用sort={!func}myfunc() desc,進行過濾,應該使用solr的frange命令{!frange l=13}myfunc() 過濾小於13分的doc。
注:除了在reRank中使用外,其他查詢、排序、過濾都會在召回的所有doc中進行計算操作,可能對性能影響比較大。
獲取三個字段的信息