Lucene技术专门解决海量数据下的模糊搜索问题。
Lucene主要完成的是数据预处理、建立倒排索引,及搜索、排名、高亮显示等功能
全文检索相关词语概要:
单词和文档矩阵:
文档(Document):就是索引库中的一条原始数据,比如一个网页,一件商品
文档编号(DocID):索引库存储文档时,会根据文档创建时间,进行编号,称为文档编号
单词(term):就是对原始数据中的文本进行分词,得到的每一个词条
文档列表:把原始数据,及其编号形成一个列表,称为文档列表
倒排索引列表:以单词及单词编号为索引,保存包含该单词的所有文档的文档编号,形成的列表。
倒排索引的流程:
1. 创建索引的过程:
A:创建文档列表:给所有的文档形成编号,然后以编号为索引保存所有的文档数据
B:创建倒排索引列表:以所有的词条为索引,然后保存包含该词条的所有文档的编号信息。形成的列表
2. 搜索的过程:
当用户输入一个词条,我们会先去倒排索引列表快速定位到这个词条,然后就能知道包含该词条的所有文档的编号。然后就能快速根据编号找到文档
1、Lucene的概述
1.1、什么是Lucene?
1. Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
2. Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
3. Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
1.2、什么是全文检索?
1.3、Lucene和Solr的关系
1. Lucene:一套实现了全文检索的底层API
2. Solr:基于Lucene开发的企业级搜索应用服务器
1.4、Lucene的基本使用
使用Lucene实现索引库的增(创建索引)、删(删除索引)、改(修改索引)、查(搜索数据)。
2.1、使用Lucene创建索引
2.1.1、添加依赖
1 <!-- Junit单元测试 --> 2 <dependency> 3 <groupId>junit</groupId> 4 <artifactId>junit</artifactId> 5 <version>4.12</version> 6 </dependency> 7 <!-- lucene核心库 --> 8 <dependency> 9 <groupId>org.apache.lucene</groupId> 10 <artifactId>lucene-core</artifactId> 11 <version>4.10.2</version> 12 </dependency> 13 <!-- Lucene的查询解析器 --> 14 <dependency> 15 <groupId>org.apache.lucene</groupId> 16 <artifactId>lucene-queryparser</artifactId> 17 <version>4.10.2</version> 18 </dependency> 19 <!-- lucene的默认分词器库 --> 20 <dependency> 21 <groupId>org.apache.lucene</groupId> 22 <artifactId>lucene-analyzers-common</artifactId> 23 <version>4.10.2</version> 24 </dependency> 25 <!-- lucene的高亮显示 --> 26 <dependency> 27 <groupId>org.apache.lucene</groupId> 28 <artifactId>lucene-highlighter</artifactId> 29 <version>4.10.2</version> 30 </dependency> 31 32 <build> 33 <plugins> 34 <!-- java编译插件 --> 35 <plugin> 36 <groupId>org.apache.maven.plugins</groupId> 37 <artifactId>maven-compiler-plugin</artifactId> 38 <version>3.2</version> 39 <configuration> 40 <source>1.7</source> 41 <target>1.7</target> 42 <encoding>UTF-8</encoding> 43 </configuration> 44 </plugin> 45 </plugins> 46 </build>
2.1.2、创建索引的流程分析
2.1.3、创建索引的代码实现
1 /** 2 * 创建索引 3 * @throws Exception 4 */ 5 @Test 6 public void testCreateIndex() throws Exception{ 7 // 创建文档对象 8 Document document = new Document(); 9 // 添加字段,这里字段的参数:字段的名称、字段的值、是否存储。Store.YES存储,Store.NO是不存储 10 document.add(new StringField("id", "1", Store.YES)); 11 // StringField会创建索引,但是不做分词,而TextField会创建索引并且分词 12 document.add(new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES)); 13 14 // 创建索引目录对象 15 Directory directory = FSDirectory.open(new File("indexDir")); 16 // 创建分词器对象 17 Analyzer analyzer = new StandardAnalyzer(); 18 // 创建索引写出工具的配置类 19 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer); 20 // 创建索引写出工具 21 IndexWriter writer = new IndexWriter(directory, conf); 22 23 // 添加文档到索引写出工具 24 writer.addDocument(document); 25 // 提交 26 writer.commit(); 27 // 关闭 28 writer.close(); 29 }
2.2、使用工具查看创建好的索引
2.3、创建索引的核心API
2.3.1、Document(文档类)
Document:代表的是文档列表中的一条原始数据。
2.3.2、Field(字段类)
Field:一个文档中可以有很多的字段,每一个字段都是一个Field类的对象。Field类有很多的子类,这些子类有不同的特性,分别对应文档中具备不同特性的字段。
Field子类特点:
A:IntField、DoubleField、LongField、FloatField、StringField、TextField这几个子类他们一定会被创建索引。但是不一定会被存储。需要通过创建字段时的参数来指定:Stroe.YES代表存储,Store.NO代表不存储
B:StringField和TextField都会被创建索引,但是StringField不会被分词,而TextField会被分词
C:StoredField 一定会被存储,但是一定不会创建索引。
问题1:如何判断是否需要创建索引?
如果需要根据某个字段进行搜索,那么这个字段就必须创建索引。不能用StoreField
问题2:如何判断是否需要存储呢?
如果一个字段最终需要返回给用户看,那么就必须存储,如果不需要,你就不存储。
问题3:是否需要分词?
是否需要分词的前提,就是字段要先需要创建索引。
但是如果这个字段的内容是不可分割的整体,那么就不需要分词。比如:id
2.3.3、Directory(目录类)
Directory:代表的是索引库所在的目录
FSDirectory:把数据存储在硬盘,好处:数据比较安全。弊端:查询速度略慢
RAMDirectory:把数据存在内存:查询速度快,数据不安全
2.3.4、Analyzer(分词器类)
2.3.4.1、分词器的种类:
Analyzer有非常多的子类,支持大部分国家的语言,但是对中文的支持非常差。
所以,我们一旦要做中文分词,就要用中文分词器。
专业的中文分词器,有以下这些:
2.3.4.2、IKAnalyzer分词器使用:
IKAnalyzer(中文分词器)
IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X
2.3.4.3、IKAnalyzer的使用
添加依赖
一些后来出现的新词,在IK分词器中,是没有的。因此IK分词器提供了扩展词库和停止词库
扩展词库:我们自己指定一些自定义的词条。
停止词库:某些不需要做分词的内容。啊、哦、的、额
2.3.4.4、IndexWriterConfig(写出配置类)
IndexWriterConfig:用来设置写出工具的配置信息。在构造函数中,指定版本号和分词器对象
设置创建索引时是追加还是覆盖:
2.3.4.5、IndexWriter(索引写出类)
1)一次创建一个索引
IndexWriter的作用:就是实现对索引库的更新操作:增(创建索引)、删(删除索引)、改(修改索引)
2)一次创建多个索引
1 /** 2 * 批量创建索引 3 * @throws Exception 4 */ 5 @Test 6 public void testCreateIndexes() throws Exception{ 7 // 创建文档的集合 8 Collection<Document> docs = new ArrayList<>(); 9 // 创建文档对象 10 Document document1 = new Document(); 11 document1.add(new StringField("id", "1", Store.YES)); 12 document1.add(new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES)); 13 docs.add(document1); 14 Document document2 = new Document(); 15 document2.add(new StringField("id", "2", Store.YES)); 16 document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Store.YES)); 17 docs.add(document2); 18 Document document3 = new Document(); 19 document3.add(new StringField("id", "3", Store.YES)); 20 document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Store.YES)); 21 docs.add(document3); 22 Document document4 = new Document(); 23 document4.add(new StringField("id", "4", Store.YES)); 24 document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Store.YES)); 25 docs.add(document4); 26 Document document5 = new Document(); 27 document5.add(new StringField("id", "5", Store.YES)); 28 document5.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Store.YES)); 29 docs.add(document5); 30 31 // 创建索引目录对象 32 Directory directory = FSDirectory.open(new File("indexDir")); 33 // 创建分词器对象 34 Analyzer analyzer = new IKAnalyzer(); 35 36 // 创建索引写出工具的配置类 37 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer); 38 // 设置打开方式:默认是OpenMode.APPEND,代表每次添加索引都追加到末尾;OpenMode.CREATE代表先清空索引,再添加 39 conf.setOpenMode(OpenMode.CREATE); 40 // 创建索引写出工具 41 IndexWriter writer = new IndexWriter(directory, conf); 42 43 // 添加文档集合 到索引写出工具 44 writer.addDocuments(docs); 45 // 提交 46 writer.commit(); 47 // 关闭 48 writer.close(); 49 }
2.4、使用Lucene搜索索引库数据
1 @Test 2 public void testSearch() throws Exception{ 3 // 创建索引目录对象 4 Directory directory = FSDirectory.open(new File("indexDir")); 5 // 创建索引读取工具 6 IndexReader reader = DirectoryReader.open(directory); 7 // 索引的搜索工具类 8 IndexSearcher searcher = new IndexSearcher(reader); 9 10 // 创建查询解析器对象。参数:默认查询的字段名称,分词器对象 11 QueryParser parser = new QueryParser("title", new IKAnalyzer()); 12 // 创建查询对象 13 Query query = parser.parse("跳槽"); 14 // 执行Query对象,搜索数据。参数:查询对象Query,查询结果的前N条数据 15 // 返回的是:相关度最高的前N名的文档信息(包含文档的编号以及查询到的总数量) 16 TopDocs topDocs = searcher.search(query, 10); 17 18 System.out.println("本次共搜索到"+topDocs.totalHits+"条数据"); 19 // 获取ScoreDoc(包含编号及得分) 数组 20 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 21 // 遍历 22 for (ScoreDoc scoreDoc : scoreDocs) { 23 // 获取一个文档的编号 24 int docID = scoreDoc.doc; 25 // 根据编号获取文档 26 Document document = reader.document(docID); 27 System.out.println("id: " + document.get("id")); 28 System.out.println("title: " + document.get("title")); 29 // 打印得分 30 System.out.println("得分: " + scoreDoc.score); 31 } 32 }
2.5、搜索索引库的核心API
2.5.1、QueryParser(查询解析器)
QueryParser:可以解析用户输入的字段,获取查询对象(单一字段的查询解析器)
MultiFieldQueryParser(多字段组合查询解析器)
2.5.2、Query(查询类)
1)解析关键词,获取查询对象
2)通过Query子类创建特殊查询对象
2.5.3、IndexSearch(索引搜索类)
IndexSearch:可以帮我们实现 搜索索引库,获取数据。还可以实现很多高级搜索功能
2.5.4、TopDocs(搜索结果的信息集合)
TopDocs:相关度最高的前N名的文档信息,N可以通过搜索时的参数指定
这个文档信息包含两部分:
int totalHits:查询到的总数量
ScoreDoc[] scoreDocs:ScoreDoc的数组,ScoreDoc里就有文档的编号
2.5.5、ScoreDoc(搜索到的某个文档信息)
ScoreDoc:包含了文档的编号和得分信息
int doc:文档的编号,我们还需要根据文档编号获取具体的文档
int score:文档的得分
2.6、抽取公共的搜索方法
1 // 公共的搜索方法。 2 public void search(Query query) throws Exception { 3 // 创建索引目录对象、 4 Directory directory = FSDirectory.open(new File("indexDir")); 5 // 创建索引读取工具 6 IndexReader reader = DirectoryReader.open(directory); 7 // 创建搜索工具 8 IndexSearcher searcher = new IndexSearcher(reader); 9 10 // 执行查询,获取前N名的 文档信息 11 TopDocs topDocs = searcher.search(query, 10); 12 13 // 获取总条数 14 int totalHits = topDocs.totalHits; 15 System.out.println("本次搜索共" + totalHits + "条数据"); 16 17 // 获取ScoreDoc(文档的得分及编号)的数组 18 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 19 for (ScoreDoc scoreDoc : scoreDocs) { 20 // 获取编号 21 int docID = scoreDoc.doc; 22 // 根据编号找文档 23 Document document = reader.document(docID); 24 System.out.println("id: " + document.get("id")); 25 System.out.println("title: " + document.get("title")); 26 // 获取得分 27 System.out.println("得分:" + scoreDoc.score); 28 } 29 }
2.7、特殊查询
2.7.1、TermQuery(词条查询)
2.7.2、WildcardQuery(模糊查询)
2.7.3、FuzzyQuery(相似度查询)
2.7.4、NumericRangeQuery(数字边界查询)
2.7.8、BooleanQuery(组合查询)
2.8、使用Lucene修改索引
1 /* 2 * 演示:修改索引。 3 * 一般情况下,我们修改索引,一定是要精确修改某一个,因此一般会根据ID字段进行修改。 4 * 但是在Lucene中,词条查询要求字段必须是字符串类型,所以,我们的ID也必须是字符串 5 * 如果ID为数值类型,要修改一个指定ID的文档。我们可以先删除,再添加。 6 */ 7 @Test 8 public void testUpdate() throws Exception{ 9 // 创建目录 10 Directory directory = FSDirectory.open(new File("indexDir")); 11 // 创建配置对象 12 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer()); 13 // 创建索引写出类 14 IndexWriter writer = new IndexWriter(directory, conf); 15 16 // 创建新的文档对象 17 Document doc = new Document(); 18 doc.add(new StringField("id","1",Store.YES)); 19 doc.add(new TextField("title", "谷歌地图之父跳槽FaceBook 加入传智播客 屌爆了", Store.YES)); 20 21 // 修改文档,两个参数:一个词条,通过词条精确匹配一个要修改的文档;要修改的新的文档数据 22 writer.updateDocument(new Term("id","1"), doc); 23 // 提交 24 writer.commit(); 25 // 关闭 26 writer.close(); 27 }
数据已经改变:
2.9、使用Lucene删除索引
1 /* 2 * 演示:删除索引。 3 * 1)一次删除一个: 4 * 一般情况下,我们删除索引,一定是要精确删除某一个,因此一般会根据ID字段进行删除。 5 * 但是在Lucene中,词条查询要求字段必须是字符串类型,所以,我们的ID也必须是字符串 6 * 2)删除所有 7 * deleteAll() 8 */ 9 @Test 10 public void testDelete() throws Exception{ 11 // 创建目录 12 Directory directory = FSDirectory.open(new File("indexDir")); 13 // 创建配置对象 14 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer()); 15 // 创建索引写出类 16 IndexWriter writer = new IndexWriter(directory, conf); 17 18 // 根据词条删除索引,一次删1条,要求ID必须是字符串 19 // writer.deleteDocuments(new Term("id", "1")); 20 21 // 如果ID为数值类型,那么无法根据Term删除,那么怎么办? 22 23 // 根据Query删除索引,我们用NumericRangeQuery精确锁定一条 指定ID的文档 24 Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true); 25 writer.deleteDocuments(query); 26 27 // 删除所有 28 writer.deleteAll(); 29 30 // 提交 31 writer.commit(); 32 // 关闭 33 writer.close(); 34 }
3、Lucene高级使用
3.1、Lucene实现关键词高亮显示
高亮显示的原理:
1)在返回的结果中,给所有关键字前后,添加一个自定义的HTML标签
2)给这个标签设置CSS样式
1 /* 2 * 演示:Lucene实现高亮 3 */ 4 @Test 5 public void testHighlighter() throws Exception { 6 // 创建索引目录对象、 7 Directory directory = FSDirectory.open(new File("indexDir")); 8 // 创建索引读取工具 9 IndexReader reader = DirectoryReader.open(directory); 10 // 创建搜索工具 11 IndexSearcher searcher = new IndexSearcher(reader); 12 13 // 查询解析器 14 QueryParser parser = new QueryParser("title", new IKAnalyzer()); 15 Query query = parser.parse("谷歌地图"); 16 17 // 创建格式化工具 18 Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>"); 19 Scorer scorer = new QueryScorer(query); 20 // 创建高亮显示的工具 21 Highlighter highlighter = new Highlighter(formatter, scorer); 22 23 // 执行查询,获取前N名的 文档信息 24 TopDocs topDocs = searcher.search(query, 10); 25 // 获取总条数 26 int totalHits = topDocs.totalHits; 27 System.out.println("本次搜索共" + totalHits + "条数据"); 28 // 获取ScoreDoc(文档的得分及编号)的数组 29 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 30 for (ScoreDoc scoreDoc : scoreDocs) { 31 // 获取编号 32 int docID = scoreDoc.doc; 33 // 根据编号找文档 34 Document document = reader.document(docID); 35 System.out.println("id: " + document.get("id")); 36 37 // 获取原始结果 38 String title = document.get("title"); 39 // 使用高亮工具把原始结果变成高亮结果:三个参数:分词器,要高亮的字段名称,原始结果 40 String highTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title); 41 42 System.out.println("title: " + highTitle); 43 // 获取得分 44 System.out.println("得分:" + scoreDoc.score); 45 } 46 }
3.2、使用Lucene实现排序
1 /* 2 * 演示:Lucene实现排序 3 */ 4 @Test 5 public void testSort() throws Exception { 6 // 创建索引目录对象、 7 Directory directory = FSDirectory.open(new File("indexDir")); 8 // 创建索引读取工具 9 IndexReader reader = DirectoryReader.open(directory); 10 // 创建搜索工具 11 IndexSearcher searcher = new IndexSearcher(reader); 12 // 查询解析器 13 QueryParser parser = new QueryParser("title", new IKAnalyzer()); 14 Query query = parser.parse("谷歌地图"); 15 16 // 创建排序的对象,然后接收排序的字段。参数:字段名称,字段类型,是否反转。false升序,true降序 17 Sort sort = new Sort(new SortField("id", Type.LONG, true)); 18 // 执行查询,获取前N名的 文档信息 19 TopDocs topDocs = searcher.search(query, 10, sort); 20 21 // 获取总条数 22 int totalHits = topDocs.totalHits; 23 System.out.println("本次搜索共" + totalHits + "条数据"); 24 // 获取ScoreDoc(文档的得分及编号)的数组 25 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 26 for (ScoreDoc scoreDoc : scoreDocs) { 27 // 获取编号 28 int docID = scoreDoc.doc; 29 // 根据编号找文档 30 Document document = reader.document(docID); 31 System.out.println("id: " + document.get("id")); 32 System.out.println("title: " + document.get("title")); 33 } 34 }
3.3、使用Lucene实现分页查询
1 /* 2 * 演示:Lucene实现分页查询 Lucene本身不提供分页功能。因此,要实现分页,我们必须自己来完成。也就是逻辑分页 3 * 先查询全部,然后返回需要的那一页数据。 4 */ 5 @Test 6 public void testPageQuery() throws Exception { 7 // 准备分页参数: 8 int pageSize = 5;// 每页条数 9 int pageNum = 2;// 当前页 10 int start = (pageNum - 1) * pageSize;// 起始角标 11 int end = start + pageSize;// 结束角标 12 13 // 创建索引目录对象、 14 Directory directory = FSDirectory.open(new File("indexDir")); 15 // 创建索引读取工具 16 IndexReader reader = DirectoryReader.open(directory); 17 // 创建搜索工具 18 IndexSearcher searcher = new IndexSearcher(reader); 19 // 查询解析器 20 QueryParser parser = new QueryParser("title", new IKAnalyzer()); 21 Query query = parser.parse("谷歌"); 22 // 创建排序的对象,然后接收排序的字段。参数:字段名称,字段类型,是否反转。false升序,true降序 23 Sort sort = new Sort(new SortField("id", Type.LONG, false)); 24 // 执行查询,获取的是0~end之间的数据 25 TopDocs topDocs = searcher.search(query, end, sort); 26 27 // 获取总条数 28 int totalHits = topDocs.totalHits; 29 // 获取总页数 30 int totalPages = (totalHits + pageSize - 1) / pageSize; 31 32 System.out.println("本次搜索共" + totalHits + "条数据,共" + totalPages + "页,当前是第" + pageNum + "页"); 33 // 获取ScoreDoc(文档的得分及编号)的数组 34 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 35 for (int i = start; i < end; i++) { 36 ScoreDoc scoreDoc = scoreDocs[i]; 37 // 获取编号 38 int docID = scoreDoc.doc; 39 // 根据编号找文档 40 Document document = reader.document(docID); 41 System.out.println("id: " + document.get("id")); 42 System.out.println("title: " + document.get("title")); 43 } 44 }
3.4、使用Lucene得分计算
Lucene会对搜索结果打分,用来表示文档数据与词条关联性的强弱,得分越高,表示查询的匹配度就越高,排名就越靠前!其算法公式是: