详解SpringBoot+Lucene案例介绍

网友投稿 485 2023-08-02

详解SpringBoot+Lucene案例介绍

详解SpringBoot+Lucene案例介绍

一、案例介绍

模拟一个商品的站内搜索系统(类似淘宝的站内搜索);

商品详情保存在mysql数据库的product表中,使用mybatis框架;

站内查询使用Lucene创建索引,进行全文检索;

增、删、改,商品需要对Lucene索引修改,搜索也要达到近实时的效果。

对于数据库的操作和配置就不在本文中体现,主要讲解与Lucene的整合。

二、引入lucene的依赖

向pom文件中引入依赖

org.apache.lucene

lucene-core

7.6.0

org.apache.lucene

lucene-queryparser

7.6.0

org.apache.lucene

lucene-analyzers-common

7.6.0

org.apache.lucene

lucene-highlighter

7.6.0

<dependency>

org.apache.lucene

lucene-analyzers-smartcn

7.6.0

三、配置初始化Bean类

初始化bean类需要知道的几点:

1.实例化 IndexWriter,IndexSearcher 都需要去加载索引文件夹,实例化是是非常消耗资源的,所以我们希望只实例化一次交给spring管理。

2.IndexSearcher 我们一般通过SearcherManager管理,因为IndexSearcher 如果初始化的时候加载了索引文件夹,那么

后面添加、删除、修改的索引都不能通过IndexSearcher 查出来,因为它没有与索引库实时同步,只是第一次有加载。

3.ControlledRealTimeReopenThread创建一个守护线程,如果没有主线程这个也会消失,这个线程作用就是定期更新让SearchManager管理的search能获得最新的索引库,下面是每25S执行一次。

4.要注意引入的lucene版本,不同的版本用法也不同,许多api都有改变。

@Configuration

public class LuceneConfig {

/**

* lucene索引,存放位置

*/

private static final String LUCENEINDEXPATH="lucene/indexDir/";

/**

* 创建一个 Analyzer 实例

*

* @return

*/

@Bean

public Analyzer analyzer() {

return new SmartChineseAnalyzer();

}

/**

* 索引位置

*

* @return

* @throws IOException

*/

@Bean

public Directory directory() throws IOException {

Path path = Paths.get(LUCENEINDEXPATH);

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());

ControlledRealTimeReopenThread cRTReopenThead = new ControlledRealTimeReopenThread(indexWriter, searcherManager,

5.0, 0.025);

cRTReopenThead.setDaemon(true);

//线程名称

cRTReopenThead.setName("更新IndexReader线程");

// 开启线程

cRTReopenThead.start();

return searcherManager;

}

}

四、创建需要的Bean类

创建商品Bean

/**

* 商品bean类

* @author yizl

*

*/

