Lucene 全文搜索 引擎 工具类

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;
    }
}
相关推荐
infiniteWei44 分钟前
【Lucene】什么是全文检索?解读结构化数据与非结构化数据
django·全文检索·lucene
infiniteWei2 小时前
【Lucene】全文检索 vs 顺序扫描,为何建立索引比逐个文件搜索更高效?
django·全文检索·lucene
infiniteWei2 小时前
【Lucene】原理学习路线
学习·搜索引擎·全文检索·lucene
Elastic 中国社区官方博客14 小时前
如何将数据从 AWS S3 导入到 Elastic Cloud - 第 3 部分:Elastic S3 连接器
大数据·elasticsearch·搜索引擎·云计算·全文检索·可用性测试·aws
晨欣20 小时前
Elasticsearch和Lucene之间是什么关系?(ChatGPT回答)
elasticsearch·chatgpt·lucene
极梦网络无忧1 天前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
xmst1 天前
短视频如何引流?抖音小红书视频号的引流策略
搜索引擎
Elastic 中国社区官方博客2 天前
释放专利力量:Patently 如何利用向量搜索和 NLP 简化协作
大数据·数据库·人工智能·elasticsearch·搜索引擎·自然语言处理
河南查新信息技术研究院2 天前
科技查新在医药健康领域的应用
大数据·科技·全文检索
hostpai2 天前
FebHost:科技公司选择.TECH域名的魅力
人工智能·科技·搜索引擎·国外域名·科技域名·.tech域名