本文记录了笔者将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是没有问题的。
@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毫秒。构造器的详细描述如下:
# 守护线程的构造器
public NRTManagerReopenThread(NRTManager manager,
double targetMaxStaleSec,
double targetMinStaleSec)
Create NRTManagerReopenThread, to periodically reopen the NRT searcher.
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.
打开新读者的最小时间间隔;这设置了调用者等待特定索引更改变为可见时重新打开的速度下限。
@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方法。接下来创建这个服务类。
@Service
public class ILuceneService {
@Autowired
LuceneDao luceneDao;
@Autowired
FrontMapper frontMapper;
public void creatIndex() throws IOException {
// 从数据库中获取你要查询的内容,这里根据你自己的数据库方案来
List<Blog> blogList = blogMapper.selectBlogList(...[你自己的参数]);
// 将数据建立索引
luceneDao.createBlogIndex(blogList);
}
}
@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));
取出来时把它转成你需要的类型就可以了。
细节请参考官方文档:
@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高亮显示
这就是最简单的使用了。本人只是出于好奇,能力有限,使用不一定合理和规范,短期内也没有深入学习的打算,权当记录一下过程。
本文主要参考学习了这篇文章,算是对其的个人梳理和总结。
SpringBoot+Lucene案例介绍