详解SpringBoot+Lucene案例介绍

一、案例介绍

  1. 模拟一个商品的站内搜索系统(类似淘宝的站内搜索);
  2. 商品详情保存在mysql数据库的product表中,使用mybatis框架;
  3. 站内查询使用Lucene创建索引,进行全文检索;
  4. 增、删、改,商品需要对Lucene索引修改,搜索也要达到近实时的效果。

对于数据库的操作和配置就不在本文中体现,主要讲解与Lucene的整合。

二、引入lucene的依赖

向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>

三、配置初始化Bean类

初始化bean类需要知道的几点:

1.实例化 IndexWriter,IndexSearcher 都需要去加载索引文件夹,实例化是是非常消耗资源的,所以我们希望只实例化一次交给spring管理。

2.IndexSearcher 我们一般通过SearcherManager管理,因为IndexSearcher 如果初始化的时候加载了索引文件夹,那么

后面添加、删除、修改的索引都不能通过IndexSearcher 查出来,因为它没有与索引库实时同步,只是第一次有加载。

3.ControlledRealTimeReopenThread创建一个守护线程,如果没有主线程这个也会消失,这个线程作用就是定期更新让SearchManager管理的search能获得最新的索引库,下面是每25S执行一次。

4.要注意引入的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;
  }
}

四、创建需要的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;

  ......

五、创建索引库

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();
  }

六、多条件查询

按条件查询,分页查询都在下面代码中体现出来了,有什么不明白的可以单独查询资料,下面的匹配查询已经比较复杂了.

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;
  }

七、删除更新索引

  @Override
  public void deleteProductIndexById(String id) throws IOException {
    indexWriter.deleteDocuments(new Term("id",id));
    indexWriter.commit();
  }

