一、底子知识
1、Lucene 是什么
Lucene 是一个本地全文搜索引擎,Solr 和 ElasticSearch 都是基于 Lucene 的封装
Lucene 适合那种轻量级的全文搜索,我就是服务器资源不够,假如上 ES 的话会很占用服务器资源,所有就选择了 Lucene 搜索引擎
2、倒排索引原理
全文搜索的原理是使用了倒排索引,那么什么是倒排索引呢?
- 先通过中文分词器,将文档中包含的关键字全部提取出来,好比我爱中国,会通过分词器分成我,爱,中国,然后分别对应‘我爱中国’
- 然后再将关键字与文档的对应关系保存起来
- 最后对关键字自己做索引排序
3、与传统数据库对比
LuceneDB数据库表(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>
- <dependency>
- <groupId>org.apache.lucene</groupId>
- <artifactId>lucene-core</artifactId>
- <version>${lucene.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.lucene</groupId>
- <artifactId>lucene-queryparser</artifactId>
- <version>${lucene.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.lucene</groupId>
- <artifactId>lucene-analyzers-common</artifactId>
- <version>${lucene.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.lucene</groupId>
- <artifactId>lucene-highlighter</artifactId>
- <version>${lucene.version}</version>
- </dependency>
- <dependency>
- <groupId>com.jianggujin</groupId>
- <artifactId>IKAnalyzer-lucene</artifactId>
- <version>${IKAnalyzer-lucene.version}</version>
- </dependency>
复制代码 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、普通查询
- Term term = new Term("title", "lucene");
- Query query = new TermQuery(term);
复制代码 上述代码表现通过精确匹配字段"title"中包含"lucene"的文档。
- 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"短语的文档
- 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示例:
- java
- WildcardQuery query = new WildcardQuery(new Term("title", "lu*n?e"));
复制代码 上述代码表现使用通配符查询匹配"title"字段中以"lu"开头,且第三个字符为任意字母,最后一个字符为"e"的词项
- 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查询来封装一个简单的搜索工具类,这个较为常用
[code]/**
* 关键词搜索
*
* @param indexName 索引目录
* @param keyword 查询关键词
* @param columns 被搜索的字段
* @param current 当前页
* @param size 每页数据量
* @return
*/
public List 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 documents = new ArrayList();
for (int i = start; i |