springboot整合lucene的基本使用:实现索引查询并显示高亮

前言

本文记录了笔者将springboot整合lucene的过程和踩坑,是对lucene最粗浅的运用,主要实现了从数据库查询并写入索引文件,查询结果高亮显示等。

引入依赖

JDK Lucene
1.8.0_291 8.11.2

在pom.xml中加入以下依赖。

<!--Lucene依赖-->
        <!--核心包-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>8.11.2</version>
        </dependency>
        <!--对分词索引查询解析-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>8.11.2</version>
        </dependency>
        <!--一般分词器,适用于英文分词-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>8.11.2</version>
        </dependency>
        <!--检索关键字高亮显示 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>8.11.2</version>
        </dependency>
        <!-- smartcn中文分词器 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>8.11.2</version>
        </dependency>

注意最新的9.3.0版本在使用jdk1.8编译时可能出现错误Class file has wrong version 55.0, should be 52.0(类文件具有错误的版本 55.0, 应为 52.0),字面上来说是新版lucene包含Java 11的class文件造成的。实测退回到8.11.2是没有问题的。

配置初始化类 LuceneConfig

@Configuration
public class LuceneConfig {
    /**
     * lucene索引存放位置,按照需要修改
     */
    private static final String LUCENE_INDEX_PATH="lucene/indexDir/";
    /**
     * 创建一个 Analyzer 实例
     *
     * @return
     */
    @Bean
    public Analyzer analyzer() {
        return new SmartChineseAnalyzer();
    }

    /**
     * 索引位置
     *
     * @return
     * @throws IOException
     */
    @Bean
    public Directory directory() throws IOException {

        Path path = Paths.get(LUCENE_INDEX_PATH);
        File file = path.toFile();
        if(!file.exists()) {
            //如果文件夹不存在则创建
            file.mkdirs();
        }
        return FSDirectory.open(path);
    }

    /**
     * 创建indexWriter
     *
     * @param directory
     * @param analyzer
     * @return
     * @throws IOException
     */
    @Bean
    public IndexWriter indexWriter(Directory directory, Analyzer analyzer) throws IOException {
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
        IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
        // 清空索引
        indexWriter.deleteAll();
        indexWriter.commit();
        return indexWriter;
    }

    /**
     * SearcherManager管理
     *
     * @param directory
     * @return
     * @throws IOException
     */
    @Bean
    public SearcherManager searcherManager(Directory directory, IndexWriter indexWriter) throws IOException {
        SearcherManager searcherManager = new SearcherManager(indexWriter, false, false, new SearcherFactory());
        // Refreshes searcher every 5 seconds when nobody is waiting, and up to 25 msec delay
        // when somebody is waiting:
        ControlledRealTimeReopenThread cRTReopenThead = new ControlledRealTimeReopenThread(indexWriter, searcherManager,
                5.0, 0.025);
        cRTReopenThead.setDaemon(true);
        //线程名称
        cRTReopenThead.setName("更新IndexReader线程");
        // 开启线程
        cRTReopenThead.start();
        return searcherManager;
    }
}

这个类用@Configuration注解了,并且里面一些对象也被@Bean注解了,这就是说这个类将在springboot启动时就注入Spring IOC容器。这样做是因为实例化IndexWriter,IndexSearcher时需要去找索引的持久化文件,频繁实例化的开销很大,所以将这些Bean托管给Spring容器。

其中IndexSearcher需要及时获取索引的更改,否则只能查到启动时第一次加载的索引,因此将它托管给SearchManager,并创建守护线程,定时让SearchManager更新IndexSearch的索引库,这个时间是可以自定义的,这里设置的上限是5秒,下限是25毫秒。构造器的详细描述如下:

NRTManagerReopenThread

# 守护线程的构造器
public NRTManagerReopenThread(NRTManager manager,
                      double targetMaxStaleSec,
                      double targetMinStaleSec)

Create NRTManagerReopenThread, to periodically reopen the NRT searcher.

Parameters:

targetMaxStaleSec - Maximum time until a new reader must be opened; this sets the upper bound on how slowly reopens may occur
必须打开新读者的最长时间;这设置了重新打开的速度上限

targetMinStaleSec - Mininum time until a new reader can be opened; this sets the lower bound on how quickly reopens may occur, when a caller is waiting for a specific indexing change to become visible.
打开新读者的最小时间间隔;这设置了调用者等待特定索引更改变为可见时重新打开的速度下限。

创建启动类 SearchRunner

@Component
@Order(value = 1)
public class SearchRunner implements ApplicationRunner {
    @Autowired
    private ILuceneService service;

    @Override
    public void run(ApplicationArguments arg0) throws Exception {
        service.creatIndex();
    }
}

这个类实现了ApplicationRunner接口,将随springboot启动。在覆写的run方法里调用了服务类IluceneService的createIndex方法。接下来创建这个服务类。

