Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。适用于小型项目的全文检索需求,不需要进行单独的服务部署,相对于elasticsearch 轻盈方便快捷实用。
以下代码亲测有效实用
工具类中涉及的依赖库可自行导入。
核心代码如下
java
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.Fragmenter;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.BytesRef;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.StringReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
/**
* <p>
* Lucene工具类
* <p>
* Lucene的主要组件包括:
* IndexWriter: 用于创建索引的主要类。它将文档加入索引,可以从索引中删除文档,也可以更新索引。
* Directory: 索引的存储方式。Lucene允许索引存储在几种不同的地方,如磁盘、内存等。
* Analyzer: 文本分析器,用于分析文本以产生索引项。(这个是重点)
* Document: 一个包含各种字段的容器,这些字段最终被建入索引。
* Field: 文档的一个组成部分,包含了一个名字和一串值。
* IndexSearcher: 用于执行搜索查询并返回查询结果的类。
* Query: 查询对象,代表用户的搜索查询条件。
* QueryParser: 用于解析用户输入的查询字符串并生成相应的Query对象。
* ScoreDocs: 一个Searcher.search()方法的结果,它包含了与查询条件相匹配的文档,以及它们的相关度分数。
* Term: 索引中的最小单位,表示文档的一个特定词。
* <p>
* 联合查询
* Query query1 = new TermQuery(new Term("", ""));
* Query query2 = new TermQuery(new Term("", ""));
* BooleanClause booleanClause1=new BooleanClause(query1, BooleanClause.Occur.MUST);
* BooleanClause booleanClause2=new BooleanClause(query1, BooleanClause.Occur.SHOULD);
* BooleanClause booleanClause2=new BooleanClause(query1, BooleanClause.Occur.MUST_NOT);
* <p>
* BooleanQuery booleanQuery = new BooleanQuery.Builder().add(booleanClause1).add(booleanClause1).build();
* searcher.search(booleanQuery,num)
* <p>
* <p>
* 分页的实现 searcher.search(query, num) num按多查询,后手动分页返回
* 或 searcher.searchAfter() 与记录当前已取得最新的文档联合查询
* </p>
*
* @author
* @since 2024/5/22 15:21
*/
@Slf4j
@Component
public final class ZLuceneUtil {
@Value("${lucene.index-path-unix}")
String indexPathUnix;
@Value("${lucene.index-path-windows}")
String indexPathWindows;
/**
* 获取标准分词器的写索引实例
*
* @return 标准分词器的写索引实例
*
* @throws Exception 异常
*/
public IndexWriter indexer() throws Exception {
String indexPath = ZOSUtil.isWindows() ? indexPathWindows : indexPathUnix;
Directory dir = FSDirectory.open(Paths.get(indexPath));
//标准分词器,会自动去掉空格啊,is a the等单词
Analyzer analyzer = new StandardAnalyzer();
//将标准分词器配到写索引的配置中
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//实例化写索引对象
IndexWriter writer;
if (StrUtil.isEmpty(indexPath)) {
writer = new IndexWriter(new ByteBuffersDirectory(), config);
} else {
writer = new IndexWriter(dir, config);
}
return writer;
}
public void writerClose(IndexWriter indexWriter) throws Exception {
indexWriter.close();
}
/**
* 删除索引,根据文档主键
*
* @throws Exception 异常
*/
public void deleteIndex(String id) throws Exception {
IndexWriter indexWriter = indexer();
Query query = new TermQuery(new Term(TextFieldConstant.FILE_ID, id));
indexWriter.deleteDocuments(query);
indexWriter.commit();
writerClose(indexWriter);
}
/**
* 获取中文分词器的写索引实例
*
* @param indexDir 索引存储路径,null为内存方式否则为磁盘方式
*
* @return 中文分词器的写索引实例
*
* @throws Exception 异常
*/
public IndexWriter indexerChinese(String indexDir) throws Exception {
Directory dir = FSDirectory.open(Paths.get(indexDir));
//标准分词器,会自动去掉空格啊,is a the等单词
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
//将标准分词器配到写索引的配置中
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//实例化写索引对象
IndexWriter writer;
if (StrUtil.isEmpty(indexDir)) {
writer = new IndexWriter(new ByteBuffersDirectory(), config);
} else {
writer = new IndexWriter(dir, config);
}
return writer;
}
/**
* 索引指定的文件
*
* @param file 文件
*
* @throws Exception 异常
*/
public void indexFile(UploadFileInfo file, String docId) throws Exception {
//调用下面的getDment方法,获取该文件的document
IndexWriter indexWriter = indexer();
deleteIndex(docId);
Document doc = getDocument(file, docId);
//将doc添加到索引中
indexWriter.addDocument(doc);
indexWriter.commit();
writerClose(indexWriter);
}
/**
* 获取文档,文档里再设置每个字段,就类似于数据库中的一行记录
*
* @param file 文件
*
* @return 文档
*
* @throws Exception 异常
*/
public Document getDocument(UploadFileInfo file, String docId) throws Exception {
Document doc = new Document();
String content = FileUtils.getFileContent(file.getAbsolutePath());
//开始添加字段
//添加内容
doc.add(new TextField(TextFieldConstant.CONTENT, content, Field.Store.YES));
//添加文件名,并把这个字段存到索引文件里
doc.add(new TextField(TextFieldConstant.FILE_NAME, file.getOriginFileName(), Field.Store.YES));
//添加文件路径
doc.add(new TextField(TextFieldConstant.FILE_PATH, file.getAbsolutePath(), Field.Store.YES));
//添加文件类型
doc.add(new TextField(TextFieldConstant.FILE_TYPE, file.getFileType(), Field.Store.YES));
//添加文件id
doc.add(new TextField(TextFieldConstant.FILE_ID, docId, Field.Store.YES));
return doc;
}
/**
* 基础查询 默认
*
* @param indexDir 索引路径
* @param queryStr 查询的字符串
* @param queryTextFieldName 查询文档字段名
* @param analyzer 分词器
* @param num 返回的文档个数
*
* @return 文档列表
*/
public List<Document> query(String indexDir, String queryStr, String queryTextFieldName, Analyzer analyzer,
int num) {
List<Document> result = new ArrayList<>();
try {
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(dir);
//构建IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
//查询解析器
QueryParser parser = new QueryParser(queryTextFieldName, analyzer);
//通过解析要查询的String,获取查询对象,queryStr为传进来的待查的字符串
Query query = parser.parse(queryStr);
//开始查询,查询前num条数据,将记录保存在docs中
TopDocs docs = searcher.search(query, num);
//取出每条查询结果
for (ScoreDoc scoreDoc : docs.scoreDocs) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
result.add(doc);
}
reader.close();
} catch (Exception e) {
log.error(ThrowableUtils.extractStackTrace(e));
}
return result;
}
/**
* 段域查询
*
* @param indexDir 索引路径
* @param startStr 开始字符
* @param endStr 结束字符
* @param queryTextFieldName 查询字段
* @param num 返回文档数量
* @param includeLower 是否包括开始字符
* @param includeUpper 是否包括结束字符
*
* @return 文档列表
*/
public List<Document> query(String indexDir, String startStr, String endStr, String queryTextFieldName, int num,
boolean includeLower, boolean includeUpper) {
List<Document> result = new ArrayList<>();
try {
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(dir);
//构建IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
//查询解析器
Query query = new TermRangeQuery(queryTextFieldName,
new BytesRef(startStr.getBytes()),
new BytesRef(endStr.getBytes()),
includeLower,
includeUpper);
TopDocs docs = searcher.search(query, num);
//取出每条查询结果
for (ScoreDoc scoreDoc : docs.scoreDocs) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
result.add(doc);
}
reader.close();
} catch (Exception e) {
log.error(ThrowableUtils.extractStackTrace(e));
}
return result;
}
/**
* 前缀查询
*
* @param indexDir 索引路径
* @param queryStr 查询字符串
* @param queryTextFieldName 查询字段
* @param num 返回文档数量
*
* @return 文档列表
*/
public List<Document> query(String indexDir, String queryStr, String queryTextFieldName, int num) {
List<Document> result = new ArrayList<>();
try {
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(dir);
//构建IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
//查询解析器
Query query = new PrefixQuery(new Term(queryTextFieldName, queryStr));
TopDocs docs = searcher.search(query, num);
//取出每条查询结果
for (ScoreDoc scoreDoc : docs.scoreDocs) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
result.add(doc);
}
reader.close();
} catch (Exception e) {
log.error(ThrowableUtils.extractStackTrace(e));
}
return result;
}
/**
* 模糊查询
*
* @param queryStr 查询字符串
*
* @return 文档主键列表
*/
public Map<String, String> queryLike(Integer page, Integer size, String queryStr) {
Map<String, String> result = new HashMap<>();
try {
String indexPath = ZOSUtil.isWindows() ? indexPathWindows : indexPathUnix;
//获取要查询的路径,也就是索引所在的位置
Directory dir = FSDirectory.open(Paths.get(indexPath));
IndexReader reader = DirectoryReader.open(dir);
//构建IndexSearcher
IndexSearcher searcher = new IndexSearcher(reader);
//查询解析器
Query query = new FuzzyQuery(new Term(TextFieldConstant.CONTENT, queryStr));
TopDocs docs = searcher.search(query, 1000);
//取出每条查询结果
if (ObjectUtil.isNotNull(docs)) {
List<ScoreDoc> scoreDocList = ZPageUtil.splitList(page, size, Arrays.asList(docs.scoreDocs.clone()));
for (ScoreDoc scoreDoc : scoreDocList) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
Document doc = searcher.doc(scoreDoc.doc);
result.put(doc.getField(TextFieldConstant.FILE_ID).stringValue(), highlighter(doc, query));
}
}
reader.close();
} catch (Exception e) {
log.error(ThrowableUtils.extractStackTrace(e));
}
return result;
}
/**
* 内容高亮
*/
public String highlighter(Document document, Query query) {
//取出每条查询结果
//如果不指定参数的话,默认是加粗,即<b><b/>
SimpleHTMLFormatter simpleHtmlFormatter = new SimpleHTMLFormatter("<b><font color=red>", "</font></b>");
//根据查询对象计算得分,会初始化一个查询结果最高的得分
QueryScorer scorer = new QueryScorer(query);
//根据这个得分计算出一个片段
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
//将这个片段中的关键字用上面初始化好的高亮格式高亮
Highlighter highlighter = new Highlighter(simpleHtmlFormatter, scorer);
//设置一下要显示的片段
highlighter.setTextFragmenter(fragmenter);
try {
String desc = document.get(TextFieldConstant.CONTENT);
//显示高亮
if (desc != null) {
Analyzer analyzer = new StandardAnalyzer();
TokenStream tokenStream = analyzer.tokenStream(TextFieldConstant.CONTENT, new StringReader(desc));
return highlighter.getBestFragment(tokenStream, desc);
}
} catch (Exception e) {
log.debug(ThrowableUtils.extractStackTrace(e));
}
return document.getField(TextFieldConstant.CONTENT).stringValue();
}
/**
* 返回文档的内容做高亮 加红加粗处理
*
* @param documents 文档列表
* @param analyzer 分词器
* @param query 查询对象
*
* @return 文档的内容做高亮 加红加粗处理
*/
public List<String> highlighter(List<Document> documents, Analyzer analyzer, Query query) {
//取出每条查询结果
List<String> list = new ArrayList<>();
//如果不指定参数的话,默认是加粗,即<b><b/>
SimpleHTMLFormatter simpleHtmlFormatter = new SimpleHTMLFormatter("<b><font color=red>", "</font></b>");
//根据查询对象计算得分,会初始化一个查询结果最高的得分
QueryScorer scorer = new QueryScorer(query);
//根据这个得分计算出一个片段
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
//将这个片段中的关键字用上面初始化好的高亮格式高亮
Highlighter highlighter = new Highlighter(simpleHtmlFormatter, scorer);
//设置一下要显示的片段
highlighter.setTextFragmenter(fragmenter);
try {
for (Document doc : documents) {
//scoreDoc.doc相当于docID,根据这个docID来获取文档
String desc = doc.get(TextFieldConstant.CONTENT);
//显示高亮
if (desc != null) {
TokenStream tokenStream = analyzer.tokenStream(TextFieldConstant.CONTENT, new StringReader(desc));
String summary = highlighter.getBestFragment(tokenStream, desc);
list.add(summary);
}
}
} catch (Exception e) {
log.debug(ThrowableUtils.extractStackTrace(e));
}
return list;
}
}