詳解SpringBoot+Lucene案例介紹
一、案例介紹
- 模擬一個(gè)商品的站內(nèi)搜索系統(tǒng)(類似淘寶的站內(nèi)搜索);
- 商品詳情保存在mysql數(shù)據(jù)庫的product表中,使用mybatis框架;
- 站內(nèi)查詢使用Lucene創(chuàng)建索引,進(jìn)行全文檢索;
- 增、刪、改,商品需要對(duì)Lucene索引修改,搜索也要達(dá)到近實(shí)時(shí)的效果。
對(duì)于數(shù)據(jù)庫的操作和配置就不在本文中體現(xiàn),主要講解與Lucene的整合。
二、引入lucene的依賴
向pom文件中引入依賴
<!--核心包-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.6.0</version>
</dependency>
<!--對(duì)分詞索引查詢解析-->
<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>
<!--檢索關(guān)鍵字高亮顯示 -->
<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>
三、配置初始化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è)也會(huì)消失,這個(gè)線程作用就是定期更新讓SearchManager管理的search能獲得最新的索引庫,下面是每25S執(zhí)行一次。
4.要注意引入的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 <T>
*/
public class PageQuery<T> {
private PageInfo pageInfo;
/**
* 排序字段
*/
private Sort sort;
/**
* 查詢參數(shù)類
*/
private T params;
/**
* 返回結(jié)果集
*/
private List<T> results;
/**
* 不在T類中的參數(shù)
*/
private Map<String, String> 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
List<Product> allProduct = mapper.getAllProduct();
// 再插入productList
luceneDao.createProductIndex(allProduct);
}
3.創(chuàng)建這些商品的索引
把List中的商品創(chuàng)建索引
我們知道,mysql對(duì)每個(gè)字段都定義了字段類型,然后根據(jù)類型保存相應(yīng)的值。
那么lucene的存儲(chǔ)對(duì)象是以document為存儲(chǔ)單元,對(duì)象中相關(guān)的屬性值則存放到Field(域)中;
Field類的常用類型
| Field類 | 數(shù)據(jù)類型 | 是否分詞 | index是否索引 | Stored是否存儲(chǔ) | 說明 |
|---|---|---|---|---|---|
| StringField | 字符串 | N | Y | Y/N | 構(gòu)建一個(gè)字符串的Field,但不會(huì)進(jìn)行分詞,將整串字符串存入索引中,適合存儲(chǔ)固定(id,身份證號(hào),訂單號(hào)等) |
| 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存儲(chǔ)在文檔中 |
| TextField | 字符串或者流 | Y | Y | Y/N | 一般此對(duì)字段需要進(jìn)行檢索查詢 |
上面是一些常用的數(shù)據(jù)類型, 6.0后的版本,數(shù)值型建立索引的字段都更改為Point結(jié)尾,F(xiàn)loatPoint,LongPoint,DoublePoint等,對(duì)于浮點(diǎn)型的docvalue是對(duì)應(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)閉前,都會(huì)把在內(nèi)存的文檔,提交到索引庫中,索引能查出來,在spring中IndexWriter是單例的,不關(guān)閉,所以每次對(duì)索引都更改時(shí),都需要進(jìn)行commit()操作;
這樣設(shè)計(jì)的目的,和數(shù)據(jù)庫的事務(wù)類似,可以進(jìn)行回滾,調(diào)用rollback()方法進(jìn)行回滾。
@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));
// 存儲(chǔ)到索引庫
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)出來了,有什么不明白的可以單獨(dú)查詢資料,下面的匹配查詢已經(jīng)比較復(fù)雜了.
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();
// 排序規(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<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;
}
七、刪除更新索引
@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<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, "未知錯(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;
}
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Spring數(shù)據(jù)庫多數(shù)據(jù)源路由配置過程圖解
這篇文章主要介紹了Spring數(shù)據(jù)庫多數(shù)據(jù)源路由配置過程圖解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
詳解Spring Boot中使用AOP統(tǒng)一處理Web請(qǐng)求日志
本篇文章主要介紹了詳解Spring Boot中使用AOP統(tǒng)一處理Web請(qǐng)求日志,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
Java數(shù)據(jù)庫連接池之DBCP淺析_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java數(shù)據(jù)庫連接池之DBCP的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
記一次springboot服務(wù)凌晨無故宕機(jī)問題的解決
這篇文章主要介紹了記一次springboot服務(wù)凌晨無故宕機(jī)問題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
詳解Mybatis攔截器安全加解密MySQL數(shù)據(jù)實(shí)戰(zhàn)
本文主要介紹了Mybatis攔截器安全加解密MySQL數(shù)據(jù)實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01

