Solr In Action 笔记(1) 之 Key Solr Concepts
题记:看了下《Solr In Action》还是收益良多的,只是奈何没有中文版,只能查看英语原版有点类,第一次看整本的英语书,就当复习下英语并顺便做下笔记吧。
1. Solr的框架
从这张图上看Solr的组件还是很齐全以及清楚明了的,但是当你看Solr源码的时候就会发现,哎呀咋看起来这么类呢。
2. Solr的查询方式
上面两张图分别举例了Solr的几个QueryComponent,比如facet,More like this,highlighting ,spatial ,以及spellcheck component 。
3. Solr的优化提示
(1) Solr支持多个Core,可以对Solr的Core按时间划分为历史Core以及现在Core,分别存放以前的历史数据以及现在的数据,或者将Core按使用类别划分。
(2) 提升Solr的并发查询性能的一个方法就是,增加shard以及replica个数,这是由于SolrCloud的查询方式是根据clusterstate.json的shard的顺序进行查询的,当shard和replica个数多的时候,对Solrcloud的并发查询就会进行分流。
4. Lucene的倒排表结构
可以通过以下表格来加深理解倒排表,通过term我们可以快速定位到具体的Document,然后再根据Document快速取出所有stored的field的内容。
5. 查询方式
5.1 BooleanQuery
BooleanQuery是很基础的一个查询方式,它就是对单个或者多个Term用AND,OR,NOT关系连接起来,它主要分为以下几种方式,假设以下的倒排表格式
通过Term Query查询new 和 home就可以分别获取他们的document如下,接下来分别对不同的boolean的方式对home和new的组合进行查询。
5.1.1 REQUIRED TERMS
+new +house
new AND house
上述两种查询虽然在逻辑上是一致的,但是在物理上还是有区别的,+是单目运算,AND是双目运算。
5.1.2 OPTIONAL TERMS
new house
new OR house
第一个查询是因为默认为操作符是OR
5.1.3 NEGATED TERMS
new house –rental
new house NOT rental
这里需要说明一下多个term的boolean查询性能,由于倒排表的特性,对多个term的boolean查询其实是需要先取出来每个term的doc然后再进行处理,也就说有几个term就需要遍历几遍,当然Lucene在这一块是有优化的,以AND为例,请看以下代码:
private int doNext(int doc) throws IOException {
for(;;) {
// doc may already be NO_MORE_DOCS here, but we don't check explicitly
// since all scorers should advance to NO_MORE_DOCS, match, then
// return that value.
advanceHead: for(;;) {
for (int i = 1; i < docsAndFreqs.length; i++) {
// invariant: docsAndFreqs[i].doc <= doc at this point. // docsAndFreqs[i].doc may already be equal to doc if we "broke advanceHead"
// on the previous iteration and the advance on the lead scorer exactly matched.
if (docsAndFreqs[i].doc < doc) {
docsAndFreqs[i].doc = docsAndFreqs[i].scorer.advance(doc); if (docsAndFreqs[i].doc > doc) {
// DocsEnum beyond the current doc - break and advance lead to the new highest doc.
doc = docsAndFreqs[i].doc;
break advanceHead;
}
}
}
// success - all DocsEnums are on the same doc
return doc;
}
// advance head for next iteration
doc = lead.doc = lead.scorer.advance(doc);
}
}
首先,获取符合第一个查询条件的第一个doc ID ,记为A,
第二,遍历其他的查询条件,获取第二个查询条件的doc id,记为B,如果B大于A,说明没有即符合A又符合B的Doc ID,那么第一个查询条件就会尝试获取大于等于B的Doc ID开始新的一轮循环。
第三,如果B刚好等于A,说明即有符合A又有符合B的Doc ID,所以获取第三个查询的条件的Doc ID,记为C,再多A和C进行比较,之后就跟第二步一样。
最后,当遍历所有查询条件,如果A符合所有查询条件则说明返回A,否则就返回最大值表示没有解。
从上面的过程可以看出,多个term的查询性能还是很耗时的。
再者就是多个Term的Boolean的准确性问题:
假设我们查询New AND House,那么结果出来的Document都包含New 和 House,但是如果我查的是要求是New House 连一起的,那么用BooleanQuery出来的结果可能会包含House New,或者 new XXXXX house这样的并不符合要求的结果,这就是Boolean查询的不足之处。
最后我们来看下,Solr是怎么处理Required Term ,OPTIONAL Term, NEGATED Term的评分因子的,以上三个分别对应required,prohibited,optional
public Scorer scorer(AtomicReaderContext context, Bits acceptDocs)
throws IOException {
List<Scorer> required = new ArrayList<>();
List<Scorer> prohibited = new ArrayList<>();
List<Scorer> optional = new ArrayList<>();
Iterator<BooleanClause> cIter = clauses.iterator();
for (Weight w : weights) {
BooleanClause c = cIter.next();
Scorer subScorer = w.scorer(context, acceptDocs);
if (subScorer == null) {
if (c.isRequired()) {
return null;
}
} else if (c.isRequired()) {
required.add(subScorer);
} else if (c.isProhibited()) {
prohibited.add(subScorer);
} else {
optional.add(subScorer);
}
} if (required.size() == 0 && optional.size() == 0) {
// no required and optional clauses.
return null;
} else if (optional.size() < minNrShouldMatch) {
// either >1 req scorer, or there are 0 req scorers and at least 1
// optional scorer. Therefore if there are not enough optional scorers
// no documents will be matched by the query
return null;
} // simple conjunction
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);
} // simple disjunction
if (required.size() == 0 && prohibited.size() == 0 && minNrShouldMatch <= 1 && optional.size() > 1) {
float coord[] = new float[optional.size()+1];
for (int i = 0; i < coord.length; i++) {
coord[i] = disableCoord ? 1.0f : coord(i, maxCoord);
}
return new DisjunctionSumScorer(this, optional.toArray(new Scorer[optional.size()]), coord);
} // Return a BooleanScorer2
return new BooleanScorer2(this, disableCoord, minNrShouldMatch, required, prohibited, optional, maxCoord);
}
5.2 短语查询
"new home" OR "new house"
"3 bedrooms" AND "walk in closet" AND "granite countertops"
在BooleanQuery中,分析了它的劣势,查询的不准性,以及性能的耗时。Phrase queries 在查找连着的term时完美的解决以上两个问题,它主要用到了Term Position。下表是带有term position的倒排表格式,term position很清楚明白的记录了,每一个term在其document的位置,虽然它增加了索引文件的大小,但是却为我们的Pharse Query带来了大大的便利。
同样出去new 和 house的信息,可以看出在document 5和8中,new 和 home是连着的,这提高了查询速度也提高了查询质量。就查询质量进行排序,PhraseQuery > BooleanQuery > FuzzyQuery
5.3 FuzzyQuery
5.3.1 通配符查询(wildcards Query)
我们可以使用*来代替1个或多个字符,?来代替单个字符进行通配符查询,比如
■ 查询: offi* 符合 office, officer, official等等
■ 查询: off*r 符合 offer, officer, officiator等等
■ 查询: off?r 符合 offer, 但不符合officer
这里需要注意点,通配符查询需要尝试去比较每一个term,所以查询是很费时间的。因此要想通配符查询速度快,那尽量让通配符的匹配范围小。比如要获取career,car*r的通配符速度速度肯定是快于ca*r的。我们来一个极端的情况,*ing这种通配符肯定是最慢的,因为需要遍历完所有的term。所以尽量将通配符放到查询term的后面位置。
但是有时候我们不得不取查*ing这样的查询,这个时候?Solr给我提供了一个方法,就是将term字段倒置,比如caring倒置为gnirac,这个时候的通配符只需要取查gni*。该方法是通过ReversedWildcardFilterFactory类实现的(需要修改分析链配置),使用这个类Solr建所有时候会存放两份数据,一份是正向caring,另一份是逆向的gnirac,当查询*ing时候,Solr会自动识别。同样需要注意,及时使用ReversedWildcardFilterFactory,查询*ing仍然是很费时间的而且增加了索引的大小,所以除非万不得已请不需要这样处理。
通配符查询只适合对单个term进行查询,像BooleanQuery那样,并不适用于PharseQuery短语查询。
5.3.2 范围查询(Range Query)
Solr支持进行范围查询(Range Query),它不但支持数字类型,同样支持字符串类型(字典序进行排列)。比如:
- Query: created:[2012-02-01T00:00.0ZTO2012-08-02T00:00.0Z]
Query: yearsOld:[18TO21] Matches 18, 19, 20, 21
Query: title:[boatTOboulder] Matches boat, boil, book, boulder, etc.
Query:price:[12.99TO14.99] Matches 12.99, 13.000009, 14.99, etc.
Query: yearsOld:{18TO21} Matches 19 and 20 but not 18 or 21
- Query: yearsOld:[18TO21} Matches 18, 19, 20, but not 21
- "["和"]"符号表示大于等于和小于等于
- "{"和"}"符合大于和小雨
由于Term在Index里面是按字典序排列的,所以对字符串类型的域进行RangeQuery时候,是按字典序进行的。
- 同样范围查询同样很费时间,同样需要遍历较多的term。
5.3.3 编辑距离查询(EDIT-DISTANCE Search)
Solr的查询方式还是很丰富的,它又提供了编辑距离查询方式。编辑距离采用了Levenshtein编辑距离算法进行的,关于《Levenshtein编辑距离算法》介绍请看我的另外一篇文章,简单的说就是对一个字符串增加或者删除或者移位其中的一个字符则表示1个编辑距离。编辑距离查询使用很简单,用~表示是编辑距离查询,~后面跟数字表示距离数,比如:
- Query: administrator~ Matches: adminstrator, administrater, administratior, and so forth 。(默认情况,等同 administrator~2)
- Query: administrator~1 Matches with in one editdistance.
- Query: administrator~2 Matches within two edit distances.
- Query:administrator~N MatcheswithinNeditdistances.
- 在进行编辑距离查询时候同样需要遍历不需要的term,所以当编辑距离大于2就会变得很慢。
- 编辑距离查询是对于单个term有效,不能应用与短语查询中。
5.3.4 临近查询(PROXIMITY SEARCHING)
相比于编辑距离查询适应于两个term近似但是不完全相同的情况,Solr还支持临近查询来支持短语查询方式来选出短语相近的结果,它将一个term为单位来计算term之间存在term的个数,比如:
- Query: "chief officer"~1 ,表示最多可以在chief和officer存在一个term,比如 "chief executive officer", "chief financial officer"
- Query: "chief officer"~2 ,表示最多可以在chief和officer存在两个term,"chief business development officer","officer chief"
- Query: "chief officer"~N, 表示最多可以在chief和officer存在多个term
- Query: "chief officer"~0, 表示一个短语查询。
- 同样临近查询的速度还是会收到影响
总结:
至此已经简单介绍了Solr的倒排表索引格式以及不同方式的关键字查询。如果对查询速度有显著要求,尽量采用Boolean查询方式。