搜索过程是由Lucene所提供的核心功能之一。下图说明了搜索过程和使用的类。 IndexSearcher是搜索过程中最重要的和核心组件。本章的需要掌握的,了解他们的存储原理后就可以方便知道如何基于这些存储结构来实现高效的搜索。
目录
1. 搜索的基本流程
2. 底层实现原理
2.1 FST
2.2 SkipList
2.3 倒排合并
2.4 结果集排序聚合
3. IndexSearcher
3.1 常用的搜索方法
3.2 DSL语法之QueryParser
3.2 示例
在使用IndexSearcher类的时候,需要一个DirectoryReader和QueryParser,其中DirectoryReader需要对应写入时候的Directory实现。QueryParser主要用来解析你的查询语句,例如你想查 “A and B",lucene内部会有机制解析出是term A和term B的交集查询。
本节点不展开讲,之前已经分享过反向索引及索引原理,对这些(反向信息)的来龙去脉已经有了一个基本的认识。好吧,那就再范范赘述一遍吧。
查询过程,在lucene中查询是基于segment。每个segment可以看做是一个独立的subindex,在建立索引的过程中,lucene会不断的flush内存中的数据持久化形成新的segment。多个segment也会不断的被merge成一个大的segment,在老的segment还有查询在读取的时候,不会被删除,没有被读取且被merge的segement会被删除。这个过程类似于LSM数据库的merge过程。
倒排本质上就是基于term的反向列表,方便进行属性查找。为了解决term非常多的问题,lucene里面就引入了term dictonary的概念,也就是term的字典,lucene在这条路上一直在不遗余力的尝试,Lucene3.x先使用跳表,Lucene4.x确实了FST的存储结构(为了解决范围查询或者前缀,后缀等复杂的查询语句)。之前系统的讲过FST在单term查询上可能相比hashmap并没有明显优势,甚至会慢一些。但是在范围,前缀搜索以及压缩率上都有明显的优势。
这里对准实时(写完不一定就可以立即检索到)有了进一步认识了吧!!!
第一步,只解决了快速查找term的所对应的docid。
为了能够快速查找docid,lucene采用了SkipList这一数据结构(空间换时间)。SkipList有以下几个特征:
有了跳表,当我们查找一个指定的id时,原来可能需要一个个扫原始链表,先访问低第一层(顶层,level=1),小于进入下一层(level=0),查找顺序是index递增(从左到到右),最终进入原链表(直达找到为止)。
到这里还有另一个问题,那就是当我们查询中出现了 name='alan' and name='alice' limit 0,20 该如何处理?在我的另一篇中进行讲解
通过之前介绍可以看出lucene通过倒排的存储模型实现term的搜索,那对于有时候我们需要拿到另一个属性的值进行聚合,或者希望返回结果按照另一个属性进行排序。在lucene4之前需要把结果全部拿到再读取原文进行排序,这样效率较低,还比较占用内存,为了加速lucene实现了fieldcache,把读过的field放进内存中。这样可以减少重复的IO,但是也会带来新的问题,就是占用较多内存。新版本的lucene中引入了DocValues,DocValues是一个基于docid的列式存储。当我们拿到一系列的docid后,进行排序就可以使用这个列式存储,结合一个堆排序进行。当然额外的列式存储会占用额外的空间,lucene在建索引的时候可以自行选择是否需要DocValue存储和哪些字段需要存储。
相对于索引的创建而言,索引的搜索是使用频繁的。所以 IndexReader 是会经常使用的,所以我们很自然地想到应该将 它设计成一个单例模式。但是索引增加、修改、删除以后,IndexReader 须要重新读取索引信息 , 使用 DirectoryReader 类的静态方法 openIfChanged 就可以达到目的,这个判断会先判断索引是否变更,如果变更,我们要先把原来的 IndexReader 释放。
注意:对于IndexReader来说,IndexReader.open()会产生很大开销(程序是把索引文件全部载入内存)
IntPoint 搜索数值型
IntPoint.newExactQuery 精确查询,使用的是 PointRangeQuery。
IntPoint.newRangeQuery 范围查询,使用的是 PointRangeQuery。
IntPoint.newSetQuery 集合查询,使用的是 PointInSetQuery。LongPoint、FloatPoint、DoublePoint 封装的和 IntPoint 都很相似
TermQuery 搜索特定的项
Query query = new TermQuery(new Term(field,value));
TermRangeQuery 搜索特定范围的项
Query query = new TermRangeQuery(field,new BytesRef(start.getBytes()),new BytesRef(end.getBytes()),true,true);
PrefixQuery 前缀匹配搜索
Query query = new PrefixQuery(new Term(field,value));
WildcardQuery 通配符搜索
Query query = new WildcardQuery(new Term(field,value));
FuzzyQuery 模糊匹配搜索
FuzzyQuery query = new FuzzyQuery(new Term(field,value),maxEdits,prefixLength);
BooleanQuery 多个条件的查询
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
Query query1 = new TermQuery(new Term(field1,value1));
Query query2 = new TermQuery(new Term(field2,value2));
booleanQuery.add(query1,BooleanClause.Occur.MUST);
booleanQuery.add(query2,BooleanClause.Occur.MUST);
PhraseQuery 短语查询
PhraseQuery phraseQuery = new PhraseQuery();
phraseQuery.setSlop(slop);
phraseQuery.add(new Term(field,value1));
phraseQuery.add(new Term(field,value2));
QueryParser 方式的查询,功能最最强大,几乎涵盖上上面几种方式的查询。QueryParser是通过JavaCC来生成词法分析器和语法分析器的。语法关键字包含:+ - && || ! ( ) { } [ ] ^ " ~ * ? : \
Term-查询词 | 一种是单一查询词,如"hello",一种是词组(phrase),如"hello world" |
Field-查询域 | 语法如,title:"Do it right",在查询语句中,可以指定从哪个域中寻找查询词,如果不指定,则从默认域中查找。如果title:Do it right,则仅表示在title中查询Do,而it right要在默认域中查询。 |
Wildcard-通配符查询 | ,?表示一个字符,*表示多个字符。通配符可以出现在查询词的中间或者末尾,如te?t,test*,te*t,但决不能出现在开始,如*test,?test。 |
Fuzzy-模糊查询 | 模糊查询的算法是基于Levenshtein Distance,也即当两个词的差别小于某个比例的时候,就算匹配,如roam~0.8,即表示差别小于0.2,相似度大于0.8才算匹配。 |
Proximity-临近查询 | 在词组后面跟随~10,表示词组中的多个词之间的距离之和不超过10,则满足查询。所谓词之间的距离,即查询词组中词为满足和目标词组相同的最小移动次数。 如索引中有词组"apple boy cat"。如果查询词为"apple boy cat"~0,则匹配;如果查询词为"boy apple cat"~2,距离设为2方能匹配,设为1则不能匹配。 |
Range-区间查询 | 一种是包含边界,用[A TO B]指定,一种是不包含边界,用{A TO B}指定。如date:[20020101 TO 20030101] |
Boost-增加一个查询词的权重 | 查询词后面加^N来设定此查询词的权重,默认是1,如果N大于1,则说明此查询词更重要,如果N小于1,则说明此查询词更不重要。如jakarta^4 apache |
布尔操作符 | AND,OR,和修饰符(NOT,+,-)默认状态下,空格被认为是OR的关系。+表示一个查询语句是必须满足的(required),NOT和-表示一个查询语句是不能满足的(prohibited)。QueryParser.setDefaultOperator(Operator.AND)设置为空格为AND。 |
组合 | 可以用括号,将查询语句进行组合,从而设定优先级。如(jakarta OR apache) AND website |
传统的解析器:QueryParser和MultiFieldQueryParser
新的框架解析器:StandardQueryParser
public class Search_test {
private static String directoryPath = "target";
private static Directory directory;
private static IndexReader reader;
private static IndexSearcher searcher;
@BeforeClass
public static void init() throws IOException {
directory = FSDirectory.open(Paths.get(directoryPath));
reader = DirectoryReader.open(directory);
searcher = new IndexSearcher(reader);
//目的是当index文件更新的时候,重新生成IndexReader,否则IndexReader不会更新,除非重启项目。
IndexReader changeReader = DirectoryReader.openIfChanged((DirectoryReader) reader);
if (changeReader != null) {
reader.close();
reader = changeReader;
searcher = new IndexSearcher(reader); //打开索引
}
}
@AfterClass
public static void disabled() throws IOException {
reader.close();
directory.close();
}
@Test
public void searchByTerm() throws IOException {
// 搜索特定的项
Query query = new TermQuery(new Term("id", "5"));
showQueryResult(query, 5);
}
@Test
public void searchByTermRange() {
Query query = new TermRangeQuery("id", new BytesRef("1".getBytes()), new BytesRef("5".getBytes()), true, true);
showQueryResult(query, 5);
}
@Test
public void searchByPrefix() {
Query query = new PrefixQuery(new Term("content", "Apache"));
showQueryResult(query, 5);
}
@Test
public void searchByWildcard() {
Query query = new WildcardQuery(new Term("content", "Apache"));
showQueryResult(query, 5);
}
@Test
public void searchByBoolean() {
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
Query query1 = new TermQuery(new Term("id", "2"));
Query query2 = new TermQuery(new Term("id", "3"));
booleanQuery.add(query1, BooleanClause.Occur.MUST);
booleanQuery.add(query2, BooleanClause.Occur.MUST);
showQueryResult(booleanQuery.build(), 5);
}
@Test
public void searchByFuzzy() {
FuzzyQuery query = new FuzzyQuery(new Term("content", "search"), 20, 10);
showQueryResult(query, 5);
}
@Test
public void go_byIntValue() throws IOException {
String colName = "intValue";
//精确查询
Query query = IntPoint.newExactQuery(colName, 5);
System.out.println(query.getClass().getName() + ":" + query);
showQueryResult(query, 10);
//范围查询,不包含边界
query = IntPoint.newRangeQuery(colName, Math.addExact(11, 1), Math.addExact(22, -1));
showQueryResult(query, 10);
//范围查询,包含边界
query = IntPoint.newRangeQuery(colName, 11, 22);
showQueryResult(query, 10);
//范围查询,左包含,右不包含
query = IntPoint.newRangeQuery(colName, 11, Math.addExact(22, -1));
showQueryResult(query, 10);
//集合查询
query = IntPoint.newSetQuery(colName, 11, 22, 33);
showQueryResult(query, 10);
}
@Test
public void go_queryParser() throws IOException, ParseException, QueryNodeException {
String defaultFiled = "content";
//用法1 传统解析器-单默认字段 QueryParser:
Analyzer analyzer = new StandardAnalyzer();
QueryParser parser = new QueryParser(defaultFiled, analyzer);
Query query = parser.parse("query String");
//用法2 传统解析器-多默认字段 MultiFieldQueryParser:
String[] multiDefaultFields = {"name", defaultFiled};
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(
multiDefaultFields, analyzer);
multiFieldQueryParser.setDefaultOperator(QueryParser.Operator.OR);// 设置默认的组合操作,默认是 OR
query = multiFieldQueryParser.parse("笔记本电脑 AND price:1999900");
//用法3 新解析框架的标准解析器
StandardQueryParser qpHelper = new StandardQueryParser(analyzer);
//qpHelper.setAllowLeadingWildcard(true);// 开启第一个字符的通配符匹配,默认关闭因为效率不高
// qpHelper.setDefaultOperator(Operator.AND);// 改变空格的默认操作符,以下可以改成AND
query = qpHelper.parse("(\"联想笔记本电脑\" OR simpleIntro:英特尔) AND type:电脑 AND price:1999900", defaultFiled);
System.out.println(query);
showQueryResult(query, 10);
}
/**
* @param query
* @param num 取回前N个文档
*/
private void showQueryResult(Query query, Integer num) {
TopDocs topDocs = null;
try {
topDocs = searcher.search(query, num);
System.out.println("实际搜索到的记录数 => " + topDocs.totalHits);
Document document = null;
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
document = searcher.doc(scoreDoc.doc);
System.out.println("doc:" + document);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结,搜索的过程比较复杂,掌握里面的算法及存储结构相对的必要,而IndexSearcher的API使用相对而言比较简单,花10分钟了解足矣。这里提问你一个问题lucene怎样去实现“猜你喜欢”?我的另一篇中给出解答。