高性能、可扩展的信息检索工具类库
简单强大的核心API
ES的底层工具
扩展性体现在多种扩展包,常见如spellchecker拼写检查模块,highlighter 搜索结果高亮模块。
只要数据源可以转换为文本形式,lucene 即可创建索引。(如pdf,xml,txt等)
Acquire Content 获取内容组件
将需要被查询搜索的数据加载到搜索程序。
该模块一般是动态的,动态是指增量数据处理。
Build Document 构建文档
原始内容转换为Lucene内部使用的结构。
该结构被称为 Document 文档。
主要包括几个带值的域,如标题,正文,摘要,作者,链接。
域由程序员控制。将原文处理后分别写入不同的域中。
Analyze Document 分析文档
核心步骤/组件,技术含量最高
搜索引擎不能直接对文本进行索引,必须将文本分割成一系列的 语汇单元。
Index Document 文档索引
单词挂到哪个文档上,建立索引
用户查询接口
搜索界面,web,app等搜索框,UI界面。
构建查询
程序将前端请求转换为Lucene 识别的“查询对象(Query)”
Lucene 提供了一个 查询解析器框架(QueryParser),可以将用户的输入转换为查询对象。
执行查询
查询索引并返回查询对象匹配的文档。使用Lucene查询组件。
查询请求这里提供了扩展机制,可自定义搜索,过滤,排序等功能。
索引过程核心组件,负责创建新索引或打开已有索引。向索引中增删改被索引文档的信息。
IndexWriter 提供对索引文件的写入操作,但不能读取或搜索。
抽象类。描述Lucene索引存放位置。
子类负责指定具体的索引存储路径。
代表域(Field)的集合。
Lucene 对 Document 对象进行存储和索引,会对Document对象中的 Field,满足被索引条件的,进行索引。
document 设计应谨慎,类似表结构设计。
向索引增加文档
// 1. 使用默认分析器添加文档
indexWriter.addDocument(Document doc)
// 2. 指定的分析器添加文档和语汇单元分析操作。
indexWriter.addDocument(Document doc, Analyzer analyzer)
文档信息删除
删除操作并不会立即执行,而是会存放在缓冲区内,与加入文档类似,最后Lucene会通过周期性刷新文档目录来执行该操作。
可以通过writer.commit() 或者 writer.close() 来立即生效。
writer.forceMerge(1); 执行这个可以让磁盘文件也强制刷新索引磁盘文件。
// 1. 负责删除包含项的所有文档
indexWriter.deleteDocuments(Term term)
// 2. 负责删除包含项数组任一元素的所有文档
indexWriter.deleteDocuments(Term[] terms)
// 3. 负责删除匹配查询语句的所有文档
indexWriter.deleteDocuments(Query query)
// 4. 负责删除匹配查询语句数组任一元素的所有文档
indexWriter.deleteDocuments(Query[] querys)
// 5. 负责删除索引中全部文档
indexWriter.deleteAll()
更新文档
求新文档必须包含旧文档的所有域,包括内容未发生改变的域。因为Lucene只能删除整个过期文档,然后再向索引中添加新文档。
更新其实就是先删除,再增加,所以注意删除的那个条件不要删太多了。
indexWriter.updateDocument(Term term, Document newDoc)
每一个域,都有域名和域值,以及一组选项来控制索引操作行为。
域可以同名,该情况下,搜索时所有域的文本会连接在一起,作为一个文本域处理。
doc.add(new Field(1, 2, 3, 4));
类 | 数据类型 | 是否分词 | 是否索引 | 是否存储 | 说明 |
---|---|---|---|---|---|
StringField | 字符串 | 否 | 是 | 自定义 | 不会进行分词,会将整个串当做一个“语汇单元”存储到索引中,比如“身份证号 |
LongPoint | Long型 | 是 | 是 | 自定义 | 用来构建一个Long型Field,进行分词和索引,比如“价格” |
StoredField | 多种类型 | 否 | 否 | 是 | 不分析,不索引,但是Field会存储在文档中 |
TextField | 字符串或流 | 是 | 是 | 自定义 | 如果值参数是Reader,Lucene猜测值比较大,会采用Unstored策略 |
// 使用Reader而非String来表示域值,在内存中保存String代价比较高或者不太方便时,存储的域值比较大的时候使用。
TextField(String name, Reader reader, Store store)
// TokenStream 用来表示一组已分析过的语汇单元,相当于自定义语汇单元
TextField(String name, TokenStream tokenStream)
权值高的容易排在前面,更容易被查出来。默认都是1.0。
field.setBoost(float boost)
第一种情况,只需要提供一个不丢弃数字的分析器
例如“WhitespaceAnalyzer” “StandardAnalyzer”
第二种情况,可以使用 IntPoint LongPoint FloatPoint DoublePoint这种特殊的域。
将日期和时间转换为等价的数值。
doc.add(new LongPoint("timestamp", new Date().getTime()));
用于搜索IndexWriter类创建的索引。
主要利用Directory实例来定位指定索引。
提供了大量搜索方法。
搜索索引的门户。
所有搜索都通过IndexSearcher进行,他们会调用该类中重载的search方法。
// 第一步:我们需要一个用于索引的目录,大多数情况下我们搜索的索引都是存在于文件系统的
Directory dir = FSDirectory.open(new File("/path/to/index").toPath());
// 第二步:创建一个用于访问dir索引资源的indexReader,这步骤比较消耗资源,不要频繁打开,尽量复用
IndexReader reader = IndexReader.open(dir);
// 第三步:创建用于搜索的门户实例IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
// 进行搜索 n 表示匹配到的前多少个结果返回
TopDocs r = searcher.search(Query query, int n)
如果IndexReader需要更新,可以使用如下方式更新
IndexReader newReader = DirectoryReader.openIfChanged(oldReader);
if(reader != newReader) {
reader.close();
reader = newReader;
searcher = new IndexSearcher(reader);
}
搜索功能的基本单元。与TermQuery对象一起使用。
例如:
// 搜索 文档中 字段名为 fieldName 中 包含 Hello 的
Query q = new TermQuery(new Term("fieldName", "Hello"));
// 10 表示前10条
TopDocs hits = searcher.search(q, 10);
查询对象,有很多子类。
封装某种查询类型的具体子类。Query实例将被传递给IndexSearch的search方法
TermQuery、BooleanQuery、RangeQuery、PrefixQuery、PhraseQuery、MultiPhraseQuery、FuzzyQuery、WildcardQuery、
RegexQuery、SpanQuery等。
对索引中特定项进行搜索是最基本的搜索方式。
常用于根据关键字查询文档。
索引中的各个Term对象会按照字典序排列,并允许Lucene的TermRangeQuery对象提供的范围进行搜索。
搜索时可以包含或者不包含 起始项 或 终止项,如果不包含则说明该端为无边界的。
// 包含起点和终点
TermRangeQuery query = new TermRangeQuery("title2", new BytesRef("s"), new BytesRef("e"), true, true);
两个Boolean对象参数表示是否包含搜索范围的起点和终点。
若使用(IntPoint、LongPoint…)作为索引域,则可使用PointRangeQuery在某个特定范围内搜索该域。
// 1. IntPoint.newExactQuery 精确查询
Query query = IntPoint.newExactQuery("age", 18);
// 2. 范围查询,包含边界
query = IntPoint.newRangeQuery("age", 18, 24);
将用户输入的(可读的)查询表达式(类似SQL) 处理成具体的Query对象
// field 默认域名
// analyzer 分析器 常用 new SimpleAnalyzer()
QueryParser parser = new QueryParser(String field, Analyzer analyzer);
Query query = parser.parse("one*");
TopDocs docs = searcher.search(query, 10);
基本查询表达式
表达式中如指定了域,则不搜索默认域。
查询表达式 | 匹配文档 |
---|---|
one | 默认域包含one的项文档 |
one two | 默认域包含one和two中一个或者两个的文档 |
one OR two | 默认域包含one和two中一个或者两个的文档 |
+one +two | 默认域中同时包含one和two文档 |
one AND two | 默认域中同时包含one和two文档 |
title:one | title域中包含one项的文档 |
title:one -subject:two | title域中包含one且subject域中不包含two的文档 |
title:one AND NOT subject:two | title域中包含one且subject域中不包含two的文档 |
(one OR two) AND three | 默认域中包含three且包含one和two中的一个域或者两个的文档 |
title: “hello world” | title域包含hello world短语的文档 |
title: “hello world”~7 | title域中hello和world之间距离小于7的文档 |
one* | 包含one开头的项的文档 |
lastDate:[1/1/22 TO 12/31/22] | lastDate域值在2022年1月1日和2022年12月31日之间的文档 |
由IndexSearcher.search()方法返回的具有较高评分(排序)的结果。
指针容器。一般指向前N个排名的搜索结果,搜索结果即匹配查询条件的文档。
通过这个可以获取真正的文档。
Document doc = indexSearcher.doc(hits.scoreDocs[i]);
提供对TopDocs中每条搜索结果的访问接口。
项:由一个字符串类型的域值和对应的域名组成
Term t = new Term("name", "value");
docs = searcher.search(new TermQuery(t), 10);
Lucene引入了“索引分段”的技术,即将一个索引文件拆分为多个子索引文件。
每个段都是一个独立的可被搜索的数据集。
段具有不变性,一旦写入磁盘,就不能再发生改变。
新建一个段来存储新增的数据。
先写到内存缓冲区,当缓冲区满16mb 或者 自动刷盘(1秒) 或者 commit()/close() 都会创建新的段文件。
在索引文件下新增了一个.del的文件,用来专门存储被删除的数据id
被删除的数据在进行段合并时才会真正被移除,否则还是会被查到。
删除和新增的组合,先在.del文件中记录旧数据,再在新段中添加一条更新后的数据。
注意为了提升性能,Lucene并不会每新增一个文档就生成一个新段,而是采用延迟写的策略,数据先写入内存缓冲区,然后根据策略再批量写入硬盘。
大量的段如果一只保留,会出现如下问题
段合并就是 将多个小段的索引数据读取到内存中,在内存中合并成一个索引段,然后再写入磁盘,再将旧段文件全部删除。
在执行了flush、commit等方法后,Lucene会基于段的合并策略对索引目录中的段集合进行合并操作。
// 索引合并到指定maxSegments个段
indexWriter.forceMerge(int maxSegments)
分析(Analysis),在Lucene中指的是将域文本转换为最基本的索引表示单元,项(Term)的过程。
分析器主要应用在
内置分析器
分析器 | 说明 |
---|---|
WhitespaceAnalyzer | 根据空格拆分语汇单元 |
SimpleAnalyzer | 根据非字母字符拆分文本,并将其转换为小写形式 |
StopAnalyzer | 根据非字母字符拆分文本,然后小写,再移除停用词 |
KeywordAnalyzer | 将整个文本作为一个单一语汇单元处理 |
StandardAnalyzer | 基于复杂的语法来生成语汇单元 |
StopAnalyzer | 完成基本的单词拆分和小写化功能之外,还负责移除一些停用词,构造方法自定义 |
封装工具类进行各种高级搜索功能。
public class SortingDemo {
private Directory directory;
public SortingDemo(Directory directory) {
this.directory = directory;
}
public void displayResults(Query query, Sort sort) throws IOException {
// 获取查询门户实例
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
// 注意:sort参数
// doDocScores :true,表示强制做评分
TopDocs results = searcher.search(query, 20,sort, true, false);
// 分值格式化:保存小数点后6位
DecimalFormat scoreFormatter = new DecimalFormat("0.######");
for(ScoreDoc sd : results.scoreDocs) {
int docID = sd.doc;
float score = sd.score;
Document doc = searcher.doc(docID);
// 对 doc 处理
}
}
}
按照索引顺序进行排序
demo.displayResults(query, Sort.INDEXORDER);
通过域进行排序
要求被排序的域整个被索引成单个语汇单元(StringField)
demo.displayResults(query, new Sort(new SortField("xxx",
SortField.Type.STRING)));
倒排序
默认Lucene针对域的排序方向使用的是自然排序。
自然排序会对相关性按照降序排列。
基于域排序的按照升序排列。
// SortField最后一个参数 true ,表示反转顺序。
demo.displayResults(allBooks, new Sort(new SortField("xxxdate", SortField.INT, true)));
通过多个域进行排序
// 按照 a 条件的 评分 排序,相同评分的按照 b 条件降序排序。
SortField a = new SortField("a", SortField.Type.STRING);
SortField b = new SortField("b", SortField.Type.INT, true);
Sort s1 = new Sort(a);
Sort s2 = new Sort(b);
example.displayResults(query, s1, SortField.FIELD_SCORE, s2);