详解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 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 publicclassLuceneConfig{ /** *lucene索引,存放位置 */ privatestaticfinalStringLUCENEINDEXPATH="lucene/indexDir/"; /** *创建一个Analyzer实例 * *@return */ @Bean publicAnalyzeranalyzer(){ returnnewSmartChineseAnalyzer(); } /** *索引位置 * *@return *@throwsIOException */ @Bean publicDirectorydirectory()throwsIOException{ Pathpath=Paths.get(LUCENEINDEXPATH); Filefile=path.toFile(); if(!file.exists()){ //如果文件夹不存在,则创建 file.mkdirs(); } returnFSDirectory.open(path); } /** *创建indexWriter * *@paramdirectory *@paramanalyzer *@return *@throwsIOException */ @Bean publicIndexWriterindexWriter(Directorydirectory,Analyzeranalyzer)throwsIOException{ IndexWriterConfigindexWriterConfig=newIndexWriterConfig(analyzer); IndexWriterindexWriter=newIndexWriter(directory,indexWriterConfig); //清空索引 indexWriter.deleteAll(); indexWriter.commit(); returnindexWriter; } /** *SearcherManager管理 * *@paramdirectory *@return *@throwsIOException */ @Bean publicSearcherManagersearcherManager(Directorydirectory,IndexWriterindexWriter)throwsIOException{ SearcherManagersearcherManager=newSearcherManager(indexWriter,false,false,newSearcherFactory()); ControlledRealTimeReopenThreadcRTReopenThead=newControlledRealTimeReopenThread(indexWriter,searcherManager, 5.0,0.025); cRTReopenThead.setDaemon(true); //线程名称 cRTReopenThead.setName("更新IndexReader线程"); //开启线程 cRTReopenThead.start(); returnsearcherManager; } }
四、创建需要的Bean类
创建商品Bean
/** *商品bean类 *@authoryizl * */ publicclassProduct{ /** *商品id */ privateintid; /** *商品名称 */ privateStringname; /** *商品类型 */ privateStringcategory; /** *商品价格 */ privatefloatprice; /** *商品产地 */ privateStringplace; /** *商品条形码 */ privateStringcode; ......
创建一个带参数查询分页通用类PageQuery类
/** *带参数查询分页类 *@authoryizl * *@param*/ publicclassPageQuery { privatePageInfopageInfo; /** *排序字段 */ privateSortsort; /** *查询参数类 */ privateTparams; /** *返回结果集 */ privateList results; /** *不在T类中的参数 */ privateMap queryParam; ......
五、创建索引库
1.项目启动后执行同步数据库方法
项目启动后,更新索引库中所有的索引。
/** *项目启动后,立即执行 *@authoryizl * */ @Component @Order(value=1) publicclassProductRunnerimplementsApplicationRunner{ @Autowired privateILuceneServiceservice; @Override publicvoidrun(ApplicationArgumentsarg0)throwsException{ /** *启动后将同步Product表,并创建index */ service.synProductCreatIndex(); } }
2.从数据库中查询出所有的商品
从数据库中查找出所有的商品
@Override publicvoidsynProductCreatIndex()throwsIOException{ //获取所有的productList ListallProduct=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 privateIndexWriterindexWriter; @Override publicvoidcreateProductIndex(ListproductList)throwsIOException{ List docs=newArrayList (); for(Productp:productList){ Documentdoc=newDocument(); doc.add(newStringField("id",p.getId()+"",Field.Store.YES)); doc.add(newTextField("name",p.getName(),Field.Store.YES)); doc.add(newStringField("category",p.getCategory(),Field.Store.YES)); //保存price, floatprice=p.getPrice(); //建立倒排索引 doc.add(newFloatPoint("price",price)); //正排索引用于排序、聚合 doc.add(newFloatDocValuesField("price",price)); //存储到索引库 doc.add(newStoredField("price",price)); doc.add(newTextField("place",p.getPlace(),Field.Store.YES)); doc.add(newStringField("code",p.getCode(),Field.Store.YES)); docs.add(doc); } indexWriter.addDocuments(docs); indexWriter.commit(); }
六、多条件查询
按条件查询,分页查询都在下面代码中体现出来了,有什么不明白的可以单独查询资料,下面的匹配查询已经比较复杂了.
searcherManager.maybeRefresh()方法,刷新searcherManager中的searcher,获取到最新的IndexSearcher。
@Autowired privateAnalyzeranalyzer; @Autowired privateSearcherManagersearcherManager; @Override publicPageQuerysearchProduct(PageQuery pageQuery)throwsIOException,ParseException{ searcherManager.maybeRefresh(); IndexSearcherindexSearcher=searcherManager.acquire(); Productparams=pageQuery.getParams(); Map queryParam=pageQuery.getQueryParam(); Builderbuilder=newBooleanQuery.Builder(); Sortsort=newSort(); //排序规则 com.infinova.yimall.entity.Sortsort1=pageQuery.getSort(); if(sort1!=null&&sort1.getOrder()!=null){ if("ASC".equals((sort1.getOrder()).toUpperCase())){ sort.setSort(newSortField(sort1.getField(),SortField.Type.FLOAT,false)); }elseif("DESC".equals((sort1.getOrder()).toUpperCase())){ sort.setSort(newSortField(sort1.getField(),SortField.Type.FLOAT,true)); } } //模糊匹配,匹配词 StringkeyStr=queryParam.get("searchKeyStr"); if(keyStr!=null){ //输入空格,不进行模糊查询 if(!"".equals(keyStr.replaceAll("",""))){ builder.add(newQueryParser("name",analyzer).parse(keyStr),Occur.MUST); } } //精确查询 if(params.getCategory()!=null){ builder.add(newTermQuery(newTerm("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); } PageInfopageInfo=pageQuery.getPageInfo(); TopDocstopDocs=indexSearcher.search(builder.build(),pageInfo.getPageNum()*pageInfo.getPageSize(),sort); pageInfo.setTotal(topDocs.totalHits); ScoreDoc[]hits=topDocs.scoreDocs; List pList=newArrayList (); for(inti=0;i 七、删除更新索引
@Override publicvoiddeleteProductIndexById(Stringid)throwsIOException{ indexWriter.deleteDocuments(newTerm("id",id)); indexWriter.commit(); }八、补全Spring中剩余代码
Controller层
@RestController @RequestMapping("/product/search") publicclassProductSearchController{ @Autowired privateILuceneServiceservice; /** * *@parampageQuery *@return *@throwsParseException *@throwsIOException */ @PostMapping("/searchProduct") privateResultBean>searchProduct(@RequestBodyPageQuery pageQuery)throwsIOException,ParseException{ PageQuery pageResult=service.searchProduct(pageQuery); returnResultUtil.success(pageResult); } } publicclassResultUtil { publicstatic ResultBean success(Tt){ ResultEnumsuccessEnum=ResultEnum.SUCCESS; returnnewResultBean (successEnum.getCode(),successEnum.getMsg(),t); } publicstatic ResultBean success(){ returnsuccess(null); } publicstatic ResultBean error(ResultEnumEnum){ ResultBean result=newResultBean (); result.setCode(Enum.getCode()); result.setMsg(Enum.getMsg()); result.setData(null); returnresult; } } publicclassResultBean implementsSerializable{ privatestaticfinallongserialVersionUID=1L; /** *返回code */ privateintcode; /** *返回message */ privateStringmsg; /** *返回值 */ privateTdata; ... publicenumResultEnum{ UNKNOW_ERROR(-1,"未知错误"), SUCCESS(0,"成功"), PASSWORD_ERROR(10001,"用户名或密码错误"), PARAMETER_ERROR(10002,"参数错误"); /** *返回code */ privateIntegercode; /** *返回message */ privateStringmsg; ResultEnum(Integercode,Stringmsg){ this.code=code; this.msg=msg; } 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。