SpringBoot+Lucene案例介绍
GitHub仓库:https://github.com/yizuoliang/blog/tree/master/Full-text Retrieval/02_SpringBoot%2BLucene
一、案例介绍
- 模拟一个商品的站内搜索系统(类似淘宝的站内搜索);
- 商品详情保存在mysql数据库的product表中,使用mybatis框架;
- 站内查询使用Lucene创建索引,进行全文检索;
- 增、删、改,商品需要对Lucene索引修改,搜索也要达到近实时的效果。
对于数据库的操作和配置就不在本文中体现,主要讲解与Lucene的整合。
一、引入lucene的依赖
向pom文件中引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 1 <!--核心包-->
2 <dependency>
3 <groupId>org.apache.lucene</groupId>
4 <artifactId>lucene-core</artifactId>
5 <version>7.6.0</version>
6 </dependency>
7 <!--对分词索引查询解析-->
8 <dependency>
9 <groupId>org.apache.lucene</groupId>
10 <artifactId>lucene-queryparser</artifactId>
11 <version>7.6.0</version>
12 </dependency>
13 <!--一般分词器,适用于英文分词-->
14 <dependency>
15 <groupId>org.apache.lucene</groupId>
16 <artifactId>lucene-analyzers-common</artifactId>
17 <version>7.6.0</version>
18 </dependency>
19 <!--检索关键字高亮显示 -->
20 <dependency>
21 <groupId>org.apache.lucene</groupId>
22 <artifactId>lucene-highlighter</artifactId>
23 <version>7.6.0</version>
24 </dependency>
25 <!-- smartcn中文分词器 -->
26 <dependency>
27 <groupId>org.apache.lucene</groupId>
28 <artifactId>lucene-analyzers-smartcn</artifactId>
29 <version>7.6.0</version>
30 </dependency>
31
32
三、配置初始化Bean类
初始化bean类需要知道的几点:
1.实例化 IndexWriter,IndexSearcher 都需要去加载索引文件夹,实例化是是非常消耗资源的,所以我们希望只实例化一次交给spring管理。
2.IndexSearcher 我们一般通过SearcherManager管理,因为IndexSearcher 如果初始化的时候加载了索引文件夹,那么
后面添加、删除、修改的索引都不能通过IndexSearcher 查出来,因为它没有与索引库实时同步,只是第一次有加载。
3.ControlledRealTimeReopenThread创建一个守护线程,如果没有主线程这个也会消失,这个线程作用就是定期更新让SearchManager管理的search能获得最新的索引库,下面是每25S执行一次。
5.要注意引入的lucene版本,不同的版本用法也不同,许多api都有改变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74 1@Configuration
2public class LuceneConfig {
3 /**
4 * lucene索引,存放位置
5 */
6 private static final String LUCENEINDEXPATH="lucene/indexDir/";
7 /**
8 * 创建一个 Analyzer 实例
9 *
10 * @return
11 */
12 @Bean
13 public Analyzer analyzer() {
14 return new SmartChineseAnalyzer();
15 }
16
17 /**
18 * 索引位置
19 *
20 * @return
21 * @throws IOException
22 */
23 @Bean
24 public Directory directory() throws IOException {
25
26 Path path = Paths.get(LUCENEINDEXPATH);
27 File file = path.toFile();
28 if(!file.exists()) {
29 //如果文件夹不存在,则创建
30 file.mkdirs();
31 }
32 return FSDirectory.open(path);
33 }
34
35 /**
36 * 创建indexWriter
37 *
38 * @param directory
39 * @param analyzer
40 * @return
41 * @throws IOException
42 */
43 @Bean
44 public IndexWriter indexWriter(Directory directory, Analyzer analyzer) throws IOException {
45 IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
46 IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
47 // 清空索引
48 indexWriter.deleteAll();
49 indexWriter.commit();
50 return indexWriter;
51 }
52
53 /**
54 * SearcherManager管理
55 *
56 * @param directory
57 * @return
58 * @throws IOException
59 */
60 @Bean
61 public SearcherManager searcherManager(Directory directory, IndexWriter indexWriter) throws IOException {
62 SearcherManager searcherManager = new SearcherManager(indexWriter, false, false, new SearcherFactory());
63 ControlledRealTimeReopenThread cRTReopenThead = new ControlledRealTimeReopenThread(indexWriter, searcherManager,
64 5.0, 0.025);
65 cRTReopenThead.setDaemon(true);
66 //线程名称
67 cRTReopenThead.setName("更新IndexReader线程");
68 // 开启线程
69 cRTReopenThead.start();
70 return searcherManager;
71 }
72}
73
74
四、创建需要的Bean类
创建商品Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 1/**
2 * 商品bean类
3 * @author yizl
4 *
5 */
6public class Product {
7 /**
8 * 商品id
9 */
10 private int id;
11 /**
12 * 商品名称
13 */
14 private String name;
15 /**
16 * 商品类型
17 */
18 private String category;
19 /**
20 * 商品价格
21 */
22 private float price;
23 /**
24 * 商品产地
25 */
26 private String place;
27 /**
28 * 商品条形码
29 */
30 private String code;
31 ......
32
33
创建一个带参数查询分页通用类PageQuery类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 1/**
2 * 带参数查询分页类
3 * @author yizl
4 *
5 * @param <T>
6 */
7public class PageQuery<T> {
8
9 private PageInfo pageInfo;
10 /**
11 * 排序字段
12 */
13 private Sort sort;
14 /**
15 * 查询参数类
16 */
17 private T params;
18 /**
19 * 返回结果集
20 */
21 private List<T> results;
22 /**
23 * 不在T类中的参数
24 */
25 private Map<String, String> queryParam;
26
27 ......
28
29
五、创建索引库
1.项目启动后执行同步数据库方法
项目启动后,更新索引库中所有的索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1/**
2 * 项目启动后,立即执行
3 * @author yizl
4 *
5 */
6@Component
7@Order(value = 1)
8public class ProductRunner implements ApplicationRunner {
9
10 @Autowired
11 private ILuceneService service;
12
13 @Override
14 public void run(ApplicationArguments arg0) throws Exception {
15 /**
16 * 启动后将同步Product表,并创建index
17 */
18 service.synProductCreatIndex();
19 }
20}
21
22
2.从数据库中查询出所有的商品
从数据库中查找出所有的商品
1
2
3
4
5
6
7
8
9 1 @Override
2 public void synProductCreatIndex() throws IOException {
3 // 获取所有的productList
4 List<Product> allProduct = mapper.getAllProduct();
5 // 再插入productList
6 luceneDao.createProductIndex(allProduct);
7 }
8
9
3.创建这些商品的索引
把List中的商品创建索引
我们知道,mysql对每个字段都定义了字段类型,然后根据类型保存相应的值。
那么lucene的存储对象是以document为存储单元,对象中相关的属性值则存放到Field(域)中;
Field类的常用类型
StringField
字符串
N
Y
Y/N
构建一个字符串的Field,但不会进行分词,将整串字符串存入索引中,适合存储固定(id,身份证号,订单号等)
FloatPoint LongPoint DoublePoint
数值型
Y
Y
N
这个Field用来构建一个float数字型Field,进行分词和索引,比如(价格)
StoredField
重载方法,,支持多种类型
N
N
Y
这个Field用来构建不同类型Field,不分析,不索引,但要Field存储在文档中
TextField
字符串或者流
Y
Y
Y/N
一般此对字段需要进行检索查询
上面是一些常用的数据类型, 6.0后的版本,数值型建立索引的字段都更改为Point结尾,FloatPoint,LongPoint,DoublePoint等,对于浮点型的docvalue是对应的DocValuesField,整型为NumericDocValuesField,FloatDocValuesField等都为NumericDocValuesField的实现类。
commit()的用法
commit()方法,indexWriter.addDocuments(docs);只是将文档放在内存中,并没有放入索引库,没有commit()的文档,我从索引库中是查询不出来的;
许多博客代码中,都没有进行commit(),但仍然能查出来,因为每次插入,他都把IndexWriter关闭.close(),Lucene关闭前,都会把在内存的文档,提交到索引库中,索引能查出来,在spring中IndexWriter是单例的,不关闭,所以每次对索引都更改时,都需要进行commit()操作;
这样设计的目的,和数据库的事务类似,可以进行回滚,调用rollback()方法进行回滚。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 1 @Autowired
2 private IndexWriter indexWriter;
3
4 @Override
5 public void createProductIndex(List<Product> productList) throws IOException {
6 List<Document> docs = new ArrayList<Document>();
7 for (Product p : productList) {
8 Document doc = new Document();
9 doc.add(new StringField("id", p.getId()+"", Field.Store.YES));
10 doc.add(new TextField("name", p.getName(), Field.Store.YES));
11 doc.add(new StringField("category", p.getCategory(), Field.Store.YES));
12 // 保存price,
13 float price = p.getPrice();
14 // 建立倒排索引
15 doc.add(new FloatPoint("price", price));
16 // 正排索引用于排序、聚合
17 doc.add(new FloatDocValuesField("price", price));
18 // 存储到索引库
19 doc.add(new StoredField("price", price));
20 doc.add(new TextField("place", p.getPlace(), Field.Store.YES));
21 doc.add(new StringField("code", p.getCode(), Field.Store.YES));
22 docs.add(doc);
23 }
24 indexWriter.addDocuments(docs);
25 indexWriter.commit();
26 }
27
28
六、多条件查询
按条件查询,分页查询都在下面代码中体现出来了,有什么不明白的可以单独查询资料,下面的匹配查询已经比较复杂了.
searcherManager.maybeRefresh()方法,刷新searcherManager中的searcher,获取到最新的IndexSearcher。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 1 @Autowired
2 private Analyzer analyzer;
3
4 @Autowired
5 private SearcherManager searcherManager;
6
7 @Override
8 public PageQuery<Product> searchProduct(PageQuery<Product> pageQuery) throws IOException, ParseException {
9 searcherManager.maybeRefresh();
10 IndexSearcher indexSearcher = searcherManager.acquire();
11 Product params = pageQuery.getParams();
12 Map<String, String> queryParam = pageQuery.getQueryParam();
13 Builder builder = new BooleanQuery.Builder();
14 Sort sort = new Sort();
15 // 排序规则
16 com.infinova.yimall.entity.Sort sort1 = pageQuery.getSort();
17 if (sort1 != null && sort1.getOrder() != null) {
18 if ("ASC".equals((sort1.getOrder()).toUpperCase())) {
19 sort.setSort(new SortField(sort1.getField(), SortField.Type.FLOAT, false));
20 } else if ("DESC".equals((sort1.getOrder()).toUpperCase())) {
21 sort.setSort(new SortField(sort1.getField(), SortField.Type.FLOAT, true));
22 }
23 }
24
25 // 模糊匹配,匹配词
26 String keyStr = queryParam.get("searchKeyStr");
27 if (keyStr != null) {
28 // 输入空格,不进行模糊查询
29 if (!"".equals(keyStr.replaceAll(" ", ""))) {
30 builder.add(new QueryParser("name", analyzer).parse(keyStr), Occur.MUST);
31 }
32 }
33
34 // 精确查询
35 if (params.getCategory() != null) {
36 builder.add(new TermQuery(new Term("category", params.getCategory())), Occur.MUST);
37 }
38 if (queryParam.get("lowerPrice") != null && queryParam.get("upperPrice") != null) {
39 // 价格范围查询
40 builder.add(FloatPoint.newRangeQuery("price", Float.parseFloat(queryParam.get("lowerPrice")),
41 Float.parseFloat(queryParam.get("upperPrice"))), Occur.MUST);
42 }
43 PageInfo pageInfo = pageQuery.getPageInfo();
44 TopDocs topDocs = indexSearcher.search(builder.build(), pageInfo.getPageNum() * pageInfo.getPageSize(), sort);
45
46 pageInfo.setTotal(topDocs.totalHits);
47 ScoreDoc[] hits = topDocs.scoreDocs;
48 List<Product> pList = new ArrayList<Product>();
49 for (int i = 0; i < hits.length; i++) {
50 Document doc = indexSearcher.doc(hits[i].doc);
51 System.out.println(doc.toString());
52 Product product = new Product();
53 product.setId(Integer.parseInt(doc.get("id")));
54 product.setName(doc.get("name"));
55 product.setCategory(doc.get("category"));
56 product.setPlace(doc.get("place"));
57 product.setPrice(Float.parseFloat(doc.get("price")));
58 product.setCode(doc.get("code"));
59 pList.add(product);
60 }
61 pageQuery.setResults(pList);
62 return pageQuery;
63 }
64
65
七、删除更新索引
1
2
3
4
5
6
7 1 @Override
2 public void deleteProductIndexById(String id) throws IOException {
3 indexWriter.deleteDocuments(new Term("id",id));
4 indexWriter.commit();
5 }
6
7
八、补全Spring中剩余代码
Controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1@RestController
2@RequestMapping("/product/search")
3public class ProductSearchController {
4
5 @Autowired
6 private ILuceneService service;
7 /**
8 *
9 * @param pageQuery
10 * @return
11 * @throws ParseException
12 * @throws IOException
13 */
14 @PostMapping("/searchProduct")
15 private ResultBean<PageQuery<Product>> searchProduct(@RequestBody PageQuery<Product> pageQuery) throws IOException, ParseException {
16 PageQuery<Product> pageResult= service.searchProduct(pageQuery);
17 return ResultUtil.success(pageResult);
18 }
19
20}
21
22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1public class ResultUtil<T> {
2
3 public static <T> ResultBean<T> success(T t){
4 ResultEnum successEnum = ResultEnum.SUCCESS;
5 return new ResultBean<T>(successEnum.getCode(),successEnum.getMsg(),t);
6 }
7
8 public static <T> ResultBean<T> success(){
9 return success(null);
10 }
11
12 public static <T> ResultBean<T> error(ResultEnum Enum){
13 ResultBean<T> result = new ResultBean<T>();
14 result.setCode(Enum.getCode());
15 result.setMsg(Enum.getMsg());
16 result.setData(null);
17 return result;
18 }
19}
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1public class ResultBean<T> implements Serializable {
2
3 private static final long serialVersionUID = 1L;
4
5 /**
6 * 返回code
7 */
8 private int code;
9 /**
10 * 返回message
11 */
12 private String msg;
13 /**
14 * 返回值
15 */
16 private T data;
17 ...
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1public enum ResultEnum {
2 UNKNOW_ERROR(-1, "未知错误"),
3 SUCCESS(0, "成功"),
4 PASSWORD_ERROR(10001, "用户名或密码错误"),
5 PARAMETER_ERROR(10002, "参数错误");
6
7 /**
8 * 返回code
9 */
10 private Integer code;
11 /**
12 * 返回message
13 */
14 private String msg;
15
16 ResultEnum(Integer code, String msg) {
17 this.code = code;
18 this.msg = msg;
19 }
20
21