简单介绍:
Lucene是一个高效的,基于Java的全文索引检索框架。我们通常在生活中会遇到各式各样的数据信息,总体上可以划分成两类:
结构化数据:
具有固定格式或者长度限制的数据,例如数据库存储的字段信息。
非结构化数据:
一些不定长的内容,例如博客文章等。
结构化数据搜索:
例如数据库信息的搜索,SQL语句等。
非结构化数据搜索:
最常见的就是谷歌百度这类搜索引擎的搜索方式了。
在初次接触全文搜索的时候,我们很容易将固有的思维转化过来思考该类问题。例如在一个磁盘里面,搜索含有相应字符串的文件内容,最简单的思路就是磁盘中的所有文件都进行遍历搜索,从而实现该功能。但是当磁盘的空间达到上百G的时候,这种方式就会显得效率异常低下了。
而全文索引的思路则是采用了一种比较新奇的方式来巧妙的优化了这一缺陷,通过将原本处于非结构化状态的数据中抽取特定的特征,然后可以根据这些特征来反向搜索到相应的源数据,这种特征我们称之为索引。
在《lucene in action》一书中有这么一张截图:
全文索引包含有两个比较重要的点:
1.索引的创建
2.索引搜索
通过索引来查找相应的内容,这个过程我们通常称之为反向索引查找,该类索引有时候也会被称之为反向索引。
为了方便理解,我们来看下图:
图中,存储了java关键字的文章id有1,3,7,9,10,存储了C++关键字的文章id有2,3,8,11,7,存储了IT关键字的文章有4,6,7,8,10 ,通常我们会称呼左边的java,c++,it这一列为词典信息,右边的id数据我们称之为倒排表(Document链表)。
当我们在存储介质中,将非结构化的数据来按照以上结构进行存储的时候,例如说查询包含有java关键字的文章,就是1,3,5,9,10, 同时存储了java和C++关键字的文章则只需要将两行倒排列表合并一下id集合便为:3和7。
使用全文索引的优缺点:
缺点:
1.在数据量较少的时候,使用这种方式查找的效率会比较底下。
2.创建索引的过程比较繁琐,索引存储也需要额外的存储空间。
3.当存储的数据项很大的时候,产生的索引量也会很大,这也是个比较耗时间的地方。
优点:
1.一次创建之后,后边的搜索会很方便。
2.通过利用空间来换时间的方式,从而达到高效率的结果。
Lucene 采用的是一种称为反向索引(inverted index)的机制。反向索引就是说我们维护了一个词 / 短语表,对于这个表中的每个词 / 短语,都有一个链表描述了有哪些文档包含了这个词 / 短语。这样在用户输入查询条件的时候,就能非常快的得到搜索结果。搜索引擎首先会对搜索的关键词进行解析,然后再在建立好的索引上面进行查找,最终返回和用户输入的关键词相关联的文档。
分词器
什么是分词器?我的理解就是将一句话分成多个重要的词组。
英文里面的分词主要技巧有以下几个:
1.首先使用空格分离词语
2.筛去Stop word (也就是英文里面的 the,a,an,this这类型没有什么特别意义的词语,减少创建索引所需要的消耗)
3.去除掉标点符号
4.将单词的大写字母变为小写字母,这个过程称之为Lowercase
5.缩减单词为原始状态(例如过去式变为原形状态,单词的复数变为单数状态,例如:boys—>boy,said—>say),这种功能的实现主要依靠的是查询字典的方式。
通常说某个句子被分词器分词之后产生的单词,我们称之为Term(词)。
中文的分词
中文的分词通常要比英文的分词复杂N倍,例如说下边的这个例子:
武汉市长江大桥 复制代码
这句话如果进行分词,可以有以下几种结果:
武汉市/长江大桥
武汉市/长江/大桥
武汉/市长/江大桥
武汉市/长江大桥
这类型的分词算法极其复杂,为了方便java程序员在实际开发中进行中文分词的操作,出现了一套效率极高的中文分词器:IK分词器。IK分词器的分词方式思路和英文分词的思路有所出入,它所采用的是字典树的方式来实现该功能:
字典树的结构有点类似于B树,效率较高,时间快,但是费空间,核心就是以空间换取速度。
Lucene这套强大的全文搜索引擎框架,内置提供了对于各类分词器的支持,包含有英文分词器,中文分词器,对于java程序员来说使用特别方便。
使用Lucene的实战操作:
首先我们需要导入pom文件,下载依赖的jar包:
<!--核心jar包--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>5.2.1</version> </dependency> <!--处理分词解析功能的包--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>5.2.1</version> </dependency> <!--处理英文的分词功能为主--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>5.2.1</version> </dependency> <!-- 分词器 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-smartcn</artifactId> <version>5.2.1</version> </dependency> <!--文本高亮为主--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-highlighter</artifactId> <version>5.2.1</version> </dependency> <!--ik-analyzer 中文分词器--> <dependency> <groupId>cn.bestwu</groupId> <artifactId>ik-analyzers</artifactId> <version>5.1.0</version> </dependency> <!--MMSeg4j 分词器--> <dependency> <groupId>com.chenlb.mmseg4j</groupId> <artifactId>mmseg4j-solr</artifactId> <version>2.4.0</version> <exclusions> <exclusion> <groupId>org.apache.solr</groupId> <artifactId>solr-core</artifactId> </exclusion> </exclusions> </dependency> 复制代码
创建一个专门用于测试的类Tips,帖子类:
import lombok.AllArgsConstructor; import lombok.Data; /** * 帖子信息 * * @author idea * @date 2019/5/20 */ @AllArgsConstructor @Data public class Tips { public String id; public String title; public String tipsContent; } 复制代码
为了方便操作中对于lucene里面的各类资源进行初始化操作,所以我选用了junit来进行案例演示:
首先是 @Before 和 @After 部分的内容:
private Directory directory; private IndexReader indexReader; private IndexSearcher indexSearcher; private IndexWriter indexWriter; private Analyzer analyzer; private IndexWriterConfig indexWriterConfig; @Before public void setUp() throws IOException { //这里用于指定索引的创建位置 directory = FSDirectory.open(Paths.get("indexData")); //由于存储的内容是偏中文的内容,所以这里选择了ik分词器 analyzer = new IKAnalyzer(); indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); } @After public void tearDown() throws IOException { if (indexReader != null) { indexReader.close(); } if (indexWriter != null) { indexWriter.close(); } } 复制代码
然后我们通过使用 @Test 注解来编写一段测试函数
创建索引,并且初始化数据信息:
/** * 模拟插入数据 * * @throws IOException * @throws IllegalAccessException */ @Test public void saveData() throws IOException, IllegalAccessException { String content[] = {"今天小明来找我玩,很开心", "广州的天气好热啊,下午要去游泳吗", "嗖的一下,我的qq邮件jdkshj111@qq.com就发送了"}; String title[]={"今天很开心","今天很激动","今天很美"}; List<Tips> tipsList = new ArrayList<>(); for (int i = 0; i < content.length; i++) { tipsList.add(new Tips(i + "",title[i], content[i])); } System.out.println("插入数据----------------"); saveData(tipsList, Tips.class); } /** * 保存数据 * * @param objectList * @param clazz * @throws IllegalAccessException * @throws IOException */ private void saveData(List objectList, Class clazz) throws IllegalAccessException, IOException { IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); indexWriter = new IndexWriter(directory, indexWriterConfig); Field[] fields = clazz.getFields(); for (Object obj : objectList) { Document document = new Document(); for (Field field : fields) { document.add(new org.apache.lucene.document.Field(field.getName(), (String) field.get(obj), org.apache.lucene.document.Field.Store.YES, org.apache.lucene.document.Field.Index.ANALYZED)); } indexWriter.addDocument(document); } indexWriter.commit(); } 复制代码
lucene会将传入进来的帖子list集合信息会被保存到指定的indexData目录底下:
存储索引信息的目录如下所示:
lucene是一款提供了丰富的数值类型进行查询的框架,在不同的业务场景中,我们可以结合不同的所需来制定我们的查询策略。lucene里面提供的查询方式有以下几种:
查询方式 | 解释 |
TermQuery | 精确查询 |
TermRangeQuery | 范围查询 |
PrefixQuery | 前缀匹配查询 |
WildcardQuery | 通配符查询 |
BooleanQuery | 多条件查询 |
PhraseQuery | 短语查询 |
FuzzyQuery | 模糊查询 |
Queryparser | 全能查询(可以包含上边的所有查询) |
为了方便理解,我列出了上述的每一个功能查询的代码进行逐一介绍和总结,lucene里面的各种功能查询主要区别在query的种类差异,因此可以进行统一的封装和查询结果的打印操作。封装的函数如下所示:
/** * 查询 * * @param query * @throws IOException */ private void doQuery(Query query) throws IOException { TopDocs topDocs = indexSearcher.search(query, 100); //打印搜索结果 printDocs(indexSearcher,topDocs); } /** * 打印搜索内容 * * @param indexSearcher * @param topDocs * @throws IOException */ private void printDocs(IndexSearcher indexSearcher, TopDocs topDocs) throws IOException { for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document document = indexSearcher.doc(scoreDoc.doc); System.out.println(document.toString()); } } 复制代码
有了这两个封装的函数之后,我们再来看看下边的查询操作
1.精确查询
/** * 精确查询 * * @throws IOException */ @Test public void searchInTermQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); Query query = new TermQuery(new Term("title", "今天很开心")); doQuery(query); } 复制代码
2.范围查询
范围查询通常应用的场景有:根据id查找指定范围数据的信息,NumericRangeQuery.newIntRange的后边两个布尔类型参数是指查询的时候是否要包含最小和最大值。
/** * 范围查询 通常是指特定的范围 * * @throws IOException */ @Test public void searchInRangeQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); Query query = NumericRangeQuery.newIntRange("id", 0, 2, true, false); doQuery(query); } 复制代码
3.前缀匹配符查询
下边这个案例,查找所有帖子中,标题以“今天”开头的内容信息
/** * 前缀查询 * * @throws IOException */ @Test public void searchInPrefixQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); Query query = new PrefixQuery(new Term("title", "今天")); doQuery(query); } 复制代码
4.通配符查询
通配符查询有些时候可以起到特别方便的作用,例如说查询帖子里面包含有qq邮件信息的内容。
/** * 通配符查询 * * @throws IOException */ @Test public void searchInWildcardQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); Query query = new WildcardQuery(new Term("tipsContent", "*@qq.com")); doQuery(query); } 复制代码
5.多条件查询
查询标题里面开头是“今天“,id范围在0-2之间的数据(包含0,2)
/** * 多条件查询 * * @throws IOException */ @Test public void searchInBooleanQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); BooleanQuery query = new BooleanQuery(); query.add(new PrefixQuery(new Term("title", "今天")), BooleanClause.Occur.MUST); query.add(NumericRangeQuery.newIntRange("id", 0, 1, true, true), BooleanClause.Occur.MUST); doQuery(query); } 复制代码
6.短语查询
/** * 短语查询 * * @throws IOException */ @Test public void searchInPhraseQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); PhraseQuery query = new PhraseQuery(); //中间跳过多少个字符 query.setSlop(1); query.add(new Term("tipsContent", "广州")); query.add(new Term("tipsContent", "天气")); doQuery(query); } 复制代码
7.模糊查询
模糊查询这个很好理解,有点类似于sql里面的like查询
/** * 模糊查询 * * @throws IOException */ @Test public void searchInFuzzyQuery() throws IOException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); Query query = new FuzzyQuery(new Term("tipsContent", "广州")); doQuery(query); } 复制代码
8.多字段查询
查找内容里面包含有“开心”或者帖子内容里面包含有“亲”这个字眼的信息内容
/** * 多字段查询 * * @throws IOException * @throws ParseException */ @Test public void searchInMultiFieldQueryParser() throws IOException, ParseException { indexReader = DirectoryReader.open(directory); indexSearcher = new IndexSearcher(indexReader); String fieldNames[]={"tipsContent","title"}; MultiFieldQueryParser queryParser = new MultiFieldQueryParser (fieldNames, new IKAnalyzer()); queryParser.setDefaultOperator(QueryParser.Operator.OR); Query query = queryParser.parse("开心 OR 亲"); doQuery(query); } 复制代码
lucene是现在市面上比较常用的一款全文索引框架,在Solr和Elasticsearch上经常会有所使用,所以对lucene的掌握是java程序员进阶路上必不可少的一段考验。