1、需求背景
1.1、普通的数据库搜索
select * from 表名 where 字段名 like ‘%吃饭睡觉敲代码%’
1、没有通过高效的索引方式,查询的速度在大量数据的情况下是很慢
2、搜索效果比较差,只能对用户输入的完整关键字首尾位进行模糊匹配。用户搜索的结果错误输入一个字符,可能就导致查询出的结果远离用户的预期
1.2、新的业务需求
1、即使在相关结果数量百万时,也能快速得出结果
2、搜索的结果不仅仅局限于完整的 “吃饭睡觉敲代码” ,而是将此内容拆分成,“吃饭”,“睡觉”,“敲代码”,“代码”等关键字进行搜索
3、对拆分后的搜索关键字进行特殊标识显示
2、 搜索技术
2.1、搜索引擎的种类
搜索引擎按照功能通常分为垂直搜索和综合搜索
1、垂直搜索是指专门针对某一类信息进行搜索。例如:会搜网 主要做商务搜索的,并且提供商务信息。除此之外还有爱看图标网、职友集等
2、综合搜索是指对众多信息进行综合性的搜索。例如:百度、谷歌、搜狗、360搜索等。
2.2、倒排索引
倒排索引又叫反向索引,以字或词为文档中出现的位置情况
在实际的运用中,我们可以对数据库中原始的数据结构(左图),在业务空闲时事先根据左图内容,创建新的倒排索引结构的数据区域(右图)
用户有查询需求时,先访问倒排索引数据区域(右图),得出文档id后,通过文档id即可快速,准确的通过左图找到具体的文档内容
这一过程,可以通过我们自己写程序来实现,也可以借用已经抽象出来的通用开源技术来实现
3、Lucene概述
3.1、Lucene
Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供,产品官网:http://lucene.apache.org/
Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具,Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎
3.2、全文检索
计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户
Lucene全文检索就是对文档中的全部内容进行分词,再对索引分词结果建立倒排索引
3.3、Lucene、Solr、Elasticsearch关系
Lucene:底层的API,工具包
Solr:基于Lucene开发的企业级的搜索引擎产品
Elasticsearch:基于Lucene开发的企业级的搜索引擎产品
4、Lucene的基本使用
使用Lucene的API来实现对索引的增(创建索引)、删(删除索引)、改(修改索引)、查(搜索数据)
4.1、创建索引
文档Document:数据库中一条具体的记录
字段Field:数据库中的每个字段
目录对象Directory:物理存储位置
写出器的配置对象:需要分词器和lucene的版本
4.1.1、添加依赖
<properties>
<lunece.version>4.10.2</lunece.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- lucene核心库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- Lucene的查询解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- lucene的默认分词器库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- lucene的高亮显示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lunece.version}</version>
</dependency>
</dependencies>
4.1.2、代码实现
// 创建索引
@Test
public void testCreate() throws Exception{
// 1 创建文档对象
Document document = new Document();
// 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
document.add(new StringField("id", "1", Field.Store.YES));
// 这里我们title字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词
document.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
// 2 索引目录类,指定索引在硬盘中的位置
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
// 3 创建分词器对象
Analyzer analyzer = new StandardAnalyzer();
// 4 索引写出工具的配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 5 创建索引的写出工具类。参数:索引的目录和配置信息
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 6 把文档交给IndexWriter
indexWriter.addDocument(document);
// 7 提交
indexWriter.commit();
// 8 关闭
indexWriter.close();
}
4.1.3、创建索引的API详解
4.1.3.1、Document(文档类)
Document:文档对象,是一条原始的数据
4.1.3.2、Field(字段类)
一个Document中可以有很多个不同的字段,每一个字段都是一个Field类的对象
一个Document中的字段其类型是不确定的,因此Field类就提供了各种不同的子类,来对应这些不同类型的字段
这些子类有一些不同的特性:
1)DoubleField、FloatField、IntField、LongField、StringField这些子类一定会被创建索引,但是不会被分词,而且不一定会被存储到文档列表,查找时一定要匹配所有的内容,否则搜索不到(例如:主键id、年龄、生日、日期、价格...)。要通过构造函数中的参数Store来指定:如果Store.YES代表存储,Store.NO代表不存储
2)TextField既会创建索引,又会被分词
StoreField一定会被存储,但是一定不创建索引,可以创建各种数据类型的字段(byte、double、float、int、long、String)
StringField会创建索引,但不会被分词,如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到
如果一个字段要显示到最终的结果中,那么一定要存储,否则就不存储
如果要根据这个字段进行搜索,那么这个字段就必须创建索引。
如果一个字段不需要分词,首先这个字段首先要创建索引,然后如果这个字段的值是不可分割的,那么就不需要分词
4.1.3.3、Directory(目录类)
FSDirectory:文件系统目录,会把索引库指向本地磁盘。特点:速度略慢,但是比较安全
RAMDirectory:内存目录,会把索引库保存在内存。特点:速度快,但是不安全
4.1.3.4、Analyzer(分词器类)
提供分词算法,可以把文档中的数据按照算法分词
这些分词器,并没有合适的中文分词器,因此一般我们会用第三方提供的分词器,一般我们用IK分词器
4.1.3.5、IK分词器
IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X,引入jar:
扩展词典和停用词典
IK分词器的词库有限,新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除:
扩展词典:用来引入一些自定义的新词
停止词典:用来停用一些不必要的词条
4.1.3.6、IndexWriterConfig(索引写出器配置类)
1) 设置配置信息:Lucene的版本和分词器类型
2)设置是否清空索引库中的数据
4.1.3.7、IndexWriter(索引写出器类)
索引写出工具,作用就是 实现对索引的增(创建索引)、删(删除索引)、改(修改索引)
可以一次创建一个,也可以批量创建索引
// 批量创建索引
@Test
public void testCreate2() throws Exception{
// 创建文档的集合
Collection<Document> docs = new ArrayList<>();
// 创建文档对象
Document document1 = new Document();
document1.add(new StringField("id", "1", Field.Store.YES));
document1.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
docs.add(document1);
// 创建文档对象
Document document2 = new Document();
document2.add(new StringField("id", "2", Field.Store.YES));
document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Field.Store.YES));
docs.add(document2);
// 创建文档对象
Document document3 = new Document();
document3.add(new StringField("id", "3", Field.Store.YES));
document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Field.Store.YES));
docs.add(document3);
// 创建文档对象
Document document4 = new Document();
document4.add(new StringField("id", "4", Field.Store.YES));
document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Field.Store.YES));
docs.add(document4);
// 创建文档对象
Document document5 = new Document();
document5.add(new StringField("id", "5", Field.Store.YES));
document5.add(new TextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Field.Store.YES));
docs.add(document5);
// 索引目录类,指定索引在硬盘中的位置
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
// 引入IK分词器
Analyzer analyzer = new IKAnalyzer();
// 索引写出工具的配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 设置打开方式:OpenMode.APPEND 会在索引库的基础上追加新索引。OpenMode.CREATE会先清空原来数据,再提交新的索引
conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
// 创建索引的写出工具类。参数:索引的目录和配置信息
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 把文档集合交给IndexWriter
indexWriter.addDocuments(docs);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
}
4.2、查询索引数据
4.2.1、代码实现
@Test
public void testSearch() throws Exception {
// 创建读取目录对象
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
// 创建索引读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建索引搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
// 创建查询解析器,两个参数:默认要查询的字段的名称,分词器
QueryParser parser = new QueryParser("title", new IKAnalyzer());
// 创建查询对象
Query query = parser.parse("谷歌");
// 搜索数据,两个参数:查询条件对象要查询的最大结果条数
// 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
TopDocs topDocs = searcher.search(query, 10);
// 获取总条数
System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
// 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 取出文档编号
int docID = scoreDoc.doc;
// 根据编号去找文档
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
// 取出文档得分
System.out.println("得分: " + scoreDoc.score);
}
}
4.2.2、核心API
4.2.2.1、QueryParser(查询解析器)
1)QueryParser(单一字段的查询解析器)
2)MultiFieldQueryParser(多字段的查询解析器)
4.2.2.2、Query(查询对象,包含要查询的关键词信息)
1)通过QueryParser解析关键字,得到查询对象
2)自定义查询对象(高级查询),我们可以通过Query的子类,直接创建查询对象,实现高级查询
4.2.2.3、IndexSearch(索引搜索对象,执行搜索功能)
IndexSearch可以帮助我们实现:快速搜索、排序、打分等功能
IndexSearch需要依赖IndexReader类
查询后得到的结果,就是打分排序后的前N名结果。N可以通过第2个参数来指定:
4.2.2.4、TopDocs(查询结果对象)
通过IndexSearcher对象,我们可以搜索,获取结果:TopDocs对象
4.2.2.5、ScoreDoc(得分文档对象)
ScoreDoc是得分文档对象,包含两部分数据(文档编号、文档的得分信息)
4.2.3、特殊查询
public void search(Query query) throws Exception {
// 索引目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 索引读取工具
IndexReader reader = DirectoryReader.open(directory);
// 索引搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
// 搜索数据,两个参数:查询条件对象要查询的最大结果条数
// 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
TopDocs topDocs = searcher.search(query, 10);
// 获取总条数
System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
// 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 取出文档编号
int docID = scoreDoc.doc;
// 根据编号去找文档
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
// 取出文档得分
System.out.println("得分: " + scoreDoc.score);
}
}
4.2.3.1、TermQuery(词条查询)
/*
* 普通词条查询
* 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串!
*/
@Test
public void testTermQuery() throws Exception {
// 创建词条查询对象
Query query = new TermQuery(new Term("title", "谷歌地图"));
search(query);
}
4.2.3.2、WildcardQuery(通配符查询)
/*
* 通配符查询
* ? 可以代表任意一个字符
* * 可以任意多个任意字符
*/
@Test
public void testWildCardQuery() throws Exception {
// 创建查询对象
Query query = new WildcardQuery(new Term("title", "*歌*"));
search(query);
}
4.2.3.3 FuzzyQuery(模糊查询)
/*
* 模糊查询
*/
@Test
public void testFuzzyQuery() throws Exception {
// 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2
// 编辑距离:一个单词到另一个单词最少要修改的次数 facebool --> facebook 需要编辑1次,编辑距离就是1
// Query query = new FuzzyQuery(new Term("title","fscevool"));
// 可以手动指定编辑距离,但是参数必须在0~2之间
Query query = new FuzzyQuery(new Term("title","facevool"),1);
search(query);
}
4.2.3.4、NumericRangeQuery(数值范围查询)
/*
* 数值范围查询
* 可以用来对非String类型的ID进行精确的查找
*/
@Test
public void testNumericRangeQuery() throws Exception{
// 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
search(query);
}
4.2.3.5、BooleanQuery(组合查询)
/*
* 布尔查询:
* 布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合
* 交集:Occur.MUST + Occur.MUST
* 并集:Occur.SHOULD + Occur.SHOULD
* 非:Occur.MUST_NOT
*/
@Test
public void testBooleanQuery() throws Exception{
Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
// 创建布尔查询的对象
BooleanQuery query = new BooleanQuery();
// 组合其它查询
query.add(query1, BooleanClause.Occur.MUST_NOT);
query.add(query2, BooleanClause.Occur.SHOULD);
search(query);
}
4.4 修改索引
/*
* 修改索引
* 注意:
* A:Lucene修改功能底层会先删除,再把新的文档添加。
* B:修改功能会根据Term进行匹配,所有匹配到的都会被删除。这样不好
* C:因此,一般我们修改时,都会根据一个唯一不重复字段进行匹配修改。例如ID
* D:但是词条搜索,要求ID必须是字符串。如果不是,这个方法就不能用。
* 如果ID是数值类型,我们不能直接去修改。可以先手动删除deleteDocuments(数值范围查询锁定ID),再添加。
*/
@Test
public void testUpdate() throws Exception{
// 创建目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
// 创建索引写出工具
IndexWriter writer = new IndexWriter(directory, conf);
// 创建新的文档数据
Document doc = new Document();
doc.add(new StringField("id","1",Store.YES));
doc.add(new TextField("title","谷歌地图之父跳槽facebook ",Store.YES));
/* 修改索引。参数:
* 词条:根据这个词条匹配到的所有文档都会被修改
* 文档信息:要修改的新的文档数据
*/
writer.updateDocument(new Term("id","1"), doc);
// 提交
writer.commit();
// 关闭
writer.close();
}
4.5 删除索引
/*
* 删除索引
* 注意:
* 一般,为了进行精确删除,我们会根据唯一字段来删除。比如ID
* 如果是用Term删除,要求ID也必须是字符串类型!
*/
@Test
public void testDelete() throws Exception {
// 创建目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
// 创建索引写出工具
IndexWriter writer = new IndexWriter(directory, conf);
// 根据词条进行删除
// writer.deleteDocuments(new Term("id", "1"));
// 根据query对象删除,如果ID是数值类型,那么我们可以用数值范围查询锁定一个具体的ID
// Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
// writer.deleteDocuments(query);
// 删除所有
writer.deleteAll();
// 提交
writer.commit();
// 关闭
writer.close();
}
5、Lucene的高级使用
5.1、高亮显示
// 高亮显示
@Test
public void testHighlighter() throws Exception {
// 创建目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建索引读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建索引搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
// 创建查询解析器
QueryParser parser = new QueryParser("title", new IKAnalyzer());
// 创建查询对象
Query query = parser.parse("谷歌地图");
// 创建格式化器
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
// 创建查询分数工具
QueryScorer scorer = new QueryScorer(query);
// 准备高亮工具
Highlighter highlighter = new Highlighter(formatter, scorer);
// 搜索
TopDocs topDocs = searcher.search(query, 10);
System.out.println("本次搜索共" + topDocs.totalHits + "条数据");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取文档编号
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
String title = doc.get("title");
// 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值
String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title);
System.out.println("title: " + hTitle);
// 获取文档的得分
System.out.println("得分:" + scoreDoc.score);
}
}
5.2、排序
// 排序
@Test
public void testSortQuery() throws Exception {
// 目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地图");
// 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
// 搜索
TopDocs topDocs = searcher.search(query, 10,sort);
System.out.println("本次搜索共" + topDocs.totalHits + "条数据");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取文档编号
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
}
}
5.3、分页
// 分页
@Test
public void testPageQuery() throws Exception {
// 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:
int pageSize = 2;// 每页条数
int pageNum = 3;// 当前页码
int start = (pageNum - 1) * pageSize;// 当前页的起始条数
int end = start + pageSize;// 当前页的结束条数(不能包含)
// 目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地图");
// 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
Sort sort = new Sort(new SortField("id", Type.LONG, false));
// 搜索数据,查询0~end条
TopDocs topDocs = searcher.search(query, end,sort);
System.out.println("本次搜索共" + topDocs.totalHits + "条数据");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (int i = start; i < end; i++) {
ScoreDoc scoreDoc = scoreDocs[i];
// 获取文档编号
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
}
}
5.4、得分算法
Lucene会对搜索结果打分,用来表示文档数据与词条关联性的强弱,得分越高,表示查询的匹配度就越高,排名就越靠前
Field field = new StoredField("url", "www.amigo.com");
// 设置激励因子
field.setBoost(10.0f);
document.add(field);