SpringBoot+Lucene案例介紹mysql
GitHub倉庫:https://github.com/yizuoliang...git
1、案例介紹github
對於數據庫的操做和配置就不在本文中體現,主要講解與Lucene的整合。spring
1、引入lucene的依賴sql
向pom文件中引入依賴數據庫
<!--核心包--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>7.6.0</version> </dependency> <!--對分詞索引查詢解析--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>7.6.0</version> </dependency> <!--通常分詞器,適用於英文分詞--> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>7.6.0</version> </dependency> <!--檢索關鍵字高亮顯示 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-highlighter</artifactId> <version>7.6.0</version> </dependency> <!-- smartcn中文分詞器 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-smartcn</artifactId> <version>7.6.0</version> </dependency>
3、配置初始化Bean類apache
初始化bean類須要知道的幾點:api
1.實例化 IndexWriter,IndexSearcher 都須要去加載索引文件夾,實例化是是很是消耗資源的,因此咱們但願只實例化一次交給spring管理。mybatis
2.IndexSearcher 咱們通常經過SearcherManager管理,由於IndexSearcher 若是初始化的時候加載了索引文件夾,那麼app
後面添加、刪除、修改的索引都不能經過IndexSearcher 查出來,由於它沒有與索引庫實時同步,只是第一次有加載。
3.ControlledRealTimeReopenThread建立一個守護線程,若是沒有主線程這個也會消失,這個線程做用就是按期更新讓SearchManager管理的search能得到最新的索引庫,下面是每25S執行一次。
5.要注意引入的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; } }
4、建立須要的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 <T> */ public class PageQuery<T> { private PageInfo pageInfo; /** * 排序字段 */ private Sort sort; /** * 查詢參數類 */ private T params; /** * 返回結果集 */ private List<T> results; /** * 不在T類中的參數 */ private Map<String, String> queryParam; ......
5、建立索引庫
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<Product> 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<Product> productList) throws IOException { List<Document> docs = new ArrayList<Document>(); 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(); }
6、多條件查詢
按條件查詢,分頁查詢都在下面代碼中體現出來了,有什麼不明白的能夠單獨查詢資料,下面的匹配查詢已經比較複雜了.
searcherManager.maybeRefresh()方法,刷新searcherManager中的searcher,獲取到最新的IndexSearcher。
@Autowired private Analyzer analyzer; @Autowired private SearcherManager searcherManager; @Override public PageQuery<Product> searchProduct(PageQuery<Product> pageQuery) throws IOException, ParseException { searcherManager.maybeRefresh(); IndexSearcher indexSearcher = searcherManager.acquire(); Product params = pageQuery.getParams(); Map<String, String> 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<Product> pList = new ArrayList<Product>(); 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; }
7、刪除更新索引
@Override public void deleteProductIndexById(String id) throws IOException { indexWriter.deleteDocuments(new Term("id",id)); indexWriter.commit(); }
8、補全Spring中剩餘代碼
Controller層
@RestController @RequestMapping("/product/search") public class ProductSearchController { @Autowired private ILuceneService service; /** * * @param pageQuery * @return * @throws ParseException * @throws IOException */ @PostMapping("/searchProduct") private ResultBean<PageQuery<Product>> searchProduct(@RequestBody PageQuery<Product> pageQuery) throws IOException, ParseException { PageQuery<Product> pageResult= service.searchProduct(pageQuery); return ResultUtil.success(pageResult); } } public class ResultUtil<T> { public static <T> ResultBean<T> success(T t){ ResultEnum successEnum = ResultEnum.SUCCESS; return new ResultBean<T>(successEnum.getCode(),successEnum.getMsg(),t); } public static <T> ResultBean<T> success(){ return success(null); } public static <T> ResultBean<T> error(ResultEnum Enum){ ResultBean<T> result = new ResultBean<T>(); result.setCode(Enum.getCode()); result.setMsg(Enum.getMsg()); result.setData(null); return result; } } public class ResultBean<T> 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; }