写在前面:本文中用到的 Apache Lucene 版本号是 4.10.2 截止到文章发布时官方的最新版本是 6.5.1 因不同的版本差异较大,请大家在学习过程中确认下版本号是否一致。本文中的所涉及到的源码分享在在 Gighub 链接地址:Part01_Lucene
1.搜索引擎
1.1 - 概述
- 概述:根据一定的策略、运用特定的计算机程序从互联网上搜集信息,再对信息进行 组织(分词) 和 处理(添加索引) 后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括 全文索引、目录索引、 元搜索引擎、垂直搜索引擎、集合式搜索引擎 、门户搜索引擎 与 免费链接列表等。
1.2 - 搜索原理
1.3 - 应用场景
- 大型综合搜索网站
- 站内搜索
- 垂直领域搜索
- 软甲内部搜索
1.4 - 搜索技术
- SQL 进行模糊查询:如果没有前置
%
可以执行索引,如果添加前置%
则全文检索。 - Lucene:解决在海量数据的情况下,利用 倒排索引 技术,实现快速的 搜索 、 打分 、 排序 等功能
1.5 - 倒排索引
根据词条查找文档
-
名词解释:
- 文档(Document):索引库中的每一条原始数据。
- 词条(Term):原始数据按照算法进行 分词,得到的每一个词语。
- 文档列表:Lucene 对原始文档进行编号(DocID),形成的列表就是文档列表。
创建文档列表:Lucene 首先对原始文档数据进行编号(DocId),形成文档列表
文档编号 | ID | Title |
---|---|---|
0 | 1 | 谷歌地图之父跳槽Facebook |
1 | 2 | 谷歌地图之父加盟Facebook |
2 | 3 | 谷歌地图创始人拉斯离开谷歌加盟Facebook |
3 | 4 | 谷歌地图之父跳槽Facebook与Wave项目取消有关 |
4 | 5 | 谷歌地图之父拉斯加盟社交网站Facebook |
- 创建倒排索引列表:对文档中数据进行分词,得到 词条(Term)。对词条添加编号并创建索引,并在词条中记录包含该词条的所有文档编号及其他信息。
词条ID | 词条 | 倒排列表(包含该词条文档 ID) |
---|---|---|
1 | 谷歌 | 0,1,2,3,4 |
2 | 地图 | 0,1,2,3,4 |
3 | 之父 | 0,1,3,4 |
4 | 跳槽 | 0,3 |
5 | 0,1,2,3,4 | |
6 | 加盟 | 1,2,4 |
7 | 创始人 | 2 |
8 | 拉斯 | 2,4 |
9 | 离开 | 2 |
10 | 与 | 3 |
11 | wave | 3 |
12 | 项目 | 3 |
13 | 取消 | 3 |
14 | 有关 | 3 |
15 | 社交 | 4 |
16 | 网站 | 4 |
- 搜索过程:
- 获得用户搜索内容,对搜索内容进行分词,得到用户搜索的所有词条。
- 将词条在倒排索引列表中进行匹配,得到包含该词条的所有文档编号。
2.Lucene
2.1 - 概述
- 用于全文检索和搜寻的开源程序库,由 Apache 软件基金会支持和提供。Lucene 提供了简单强大应用程序接口(API),可以进行全文索引和搜索,可以用来制作搜索引擎产品。
2.2 - 全文检索
- 计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的 次数 和 位置 ,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。
- 总结:Lucene 全文检索就是对文档中全部内容进行分词,然后对所有单词建立倒排索引的过程。
3.QuickStart
3.1 - 创建索引流程图
-
创建文档对象(Document),并添加索引Field字段(Field)
- 索引字段:Field
创建目录对象(Directory)并指定索引在硬盘中存储位置
创建分词器对象(Analyzer)
-
创建索引写出器配置对象(IndexWriterConfig)
- 索引分词器:Analyzer
- 版本:Version
-
创建索引写出器(IndexWriter)
- 目录:Directory
- 索引写出器配置:IndexWriterConfig
-
索引写出器,添加文档对象
- 文档:Document
提交并关闭索引写出器
3.2 - 创建索引
-
导入依赖 jar:
<dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>${lucene.version}</version> </dependency>
-
代码示例:
public class QuickStartTest { @Test public void createTest() throws IOException { /* 1.创建文档对象 */ Document document = new Document(); /* * 添加字段 * * StringField: Field.Store.YES 表示存储到文档列表 * TestField: 既创建索引 又分词 * */ document.add(new StringField("id", "1", Field.Store.YES)); document.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES)); /* * 2.创建目录类 指定索引在硬盘中位置 * */ Directory directory = FSDirectory.open(new File("/Users/zhangsiyao1/Desktop/indexDir")); /* * 3.创建分词器对象 analyzer * */ Analyzer analyzer = new StandardAnalyzer(); /* * 4.索引写出工具配置对象 config * */ IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer); /* * 5.创建索引写出工具类 * */ IndexWriter writer = new IndexWriter(directory, config); /* * 6.将文档添加到写出器工具类中 * */ writer.addDocument(document); /* * 7.提交 & 关闭 写出工具 * */ writer.commit(); writer.close(); } }
3.3 - 使用 lukeall 工具查看索引
- Google 下载地址:lukeall
4.创建索引详解
4.1 - Document
- Document:代表一行数据
- Field:代表 Document 中的一个字段
4.2 - Field
- 存储 :StoreField 支持(byte[]、BytesRef、double、float、int、long、String)
- 创建索引 + 可选存储 :DoubleField、FloatField、IntField、LongField、StringField
- 创建索引 + 可选存储 + 分词 :TestField
- 是否存储?:如果字段需要显示到最终结果中,则需要存储。
- 是否创建索引?:如果根据该字段进行索引,则需要创建索引。
- 是否分词?:前提需要创建索引,如果字段是不可分割的则不需要分词。
4.3 - Directory
- FSDirectory:文件系统目录,将索引库指向本地磁盘。
- 特点:速度略慢,较安全,节约内存。
- RAMDirectory:内存目录,将索引保存在内存中。
- 特点:速度快,不安全,占用内存。
4.4 - Analyzer
没有适合中文的分词器,ChineseAnalyzer(弃用),需要使用第三方分词器
-
maven 导入 IKAnalyzer
<dependency> <groupId>com.janeluo</groupId> <artifactId>ikanalyzer</artifactId> <version>2012_u6</version> </dependency>
-
扩展词典和停用词典:在 resources 下创建 IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!-- 配置扩展字典 --> <entry key="ext_dict">ext.dic</entry> <!-- 配置扩展停止分词字典 --> <entry key="ext_stopwords">stopword.dic</entry> </properties>
4.5 - IndexWriterConfig
- 设置写出时,是否先清除索引库中数据:
public IndexWriterConfig setOpenMode(OpenMode openMode)
- 打开索引库类型:
public static enum OpenMode {
/**
* Creates a new index or overwrites an existing one.
*/
CREATE,
/**
* Opens an existing index.
*/
APPEND,
/**
* Creates a new index if one does not exist,
* otherwise it opens the index and documents will be appended.
*/
CREATE_OR_APPEND
}
4.6 - IndexWriter
- 批量创建索引:
public void addDocuments(Iterable<? extends Iterable<? extends IndexableField>> docs)
5.查询索引
5.1 - 基本查询
public class QueryTest {
private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");
@Test
public void baseSearchTest() throws IOException, ParseException {
/* 索引目录对象 */
Directory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 索引读取工具 */
DirectoryReader directoryReader = DirectoryReader.open(directory);
/* 索引搜索工具 */
IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
/*
* 创建查询解析器
* 1.查询字段名称
* 2.分词解析器
* */
QueryParser queryParser = new QueryParser("title", new IKAnalyzer());
/* 获取查询对象 */
Query query = queryParser.parse("谷歌地图之父拉斯");
/*
* 搜索数据
* 1.查询解析器解析后的查询结果
* 2.查询结果的最大条数
* */
TopDocs topDocs = indexSearcher.search(query, 10);
/* 获取总条数 */
int totalHits = topDocs.totalHits;
System.out.println("本地搜索共查询到 " + totalHits + " 匹配记录");
/*
* 得分文档数组
* 1.doc 文档编号(ID)
* 2.score 文档得分数
* */
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
/* 文档编号 */
int docID = scoreDoc.doc;
/* 通过索引读取器 根据文档编号获取文档 */
Document document = directoryReader.document(docID);
System.out.println("DocID: " + document.get("id"));
System.out.println("Title: " + document.get("title"));
/* 文档得分 */
System.out.println("Score: " + scoreDoc.score);
}
}
}
6.查询索引详解
- 封装 Lucene 查询工具类 LuceneQueryUtils
public class LuceneQueryUtils {
private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");
public static void queryByQuery(Query query, int maxResult) throws IOException {
/* 索引目录对象 */
Directory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 索引读取工具 */
DirectoryReader directoryReader = DirectoryReader.open(directory);
/* 索引搜索工具 */
IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
/* 搜索数据 */
TopDocs topDocs = indexSearcher.search(query, maxResult);
int totalHits = topDocs.totalHits;
System.out.println("本地搜索共查询到 " + totalHits + " 匹配记录");
System.out.println("=======================================");
/*
* 得分文档数组
* */
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
/* 文档编号 */
int docID = scoreDoc.doc;
/* 通过索引读取器 根据文档编号获取文档 */
Document document = directoryReader.document(docID);
System.out.println("DocID: " + document.get("id"));
System.out.println("Title: " + document.get("title"));
System.out.println("Score: " + scoreDoc.score);
System.out.println("=======================================");
}
}
}
6.1 - MultiFieldQueryParser
-
根据多字段查询:MultiFieldQueryParser
MultiFieldQueryParser queryParser = new MultiFieldQueryParser( new String[]{"id", "title"}, new IKAnalyzer() );
6.2 - Query
- 方式一:通过 QueryParser 解析关键字,得到查询对象。
- 方式二:自定义查询对象,通过 Query 子类,创建查询对象,实现高级查询。
6.3 - IndexSearch
功能:快速搜索、排序、打分等功能,其依赖于 IndexReader 对象。
-
基本创建过程:
/* 索引目录对象 */ Directory directory = FSDirectory.open(INDEX_DIR_FILE); /* 索引读取工具 */ DirectoryReader directoryReader = DirectoryReader.open(directory); /* 索引搜索工具 */ IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
-
根据打分排序指定位置结果:
TopDocs topDocs = indexSearcher.search(query, 10);
6.4 - TopDocs
-
获取对象:
TopDocs topDocs = indexSearcher.search(query, 10);
-
包含内容:
-
int totalHints
:查询的总条数 -
ScoreDoc[] scoreDocs
:得分文档对象数组
-
6.5 - ScoreDoc
- 包含内容:
-
int doc
:文档编号(ID),根据文档 ID 获取指定文档Document document = directoryReader.document(docID);
float score
:文档得分
-
7.高级查询
7.1 - 词条查询
- 概述:词条是搜索的最小单位 不可再分割 且值必须是字符串
public void termQueryTest() throws IOException {
TermQuery termQuery = new TermQuery(new Term("title", "谷歌地图"));
LuceneQueryUtils.queryByQuery(termQuery, 10);
}
7.2 - 通配符查询
-
?
:任意 1 个字符 -
*
:任意 n 字符
public void wildcardQuery() throws IOException {
WildcardQuery query = new WildcardQuery(new Term("title", "*歌"));
LuceneQueryUtils.queryByQuery(query, 10);
}
7.3 - 模糊查询
-
maxEdits
:最大编辑距离 一个单词到另一个单词最少修改次数 [0,2]
public void fuzzyQueryTest() throws IOException {
FuzzyQuery fuzzyQuery = new FuzzyQuery(new Term("title", "facebool"), 1);
LuceneQueryUtils.queryByQuery(fuzzyQuery, 10);
}
7.4 - 数值范围查询
- 应用:
id[2L,2L]
对非 String 类型的 ID 进行精确查找
public void numericRangeQueryTest() throws IOException {
NumericRangeQuery<Long> numericRangeQuery = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
LuceneQueryUtils.queryByQuery(numericRangeQuery, 10);
}
7.5 - 组合查询
- 交集:
Occur.MUST
+Occur.MUST
- 并集:
Occur.SHOULD
+Occur.SHOULD
- 补集:
Occur.MUST_NOT
public void booleanQueryTest() throws IOException {
NumericRangeQuery<Long> numericRangeQuery1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
NumericRangeQuery<Long> numericRangeQuery2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(numericRangeQuery1, BooleanClause.Occur.MUST_NOT);
booleanQuery.add(numericRangeQuery2, BooleanClause.Occur.SHOULD);
LuceneQueryUtils.queryByQuery(booleanQuery, 10);
}
8.修改索引
- 问题:修改索引时,只能指定词条(Term)进行更新,词条只能是 String 类型,如果 id 为数值类型怎么更新?
- 答案:先删除,再更新
public class UpdateIndexTest {
private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");
/*
* 1.Lucene 底层先删除所有匹配的索引 再添加新文档
* 2.一般修改功能会根据 Term 词条进行匹配
* 3.根据一个唯一不重复字段进行匹配(ID)
*
* 问题: update 时 Term 词条搜索 要求 ID 必须是字符串 如果不是则不能使用这个方法
* 解决: 先删除该词条 再添加更新后的词条
* */
@Test
public void updateTest() throws IOException {
/* 创建目录对象 */
FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 创建索引写出器配置对象 */
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
/* 创建索引写出器 */
IndexWriter writer = new IndexWriter(directory, config);
Document document = new Document();
document.add(new StringField("id", "1", Field.Store.YES));
document.add(new TextField("title", "谷歌地图之父跳槽facebook为了加入Amazon", Field.Store.YES));
writer.updateDocument(new Term("id", "1"), document);
writer.commit();
writer.close();
}
}
9.删除索引
- 方式一:根据 Term 删除,只能根据 String 类型的词条进行匹配删除。
- 方式二:根据 Query 删除,可以是任意类型的词条进行匹配(更新 ID 非 String 类型文档的解决方案)。
- 方式三:删除所有。
public class UpdateIndexTest {
/*
* 删除索引
* */
@Test
public void deleteTest() throws IOException {
/* 创建目录对象 */
FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 创建索引写出器配置对象 */
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
/* 创建索引写出器 */
IndexWriter writer = new IndexWriter(directory, config);
/*
* 1.根据词条 Term 进行删除 只能匹配 字符串类型 字段
* */
writer.deleteDocuments(new Term("id", "1"));
/*
* 2.根据 Query 删除 可以匹配任何类型的字段
* */
NumericRangeQuery<Long> numericRangeQuery = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
writer.deleteDocuments(numericRangeQuery);
/* 3.删除所有 */
writer.deleteAll();
writer.commit();
writer.close();
}
}
10.Lucene 高级使用
10.1 - 高亮显示
-
SimpleHTMLFormatter
:HTML 格式化工具 -
Highlighter
:高亮工具
@Test
public void highLightTest() throws IOException, ParseException, InvalidTokenOffsetsException {
/* 目录对象 */
FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 读取工具 */
DirectoryReader reader = DirectoryReader.open(directory);
/* 搜索工具 */
IndexSearcher searcher = new IndexSearcher(reader);
/* parse 方式获得 Query 对象 */
QueryParser queryParser = new QueryParser("title", new IKAnalyzer());
Query query = queryParser.parse("谷歌地图");
/* HTML 格式化器 */
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
QueryScorer queryScorer = new QueryScorer(query);
/* 准备高亮工具 */
Highlighter highlighter = new Highlighter(formatter, queryScorer);
/* 搜索 */
TopDocs topDocs = searcher.search(query, 10);
System.out.println("TotalHits: " + topDocs.totalHits);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document document = reader.document(scoreDoc.doc);
/*
* 高亮工具处理普通查询结果
* 参数一: 分词器
* 参数二: 高亮字段名
* 参数三: 高亮字段原始值
* */
String highLightTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", document.get("title"));
System.out.println(highLightTitle);
}
}
- 导入依赖 jar:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lucene.version}</version>
</dependency>
10.2 - 排序
Sort sortArray = new Sort(new SortField("id", SortField.Type.LONG, true));
TopDocs topDocs = searcher.search(query, 10, sortArray);
悄悄话 🌈
- 最近项目进度比较紧,基本是有时间学习技术,没时间写出来的样子,这两天趁着休息时间将之前的一些学习内容按照先后顺序陆续整理一下与大家分享。
彩蛋 🐣
-
最近开通了文集的同名专题 《Java大数据开发》 并会从大数据开发的基础技术向下延伸至云服务,有兴趣的朋友可以来一同交流进步。
如果你觉得我的分享对你有帮助的话,请在下面👇随手点个喜欢 💖,你的肯定才是我最大的动力,感谢。