Lucene搜索/索引过程笔记
lucene索引文档过程: > 初始化IndexWriter > 构建Document > 调用IndexWriter.addDocument执行写入 > 初始化DocumentWriter。参数指定写出位置为内存 > 生成自增段ID > 调用DocumentWriter.addDocument(); 执行写入 > 写出FieldInfos到内存 > 写出FieldValues到内存 > 计算词元列表 > 排序词元列表 > 写出词元到内存文件 > 写出归一化变量到内存文件 > 全局变量segmentInfos添加新增段 > 增量合并段 > 调用IndexWriter.optimize()优化索引 > 合并内存中的段并将合并后的段写出到磁盘 > 如果当前索引里有多个索引,则合并这些索引 > 调用IndexWriter.close()关闭索引 归一化变量是什么? 干什么用的? 为什么要在searchable接口上放一个rewrite方法? 为什么Weight的创建要用query的createWeight,而不是直接new Weight(); 为什么search接口不返回一个包含查询结果的list,而是把查询过程包含在返回对象的构造方法里面? searcher可以执行多次Query,query和result是绑定的,如果search接口只返回一个list,那同一个查询后面页数查询的时候又待重新parse等一些过程,把查询过程包含在返回对象中实现了一个query的自治,可以在返回对象中做一些优化,比如缓存啥的 为什么searcher不能带pageNo pageSize? 打分是在内存里做的,肯定需要把所有符合的文档全都查出来 为什么要把搜索任务放在query头上?不同的query搜索逻辑有什么不一样? 如果要把搜索逻辑放在searcher上,则在搜索的时候需要判断是哪类搜索然后从里面取出来参数,与其这样判断,不如直接把搜索逻辑放在query上,以后加新的query逻辑也不需要修改已有的代码 Term搜索过程: > 初始化IndexSearcher > 根据查询语句和解析器解析出Query > 调用IndexSearcher.search(query); 执行搜索 > 执行搜索,获取前100条 > 重写query > 创建Weight > 计算Weight的平方和(?) > 计算权重归一化因子(?) > 执行weight的归一化操作 > 创建Scorer > 通过reader获取命中的文档列表 > 获取搜索term所属field的归一化因子 > 获取当前Query的相似性算法 > 返回TermScorer > 通过Scorer对命中的文档打分,并获取得分前100条文档 > 计算得分归一化值 > 将命中的100条文档得分乘上归一化因子,然后添加到hitDocs缓存中 > 遍历hitDocs获取最终命中的文档列表数据 > 如果当前遍历的文档没有文档内容数据,则通过searcher获取该文档内容 > 如果遍历超过100条,则重新执行搜索获取当前遍历的位置数据 lucene文档得分是如何计算的? 计算公式是什么?
Query是如何简单化的?
BoolQuery执行步骤?
由QueryParser生成BoolQuery, 每添加一个子句都会给这个子句添加required和prohibited参数,required表示这个子句必须满足,prohibited表示这个子句一定不能满足
当所有的子句都必须满足,且所有的子句已经是最基础子句了,则使用ConjunctionScorer打分器,该打分器里会通过一个算法来获取多个子句都包含的文档。具体代码如下:
while (more && first().doc() < last().doc()) { // find doc w/ all clauses
more = first().skipTo(last().doc()); // skip first upto last
scorers.addLast(scorers.removeFirst()); // move first to last
}
注: 所有Scorer的doc都是按照从小到大排序的,这个在写索引的时候就确定下来了(termQuery),在ConjunctionScorer里第一次执行next时,会对所有的子Scorer按照第一个文档编号从小到大排序,
每当执行next寻找下一个文档时,先看排在第一位的Scorer当前文档号是否小于排在最后一位的Scorer的当前文档号,如果小于,则表明排在第一位的Scorer当前文档并不是所有Scorer都具备的,所以
排在第一位的Scorer会跳到排在最后一位Scorer当前文档编号的位置,一直找到排在第一位的Scorer和排在最后一位Scorer都具备的一个文档,这样的文档满足AND的关系,可以返回。
对于子句不满足所有子句都是required的情况,使用BooleanScorer,BooleanScorer的逻辑是,每往该打分器里添加一个子Scorer,这个子Scorer都带rquired和prohibited属性,至于这两个属性是从哪里得来的,
目前我猜测应该是从QueryParser中已经计算好的。每调用BooleanScorer的next时,都会按顺序从添加进来的子Scorer中取命中的文档(批量取),然后判断取的这个文档是否应该排除掉(所属的子Scorerprohibited属性值为true),
如果应该排除掉,则直接丢弃,再取下一个,直到找到一个文档符合所有子打分器要求,然后返回。重要的代码片段如下:
while (bucketTable.first != null) { // more queued
current = bucketTable.first;
bucketTable.first = current.next; // pop the queue
// check prohibited & required
if ((current.bits & prohibitedMask) == 0 && // prohibitedMask里每一位代表一个子打分器的prohibited属性,1表示决定不能匹配
(current.bits & requiredMask) == requiredMask) { // requierdMask里每一位代表一个子打分器required属性,1表示必须匹配
return true;
}
}
在取命中的文档ID的时候,是批量取的,内存里会缓存在一个叫BucketTable的数据结构里面,按照文档ID分组缓存。 比如第一批缓存的文档ID为 0 ~ 1024。 第二组为 1024 ~ 2048 ... 为什么要这样取,而不是先取100个,再取一百个?
我理解应该是为了防止一直遍历一个必须排除掉的子打分器命中的文档,这样可能会大大增加搜索的延迟,通过文档ID,可以将缓存的文档均匀的分散在多个打分器上,增加命中文档的比率。那为啥不先遍历required=true的打分器命中的文档呢?
如果先遍历这样的打分器,命中率不是更高么?我理解是应该可以这样来优化的,先遍历requrired=true的子打分器,然后再遍历prohibited=true的打分器,增加文档命中率。不知道作者这样写是不是有什么其他的考虑,关键代码如下:
// refill the queue
more = false;
end += BucketTable.SIZE;
for (SubScorer sub = scorers; sub != null; sub = sub.next) { // 按照子打分器add的顺序遍历
Scorer scorer = sub.scorer;
while (!sub.done && scorer.doc() < end) { // 每一个文档都取文档ID在固定范围内的命中文档
sub.collector.collect(scorer.doc(), scorer.score());
sub.done = !scorer.next();
}
if (!sub.done) {
more = true;
}
}
} while (bucketTable.first != null | more);
queryParser是如何解析查询脚本的?
是不是索引都会加载到内存里?
这不是的,在根据Term搜索的时候,只会把tii文件内容加载到内存里,tii文件是词元字典的索引,在初始化tisReader的时候就会把所有tii文件中的内容给加载到内存里
SkipInterval是干什么用的?
frq文件中存储了某个词元命中的文档列表,skipInterval记录了隔几个文档记录一下该词元命中的文档列表索引
TermVector干啥用的? 可以实现从文档到属性到词元的映射
段合并是如何维护合并后文档ID的?
1. 在写合并文档数据(.fdt)数据时,是按照SegmentInfos里的顺序按顺序写入的。
2. 在合并时写出合并的词元数据时,会修改每个词元下的文档ID,会把当前词元所属段的base documentid加上写出的文档ID
如果在查询时写入的文档导致同一个查询结果不一样lucene是如何处理的?
先前查询的不会变化,但是新页的数据是按照新查询的结果分页得到。因为ES用了一个缓存,先前查询的不会再更新。
每一次查询都会把所有符合的文档和相应的打分加载到内存里,然后在内存里做排序、过滤、分页