public class Product {

/**

* 商品id

*/

private int id;

/**

* 商品名称

*/

private String name;

/**

* 商品类型

*/

private String category;

/**

* 商品价格

*/

private float price;

/**

* 商品产地

*/

private String place;

/**

* 商品条形码

*/

private String code;

......

创建一个带参数查询分页通用类PageQuery类

/**

* 带参数查询分页类

* @author yizl

*

* @param

*/

public class PageQuery {

private PageInfo pageInfo;

/**

* 排序字段

*/

private Sort sort;

/**

* 查询参数类

*/

private T params;

/**

* 返回结果集

*/

private List results;

/**

* 不在T类中的参数

*/

private Map queryParam;

......

五、创建索引库

1.项目启动后执行同步数据库方法

项目启动后,更新索引库中所有的索引。

/**

* 项目启动后,立即执行

* @author yizl

*

*/

@Component

@Order(value = 1)

public class ProductRunner implements ApplicationRunner {

@Autowired

private ILuceneService service;

@Override

public void run(ApplicationArguments arg0) throws Exception {

/**

* 启动后将同步Product表,并创建index

*/

service.synProductCreatIndex();

}

}

2.从数据库中查询出所有的商品

从数据库中查找出所有的商品

@Override

public void synProductCreatIndex() throws IOException {

// 获取所有的productList

List allProduct = mapper.getAllProduct();

// 再插入productList

luceneDao.createProductIndex(allProduct);

}

3.创建这些商品的索引

把List中的商品创建索引

我们知道,mysql对每个字段都定义了字段类型,然后根据类型保存相应的值。

那么lucene的存储对象是以document为存储单元,对象中相关的属性值则存放到Field(域)中;

Field类的常用类型

Field类

数据类型

是否分词

index是否索引

Stored是否存储

说明

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()方法进行回滚。

@Autowired

private IndexWriter indexWriter;

@Override

public void createProductIndex(List productList) throws IOException {

List docs = new ArrayList();

for (Product p : productList) {

Document doc = new Document();

doc.add(new StringField("id", p.getId()+"", Field.Store.YES));

doc.add(new TextField("name", p.getName(), Field.Store.YES));

doc.add(new StringField("category", p.getCategory(), Field.Store.YES));

// 保存price,

float price = p.getPrice();

// 建立倒排索引

doc.add(new FloatPoint("price", price));

// 正排索引用于排序、聚合

doc.add(new FloatDocValuesField("price", price));

// 存储到索引库

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

doc.add(new TextField("place", p.getPlace(), Field.Store.YES));

doc.add(new StringField("code", p.getCode(), Field.Store.YES));

docs.add(doc);

}

indexWriter.addDocuments(docs);

indexWriter.commit();

}

六、多条件查询

按条件查询,分页查询都在下面代码中体现出来了,有什么不明白的可以单独查询资料,下面的匹配查询已经比较复杂了.

searcherManager.maybeRefresh()方法,刷新searcherManager中的searcher,获取到最新的IndexSearcher。

@Autowired

private Analyzer analyzer;

@Autowired

private SearcherManager searcherManager;

@Override

public PageQuery searchProduct(PageQuery pageQuery) throws IOException, ParseException {

searcherManager.maybeRefresh();

IndexSearcher indexSearcher = searcherManager.acquire();

Product params = pageQuery.getParams();

Map queryParam = pageQuery.getQueryParam();

Builder builder = new BooleanQuery.Builder();

Sort sort = new Sort();

// 排序规则

com.infinova.yimall.entity.Sort sort1 = pageQuery.getSort();

if (sort1 != null && sort1.getOrder() != null) {

if ("ASC".equals((sort1.getOrder()).toUpperCase())) {

sort.setSort(new SortField(sort1.getField(), SortField.Type.FLOAT, false));

} else if ("DESC".equals((sort1.getOrder()).toUpperCase())) {

sort.setSort(new SortField(sort1.getField(), SortField.Type.FLOAT, true));

}

}

// 模糊匹配,匹配词

String keyStr = queryParam.get("searchKeyStr");

if (keyStr != null) {

// 输入空格,不进行模糊查询

if (!"".equals(keyStr.replaceAll(" ", ""))) {

builder.add(new QueryParser("name", analyzer).parse(keyStr), Occur.MUST);

}

}

// 精确查询

if (params.getCategory() != null) {

builder.add(new TermQuery(new Term("category", params.getCategory())), Occur.MUST);

}

if (queryParam.get("lowerPrice") != null && queryParam.get("upperPrice") != null) {

// 价格范围查询

builder.add(FloatPoint.newRangeQuery("price", Float.parseFloat(queryParam.get("lowerPrice")),

Float.parseFloat(queryParam.get("upperPrice"))), Occur.MUST);

}

PageInfo pageInfo = pageQuery.getPageInfo();

TopDocs topDocs = indexSearcher.search(builder.build(), pageInfo.getPageNum() * pageInfo.getPageSize(), sort);

pageInfo.setTotal(topDocs.totalHits);

ScoreDoc[] hits = topDocs.scoreDocs;

List pList = new ArrayList();

for (int i = 0; i < hits.length; i++) {

Document doc = indexSearcher.doc(hits[i].doc);

System.out.println(doc.toString());

Product product = new Product();

product.setId(Integer.parseInt(doc.get("id")));

product.setName(doc.get("name"));

product.setCategory(doc.get("category"));

product.setPlace(doc.get("place"));

product.setPrice(Float.parseFloat(doc.get("price")));

product.setCode(doc.get("code"));

pList.add(product);

}

pageQuery.setResults(pList);

return pageQuery;

}

七、删除更新索引

@Override

public void deleteProductIndexById(String id) throws IOException {

indexWriter.deleteDocuments(new Term("id",id));

indexWriter.commit();

}

八、补全Spring中剩余代码

Controller层

@RestController

@RequestMapping("/product/search")

public class ProductSearchController {

@Autowired

private ILuceneService service;

/**

*

* @param pageQuery

* @return

* @throws ParseException

* @throws IOException

*/

@PostMapping("/searchProduct")

private ResultBean> searchProduct(@RequestBody PageQuery pageQuery) throws IOException, ParseException {

PageQuery pageResult= service.searchProduct(pageQuery);

return ResultUtil.success(pageResult);

}

}

public class ResultUtil {

public static ResultBean success(T t){

ResultEnum successEnum = ResultEnum.SUCCESS;

return new ResultBean(successEnum.getCode(),successEnum.getMsg(),t);

}

public static ResultBean success(){

return success(null);

}

public static ResultBean error(ResultEnum Enum){

ResultBean result = new ResultBean();

result.setCode(Enum.getCode());

result.setMsg(Enum.getMsg());

result.setData(null);

return result;

}

}

public class ResultBean implements Serializable {

private static final long serialVersionUID = 1L;

/**

* 返回code

*/

private int code;

/**

* 返回message

*/

private String msg;

/**

* 返回值

*/

private T data;

...

public enum ResultEnum {

UNKNOW_ERROR(-1, "未知错误"),

SUCCESS(0, "成功"),

PASSWORD_ERROR(10001, "用户名或密码错误"),

PARAMETER_ERROR(10002, "参数错误");

/**

* 返回code

*/

private Integer code;

/**

* 返回message

*/

private String msg;

ResultEnum(Integer code, String msg) {

this.code = code;

this.msg = msg;

}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:怎么设计开发小程序 - 从零开始打造你的小程序
下一篇:为什么阿里要慎重使用ArrayList中的subList方法
相关文章

 发表评论

暂时没有评论,来抢沙发吧~