Lucene轻量级搜索引擎,真的太强了!!!Solr 和 ES 都是基于它
一、基础知识
1、Lucene 是什么
Lucene 是一个本地全文搜索引擎,Solr 和 ElasticSearch 都是基于 Lucene 的封装
Lucene 适合那种轻量级的全文搜索,我就是服务器资源不够,如果上 ES 的话会很占用服务器资源,所有就选择了 Lucene 搜索引擎
2、倒排索引原理
全文搜索的原理是使用了倒排索引,那么什么是倒排索引呢?
先通过中文分词器,将文档中包含的关键字全部提取出来,比如我爱中国,会通过分词器分成我,爱,中国,然后分别对应‘我爱中国’ 然后再将关键字与文档的对应关系保存起来 最后对关键字本身做索引排序
3、与传统数据库对比
Lucene | DB |
---|---|
数据库表(table) | 索引(index) |
行(row) | 文档(document) |
列(column) | 字段(field) |
4、数据类型
常见的字段类型
StringField:这是一个不可分词的字符串字段类型,适用于精确匹配和排序。 TextField:这是一个可分词的字符串字段类型,适用于全文搜索和模糊匹配。 IntField、LongField、FloatField、DoubleField:这些是数值字段类型,用于存储整数和浮点数。 DateField:这是一个日期字段类型,用于存储日期和时间。 BinaryField:这是一个二进制字段类型,用于存储二进制数据,如图片、文件等。 StoredField:这是一个存储字段类型,用于存储不需要被索引的原始数据,如文档的内容或其他附加信息。
Lucene 分词器是将文本内容分解成单独的词汇(term)的工具。Lucene 提供了多种分词器,其中一些常见的包括
StandardAnalyzer:这是 Lucene 默认的分词器,它使用 UnicodeText 解析器将文本转换为小写字母,并且根据空格、标点符号和其他字符来进行分词。 CJKAnalyzer:这个分词器专门为中日韩语言设计,它可以正确地处理中文、日文和韩文的分词。 KeywordAnalyzer:这是一个不分词的分词器,它将输入的文本作为一个整体来处理,常用于处理精确匹配的情况。 SimpleAnalyzer:这是一个非常简单的分词器,它仅仅按照非字母字符将文本分割成小写词汇。 WhitespaceAnalyzer:这个分词器根据空格将文本分割成小写词汇,不会进行任何其他的处理。
但是对于中文分词器,我们一般常用第三方分词器IKAnalyzer,需要引入它的POM文件
二、最佳实践
1、依赖导入
<lucene.version>8.1.1</lucene.version>
<IKAnalyzer-lucene.version>8.0.0</IKAnalyzer-lucene.version>
<!--============lucene start================-->
<!-- Lucene核心库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- Lucene的查询解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- Lucene的默认分词器库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- Lucene的高亮显示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- ik分词器 -->
<dependency>
<groupId>com.jianggujin</groupId>
<artifactId>IKAnalyzer-lucene</artifactId>
<version>${IKAnalyzer-lucene.version}</version>
</dependency>
<!--============lucene end================-->
2、创建索引
先制定索引的基本数据,包括索引名称和字段
/**
* @author: sunhhw
* @date: 2023/12/25 17:39
* @description: 定义文章文档字段和索引名称
*/
public interface IArticleIndex {
/**
* 索引名称
*/
String INDEX_NAME = "article";
// --------------------- 文档字段 ---------------------
String COLUMN_ID = "id";
String COLUMN_ARTICLE_NAME = "articleName";
String COLUMN_COVER = "cover";
String COLUMN_SUMMARY = "summary";
String COLUMN_CONTENT = "content";
String COLUMN_CREATE_TIME = "createTime";
}
创建索引并新增文档
/**
* 创建索引并设置数据
*
* @param indexName 索引地址
*/
public void addDocument(String indexName, List<Document> documentList) {
// 配置索引的位置 例如:indexDir = /app/blog/index/article
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
File file = new File(indexDir);
// 若不存在,则创建目录
if (!file.exists()) {
FileUtils.forceMkdir(file);
}
// 读取索引目录
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 中文分析器
Analyzer analyzer = new IKAnalyzer();
// 索引写出工具的配置对象
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
// 创建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
long count = indexWriter.addDocuments(documentList);
log.info("[批量添加索引库]总数量:{}", documentList.size());
// 提交记录
indexWriter.commit();
// 关闭close
indexWriter.close();
} catch (Exception e) {
log.error("[创建索引失败]indexDir:{}", indexDir, e);
throw new UtilsException("创建索引失败", e);
}
}
注意这里有个坑,就是这个 indexWriter.close();
必须要关闭, 不然在执行其他操作的时候会有一个write.lock
文件锁控制导致操作失败indexWriter.addDocuments(documentList)
这是批量添加,单个添加可以使用indexWriter.addDocument()
单元测试
@Test
public void create_index_test() {
ArticlePO articlePO = new ArticlePO();
articlePO.setArticleName("git的基本使用" + i);
articlePO.setContent("这里是git的基本是用的内容" + i);
articlePO.setSummary("测试摘要" + i);
articlePO.setId(String.valueOf(i));
articlePO.setCreateTime(LocalDateTime.now());
Document document = buildDocument(articlePO);
LuceneUtils.X.addDocument(IArticleIndex.INDEX_NAME, document);
}
private Document buildDocument(ArticlePO articlePO) {
Document document = new Document();
LocalDateTime createTime = articlePO.getCreateTime();
String format = LocalDateTimeUtil.format(createTime, DateTimeFormatter.ISO_LOCAL_DATE);
// 因为ID不需要分词,使用StringField字段
document.add(new StringField(IArticleIndex.COLUMN_ID, articlePO.getId() == null ? "" : articlePO.getId(), Field.Store.YES));
// 文章标题articleName需要搜索,所以要分词保存
document.add(new TextField(IArticleIndex.COLUMN_ARTICLE_NAME, articlePO.getArticleName() == null ? "" : articlePO.getArticleName(), Field.Store.YES));
// 文章摘要summary需要搜索,所以要分词保存
document.add(new TextField(IArticleIndex.COLUMN_SUMMARY, articlePO.getSummary() == null ? "" : articlePO.getSummary(), Field.Store.YES));
// 文章内容content需要搜索,所以要分词保存
document.add(new TextField(IArticleIndex.COLUMN_CONTENT, articlePO.getContent() == null ? "" : articlePO.getContent(), Field.Store.YES));
// 文章封面不需要分词,但是需要被搜索出来展示
document.add(new StoredField(IArticleIndex.COLUMN_COVER, articlePO.getCover() == null ? "" : articlePO.getCover()));
// 创建时间不需要分词,仅需要展示
document.add(new StringField(IArticleIndex.COLUMN_CREATE_TIME, format, Field.Store.YES));
return document;
}
3、更新文档
更新索引方法
/**
* 更新文档
*
* @param indexName 索引地址
* @param document 文档
* @param condition 更新条件
*/
public void updateDocument(String indexName, Document document, Term condition) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 读取索引目录
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 中文分析器
Analyzer analyzer = new IKAnalyzer();
// 索引写出工具的配置对象
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
// 创建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.updateDocument(condition, document);
indexWriter.commit();
indexWriter.close();
} catch (Exception e) {
log.error("[更新文档失败]indexDir:{},document:{},condition:{}", indexDir, document, condition, e);
throw new ServiceException();
}
}
单元测试
@Test
public void update_document_test() {
ArticlePO articlePO = new ArticlePO();
articlePO.setArticleName("git的基本使用=编辑");
articlePO.setContent("这里是git的基本是用的内容=编辑");
articlePO.setSummary("测试摘要=编辑");
articlePO.setId("2");
articlePO.setCreateTime(LocalDateTime.now());
Document document = buildDocument(articlePO);
LuceneUtils.X.updateDocument(IArticleIndex.INDEX_NAME, document, new Term("id", "2"));
}
更新的时候,如果存在就更新那条记录,如果不存在就会新增一条记录 new Term("id", "2")
搜索条件,跟数据库里的where id = 2
差不多IArticleIndex.INDEX_NAME = article
索引名称
4、删除文档
删除文档方法
/**
* 删除文档
*
* @param indexName 索引名称
* @param condition 更新条件
*/
public void deleteDocument(String indexName, Term condition) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 读取索引目录
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 索引写出工具的配置对象
IndexWriterConfig conf = new IndexWriterConfig();
// 创建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.deleteDocuments(condition);
indexWriter.commit();
indexWriter.close();
} catch (Exception e) {
log.error("[删除文档失败]indexDir:{},condition:{}", indexDir, condition, e);
throw new ServiceException();
}
}
单元测试
@Test
public void delete_document_test() {
LuceneUtils.X.deleteDocument(IArticleIndex.INDEX_NAME, new Term(IArticleIndex.COLUMN_ID, "1"));
}
删除文档跟编辑文档类似
5、删除索引
把改索引下的数据全部清空
/**
* 删除索引
*
* @param indexName 索引地址
*/
public void deleteIndex(String indexName) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 读取索引目录
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 索引写出工具的配置对象
IndexWriterConfig conf = new IndexWriterConfig();
// 创建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.deleteAll();
indexWriter.commit();
indexWriter.close();
} catch (Exception e) {
log.error("[删除索引失败]indexDir:{}", indexDir, e);
throw new ServiceException();
}
}
6、普通查询
TermQuery查询
Term term = new Term("title", "lucene");
Query query = new TermQuery(term);
上述代码表示通过精确匹配字段"title"中包含"lucene"的文档。
PhraseQuery查询
PhraseQuery.Builder builder = new PhraseQuery.Builder();
builder.add(new Term("content", "open"));
builder.add(new Term("content", "source"));
PhraseQuery query = builder.build();
上述代码表示在字段"content"中查找包含"open source"短语的文档
BooleanQuery查询
TermQuery query1 = new TermQuery(new Term("title", "lucene"));
TermQuery query2 = new TermQuery(new Term("author", "john"));
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(query1, BooleanClause.Occur.MUST);
builder.add(query2, BooleanClause.Occur.MUST);
BooleanQuery query = builder.build();
上述代码表示使用布尔查询同时满足"title"字段包含"lucene"和"author"字段包含"john"的文档。
WildcardQuery查询
WildcardQuery示例:
java
WildcardQuery query = new WildcardQuery(new Term("title", "lu*n?e"));
上述代码表示使用通配符查询匹配"title"字段中以"lu"开头,且第三个字符为任意字母,最后一个字符为"e"的词项
MultiFieldQueryParser查询
String[] fields = {"title", "content", "author"};
Analyzer analyzer = new StandardAnalyzer();
MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer);
Query query = parser.parse("lucene search");
a. 在"title", "content", "author"三个字段中搜索关键字"lucene search"的文本数据
b. MultiFieldQueryParser 默认使用 OR 运算符将多个字段的查询结果合并,即只要在任意一个字段中匹配成功即
可以使用MultiFieldQueryParser查询来封装一个简单的搜索工具类,这个较为常用
/**
* 关键词搜索
*
* @param indexName 索引目录
* @param keyword 查询关键词
* @param columns 被搜索的字段
* @param current 当前页
* @param size 每页数据量
* @return
*/
public List<Document> search(String indexName, String keyword, String[] columns, int current, int size) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 打开索引目录
Directory directory = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
// 中文分析器
Analyzer analyzer = new IKAnalyzer();
// 查询解析器
QueryParser parser = new MultiFieldQueryParser(columns, analyzer);
// 解析查询关键字
Query query = parser.parse(keyword);
// 执行搜索,获取匹配查询的前 limit 条结果。
int limit = current * size;
// 搜索前 limit 条结果
TopDocs topDocs = searcher.search(query, limit);
// 匹配的文档数组
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
// 计算分页的起始 - 结束位置
int start = (current - 1) * size;
int end = Math.min(start + size, scoreDocs.length);
// 返回指定页码的文档
List<Document> documents = new ArrayList<>();
for (int i = start; i < end; i++) {
Document doc = searcher.doc(scoreDocs[i].doc);
documents.add(doc);
}
// 释放资源
reader.close();
return documents;
} catch (Exception e) {
log.error("查询 Lucene 错误: ", e);
return null;
}
}
7、关键字高亮
@Test
public void searchArticle() throws InvalidTokenOffsetsException, IOException, ParseException {
String keyword = "安装";
String[] fields = {IArticleIndex.COLUMN_CONTENT, IArticleIndex.COLUMN_ARTICLE_NAME};
// 先查询出文档列表
List<Document> documentList = LuceneUtils.X.search(IArticleIndex.INDEX_NAME, keyword, fields, 1, 100);
// 中文分词器
Analyzer analyzer = new IKAnalyzer();
// 搜索条件
QueryParser queryParser = new MultiFieldQueryParser(fields, analyzer);
// 搜索关键词,也就是需要高亮的字段
Query query = queryParser.parse(keyword);
// 高亮html语句
Formatter formatter = new SimpleHTMLFormatter("<span style=\"color: #f73131\">", "</span>");
QueryScorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter, scorer);
// 设置片段长度,一共展示的长度
highlighter.setTextFragmenter(new SimpleFragmenter(50));
List<SearchArticleVO> list = new ArrayList<>();
for (Document doc : documentList) {
SearchArticleVO articleVO = new SearchArticleVO();
articleVO.setId(doc.get(IArticleIndex.COLUMN_ID));
articleVO.setCover(doc.get(IArticleIndex.COLUMN_COVER));
articleVO.setArticleName(doc.get(IArticleIndex.COLUMN_ARTICLE_NAME));
articleVO.setSummary(doc.get(IArticleIndex.COLUMN_SUMMARY));
articleVO.setCreateTime(LocalDate.parse(doc.get(IArticleIndex.COLUMN_CREATE_TIME)));
for (String field : fields) {
// 为文档生成高亮
String text = doc.get(field);
// 使用指定的分析器对文本进行分词
TokenStream tokenStream = TokenSources.getTokenStream(field, text, analyzer);
// 找到其中一个关键字就行了
String bestFragment = highlighter.getBestFragment(tokenStream, text);
if (StringUtils.isNotBlank(bestFragment)) {
// 输出高亮结果,取第一条即可
if (field.equals(IArticleIndex.COLUMN_ARTICLE_NAME)) {
articleVO.setArticleName(bestFragment);
}
if (field.equals(IArticleIndex.COLUMN_CONTENT)) {
articleVO.setSummary(bestFragment);
}
}
}
list.add(articleVO);
}
}
我是一零贰肆,一个关注Java技术和记录生活的博主。
欢迎扫码关注“一零贰肆”的公众号,一起学习,共同进步,多看路,少踩坑。
Lucene轻量级搜索引擎,真的太强了!!!Solr 和 ES 都是基于它的更多相关文章
- 搜索引擎选择: Elasticsearch与Solr
我用过这两种搜索引擎,但也仅仅是用过而已,没有非常深入研究,以下是我的看法 lucene是完全用java实现,而sphinx是支持java api.显然这两者是有差别的,用java实现的意义在于,你可 ...
- 微信清理H5真的太早了?这会是应用号发布的前兆吗
三少爷的剑 2016-04-18 21:05 收藏35 评论7 两天之内,整个 H5 游戏创业陷入了两年狂热期以来最冷的冰点. 每一个正在忙于 H5 小游戏开发的开发者都在忙于砍掉游戏代码中有关 ...
- Apache Solr采用Java开发、基于Lucene的全文搜索服务器
http://docs.spring.io/spring-data/solr/ 首先介绍一下solr: Apache Solr (读音: SOLer) 是一个开源.高性能.采用Java开发.基于Luc ...
- 聊聊基于Lucene的搜索引擎核心技术实践
最近公司用到了ES搜索引擎,由于ES是基于Lucene的企业搜索引擎,无意间在“聊聊架构”微信公众号里发现了这篇文章,分享给大家. 请点击链接:聊聊基于Lucene的搜索引擎核心技术实践
- Lucene.net 搜索引擎的中文资料
以下是我找到的网上一些关于Lucene.net 搜索引擎的介绍资料 https://code.i-harness.com/zh-CN/tagged/lucene?page=5 http://jingp ...
- Delphi程序员如何找到高薪的工作?(赚不到钱,原因只有一个,就是他们没有被公司录取。Delphi必须要独自进行深入研究,才能精通,同时也不能自由性太强)
转帖自:http://www.tommstudio.com/ViewNews.aspx?ID=187http://hi.baidu.com/rarnu/blog/ 本文翻译自<美国优秀经理观念大 ...
- 利用Lucene.net搜索引擎进行多条件搜索的做法
利用Lucene.net搜索引擎进行多条件搜索的做法 2018年01月09日 ⁄ 搜索技术 ⁄ 共 613字 ⁄ 字号 小 中 大 ⁄ 评论关闭 利用Lucene.net搜索引擎进行多条件搜索的做法 ...
- 一文带你入门Java Stream流,太强了
两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:"就想看你写的啊!"你看你看,多么苍白的喜欢啊.那就&qu ...
- Solr和ES对比
Solr与ES(ElasticSearch)对比 搜索引擎选择: Elasticsearch与Solr 搜索引擎选型调研文档 Elasticsearch简介* Elasticsearch是一个实时的分 ...
- 关于hermes与solr,es的定位与区别
Hermes与开源的Solr.ElasticSearch的不同 谈到Hermes的索引技术,相信很多同学都会想到Solr.ElasticSearch.Solr.ElasticSearch在真可谓是大名 ...
随机推荐
- 深入理解TF-IDF、BM25算法与BM25变种:揭秘信息检索的核心原理与应用
深入理解TF-IDF.BM25算法与BM25变种:揭秘信息检索的核心原理与应用 1.文本特征表示方法: TF-IDF 在信息检索, 文本挖掘和自然语言处理领域, IF-IDF 这个名字, 从它在 20 ...
- 样本数量不平衡问题方案(Focal Loss & Circle Loss)
1.Focal Loss focal loss是最初由何恺明提出的,最初用于图像领域解决数据不平衡造成的模型性能问题.本文试图从交叉熵损失函数出发,分析数据不平衡问题,focal loss与交叉熵损失 ...
- P8659 [蓝桥杯 2017 国 A] 数组操作 题解
题目链接:洛谷 或者 蓝桥杯 或者 C语言中文网 几个OJ的AC记录: 忘了哪个OJ的: 洛谷: C语言中文网: 蓝桥杯: emmmmmmm,好像每个OJ给的时限和空间还不一样,蓝桥杯官方还给了 $3 ...
- Windows上同时使用有线网络及无线网络连接配置
由于公司搬到了新的办公地点,公司内部只有内网,当需要连接互联网查询资料时只能切换网络,非常麻烦.所以为了能够同时连接连接公司内网,又能够访问互联网,这里介绍如何同时连接无线和有线. 有线网络:10.3 ...
- CF1499
A 氵 B 如果 11 后出现了 00 就不行. C 枚举走几段. 横竖可以分开算. 一定是:除了费用最小的都是走长度 \(1\),费用最小的包揽剩下的. D \(c\cdot lcm(a,b)-d\ ...
- Skywalking-Aop Docker单机环境搭建
1.OAP-SERVER和UI环境搭建 本次搭建是基于MySQL进行持久化,因此需要提前准备好一个MySQL容器 (MySQL容器部署略过).如有错误还请指正. 1.1 OAP服务搭建 拉取skywa ...
- 回顾复习之坐标DP
定义 坐标型动态规划一般是给定网格.序列,求满足条件的MAX或MIN. 开数组时,dp[i]一般代表以ai结尾的满足条件的子序列,dp[i][j]代表以i.j结尾的满足条件的最优解 例题 数塔 典中典 ...
- 2023年多校联训NOIP层测试4+洛谷 8 月月赛 I & RiOI Round 2
2023年多校联训NOIP层测试4 爆零了 T1 幸运数字 \(0pts\) 首先考虑一个结论: \(4\) 的倍数一定满足最后两位能被 \(4\) 整除. 从 \(1\) 进行输入,方便处理.若枚举 ...
- NC17508 指纹锁
题目链接 题目 题目描述 HA实验有一套非常严密的安全保障体系,在HA实验基地的大门,有一个指纹锁. 该指纹锁的加密算法会把一个指纹转化为一个不超过1e7的数字,两个指纹数值之差越小,就说明两 ...
- InnoDB中不同SQL语句设置的锁
锁定读(locking read).更新(UPDATE)或删除(DELETE)通常会在SQL语句处理过程中扫描的每个索引记录上设置记录锁.语句中是否存在排除行的WHERE条件并不重要.InnoDB不记 ...