以下来自solr in action。
包含:
- 词项频次。查询词项出现在当前查询文档中的次数。
- 反向文档频次。查询词项出现在所有文档总的次数。
- 此项权重。
-
标准化因子:
-
字段规范:
- 文档权重。
- 字段权重。
- 长度归一化。消除长文档的优势。因为长文档的词项频次一般会比较大。
- 协调因子。避免一个文档中出现某一个词项的次数太多导致总分值太大。目的是让结果中包含更多的是出现所有词项的文档。
-
具体说明见下文。
以下转载自网络。原文地址: http://tec.5lulu.com/detail/110d8n2ehpg2j85ec.html
简述
内容的相似性计算由搜索引擎的几种常见的检索模型有:
向量空间模型
简述中介绍了好多种相似性计算方法, Solr的索引文件中有.tvx,.tvd,tvf存储了term vector的信息,首先我们学习如何利用term vector来反映相似性程度。
用v(d1)表示了term d1的term向量,termterm 给定一个查询以及一个文档,如何计算他们的相似值呢,请看以下公式,它使用了以下概念:term frequency (tf), inverse document frequency (idf), term boosts (t.getBoost), field normalization (norm), coordination factor (coord), and query normalization (queryNorm).
- tf(t in d ) 表示该idf(t) 表示 t.getBoost() 也叫此项权重。标准化因子,它包括三个参数:
- :此值越大,说明此文档越重要。也叫文档权重。:此域越大,说明此域越重要。也叫字段权重。:一个域中包含的总数越多(我理解的是所有这个文档的所有,而不局限于查询中的),也即文档越长,此值越小,文档越短,此值越大。也叫长度归一化。目的是消除长字段的优势。
coord(q,d):一次搜索可能包含多个搜索词,而一篇文档中也可能包含多个搜索词,此项表示,当一篇文档中包含的搜索词越多,则此文档则打分越高 ,numTermsInDocumentFromQuery / numTermsInQuery
queryNorm(q):计算每个查询条目的方差和,此值并不影响排序,而仅仅使得不同的query之间的分数可以比较。
ps:理解到这里就可以了,下面的细节可以以后再研究。
评分机制
表示匹配文章的程度,如果在一篇文章中该出现了次数越多,说明该对该文章的重要性越大,因而更加匹配。相反的出现越少说明该越不匹配文章。但是这里需要注意,出现次数与重要性并不是成正比的,比如出现次,出现次,对于该文章的重要性并不是的倍,所以这里的值进行平方根计算。
tf(t in d) = numTermOccurrencesInDocument 1/2
-
idf, 表示包含该文章的个数,与tf不同,idf 越大表明该term越不重要。比如this很多文章都包含,但是它对于匹配文章帮助不大。这也如我们程序员所学的技术,对于程序员本身来说,这项技术掌握越深越好(掌握越深说明花时间看的越多,tf越大),找工作时越有竞争力。然而对于所有程序员来说,这项技术懂得的人越少越好(懂得的人少df小),找工作越有竞争力。人的价值在于不可替代性就是这个道理。
idf(t) = 1 + log (numDocs / (docFreq +1))
- t.getBoost,boost是人为给term提升权重的过程,我们可以在Index和Query中分别加入term boost,但是由于Query过程比较灵活,所以这里介绍给Query boost。term boost 不仅可以对Pharse进行,也可以对单个term进行,在查询的时候用^后面加数字表示:
- title:(solr in action)^2.5 对solr in action 这个pharse设置boost
- title:(solr in action) 默认的boost时1.0
- title:(solr^2in^.01action^1.5)^3OR"solrinaction"^2.5
-
norm(t,d) 即field norm,它包含Document boost,Field boost,lengthNorm。相比于t.getBoost()可以在查询的时候进行动态的设置,norm里面的f.getBoost()和d.getBoost()只能建索引过程中设置,如果需要对这两个boost进行修改,那么只能重建索引。他们的值是存储在.nrm文件中。
norm(t,d) = d.getBoost() • lengthNorm(f) • f.getBoost()
- d.getBoost() document的boost,对document设置boost是通过对每一个field设置boost实现的。
- f .getBoost() field的boost,这里需要提以下,Solr是支持多值域方式建索引的,即同一个field多个value,如以下代码。当一个文档里出现同名的多值域时候,倒排索引和项向量都会在逻辑上将这些域的词汇单元附加进去。当对多值域进行存储的时候,它们在文档中的存储顺序是分离的,因此当你在搜索期间对文档进行检索时,你会发现多个Field实例。如下图例子所示,当查询author:Lucene时候出现两个author域,这就是所谓的多值域现象。
-
Document doc = new Document(); for (String author : authors){ doc.add(new Field("author",author,Field.Store.YES,Field.Index.ANALYZED)); } //首先对多值域建立索引 Directory dir = FSDirectory.open(new File("/Users/rcf/workspace/java/solr/Lucene")); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LUCENE_48,new WhitespaceAnalyzer(Version.LUCENE_48)); @SuppressWarnings("resource") IndexWriter writer = new IndexWriter(dir,indexWriterConfig); Document doc = new Document(); doc.add(new Field("author","lucene",Field.Store.YES,Field.Index.ANALYZED)); doc.add(new Field("author","solr",Field.Store.YES,Field.Index.ANALYZED)); doc.add(new Field("text","helloworld",Field.Store.YES,Field.Index.ANALYZED)); writer.addDocument(doc); writer.commit(); //对多值域进行查询 IndexReader reader = IndexReader.open(dir); IndexSearcher search = new IndexSearcher(reader); Query query = new TermQuery(new Term("author","lucene")); TopDocs docs = search.search(query, 1); Document doc = search.doc(docs.scoreDocs[0].doc); for(IndexableField field : doc.getFields()){ System.out.println(field.name()+":"+field.stringValue()); } System.out.print(docs.totalHits); //运行结果 author:lucene author:solr text:helloworld 2
- 当对多值域设置boost的时候,那么该field的boost最后怎么算呢?即为每一个值域的boost相乘。比如title这个field,第一次boost是3.0,第二次1,第三次0.5,那么结果就是3*1*0.5.
- Boost: (3) · (1) · (0.5) = 1.5
-
lengthNorm, Norm的长度是field中term的个数的平方根的倒数,field的term的个数被定义为field的长度。field长度越大,Norm Field越小,说明term越不重要,反之越重要,这很好理解,在10个词的title中出现北京一次和在有200个词的正文中出现北京2次,哪个field更加匹配,当然是title。
- 最后再说明下,document boost,field boost 以及lengthNorm在存储为索引是以byte形式的,编解码过程中会使得数值损失,该损失对相似值计算的影响微乎其微。
-
queryNorm, 计算每个查询条目的方差和,此值并不影响排序,而仅仅使得不同的query之间的分数可以比较。也就说,对于同一词查询,他对所有的document的影响是一样的,所以不影响查询的结果,它主要是为了区分不同query了。
queryNorm(q) = 1 / (sumOfSquaredWeights )
sumOfSquaredWeights = q.getBoost()2 • ∑ ( idf(t) • t.getBoost() )2
coord(q,d),表示文档中符合查询的term的个数,如果在文档中查询的term个数越多,那么这个文档的score就会更高。
numTermsInDocumentFromQuery / numTermsInQuery
比如Query:AccountantAND("SanFrancisco"OR"NewYork"OR"Paris")
文档A包含了上面的3个term,那么coord就是3/4,如果包含了1个,则coord就是4/4
4源码
上面介绍了相似值计算的公式,那么现在就来查看Solr实现的代码,这部分实现是在DefaultSimilarity类中。
@Override public float coord(int overlap, int maxOverlap) { return overlap / (float)maxOverlap; } @Override public float queryNorm(float sumOfSquaredWeights) { return (float)(1.0 / Math.sqrt(sumOfSquaredWeights)); } @Override public float lengthNorm(FieldInvertState state) { final int numTerms; if (discountOverlaps) numTerms = state.getLength() - state.getNumOverlap(); else numTerms = state.getLength(); return state.getBoost() * ((float) (1.0 / Math.sqrt(numTerms))); } @Override public float tf(float freq) { return (float)Math.sqrt(freq); } @Override public float idf(long docFreq, long numDocs) { return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0); }
Solr计算score(q,d)的过程如下:
1:调用IndexSearcher.createNormalizedWeight()计算queryNorm()
public Weight createNormalizedWeight(Query query) throws IOException { query = rewrite(query); Weight weight = query.createWeight(this); float v = weight.getValueForNormalization(); float norm = getSimilarity().queryNorm(v); if (Float.isInfinite(norm) || Float.isNaN(norm)) { norm = 1.0f; } weight.normalize(norm, 1.0f); return weight; }
-
具体实现步骤如下:
- Weight weight = query.createWeight(this);
- 创建BooleanWeight->new TermWeight()->this.stats = similarity.computeWeight)->this.weight = idf * t.getBoost()
-
public IDFStats(String field, Explanation idf, float queryBoost) { // TODO: Validate? this.field = field; this.idf = idf; this.queryBoost = queryBoost; this.queryWeight = idf.getValue() * queryBoost; // compute query weight } 计算sumOfSquaredWeights s = weights.get(i).getValueForNormalization()计算( idf(t) • t.getBoost() )2 如以下代码所示,queryWeight在上一部中计算出 public float getValueForNormalization() { // TODO: (sorta LUCENE-1907) make non-static class and expose this squaring via a nice method to subclasses? return queryWeight * queryWeight; // sum of squared weights }
- BooleanWeight->getValueForNormalization->sum = (q.getBoost)2 *∑(this.weight)2 = (q.getBoost)2 *∑(idf * t.getBoost())2
-
-
public float getValueForNormalization() throws IOException { float sum = 0.0f; for (int i = 0 ; i < weights.size(); i++) { // call sumOfSquaredWeights for all clauses in case of side effects float s = weights.get(i).getValueForNormalization(); // sum sub weights if (!clauses.get(i).isProhibited()) { // only add to sum for non-prohibited clauses sum += s; } } sum *= getBoost() * getBoost(); // boost each sub-weight return sum ; }
- 计算完整的querynorm() = 1 / Math.sqrt(sumOfSquaredWeights));
-
public float queryNorm(float sumOfSquaredWeights) { return (float)(1.0 / Math.sqrt(sumOfSquaredWeights)); }
- weight.normalize(norm, 1.0f) 计算norm()
- topLevelBoost *= getBoost();
- 计算value = idf()*queryWeight*queryNorm=idf()2*t.getBoost()*queryNorm(queryWeight在前面已计算出)
public void normalize(float queryNorm, float topLevelBoost) { this.queryNorm = queryNorm * topLevelBoost; queryWeight *= this.queryNorm; // normalize query weight value = queryWeight * idf.getValue(); // idf for document }
-
2:调用IndexSearch.weight.bulkScorer()计算coord(q,d),并获取每一个term的docFreq,并将docFreq按td从小到大排序。
if (optional.size() == 0 && prohibited.size() == 0) { float coord = disableCoord ? 1.0f : coord(required.size(), maxCoord); return new ConjunctionScorer(this, required.toArray(new Scorer[required.size()]), coord); }
-
3:score.score()进行评分计算,获取相似值,并放入优先级队列中获取评分最高的doc id。
- weightValue= value =idf()2*t.getBoost()*queryNorm
- sore = ∑(tf()*weightValue)*cood 计算出最终的相似值
- 这里貌似没有用到lengthNorm,
-
public float score(int doc, float freq) { final float raw = tf(freq) * weightValue; // compute tf(f)*weight return norms == null ? raw : raw * decodeNormValue(norms.get(doc)); // normalize for field } public float score() throws IOException { // TODO: sum into a double and cast to float if we ever send required clauses to BS1 float sum = 0.0f; for (DocsAndFreqs docs : docsAndFreqs) { sum += docs.scorer.score(); } return sum * coord; } public void collect(int doc) throws IOException { float score = scorer.score(); // This collector cannot handle these scores: assert score != Float.NEGATIVE_INFINITY; assert !Float.isNaN(score); totalHits++; if (score <= pqTop.score) { // Since docs are returned in-order (i.e., increasing doc Id), a document // with equal score to pqTop.score cannot compete since HitQueue favors // documents with lower doc Ids. Therefore reject those docs too. return; } pqTop.doc = doc + docBase; pqTop.score = score; pqTop = pq.updateTop(); }