摘要:我們一般通過管理,因?yàn)槿绻跏蓟臅r(shí)候加載了索引文件夾,那么后面添加刪除修改的索引都不能通過查出來,因?yàn)樗鼪]有與索引庫實(shí)時(shí)同步,只是第一次有加載。
SpringBoot+Lucene案例介紹
GitHub倉庫:https://github.com/yizuoliang...
一、案例介紹
模擬一個(gè)商品的站內(nèi)搜索系統(tǒng)(類似淘寶的站內(nèi)搜索);
商品詳情保存在mysql數(shù)據(jù)庫的product表中,使用mybatis框架;
站內(nèi)查詢使用Lucene創(chuàng)建索引,進(jìn)行全文檢索;
增、刪、改,商品需要對Lucene索引修改,搜索也要達(dá)到近實(shí)時(shí)的效果。
對于數(shù)據(jù)庫的操作和配置就不在本文中體現(xiàn),主要講解與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類需要知道的幾點(diǎn):
1.實(shí)例化 IndexWriter,IndexSearcher 都需要去加載索引文件夾,實(shí)例化是是非常消耗資源的,所以我們希望只實(shí)例化一次交給spring管理。
2.IndexSearcher 我們一般通過SearcherManager管理,因?yàn)镮ndexSearcher 如果初始化的時(shí)候加載了索引文件夾,那么
后面添加、刪除、修改的索引都不能通過IndexSearcher 查出來,因?yàn)樗鼪]有與索引庫實(shí)時(shí)同步,只是第一次有加載。
3.ControlledRealTimeReopenThread創(chuàng)建一個(gè)守護(hù)線程,如果沒有主線程這個(gè)也會消失,這個(gè)線程作用就是定期更新讓SearchManager管理的search能獲得最新的索引庫,下面是每25S執(zhí)行一次。
5.要注意引入的lucene版本,不同的版本用法也不同,許多api都有改變。
@Configuration public class LuceneConfig { /** * lucene索引,存放位置 */ private static final String LUCENEINDEXPATH="lucene/indexDir/"; /** * 創(chuàng)建一個(gè) Analyzer 實(shí)例 * * @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()) { //如果文件夾不存在,則創(chuàng)建 file.mkdirs(); } return FSDirectory.open(path); } /** * 創(chuàng)建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; } }
四、創(chuàng)建需要的Bean類
創(chuàng)建商品Bean
/** * 商品bean類 * @author yizl * */ public class Product { /** * 商品id */ private int id; /** * 商品名稱 */ private String name; /** * 商品類型 */ private String category; /** * 商品價(jià)格 */ private float price; /** * 商品產(chǎn)地 */ private String place; /** * 商品條形碼 */ private String code; ......
創(chuàng)建一個(gè)帶參數(shù)查詢分頁通用類PageQuery類
/** * 帶參數(shù)查詢分頁類 * @author yizl * * @param*/ public class PageQuery { private PageInfo pageInfo; /** * 排序字段 */ private Sort sort; /** * 查詢參數(shù)類 */ private T params; /** * 返回結(jié)果集 */ private List results; /** * 不在T類中的參數(shù) */ private Map queryParam; ......
五、創(chuàng)建索引庫
1.項(xiàng)目啟動(dòng)后執(zhí)行同步數(shù)據(jù)庫方法
項(xiàng)目啟動(dòng)后,更新索引庫中所有的索引。
/** * 項(xiàng)目啟動(dòng)后,立即執(zhí)行 * @author yizl * */ @Component @Order(value = 1) public class ProductRunner implements ApplicationRunner { @Autowired private ILuceneService service; @Override public void run(ApplicationArguments arg0) throws Exception { /** * 啟動(dòng)后將同步Product表,并創(chuàng)建index */ service.synProductCreatIndex(); } }
2.從數(shù)據(jù)庫中查詢出所有的商品
從數(shù)據(jù)庫中查找出所有的商品
@Override public void synProductCreatIndex() throws IOException { // 獲取所有的productList ListallProduct = mapper.getAllProduct(); // 再插入productList luceneDao.createProductIndex(allProduct); }
3.創(chuàng)建這些商品的索引
把List中的商品創(chuàng)建索引
我們知道,mysql對每個(gè)字段都定義了字段類型,然后根據(jù)類型保存相應(yīng)的值。
那么lucene的存儲對象是以document為存儲單元,對象中相關(guān)的屬性值則存放到Field(域)中;
Field類的常用類型
Field類 | 數(shù)據(jù)類型 | 是否分詞 | index是否索引 | Stored是否存儲 | 說明 |
---|---|---|---|---|---|
StringField | 字符串 | N | Y | Y/N | 構(gòu)建一個(gè)字符串的Field,但不會進(jìn)行分詞,將整串字符串存入索引中,適合存儲固定(id,身份證號,訂單號等) |
FloatPoint LongPoint DoublePoint |
數(shù)值型 | Y | Y | N | 這個(gè)Field用來構(gòu)建一個(gè)float數(shù)字型Field,進(jìn)行分詞和索引,比如(價(jià)格) |
StoredField | 重載方法,,支持多種類型 | N | N | Y | 這個(gè)Field用來構(gòu)建不同類型Field,不分析,不索引,但要Field存儲在文檔中 |
TextField | 字符串或者流 | Y | Y | Y/N | 一般此對字段需要進(jìn)行檢索查詢 |
上面是一些常用的數(shù)據(jù)類型, 6.0后的版本,數(shù)值型建立索引的字段都更改為Point結(jié)尾,F(xiàn)loatPoint,LongPoint,DoublePoint等,對于浮點(diǎn)型的docvalue是對應(yīng)的DocValuesField,整型為NumericDocValuesField,F(xiàn)loatDocValuesField等都為NumericDocValuesField的實(shí)現(xiàn)類。
commit()的用法
commit()方法,indexWriter.addDocuments(docs);只是將文檔放在內(nèi)存中,并沒有放入索引庫,沒有commit()的文檔,我從索引庫中是查詢不出來的;
許多博客代碼中,都沒有進(jìn)行commit(),但仍然能查出來,因?yàn)槊看尾迦?他都把IndexWriter關(guān)閉.close(),Lucene關(guān)閉前,都會把在內(nèi)存的文檔,提交到索引庫中,索引能查出來,在spring中IndexWriter是單例的,不關(guān)閉,所以每次對索引都更改時(shí),都需要進(jìn)行commit()操作;
這樣設(shè)計(jì)的目的,和數(shù)據(jù)庫的事務(wù)類似,可以進(jìn)行回滾,調(diào)用rollback()方法進(jìn)行回滾。
@Autowired private IndexWriter indexWriter; @Override public void createProductIndex(ListproductList) 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(); }
六、多條件查詢
按條件查詢,分頁查詢都在下面代碼中體現(xiàn)出來了,有什么不明白的可以多帶帶查詢資料,下面的匹配查詢已經(jīng)比較復(fù)雜了.
searcherManager.maybeRefresh()方法,刷新searcherManager中的searcher,獲取到最新的IndexSearcher。
@Autowired private Analyzer analyzer; @Autowired private SearcherManager searcherManager; @Override public PageQuerysearchProduct(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(); // 排序規(guī)則 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) { // 輸入空格,不進(jìn)行模糊查詢 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) { // 價(jià)格范圍查詢 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(); }
八、補(bǔ)全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, "未知錯(cuò)誤"), SUCCESS(0, "成功"), PASSWORD_ERROR(10001, "用戶名或密碼錯(cuò)誤"), PARAMETER_ERROR(10002, "參數(shù)錯(cuò)誤"); /** * 返回code */ private Integer code; /** * 返回message */ private String msg; ResultEnum(Integer code, String msg) { this.code = code; this.msg = msg; }
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/75016.html
摘要:前言由于寫的文章已經(jīng)是有點(diǎn)多了,為了自己和大家的檢索方便,于是我就做了這么一個(gè)博客導(dǎo)航。 前言 由于寫的文章已經(jīng)是有點(diǎn)多了,為了自己和大家的檢索方便,于是我就做了這么一個(gè)博客導(dǎo)航。 由于更新比較頻繁,因此隔一段時(shí)間才會更新目錄導(dǎo)航哦~想要獲取最新原創(chuàng)的技術(shù)文章歡迎關(guān)注我的公眾號:Java3y Java3y文章目錄導(dǎo)航 Java基礎(chǔ) 泛型就這么簡單 注解就這么簡單 Druid數(shù)據(jù)庫連接池...
摘要:創(chuàng)建用來對查詢語句進(jìn)行詞法分析和語言處理。調(diào)用對查詢語法樹進(jìn)行搜索,得到結(jié)果。代碼中用到了分詞器,是第三方實(shí)現(xiàn)的分詞器,繼承自的類,針對中文文本進(jìn)行處理的分詞器。 Lucene介紹與應(yīng)用 GitHub地址:https://github.com/yizuoliang... 一、全文檢索介紹 1.數(shù)據(jù)結(jié)構(gòu) 結(jié)構(gòu)化數(shù)據(jù): 指具有固定格式 或限定長度的數(shù)據(jù); 例如:數(shù)據(jù)庫中的數(shù)據(jù)、元數(shù)據(jù)…...
摘要:全文檢索概述數(shù)據(jù)分類結(jié)構(gòu)化數(shù)據(jù)具有固定格式或者長度有限的數(shù)據(jù),例如數(shù)據(jù)庫中的表。語句非結(jié)構(gòu)化數(shù)據(jù)與結(jié)構(gòu)化數(shù)據(jù)對立,例如郵件網(wǎng)頁文檔。 全文檢索概述 數(shù)據(jù)分類 結(jié)構(gòu)化數(shù)據(jù):具有固定格式或者長度有限的數(shù)據(jù),例如數(shù)據(jù)庫中的表。【SQL語句】 非結(jié)構(gòu)化數(shù)據(jù):與結(jié)構(gòu)化數(shù)據(jù)對立,例如:郵件、網(wǎng)頁、word文檔?!緮?shù)據(jù)掃描、全文檢索】 半結(jié)構(gòu)化數(shù)據(jù):介于兩者之間,例如xml或者json格式的數(shù)據(jù)。...
摘要:概述是的一個(gè)頂級開源項(xiàng)目,采用開發(fā),它是基于的全文搜索服務(wù)器。提供了比更為豐富的查詢語言,同時(shí)實(shí)現(xiàn)了可配置可擴(kuò)展,并對索引搜索性能進(jìn)行了優(yōu)化。搜索只需要發(fā)送請求,然后對返回等格式的查詢結(jié)果進(jìn)行解析,組織頁面布局。 Solr概述 Solr 是Apache的一個(gè)頂級開源項(xiàng)目,采用Java開發(fā),它是基于Lucene的全文搜索服務(wù)器。Solr提供了比Lucene更為豐富的查詢語言,同時(shí)實(shí)現(xiàn)了可...
閱讀 1252·2019-08-30 15:55
閱讀 995·2019-08-30 15:55
閱讀 2208·2019-08-30 15:44
閱讀 2976·2019-08-29 14:17
閱讀 1182·2019-08-29 12:45
閱讀 3352·2019-08-26 10:48
閱讀 3179·2019-08-23 18:18
閱讀 2657·2019-08-23 16:47