八、补全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;
  }

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解Spring Boot 中使用 Java API 调用 lucene

    Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言).Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎 全文检索概述 比如,我们一个文件夹中,或者一个磁盘中有很多的文件,记事本.world.Excel.pdf,我们

  • 详解SpringBoot+Lucene案例介绍

    一.案例介绍 模拟一个商品的站内搜索系统(类似淘宝的站内搜索): 商品详情保存在mysql数据库的product表中,使用mybatis框架: 站内查询使用Lucene创建索引,进行全文检索: 增.删.改,商品需要对Lucene索引修改,搜索也要达到近实时的效果. 对于数据库的操作和配置就不在本文中体现,主要讲解与Lucene的整合. 二.引入lucene的依赖 向pom文件中引入依赖 <!--核心包--> <dependency> <groupId>org.apach

  • 详解SpringBoot开发案例之整合Dubbo分布式服务

    前言 在 SpringBoot 很火热的时候,阿里巴巴的分布式框架 Dubbo 不知是处于什么考虑,在停更N年之后终于进行维护了.在之前的微服务中,使用的是当当维护的版本 Dubbox,整合方式也是使用的 xml 配置方式. 改造前 之前在 SpringBoot 中使用 Dubbox是这样的.先简单记录下版本,Dubbox-2.8.4.zkclient-0.6.zookeeper-3.4.6. 项目中引入 spring-context-dubbo.xml 配置文件如下: <?xml versio

  • 详解SpringBoot开发案例之整合定时任务(Scheduled)

    来来来小伙伴们,基于上篇的邮件服务,定时任务就不单独分项目了,天然整合进了邮件服务中. 不知道,大家在工作之中,经常会用到那些定时任务去执行特定的业务,这里列举一下我在工作中曾经使用到的几种实现. 任务介绍 Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务.Timer的优点在于简单易用:缺点是Timer的所有任务都是由同一个线程调度的,因此所有任务都是串行执行的.同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任

  • 详解SpringBoot集成消息队列的案例应用

    目录 背景 方案规划 统一设计 集成Redis消息队列 集成ActiveMQ消息队列 使用示例 背景 最近在对公司开发框架进行优化,框架内涉及到多处入库的日志记录,例如登录日志/操作日志/访问日志/业务执行日志,集成在业务代码中耦合度较高且占用业务操作执行时间,所以准备集成相关消息队列进行代码解耦 方案规划 现有的成熟消息队列组件非常多,例如RabbitMQ,ActiveMQ,Kafka等,考虑到业务并发量不高且框架已经应用于多个项目平稳运行,准备提供基于Redis的消息队列和集成ActiveM

  • 详解SpringBoot基于Dubbo和Seata的分布式事务解决方案

    1. 分布式事务初探 一般来说,目前市面上的数据库都支持本地事务,也就是在你的应用程序中,在一个数据库连接下的操作,可以很容易的实现事务的操作. 但是目前,基于SOA的思想,大部分项目都采用微服务架构后,就会出现了跨服务间的事务需求,这就称为分布式事务. 本文假设你已经了解了事务的运行机制,如果你不了解事务,那么我建议先去看下事务相关的文章,再来阅读本文. 1.1 什么是分布式事务 对于传统的单体应用而言,实现本地事务可以依赖Spring的@Transactional注解标识方法,实现事务非常简

  • 详解SpringBoot之添加单元测试

    本文介绍了详解SpringBoot之添加单元测试,分享给大家,希望此文章对各位有所帮助 在SpringBoot里添加单元测试是非常简单的一件事,我们只需要添加SpringBoot单元测试的依赖jar,然后再添加两个注解就可搞定了. 首先我们来添加单元测试所需要的jar <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test<

  • 详解Springboot配置文件的使用

    如果使用IDEA创建Springboot项目,默认会在resource目录下创建application.properties文件,在springboot项目中,也可以使用yml类型的配置文件代替properties文件 一.单个的获取配置文件中的内容 在字段上使用@Value("${配置文件中的key}")的方式获取单个的内容 1.在resource目录下创建application.yml文件,并添加一些配置,在yml文件中,key:后面需要添加一个空格,然后是value值,假设配置如

  • 详解Springboot整合ActiveMQ(Queue和Topic两种模式)

    写在前面: 从2018年底开始学习SpringBoot,也用SpringBoot写过一些项目.这里对学习Springboot的一些知识总结记录一下.如果你也在学习SpringBoot,可以关注我,一起学习,一起进步. ActiveMQ简介 1.ActiveMQ简介 Apache ActiveMQ是Apache软件基金会所研发的开放源代码消息中间件:由于ActiveMQ是一个纯Java程序,因此只需要操作系统支持Java虚拟机,ActiveMQ便可执行. 2.ActiveMQ下载 下载地址:htt

  • 详解springboot+aop+Lua分布式限流的最佳实践

    一.什么是限流?为什么要限流? 不知道大家有没有做过帝都的地铁,就是进地铁站都要排队的那种,为什么要这样摆长龙转圈圈?答案就是为了限流!因为一趟地铁的运力是有限的,一下挤进去太多人会造成站台的拥挤.列车的超载,存在一定的安全隐患.同理,我们的程序也是一样,它处理请求的能力也是有限的,一旦请求多到超出它的处理极限就会崩溃.为了不出现最坏的崩溃情况,只能耽误一下大家进站的时间. 限流是保证系统高可用的重要手段!!! 由于互联网公司的流量巨大,系统上线会做一个流量峰值的评估,尤其是像各种秒杀促销活动,

  • 详解springboot启动时是如何加载配置文件application.yml文件

    今天启动springboot时,明明在resources目录下面配置了application.yml的文件,但是却读不出来,无奈看了下源码,总结一下springboot查找配置文件路径的过程,能力有限,欢迎各位大牛指导!!! spring加载配置文件是通过listener监视器实现的,在springboot启动时: 在容器启动完成后会广播一个SpringApplicationEvent事件,而SpringApplicationEvent事件是继承自ApplicationEvent时间的,代码如下

随机推荐