前言
用Lucene一定不能不知道Filter,Filter在合适的场景下能大大提升搜索性能
背景
最近在折腾solr,这个3年前“玩过”的东西,现在又来玩了,3年前是瞎比玩,只知道最上层的一些接口,却不知其所以然,而现在的目标就是要把solr以及lucene底层的核心代码都分析一遍,并成功的部署一套电商搜索解决方案。
Filter逻辑
Filter的构造逻辑其实本身和Query的构造逻辑差距不大,唯一一点不一样的就是Query之后会在collector中进行打分,并使用堆来进行一个结果的取舍。
那么在lucene中Filter是怎么用的呢?首先需要知道Filter的功能,那就是通过某个条件把所有符合这个条件的docid拿到, 然后在构造scorer的时候把这些符合条件docid传过来,然后遍历这些符合条件的docid,通过query对应的相关信息,例如similarity等对这个doc打分。 所以说,如果你的filter能够把结果限制在很少的范围内的话,那么即使你的similarity或者是customScoreProvider稍微复杂点,那也是可以接受的。
调用层次:
1. Filter和Query都传给searcher
2. 把Filter和Query通过wrapFilter构造出FilteredQuery,通过filter条件过滤docid的逻辑就在FilteredQuery中:
public Scorer scorer(AtomicReaderContext context, boolean scoreDocsInOrder, boolean topScorer, final Bits acceptDocs) throws IOException {
assert filter != null;
final DocIdSet filterDocIdSet = filter.getDocIdSet(context, acceptDocs);
if (filterDocIdSet == null) {
// this means the filter does not accept any documents.
return null;
}
return strategy.filteredScorer(context, scoreDocsInOrder, topScorer, weight, filterDocIdSet);
}
- Scorer在collector文档的时候,只要扫一遍通过filter query筛选出来的候选集即可
public void score(Collector collector) throws IOException {
collector.setScorer(this);
int doc;
while ((doc = nextDoc()) != NO_MORE_DOCS) {
collector.collect(doc);
}
}
SOLR
那么在solr里的filter query是怎么玩的呢?
- solr里的fq参数会被searchHandler中的QueryComponent.prepare方法解析,并且构造这些field:value对应的query存到QueryComponent的rb(ResponseBuilder)中,并把rb存放到见下代码:
String[] fqs = req.getParams().getParams(CommonParams.FQ);
if (fqs!=null && fqs.length!=0) {
List<Query> filters = rb.getFilters();
if (filters==null) {
filters = new ArrayList<Query>(fqs.length);
}
for (String fq : fqs) {
if (fq != null && fq.trim().length()!=0) {
QParser fqp = QParser.getParser(fq, null, req);
filters.add(fqp.getQuery());
}
}
// only set the filters if they are not empty otherwise
// fq=&someotherParam= will trigger all docs filter for every request
// if filter cache is disabled
if (!filters.isEmpty()) {
rb.setFilters( filters );
}
}
- QueryComponent的prepare进行完后,执行process,进行搜索,这里把rb中的filter,query等一系列参数传给SolrIndexSearcher.QueryCommand,然后执行SolrIndexSearcher的search方法
SolrIndexSearcher.QueryCommand cmd = rb.getQueryCommand();
。。。。
searcher.search(result,cmd);
- 然后在SolrIndexSearcher中解析cmd中的filter,其中所有filter都存放在一个以Query类为模板的List中。然后在getDocListNC中调用getProcessedFilter,这个函数是干什么的呢,就是把每个filter对应的Query解析成一个个DocSet,然后根据这些filter的AND或者OR的关系去对这些DocSet取交集或者并集,这样就生成了一个候选的DocSet,大大减小了待查询打分的候选集。见getProcessedFilter的部分代码:
for (Query q : queries) {
if (q instanceof ExtendedQuery) {
ExtendedQuery eq = (ExtendedQuery) q;
if (!eq.getCache()) {
if (eq.getCost() >= 100 && eq instanceof PostFilter) {
if (postFilters == null)
postFilters = new ArrayList<Query>(sets.length - end);
postFilters.add(q);
} else {
if (notCached == null)
notCached = new ArrayList<Query>(sets.length - end);
notCached.add(q);
}
continue;
}
}
Query posQuery = QueryUtils.getAbs(q);
sets[end] = getPositiveDocSet(posQuery);
// Negative query if absolute value different from original
if (q == posQuery) {
neg[end] = false;
// keep track of the smallest positive set.
// This optimization is only worth it if size() is cached, which
// it would
// be if we don't do any set operations.
int sz = sets[end].size();
if (sz < smallestCount) {
smallestCount = sz;
smallestIndex = end;
answer = sets[end];
}
} else {
neg[end] = true;
}
end++;
}
这里还有个比较有意思的地方,对于每个Filter,要先看看它是不是负的,即”-brand_name:宝马”就是所有品牌不是宝马的内容,这里的方法是先取这个Filter的“绝对值”,即“brand_name:宝马”得到DocSet A,然后通过matchAllDocsQuery得到包含所有文档的DocSet B,然后B.andNot(A)就可以把B中的A的内容排除掉,实现了取反的逻辑。
至此,Filter Query的任务也就告一段落了,不过还有一些Cache相关的东西