创建服务类 IluceneService

@Service
public class ILuceneService {

    @Autowired
    LuceneDao luceneDao;
    @Autowired
    FrontMapper frontMapper;

    public void creatIndex() throws IOException {
        // 从数据库中获取你要查询的内容,这里根据你自己的数据库方案来
        List<Blog> blogList = blogMapper.selectBlogList(...[你自己的参数]);
        // 将数据建立索引
        luceneDao.createBlogIndex(blogList);
    }
}

创建数据操作类 LuceneDao

@Component
public class LuceneDao {
    @Autowired
    private IndexWriter indexWriter;

    public void createBlogIndex(List<Blog> blogList) throws IOException {
        List<Document> docs = new ArrayList<Document>();
        // 循环将取回的实体类集合里的每一项插入Document
        for (Blog p : blogList) {
            Document doc = new Document();
            Long id = p.getId();
            doc.add(new StoredField("id", id));
            doc.add(new NumericDocValuesField("sort_id", id));
            doc.add(new TextField("title", p.getTitle(), Field.Store.YES));
            log.info(doc.toString());
            // 存储到索引库
            docs.add(doc);
        }
        // 将Document写入索引文件
        indexWriter.addDocuments(docs);
        indexWriter.commit();
    }
}

Lucene使用的对象存储单元为Document,你定义的实体类中的每一个属性对应着一个Document中的一个Field(域)。Document有如下这些域,注意并不是所有的都能保存在索引里,而有些域可以自行选择是否存储。阅读能力强的建议看一下文档原文,或者参考一下这几篇文章:
lucene的Document对象的详解
Lucene深入学习(5)Lucene的Document与Field
Lucene 中文文档 - Field
(二)Lucene中Field.Store.YES或者NO
lucene 中关于Store.YES 关于Store.NO的解释

举个例子,你的类有一个成员属性是Integer整型,你想保存到索引中,于是你看到IntPoint并且想当然地使用它:

doc.add(new IntPoint("id", id));

那么恭喜你,你永远都别想在搜索时把它从索引值里取出来。原因是如下这段话:

public final class IntPoint
extends Field
An indexed int field for fast range filters. If you also need to store the value, you should add a separate StoredField instance.

所以你需要这样去存储它:

doc.add(new StoredField("id", id));

取出来时把它转成你需要的类型就可以了。

细节请参考官方文档:

Class Field

创建查找类 TestLuceneManager 并设置查询结果高亮

@Service
public class BlogLuceneManager {
    @Autowired
    private Analyzer analyzer;

    @Autowired
    private SearcherManager searcherManager;

    // 模糊匹配,匹配词
//    String keyStr = queryParam.get("searchKeyStr");
    public List<Blog> searchBlog(String keyStr) throws IOException, ParseException, InvalidTokenOffsetsException {
        searcherManager.maybeRefresh();
        IndexSearcher indexSearcher = searcherManager.acquire();
        BooleanQuery.Builder builder = new BooleanQuery.Builder();
        QueryParser queryParser;
        Query query = null;
//        String keyStr = "配置";
        if (keyStr != null) {
            // 输入空格,不进行模糊查询
            if (!"".equals(keyStr.replaceAll(" ", ""))) {
                queryParser = new QueryParser("title", analyzer);
                query = queryParser.parse(keyStr);
                builder.add(query, BooleanClause.Occur.MUST);
            }
        }
        Sort sort = new Sort();
        sort.setSort(new SortField("sort_id", SortField.Type.LONG, false));
        TopDocs topDocs = indexSearcher.search(builder.build(), 10, sort);
//        pageInfo.setTotal(topDocs.totalHits);
        ScoreDoc[] hits = topDocs.scoreDocs;
        Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("", ""),   //高亮格式,用标签包裹
                new QueryScorer(query));
        Fragmenter fragmenter = new SimpleFragmenter(100);   //设置高亮后的段落范围在100字内
        highlighter.setTextFragmenter(fragmenter);
        List<Blog> blogList = new ArrayList<>();
        for (ScoreDoc hit : hits) {

            Document doc = indexSearcher.doc(hit.doc);
            String title = highlighter.getBestFragment(analyzer, "title", doc.get("title"));
            log.info(doc.toString());
            Blog blog = new Blog();
            blog.setId(Long.valueOf(doc.get("id")));
            blog.setTitle(title);
            blogList.add(blog);
        }
        return blogList;
    }
}

搜索学习入门–使用LuceneHighlighter高亮显示Lucene检索结果的关键词
lucene高亮显示

这就是最简单的使用了。本人只是出于好奇,能力有限,使用不一定合理和规范,短期内也没有深入学习的打算,权当记录一下过程。

Quote

本文主要参考学习了这篇文章,算是对其的个人梳理和总结。
SpringBoot+Lucene案例介绍

你可能感兴趣的:(lucene,spring,boot,java)