今天在用Solr5.1测试检索时,发现一个奇怪的问题,便于大家对比,先介绍下散仙的环境:
JDK1.7
Lucene5.1
Solr5.1
分词器用的ik(改的ik源码)
先看下测试的5条数据:
id,name,count
1503486364953346048,北京奇虎测腾科技有限公司,1
1503486365060300800,北京奇虎网力科技有限公司,2
1503486365065543680,北京奇虎科技有限公司,3
1503486826976903168,中国,7
1503486827011506176,中国人,8
使用IK(细粒度切词如下:)
北京 奇虎 测 腾 科技 有限公司 有限 有 限 公司
======================
北京 奇虎 网 力 科技 有限公司 有限 有 限 公司
======================
北京 奇虎 科技 有限公司 有限 有 限 公司
======================
中国人 中国 国人
======================
中国
OK,然后,大家想一个场景,假如我在搜索框里面搜索 北京奇虎 时,第几个搜索结果应该被排在第一位?大部分情况下我们都希望第三个结果,排在第一位,但结果会是这样吗?
不好意思,经实际验证,在Lucene5.1作为底层的Solr中,却并不是这样,截图如下:
等等,刚才里面不是还有一个中国人和中国的例子吗,我们搜个中国测试下,事实是这样吗?
奇怪了,因为在Lucene评分中,字段的长度也是影响评分的因素,默认情况下,是文本越短的会排在前面,但是上面两个例子中,竟然有一个出了异常,这难道是一个bug么?
在lucene的评分因素中,主要有如下几个因子:
1,tf
2,idf
3,coord
4,queryNorm
5,boost
6,norm(t,d)
7,payload
8,自定义评分
具体的介绍,请参考我以前的文章:
http://qindongliang.iteye.com/blog/2008396
针对本文这个评分问题,我们主要关注norm这个评分因子,因为它里面包含了长度因子的评分:
norm(t,d)压缩几个索引期间的加权和长度因子:
Document boost - 文档加权,在索引之前使用 doc.setBoost()
Field boost - 字段加权,也在索引之前调用 field.setBoost()
lengthNorm(field) - 由字段内的 Token 的个数来计算此值,字段越短,评分越高,在做索引的时候由 Similarity.lengthNorm 计算。
以上所有因子相乘得出 norm 值,如果文档中有相同的字段,它们的加权也会相乘:
norm(t,d) = doc.getBoost() · lengthNorm(field) · ∏ f.getBoost()
field f in d named as t
索引的时候,把 norm 值压缩(encode)成一个 byte 保存在索引中。搜索的时候再把索引中 norm 值解压(decode)成一个 float 值,这个 encode/decode 由 Similarity 提供。官方说:这个过程由于精度问题,以至不是可逆的,如:decode(encode(0.89)) = 0.75。
接下来,查看Lucene的DefaultSimilarity类源码,看下核心的几个方法代码
/** Cache of decoded bytes. */
private static final float[] NORM_TABLE = new float[256];
static {
for (int i = 0; i < 256; i++) {
NORM_TABLE[i] = SmallFloat.byte315ToFloat((byte)i);
}
}
//索引期间执行,将norm编码成一个8位字节
public final long encodeNormValue(float f) {
return SmallFloat.floatToByte315(f);
}
//搜索期间执行,将norm,还原成具体的分数,参与评分
public final float decodeNormValue(long norm) {
return NORM_TABLE[(int) (norm & 0xFF)]; // & 0xFF maps negative bytes to positive above 127
}
仔细看decodeNormValue方法,这个代码,发现里面竟然有将float强制转换为int一个强转,这意味着,精度损失。
什么意思?请看如下代码:
float a=1.524f;
float b=1.589f;
System.out.println((int)a);
System.out.println((int)b);
//结果都是1
知道这个东东后,就发现,文头题的那个问题,没错,就是因为两者的norm编码相差太少,所以导致他们的解码分数一样,从而出现了,排名问题,这也不能算Lucene的bug,可能Lucene的设计者认为,两个norm,相差的值只有大于0.1的情况下,才真正管用,否则一视同仁。
这就是答案了,所以如果想解决上面的问题,可以重新定义一个自己的评分类,并加入自己的逻辑,不建议直接修改源码实现。
最后,给出,本文测试的全部代码,以供参考:
package com.lucene.opera;
import java.io.StringReader;
import java.nio.file.Paths;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.TextField;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.similarities.DefaultSimilarity;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.wltea.analyzer.lucene.IKAnalyzer;
public class LuceneTest {
static final String indexpath="D:\\tmp\\luceneindex";
public static void index()throws Exception{
Directory dir=FSDirectory.open(Paths.get(indexpath));
IKAnalyzer ik=new IKAnalyzer(false);
IndexWriterConfig iwc=new IndexWriterConfig(ik);
iwc.setOpenMode(OpenMode.CREATE);
IndexWriter iw=new IndexWriter(dir, iwc);
// FieldType type=new FieldType();
// type.setTokenized(true);
// type.setStored(true);
// type.setIndexOptions(IndexOptions.DOCS);
// type.setOmitNorms(true);
// type.freeze();
//
Document doc=null;
// Document doc=new Document();
// doc.add(new TextField("name", "北京奇虎测腾科技有限公司", Store.YES));
//// doc.add(new Field("pname", "北京奇虎测腾科技有限公司", type));
// iw.addDocument(doc);
//
// doc=new Document();
// doc.add(new TextField("name", "北京奇虎网力科技有限公司", Store.YES));
//// doc.add(new Field("pname", "北京奇虎网力科技有限公司", type));
// iw.addDocument(doc);
//
////
// doc=new Document();
// doc.add(new TextField("name", "北京奇虎科技有限公司", Store.YES));
//// doc.add(new Field("pname", "北京奇虎科技有限公司", type));
// iw.addDocument(doc);
////
doc=new Document();
doc.add(new TextField("name", "北京奇虎测腾科技有限公司", Store.YES));
// doc.add(new Field("pname", "中国", type));
iw.addDocument(doc);
doc=new Document();
doc.add(new TextField("name", "北京奇虎科技有限公司", Store.YES));
// doc.add(new Field("pname", "中国人", type));
iw.addDocument(doc);
iw.commit();
iw.close();
System.out.println("索引成功!");
}
public static void search(String kw)throws Exception{
Directory dir=FSDirectory.open(Paths.get(indexpath));
IKAnalyzer ik=new IKAnalyzer(false);
IndexReader ir=DirectoryReader.open(dir);
IndexSearcher search=new IndexSearcher(ir);
// search.setSimilarity(new DefaultSimilarity());
QueryParser qr=new QueryParser("name", ik);
Query parse = qr.parse(kw);
System.out.println(parse.toString());
// Explanation explain = search.explain(parse , 100);
// System.out.println(explain.toString());
TopDocs td=search.search(parse, 100);
for(ScoreDoc sd:td.scoreDocs){
int i=sd.doc;
Document doc=search.doc(i);
// Explanation explain = search.explain(parse , i);
// System.out.println(explain.toString());
System.out.println(" 名称: "+doc.get("name")+" 评分: "+sd.score);
}
ir.close();
dir.close();
}
public static void main(String[] args)throws Exception {
// float []s=DefaultSimilarity.NORM_TABLE;
// for(int i=0;i<s.length;i++){
// System.out.println(i+" "+s[i]);
// }
// index();
// search("奇虎");
//
float a=1.524f;
float b=1.589f;
System.out.println((int)a);
System.out.println((int)b);
//结果都是1
// test("北京奇虎测腾科技有限公司");
// System.out.println("======================");
// test("北京奇虎网力科技有限公司");
// System.out.println("======================");
// test("北京奇虎科技有限公司");
// System.out.println("======================");
// test("中国人");
// System.out.println("======================");
// test("中国");
}
// "北京奇虎测腾科技有限公司"
// "北京奇虎网力科技有限公司"
// "北京奇虎科技有限公司"
// "中国"
// "中国人"
/****
* 测试分词token
* @param kw
* @throws Exception
*/
public static void test(String kw)throws Exception{
IKAnalyzer ik=new IKAnalyzer(false);
TokenStream token=ik.tokenStream("", new StringReader(kw));
CharTermAttribute term=token.addAttribute(CharTermAttribute.class);
token.reset();
while(token.incrementToken()){
System.out.print(term.toString()+" ");
}
System.out.println();
token.end();
token.close();
}
}
最后欢迎大家扫码关注微信公众号:我是攻城师(woshigcs),我们一起学习,进步和交流!(woshigcs)
本公众号的内容是有关搜索和大数据技术和互联网等方面内容的分享,也是一个温馨的技术互动交流的小家园,有什么问题随时都可以留言,欢迎大家来访!