SpringBoot+Lucene案例介紹

SpringBoot+Lucene案例介紹mysql

GitHub倉庫:https://github.com/yizuoliang...git

1、案例介紹github

  • 模擬一個商品的站內搜索系統(相似淘寶的站內搜索);
  • 商品詳情保存在mysql數據庫的product表中,使用mybatis框架;
  • 站內查詢使用Lucene建立索引,進行全文檢索;
  • 增、刪、改,商品須要對Lucene索引修改,搜索也要達到近實時的效果。

對於數據庫的操做和配置就不在本文中體現,主要講解與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;
    }
相關文章
相關標籤/搜索