前面已经对Lucene的基本内容有一个相关的了解,特别是反向索引 ,接下来将重点介绍下相关度排名。
1. 什么是相关度排名
直观理解搜索结果精准由高到低
如:要查询 苍老师、tony、火锅有关的新闻:
含有三个关键字(相关度最高)的新闻排前面,含两个关键字(相关度次之)排次之,含一个关键字 的排次次之。
当然,这是由一个算法完成的。
2. 如何设计相关性模型
利用出现次数来建立模型
规则1: 统计出现次数,根据次数从高到低排
{{1,5},{5,3},{12,1},{8,1}}:文章1出现5次,文章5出现3次,文章12出现1次,文章8出现1次。问题:标题中出现苍老师,与新闻的内容中出现苍老师,哪个是专门写苍老师的相关度高些?
规则2:加入权重,标题权重10,内容权重1,计算权重得分,按高-低排序
{{1,23},{12,10},{5,3},{8,1}},总结:利用出现次数来建立模型这个相关性模型很简单。有时排序会不是很准确。
复杂的相关性计算模型有:
- TF/IDF和字段长度准则(长度越长,相关性越低),即检索词频率【检索词在该字段出现的频率】/反向文档频率【检索词在索引中出现的频率】,具体参参阅es-search
- 向量空间模型
- 贝叶斯概率模型,如:BM25
3. lucene相关度排序
官方介绍,在Lucene 7.4中,我们打分的对象是文档。文档是字段的集合。每个字段都有关于如何创建和存储 (tokenized
, stored
)的语义。重要的是要注意,Lucene评分在Fields上工作,然后组合结果返回文档。这是很重要的,因为两个文档具有相同的内容,但是由于长度规范化,一个文档在两个字段中具有内容,而另一个文档在一个字段中具有内容,所以对于相同的查询,可能返回不同的分数。lucene 为查询出来的结果(document)都打一个分,得分越高,排列的顺序越靠前,当然,我们也可以自己去干预。
-
相关度得分
- 相关度得分是在查询时根据查询条件实进计算出来的;
- 如果索引库数据不变,查询条件不变,查出的文档得分也不变;
-
注意事项
- 排序我们可以通过设置这个权重值来影响他的得分;
- 得分跟命中的次数有关系;
- 这个得分跟文档出现的搜索关键字的次数或者位置有关系...
-
提高分数
- Lucene允许通过包装
BoostQuery
来影响查询的各个部分的得分贡献,也就是通过设置boost值。
- Lucene允许通过包装
-
改变得分-相似性
调整Similarity
是改变评分算法,这在索引时使用IndexWriterConfig.setSimilarity(Similarity)
进行,在查询时使用IndexSearcher.setSimilarity(Similarity)
进行。确保在查询时使用与索引时相同的相似性(以便规范被正确编码/解码);Lucene不努力验证这一点。
4. Boost
上面提到过Boost,它影响着索引阶段和搜索阶段。在索引阶段设定文档的Document.setBoost和域的Field.setBoost值,这些值是在索引阶段就写入索引文件的,存储在标准化因子(.nrm)文件中,一旦设定,除非删除此文档,否则无法改变。如果不进行设定,则Document Boost和Field Boost默认为1。在6.4版本中已经不再支持了。
5. Similarity
Similarity是非常重要的接口,它的实现非常多。ClassicSimilarity曾经是Lucene/Solr默认评分公式,但是从lucene-6.0
开始已经改成BM25Similarity
了。
5.1 ClassicSimilarity
它是TFIDFSimilarity的实现类,它的计算公式:tf * idf * boost * lengthNorm
- tf ,查询的词在文档中出现的次数的平方根
- idf ,表示反转文档频率,观察了一下所有的文档都一样,所以那就没什么用处,不会起什么决定作用
- boost ,激励因子,值越大代表越重要,score得分越高
- lengthNorm,一个域中包含的Term总数越多,也即文档越长,此值越小,文档越短,此值越大,
公式:(1.0 / Math.sqrt(numTerms))
@Test
public void go_explain() throws IOException {
Directory directory = new RAMDirectory();
IndexWriterConfig config = new IndexWriterConfig();
config.setUseCompoundFile(true);
IndexWriter writer = new IndexWriter(directory, config);
String feildName="title";
Field f1 = new TextField(feildName, "bc bc", Field.Store.YES);
Field f2 = new TextField(feildName, "ab bc", Field.Store.YES);
Field f3 = new TextField(feildName, "ab bc cd", Field.Store.YES);
Field f4 = new TextField(feildName, "abc", Field.Store.YES);
Document doc1 = new Document();
Document doc2 = new Document();
Document doc3 = new Document();
Document doc4 = new Document();
doc1.add(f1);
doc2.add(f2);
doc3.add(f3);
doc4.add(f4);
writer.addDocument(doc1);
writer.addDocument(doc2);
writer.addDocument(doc3);
writer.addDocument(doc4);
writer.close();
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
searcher.setSimilarity(new ClassicSimilarity());//切换算法
TermQuery q = new TermQuery(new Term(feildName, "bc"));
//Weight weight = q.createWeight(searcher, true, 3); //不要再这样使用,否则导致TFIDFSimilarity不生效
BoostQuery bq=new BoostQuery(q,2f);
TopDocs topDocs = searcher.search(bq, 10);
System.out.println("totalHits:" + topDocs.totalHits + ",MaxScore" + topDocs.getMaxScore());
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
Document doc = searcher.doc(scoreDoc.doc);
System.out.print(doc.get(feildName));
System.out.print(" ");
System.out.print(scoreDoc.score);
System.out.print(" ");
System.out.println(searcher.explain(bq, i));
}
reader.close();
directory.close();
}
这里思考一个问题:searcher.explain(bq, i)中的i默认是从0开始,是随意写的吗?其实不是的,它就是文档编号,为什么呢?具体可参阅我之前的分享的系统架构及核心模块。
totalHits:3,MaxScore1.9999999
bc bc 1.9999999 1.9999999 = weight(title:bc in 0) [ClassicSimilarity], result of:
1.9999999 = score(doc=0,freq=2.0), product of:
2.0 = boost
0.99999994 = fieldWeight in 0, product of:
1.4142135 = tf(freq=2.0), with freq of:
2.0 = termFreq=2.0
1.0 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
3.0 = docFreq
3.0 = docCount
0.70710677 = fieldNorm(doc=0)ab bc 1.4142135 1.4142135 = weight(title:bc in 1) [ClassicSimilarity], result of:
1.4142135 = score(doc=1,freq=1.0), product of:
2.0 = boost
0.70710677 = fieldWeight in 1, product of:
1.0 = tf(freq=1.0), with freq of:
1.0 = termFreq=1.0
1.0 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
3.0 = docFreq
3.0 = docCount
0.70710677 = fieldNorm(doc=1)ab bc cd 1.1547005 1.1547005 = weight(title:bc in 2) [ClassicSimilarity], result of:
1.1547005 = score(doc=2,freq=1.0), product of:
2.0 = boost
0.57735026 = fieldWeight in 2, product of:
1.0 = tf(freq=1.0), with freq of:
1.0 = termFreq=1.0
1.0 = idf, computed as log((docCount+1)/(docFreq+1)) + 1 from:
3.0 = docFreq
3.0 = docCount
0.57735026 = fieldNorm(doc=2)
这里解析下,记录f1(bc bc),score(1.9999999)=tf (1.4142135)* idf(1.0) * boost(2.0) * lengthNorm(0.70710677),termFreq(2)是词频代表出现了2次
5.2 BM25Similarity
这里注释掉代码中“切换算法”那一行即可,从下面结果中可以看出,BM25的idf和tf都做了一些变形,特别是tf公式(它叫tfNorm),还加入了两个经验参数k1和b(主要用来调整精准度),默认取K1=1.2,b=0.75。
totalHits:3,MaxScore0.38258293
bc bc 0.38258293 0.38258293 = weight(title:bc in 0) [BM25Similarity], result of:
0.38258293 = score(doc=0,freq=2.0 = termFreq=2.0
), product of:
2.0 = boost
0.13353139 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
3.0 = docFreq
3.0 = docCount
1.4325581 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
2.0 = termFreq=2.0
1.2 = parameter k1
0.75 = parameter b
2.3333333 = avgFieldLength
2.0 = fieldLengthab bc 0.28363907 0.28363907 = weight(title:bc in 1) [BM25Similarity], result of:
0.28363907 = score(doc=1,freq=1.0 = termFreq=1.0
), product of:
2.0 = boost
0.13353139 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
3.0 = docFreq
3.0 = docCount
1.0620689 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
1.0 = termFreq=1.0
1.2 = parameter k1
0.75 = parameter b
2.3333333 = avgFieldLength
2.0 = fieldLengthab bc cd 0.23911436 0.23911436 = weight(title:bc in 2) [BM25Similarity], result of:
0.23911436 = score(doc=2,freq=1.0 = termFreq=1.0
), product of:
2.0 = boost
0.13353139 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
3.0 = docFreq
3.0 = docCount
0.89534885 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
1.0 = termFreq=1.0
1.2 = parameter k1
0.75 = parameter b
2.3333333 = avgFieldLength
3.0 = fieldLength
总结,搜索引擎中会提供一种、或多种实现供选择使用,电商网站中的搜索相关性计算会考虑更多,更复杂。这里提问你一个问题lucene怎样去实现“猜你喜欢”?我的另一篇中给出解答。