lucene正则查询_Lucene⾼阶查询技巧
在前⾯的章节中我们使⽤了最基础的关键词查询 TermQuery 和 复合查询 BooleanQuery,本节我们来尝试 Lucene 内置的其它⾼级查询功能。
字符串前缀查询 PrefixQuery
同关系数据库索引⼀样,得益于 FST 的前缀共享属性,Lucene 也⽀持前缀查询。它会将共享同样前缀的⼀系列关键词都出来,然后再merge 它们的⽂档列表。
var query = new PrefixQuery(new Term("title", "动"));
查询结果如下,可以看到 「动物」和「动作」等词汇都搜索出来了
但是默认的 PrefixQuery 不会对搜索的结果进⾏排序,它对所有被搜索出来的⽂档统⼀打分 1.0,在实现上可以让查询效率快很多,直接省去了收集所有⽂档进⾏排序的过程。如果你希望结果可以排序,可以给 PrefixQuery 设置⼀个选项,查询的结果可以观察 hit.score 值的⼤⼩。
var query = new PrefixQuery(new Term("content", "动"));
query.setRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_REWRITE);
var hits = searcher.search(query, 10).scoreDocs;
for (var hit : hits) {
var doc = searcher.doc(hit.doc);
System.out.printf("%s:%.2f => %sn", ("url"), hit.score, ("title"));
}
PrefixQuery 为什么要默认关闭排序呢?这是因为前缀查询能匹配到的关键词可能会很多,merge 所有的⽂档列表并排序将会是⼀个⾮常耗费性能的过程。
数字范围查询 NumericRangeQuery
数字查询和字符串查询不太⼀样,在内部实现结构上它并不是像字符串那样使⽤ FST 来组织关键词。如果每⼀个数字都是关键词,那么使⽤数字范围定位出的关键词会⾮常多,需要 merge 的⽂档列表也会⾮常多,这样的查询效率就会很差。如果是浮点型数字,那这个问题就更加突出了,可能每个浮点数字只会关联⼀个⽂档,⽽浮点数关键词将会太多太多。
Lucene 将数字进⾏特殊处理,内部使⽤ BKD-Tree 来租户数字键。BKD 树也是⼀个较为复杂的数据结构,简单理解就是将数值⽐较接近的⼀些数字联合起来作为⼀个叶节点共享⼀个 PostingList,同时在 PostingList 的元素需要存储数字键的值,在查询时会额外多出⼀个过滤步骤。粗略来看,BKD 树会将⼀个⼤的数值范围进⾏⼆分,然后再继续⼆分,⼀直分到⼏层之后发现关联的⽂档数量⼩于设定的阈值,就不再继续拆分了。BKD 树还可以索引多维数值,这样它就可以应⽤于地理位置查询。
下⾯我们给前⾯的⽂章索引增加⼀个字段,⽂章的 ID,我们需要将⽬录⾥的所有⽂件全部删除,然后运⾏ Indexer.java 重新构建索引。
var doc = new Document();
doc.add(new TextField("title", title, Field.Store.YES));
doc.add(new TextField("content", content, Field.Store.YES));
doc.add(new IntPoint("article_id", Integer.parseInt(id)));
doc.add(new StoredField("url", url));
indexWriter.addDocument(doc);
IntPoint 类型只索引,不存储,对应的选项默认是 Field.Store.NO,Lucene 只会将它的值放⼊ PostingList 的元素中。如果需要存储还需要单独增加⼀个 StoreField 类型,在本例中我们不需要存储。之所以叫 Point 类型是因为它可以⽀持多维数值,IntPoint 的构造器是⼀个可变参数,可以填⼊多个整数形成的数值对。下⾯我们尝试查询
var query = wRangeQuery("article_id", 400000, 450000);
var hits = searcher.search(query, 10).scoreDocs;
查询结果如下
对数值型的查询结果进⾏排序是没有意义的,返回的⽂档 score 值都是默认的 1.0。
关于 BKD 树的更多细节,我们后⾯再继续讨论。
字符串范围查询 TermRangeQuery
同数字范围查询类似,字符串也有范围查询,它是通过遍历关键词前缀树 FST 来实现的,它会按照字典序将范围内的所有词汇都列出来,然后 merge 所有关键词的⽂档列表。如果这个范围覆盖的关键词很多,可以预见性能是很差的。不过,字典序对于中⽂来说是没有意义的,所以通常我们不会去使⽤它。
var query = wStringRange("level", "level1", "level9", true, true);
上⾯的查询表⽰等级范围在 [level1, level9] 之间的所有数据,参数中的两个 bool 值表⽰是否包含边界值。如果是两个 false,那么等级范围就是 (level1, level9)。
短语查询 PhraseQuery
「北京⼤学」是⼀个词汇,我们可以使⽤ TermQuery 关键词查询来快速定位所有包含「北京⼤学」的内容。⽐如下⾯这个查询就可以查处很多⽂章,因为「北京⼤学」上镜率很⾼。
var query = new TermQuery(new Term("content", "北京⼤学"));
那如何定位「北京xx⼤学」这样的内容呢?它可以是「北京科技⼤学」、「北京交通⼤学」、「北京化⼯⼤学」等词,但是不可以匹配「我是北京⼈,我没上过⼤学」这样的语句。这时候就可以⽤到短语查询 PhraseQuery。
var query = new PhraseQuery.Builder()
.add(new Term("content", "北京"))
.add(new Term("content", "⼤学"))
.setSlop(2)
.build();
这⾥有⼀个关键参数 Slop 表⽰两个词汇之间最多允许间隔的单词数量,注意这⾥是「单词」数量,⽽不是「字符」数量。如此它就可以匹配「北京⼯业⼤学」,同时还可以匹配「北京⼤学」。下⾯我们来看看它的查询结果
奇怪的是它只搜寻到了三篇⽂章,点开链接进去后发现它只匹配到了「北京林业⼤学」、「北京体育⼤学」,就是没有匹配到「北京⼤学」,难道 PhraseQuery 的 Slop 字段含义另有解释?
我也被这个问题愣了⼀段时间,结果发现是因为在构建索引的时候使⽤的中⽂切词分析器的问题。我们⽤的是 HanLPAnalyser,它会将「北京⼤学」切成单个词汇,⽽不是切成三个词汇「北京」、「⼤学」和「北京⼤学」,这样就会导致相关⽂章只会收录到倒排索引中的「北京⼤学」这个词,⽽不会索引到「北京」和「⼤学」这两个词上⾯。那该如何解决这个问题呢?这需要我们在构建索引时对分词器进⾏适当的修改。
var analyser = new Analyzer() {
protected TokenStreamComponents createComponents(String fieldName) {
var tokenizer = new wSegment().enableOffset(true).enableIndexMode(true), null, false);
return new TokenStreamComponents(tokenizer);
}
};
regex匹配将 HanLP 的 indexMode 打开,如此就可以将 「北京⼤学」分词成三个词汇「北京」、「⼤学」和「北京⼤学」。重新建⽴索引后,再次尝试查询,就可以看到期望的搜寻结果。
从结果中我们可以注意到⽂章是携带排序分值信息的,「北京」和「⼤学」词汇越接近,出现的越频繁,⽂章的评分就越⾼。同时我们还要注意到它是携带顺序的,它不能匹配「⼤学xx北京」这样的内容。
正则查询 WildcardQuery
查询「北京xx⼤学」的⽅式除了上⾯的短语查询之外,Lucene 还提供了正则查询。⽐如上⾯的查询可以改写成
var query = new WildcardQuery(new Term("content", "北京*⼤学"));
它可以匹配「北京⼤学」和「北京林业⼤学」,* 号表⽰ 0 到多个字符。但是如果你仔细查看查询结果的内容你会发现,所有的内容都是包含「北京⼤学」这样的词汇,「北京xx⼤学」这样的⽂章却没有出来。这是为什么呢?因为正则匹配的对象是倒排索引的关键词。使⽤上⾯的分词器是不会得到「北京xx⼤学」这样的词汇的,分词的结果是「北京」、「xx」和「⼤学」这三个词汇。从这点来说它和PhraseQuery 差异还是很⼤的。除了 * 号之外还有 ? 号表⽰单个字符,它不能使⽤任意的复杂正则表达式。注意如果 * 号位于词汇开头,查询将会尝试扫描所有关键词来寻出匹配的候选词,这对性能将是⼀个很⼤的伤害。所以我们要尽可能避免使⽤⾸字母 * 号的正则查询,词汇的前缀越长查询性能越好。
模糊查询 FuzzyQuery
最后我们来看⼀个更加⾼级的查询模式 —— 模糊查询。它跟上⾯的查询⽅式都不太⼀样,它是基于「编辑距离」的查询。当我们⽬标查询是「北京⼤学」时它可以匹配「北⽅⼤学」,还可以匹配「北京中学」,它的性能不怎么样,因为和指定词汇相似的词汇会有很多选择,如此就会匹配⾮常多的词汇,需要 merge ⾮常多的⽂档列表,然后还需要根据编辑距离和词汇的频率进⾏评分排序。除了 merge ⽂档列表和排序的代价之外,寻到相似的词汇也需要⼀定的代价。它需要搜寻整个关键词的前缀树(FST),然后计算它们之间的编辑距离,再挑选出「最⼤编辑距离」范围内的词汇。
var query = new FuzzyQuery(new Term("content", "北京⼤学"));
为了加速关键词匹配的效率,减少相似的关键候选词,FuzzyQuery 给查询做了若⼲限制条件。它可以设置关键词的前缀数量prefixLength,⽐如如果前缀数量为2,那么指定「北京⼤学」,就不会匹配「北⽅⼤学」,但是可以匹配「北京中学」。它可以设置编辑距离的最⼤值 maxEdits,跟关键词差距太⼤的词汇就不⽤考虑了。它还可以设置相似词汇候选集的最⼤数量 maxExpansions,避免需要merge 太多的⽂档列表。这三个选项的默认值分别是
var defaultMaxEdits = 2
var defaultPrefixLength = 0
var defaultMaxExpansions = 50
因为默认的前缀长度是 0,所以它需要扫描整颗关键词树,这个性能很差。使⽤时务必注意,保险起见,还是别轻易使⽤ FuzzyQuery。全表遍历 MatchAllDocsQuery
同关系数据库⼀样,Lucene 也提供了全表遍历查询 MatchAllDocsQuery,它是不⾛倒排索引的,因为基数太⼤,所以默认不评分不排序。如果数据量很⼤,进⾏全量评分排序估计会把服务器卡死,最好也不要使⽤它。
var query = new MatchAllDocsQuery();
下⼀节我们来讲 Lucene 最⾼级的查询模式 —— 表达式查询 QueryParser,它可以将⼀个⽂本表达式字符串解析成上⾯各种 Query 的逻辑组合 BooleanQuery,有了 QueryParser 我们就可以⾮常⽅便地构造出⼀个⼗分复杂的组合逻辑查询。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论