java项目_Java项目笔记之旅游点评项目总结04

不点蓝字,我们哪来故事?

Redis操作流程:

  1. 预热——初始化:以前在操作Redis中的数据之前,需要判断数据是否存在,存在获取,不存在创建一个(即准备数据)。实际中尽量避免,即操作之前就先将数据准备好;

  2. 缓存的逻辑操作:对缓存中的数据做CRUD等操作;

  3. 缓存数据的持久化:缓存中的数据被操作之后,数据库中的数据要不要和Redis被写的数据保持同步;读操作不会改变,不需要持久化;

初始化(预热):
凡是用到Redis的都要思考上面的三个步骤

思考:使用Redis都要想清楚的问题:

  1. 需要初始化什么数据

  • 需要频繁改动的数据(DML)[ 此处需要将统计相关的vo数据初始化到Redis中 ]

  • 不需要频繁改动的数据(DQL)

  1. 在哪一个项目执行初始化逻辑?(mgrsite、website-api)

  • website-api :接口对外暴露,安全性不可保证;website-api后续拓展可能做集群操作,那样就会导致多次执行了初始化操作;

  • 数据初始化数据数据管理范畴,你应该使用mgrsite来管理;

在JavaWeb中的哪一个组件中实现初始化逻辑?(filter、servlet、intercept、controller、listener)

  • javaweb监听器:监听web(Tomcat)容器的启动,功能简单;

  • spring容器的监听器:监听的是spring容器的启动,功能强大,操作简单;

  • 监听器可以实现一次初始化,后续只有不启动项目,就不会再初始化;

  • 在项目启动后,就应该将数据都准备好,让用户访问更加顺畅,监听器的上帝视角可以完美实现;

  • 初始化逻辑的特点:一次初始化即可;filter和intercept属于拦截过滤范畴,每次都拦截初始化,这样不符合;servlet、controller发起请求的方式初始化,不安全,可以通过代码逻辑控制,这样不适合;最优的选择:listener,容器启动一次,我就初始化,不关闭容器就不会再执行初始化操作;

  • 初始化选择listener

  • 选谁的监听器

Spring 中的5种标准的事件

1.上下文更新事件(ContextRefreshedEvent):该事件会在ApplicationContext被初始化或者更新时发布。也可以在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。

2.上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。

3.上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。

4.上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。

5.请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。

初始化设计

当容器准备好之后马上执行,使用上下文更新事件(ContextRefreshedEvent)

自定义Redis数据初始化监听器:

需要初始化的数据:点赞数、回复数、收藏数、分享数、评论数;

思考:如果要初始化的数据在Redis中已经存在了,是否需要再初始化一次?

如果vo在Redis中已经存在了,初始化的时候必须跳过;

原因:Redis中存了写的操作,还没有持久化到mongodb中,此时如果再初始化,name数据vo就会被覆盖了,原来的写操作修改了的数据就会丢失。所以,已经存在的数据必须跳过而不是覆盖;

如何实现:

在初始化操作的时候,判断vo是否已经存在,存在就跳过;如果key存在,说明vo已经存在了;

 /**  * redis数据初始化的监听器(为什么选择监听器?为什么要在mgr项目中创建初始化监听器)  * spring容器准备好之后,马上将需要初始化的数据准备好  * 将mongodb数据vo加载到redis中,用户访问更流畅  */ //@Component  //需要交给spring管理 public class RedisDataInitListener implements ApplicationListener{      @Autowired     private IStrategyService strategyService;      @Autowired     private IStrategyStatiesVORedisService strategyStatiesVORedisService;       //初始化逻辑:spring容器创建完成(aop、ioc、di)之后立即执行初始化的逻辑     @Override     public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {         System.out.println("++++++++++初始化开始++++++++++");          //需要初始化的数据:vo中的统计数据(阅读量、点赞、分享、评论、顶数等)         //1.从mongodb中将数据查出来         Listlist = strategyService.list();          //2.将查到的vo数据保存到Redis中完成初始化         for (Strategy strategy : list) {             StrategyStatisVO vo = new StrategyStatisVO();             BeanUtils.copyProperties(strategy, vo);             vo.setStrategyId(strategy.getId());              //必须的判断:如果vo在Redis中已经存在,那么我们初始化化的时候必须跳过,而不能覆盖(防止数据丢失)             if (strategyStatiesVORedisService.isVoExist(strategy.getId())){                 //如果已经存在,就跳过                 continue;             }              //将vo添加进Redis中             strategyStatiesVORedisService.setStrategyStatisVo(vo);         }                  System.out.priln("++++++++++初始化结束++++++++++");     } }

注意

之前的if判断创没创建的逻辑不能删,因为,如果数据量很大的时候,不是所有的数据都拿来初始化存起来,只初始化活跃的数据(即初始化还要条件筛选),此时if逻辑就起到排除漏网之鱼的作用了;

持久化
问题
  1. 为什么要持久化?

    缓存数据发生变动,如果不进行初始化,DML操作都白做了嘛,所以要持久化到数据库中保存;

  2. 需要持久化什么数据?

    缓存中有用的数据(被改变并且要保存防止丢失),此处需要初始化的数据:a、vo数据;b、用户攻略收藏数据 [ 拓展 ]

  3. 在哪一个项目执行持久化逻辑(mgrsite、website-api)?

    原则上两个都不用,真实项目开发中会额外开发一个项目:定时任务项目,统一执行有定时要求的逻辑;此处选择用mgrsite,因持久化是数据管理范畴,api后续开发会拓展使用集群,若在website-api上定时任务就可能会重复执行了。

  4. 在javaweb的哪一个组件实现持久化逻辑(filter、servlet、intercept、controller、listener)?

  • 持久化特点:多次执行,人工干预请求进行持久化,程序周期性执行(定时器);

  • 此时上面的组件都不合适了,servlet。controller勉强可以,但是我们不希望人工干预执行,不可靠;

  • 持久化逻辑的最佳实践:使用定时器周期性执行(每隔一天执行一次);

SpringBoot定时任务处理:
  • Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。

  • ScheduledExecutorService:也jdk自带的一个类;是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。

  • Spring Task:Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。

  • Quartz [ 拓展 ]:这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂。

实现定时任务处理:
  • @Component:给spring管理;

  • @Scheduled(cron = "0/20 * * * * *"):定时任务,方法的执行周期;

  • @EnableScheduling:在主配置类上使用@EnableScheduling注解开启对定时任务的支持

  /**  * 将Redis中的数据 定时 的持久化到数据库中  */ @Component public class RedisDataPersistenceJob {      @Autowired     private IStrategyStatiesVORedisService strategyStatiesVORedisService;      @Autowired     private IStrategyService strategyService;       //定时任务标签:cron制定任务计划,什么时候执行该方法,周期是多少     @Scheduled(cron = "0/20 * * * * ? ")     public void doWork() {         System.out.println("__________vo持久化开始__________");         //1.从Redis中获取所有的攻略vo的数据         String pattern = RedisKeys.STRATEGY_STATIS_VO.join("*");//匹配所有前缀一样的vo统计数据         List list = strategyStatiesVORedisService.listStrategyVoByPattern(pattern);          //2.将获取到的vo数据,vo中的数据全部set给攻略对象,保存到mongodb中         for (StrategyStatisVO vo : list) {             strategyService.updateStrategy(vo);         }                  System.out.println("__________vo持久化结束__________");      }  }
执行时间的配置cron表达式

cron:通过表达式来配置任务执行时间

一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。按顺序依次为:

字段允许值允许的特殊字符
0~59, - * /
0~59, - * /
小时0~23, - * /
日期1~31, - * ? / L W C
月份1~12或者JAN~DEC, - * /
星期1~7或者SUN~SAT, - * ? / L C #
年(可选)留空,1970~2099, - * /

例子:对比着来,实在不行在线生成http://cron.qqe2.com/

1a0ffe99be5ba76cb730ea3f5ecb4b7f.png

定时持久化任务逻辑实现

1.获取所有的攻略vo数据(用前缀批量获取到指定前缀的key集合);

2ff07ced24819544ffa58e8bb0abc5d2.png

2.更新vo对应的攻略,改动攻略中的统计字段;

从Redis中获得所有攻略的vo:

将vo的list中的vo对象保存到mongodb中:

     //通过模板获取到匹配所有前缀一样的vo统计数据     @Override     public List listStrategyVoByPattern(String pattern) {         //拿到vo的key集合         //strategy_statis_vo:*         Set<String> keys = template.keys(pattern);         //遍历keys拿到每一条数据,将数据添加进list中         List list = new ArrayList<>();         if (keys != null) {//keys有东西才遍历             for (String voKey : keys) {                 String voStr = template.opsForValue().get(voKey);                 //转换成对象,在添加                 list.add(JSON.parseObject(voStr, StrategyStatisVO.class));             }         }         return list;     }

数据库中没有vo表,怎么更新vo,new 一个攻略对象把vo的属性封装进去,更新攻略表;

     //将Redis中的vo对象里的统计数据持久到mongodb中     @Override     public void updateStrategy(StrategyStatisVO vo) {         Query query = new Query();         //这里是为什么vo对象中定义StrategyId字段的原因         query.addCriteria(Criteria.where("_id").is(vo.getStrategyId()));          //vo是在Redis中的,MongoDB中是攻略,所以持久化就是将vo中对应攻略变化的数据保存起来         //vo中的统计数据要封装进攻略里才能保存进mongodb,         Strategy strategy = new Strategy();         BeanUtils.copyProperties(vo,strategy );          //更新攻略中设置好的统计数据         DBHelper.update(template, query, strategy,                 "viewnum","replynum","favornum","sharenum","thumbsupnum");     } 

为什么不能用vo直接update,而是用攻略?

51a209c9605afd3daf58a405b424154d.png

面试细节:定时任务的时间间隔,没有标准,根据Redis中的数据量来决定,如果数据量较大,时间周期会短一点(经验值)。一般都是一天/半天,凌晨服务器压力小的时候执行持久化同步;

网站首页:

首页,网站默认页面, 是数据整合页面

banner, 指需要在前端网站首页/显示的攻略文章或游记,后续配合爆款营销而设计模块。(可以很复杂,自己了解banner),与钱有关的业务都不简单;

banner推荐:

3615a28f8acb9703ef37319412e48961.png

首页封面类似banner的组件,使用推荐游记前4篇进行列表

需求:设计一个banner表展示

a78c960be706d6673ca3f7ee3f2f74be.png

设计banner表之后,做banner的CRUD操作,基本的数据结构;

后端数据的维护mgrsite
banner添加编辑

8632b2e4870e100ee0d48d2997010cd4.png

关键点:二级联动

后台实现:

     //前台需要准备的数据     @RequestMapping("/getAllType")     @ResponseBody     public Object getAllType() {          //1.查询攻略         List sts = strategyService.list();         //2.查询游记         List ts = travelService.list();          return JsonResult.success(new ParamMap()                 .put("sts", sts)                 .put("ts", ts)         );     } 
添加更新:

e636fa55bdfeba7bddeb1d69ad5880e2.png

     @Override     public void saveOrUpdate(Banner banner) {         if (!StringUtils.hasLength(banner.getId())) {             //保存             banner.setId(null);             repository.save(banner);         }else {             Query query = new Query();             query.addCriteria(Criteria.where("_id").is(banner.getId()));             //更新维护的字段             DBHelper.update(template, query, banner,                     "title","subTitle","coverUrl",                     "state","type","refId","sequence");         }         repository.save(banner);     }
website

banner中显示的是游记,需要在首页中将游记的文章查出来;

controller-api

     @GetMapping("/query")     public Object query(){         //首页banner游记前五推荐         //vue.banners = map.banners;         List banners = bannerService.queryBanner(Banner.TYPE_TRAVEL);          //攻略推荐,推荐一个         //vue.stBanner = map.stBanner;         Banner stBanner = bannerService.queryBanner(Banner.TYPE_STRATEGY).get(0);//拿第一个          //热门游记列表         //vue.page = map.page;         TravelQuery qo = new TravelQuery();         qo.setOrderType(2); //热门排序,默认是-1         Page page = travelService.query(qo);          return JsonResult.success(new ParamMap()                 .put("banners", banners)                 .put("stBanner", stBanner)                 .put("page", page)         );     }

但是我们要取前五个:

可以使用query方法来实现,或者直接使用Pageable

     //通过类型查banner集合     @Override     public List queryBanner(int type) {         Pageable of = PageRequest.of(0, 5, Sort.Direction.ASC, "sequence");         //查询 类型为游记 ,状态为正常的前5个         return repository.findByTypeAndState(type, Banner.STATE_NORMAL, of);     } 

攻略的推荐;

fc61a28e9eef008c81982698769e4938.png

攻略推荐一个:

        //攻略推荐,推荐一个        Banner stBanner = bannerService.queryBanner(Banner.TYPE_STRATEGY).get(0);//拿第一个

d4b25f5a5d162ed0deb36ef67245c3a0.png

游记热门排序:

        //热门游记列表        //vue.page = map.page;        TravelQuery qo = new TravelQuery();        qo.setOrderType(2); //热门排序,默认是-1        Page page = travelService.query(qo);

首页全文搜索:

elasticsearch:非关系型数据库。是一个接近实时的搜索平台

进入首页后,输入关键字, 选择不同搜索维度(默认是全部), 进入搜索页面

625242435958cf7aab4f7671a7b295c0.png

核心:怎么将mongodb中的数据添加到elasticsearch中,同步哪一些数据?

比如:搜索游记中title和summary中含有广州字样的游记,作为以广州为条件搜索的结果,首先要到mongodb中去把满足条件的数据找到显示出来。

  1. 从mongodb中同步条件列数据以及主键id到es中(推荐:因为内存资源宝贵,选择牺牲性能)

    先匹配es中条件列搜索满足条件的数据,得到主键id集合,然后以id集合作为条件去mongodb中对应的id数据集合,之后再页面显示;

  • 优点:节省内存空间(数据量小了);

  • 缺点:稍微有损性能(去两个数据库中查询了);

从mongodb中同步页面需要的所有数据(包括条件列数据)以及主键id,把数据都放到es中存起来

先匹配es中条件列搜索满足条件的数据,得到数据集合,直接在页面显示;

  • 优点:查询快(所有的数据都在es中了);

  • 缺点:内存空间消耗大(数据量大了);

关键字搜索, 也称之站内搜索, 系统暂时仅对攻略, 游记, 目的地, 用户进行关键字查询, 当然也支持全部查询。

1:关键词搜索

全部搜索, 会对目的地, 攻略, 游记, 用户对象(关键字段)进行全文搜索

目的地:名称(name), 简介(info)

攻略:标题(title), 副标题(subTitle), 概要(summary)

游记:标题(title), 概要(summary)

用户:简介(info), 城市(city)

查询到的关键词进行高亮显示

添加依赖:
        <dependency>        <groupId>org.springframework.bootgroupId>        <artifactId>spring-boot-starter-data-elasticsearchartifactId>    dependency>    <dependency>        <groupId>commons-beanutilsgroupId>        <artifactId>commons-beanutilsartifactId>        <version>1.9.3version>    dependency>

es的配置:

#elasticsearch 配置# elasticsearch集群名称,默认的是elasticsearchspring.data.elasticsearch.cluster-name=elasticsearch#节点的地址 注意api模式下端口号是9300,千万不要写成9200#集群时,用逗号隔开,es会自动寻找节点spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300#是否开启本地存储spring.data.elasticsearch.repositories.enable=true
es中数据的初始化:
目的地:

domain:

/** * 目的地搜索对象 */@Getter@Setter@Document(indexName="luowowo_destination",type="destination")public class DestinationEs implements Serializable {    public static final String INDEX_NAME = "luowowo_destination";    public static final String TYPE_NAME = "destination";    @Id    //@Field 每个文档的字段配置(类型、是否分词、是否存储、分词器 )    @Field(store=true, index = false,type = FieldType.Keyword)    private String id;  //攻略id    @Field(index=true,store=true,type = FieldType.Keyword)    private String name;    @Field(index=true,analyzer="ik_max_word",store=true,searchAnalyzer="ik_max_word",type = FieldType.Text)    private String info;}

repostory:

@Repositorypublic interface DestinationEsRepository extends ElasticsearchRepository<DestinationEs, String> {    /**     * 通过名字查询目的地     *     * @param name     */    DestinationEs findByName(String name);}

service:

攻略:
/** * 攻略搜索对象 */@Getter@Setter@Document(indexName = "luowowo_strategy", type = "strategy")public class StrategyEs implements Serializable {    public static final String INDEX_NAME = "luowowo_strategy";    public static final String TYPE_NAME = "strategy";    @Id    //@Field 每个文档的字段配置(类型、是否分词、是否存储、分词器 )    @Field(store = true, index = false, type = FieldType.Keyword)    private String id;  //攻略id    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String title;  //攻略标题    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String subTitle;    //攻略副标题    @Field(store = true, index = false, type = FieldType.Keyword)    private String destId;      //目的地Id    @Field(index = true, store = true, type = FieldType.Keyword)    private String destName;    //目的地名称    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String summary;     //概要}
游记:
/** * 游记搜索对象 */@Getter@Setter@Document(indexName = "luowowo_travel", type = "travel")public class TravelEs implements Serializable {    public static final String INDEX_NAME = "luowowo_travel";    public static final String TYPE_NAME = "travel";    //@Field 每个文档的字段配置(类型、是否分词、是否存储、分词器 )    @Id    @Field(store = true, index = false, type = FieldType.Keyword)    private String id;  //游记id    @Field(store = true, index = false, type = FieldType.Keyword)    private String destId;  //游记地点    @Field(index = true, store = true, type = FieldType.Keyword)    private String destName; //游记地点名称    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String title;  //游记标题    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String summary; //游记简介}
用户:
/** * 用户搜索对象 */@Getter@Setter@Document(indexName = "luowowo_userinfo", type = "userinfo")public class UserInfoEs implements Serializable {    public static final String INDEX_NAME = "luowowo_userinfo";    public static final String TYPE_NAME = "userinfo";    @Id    //@Field 每个文档的字段配置(类型、是否分词、是否存储、分词器 )    @Field(store = true, index = false, type = FieldType.Keyword)    private String id;  //用户id    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String nickname;    @Field(index = true, store = true, type = FieldType.Keyword)    private String city;    @Field(index = true, analyzer = "ik_max_word", store = true, searchAnalyzer = "ik_max_word", type = FieldType.Text)    private String info;}
初始化controller:
@RestControllerpublic class DataController {    //es相关服务    @Autowired    private IDestinationEsService destinationEsService;    @Autowired    private IStrategyEsService strategyEsService;    @Autowired    private ITravelEsService travelEsService;    @Autowired    private IUserInfoEsService userInfoEsService;    //mongodb相关服务    @Autowired    private IDestinationService destinationService;    @Autowired    private IStrategyService strategyService;    @Autowired    private ITravelService travelService;    @Autowired    private IUserInfoService userInfoService;    @GetMapping("/dataInit")    public Object dataInit() {        //攻略需要存到es中的数据初始化        List sts = strategyService.list();        for (Strategy st : sts) {            StrategyEs es = new StrategyEs();            BeanUtils.copyProperties(st, es);            strategyEsService.save(es);        }                //游记需要存到es中的数据初始化        List ts = travelService.list();        for (Travel t : ts) {            TravelEs es = new TravelEs();            BeanUtils.copyProperties(t, es);            travelEsService.save(es);        }        //用户需要存到es中的数据初始化        List uf = userInfoService.list();        for (UserInfo u : uf) {            UserInfoEs es = new UserInfoEs();            BeanUtils.copyProperties(u, es);            userInfoEsService.save(es);        }        //目的地需要存到es中的数据初始化        List dests = destinationService.list();        for (Destination d : dests) {            DestinationEs es = new DestinationEs();            BeanUtils.copyProperties(d, es);            destinationEsService.save(es);        }        return "ok";    }}

浏览器发起初始化的请求:

852853b471f9209afc4bd7a1ff4df199.png

记得初始化之后检查初始化的数据有没有问题:

d8b0a49e1136986cd5f934c5bad42e55.png

关键字搜索

注意:目的地是精确搜索,无高亮显示,找不到就找不到;其他的是全文搜索,关键字高亮显示;

目的地关键词搜索

目的查询:输入关键词是精确查询输入的地区, 如果找到, 显示该目的地下所有攻略, 游记, 用户。如果目的找不到, 不显示:

高查条件的封装,后面要用于分页,根据前台以int类型来区分集中不同的搜索目标来设计qo:

@Setter@Getterpublic class SearchQuery extends QueryObject {    public static final int SEARCH_TYPE_ALL = -1; //全部    public static final int SEARCH_TYPE_DEST = 0;  //目的地    public static final int SEARCH_TYPE_STRATEGY = 1; //攻略    public static final int SEARCH_TYPE_TRAVEL = 2; //游记    public static final int SEARCH_TYPE_USER = 3;  //用户        private int type = SEARCH_TYPE_ALL;    public Pageable getPageable() {        return PageRequest.of(super.getCurrentPage() - 1, super.getPageSize());    }}

所有的搜索请求都是同一个映射地址:

一个方法中完成不同的搜索目的,如何区分?答案是qo中的type,用它来完成请求的分发:

这样处理还有一个问题:不同的搜索目标类型,请求的返回数据是不一样的

如何处理:由分支的方法自己来处理;

    @GetMapping("/q")    public Object search(SearchQuery qo) throws Exception {                //解码:前台传过来的中文经过了编码处理        qo.setKeyword(URLDecoder.decode(qo.getKeyword(), "UTF-8"));        //根据qo的搜索条件里的类型,用switch将请求分开        switch (qo.getType()) {            case SearchQuery.SEARCH_TYPE_DEST:                //搜索目的地                return searchDest(qo);            case SearchQuery.SEARCH_TYPE_STRATEGY:                //搜索攻略                return searchStrategy(qo);            case SearchQuery.SEARCH_TYPE_TRAVEL:                //搜索游记                return searchTravel(qo);            case SearchQuery.SEARCH_TYPE_USER:                //搜索用户                return searchUser(qo);            default:                //搜索全部                return searchAll(qo);        }    }
目的地关键词搜索:

显然result是键值对的存在,使用map还是用对象(类似vo)封装,选择第二种;

设计封装数据的VO:

/** * 封装result中的数据 */@Setter@Getterpublic class SearchResultVO implements Serializable {    private Long total = 0L;    private List dests = new ArrayList<>();    private List travels = new ArrayList<>();    private List strategys = new ArrayList<>();    private List users = new ArrayList<>();}
    //目的地    private Object searchDest(SearchQuery qo){        //es中查询当前输入的keyword是不是一个目的地        //DestinationEs dest = destinationEsService.findByName(qo.getKeyword()); //信息不全查不到用户头像信息        Destination dest = destinationService.getByName(qo.getKeyword());        SearchResultVO result = new SearchResultVO();        if (dest != null) {            //查到目的地,就查询该目的地下所有的攻略,游记,用户            //result包含了:total  strategys  travels  users            //去哪里查询目的地的信息,由前台所需的数据决定,因为es中数据不全            List sts = strategyService.findByDestName(qo.getKeyword());            List ts = travelService.findByDestName(qo.getKeyword());            List us = userInfoService.findByCity(qo.getKeyword());            long total = sts.size() + ts.size() + us.size();            result.setTotal(total);            result.setStrategys(sts);            result.setTravels(ts);            result.setUsers(us);        }        //dest        //qo        return JsonResult.success(new ParamMap()                .put("qo", qo)                .put("result", result)                .put("dest", dest)        );    }
攻略全文搜索:

仅仅对攻略进行全文搜索

攻略:标题(title), 副标题(subTitle), 概要(summary)

67f78692b0fc0981c3fd90a23cfde249.png

高亮显示关键词的接口:

如何设计全文搜索的方法:这个方法中有重复的操作,怎么保证通用性呢?————使用泛型设计方法,方法的可变参数

/** * 所有 es 公共服务,全文搜索并高亮显示关键词 */public interface ISearchService {    /**     * 全文搜索 + 高亮显示     *     * @param index  索引     * @param type   类型     * @param clz    通过字节码对象告诉Page中的 T 到底是什么类型,传什么封装什么     * @param qo     高查条件(关键词等)都在qo中     * @param fields 字段:需要对哪些字段中的内容做关键词匹配,不同的需求字段不一样,可变参数可完美匹配     * @param      * @return 带有分页的全文搜索(高亮显示)结果集,返回的结果集用泛型来达到通用的目的     *      *  泛型方法的语法:申明泛型,让java不去解析 T 具体是什么类型,不加就报无法解析的错。     */     Page searchWithHighlight(String index, String type, Class<T> clz,                                    SearchQuery qo, String... fields);}

高亮显示接口的实现类,理解即可:

方法中需要做什么:

6499a6d90a664d917e9e16f0984e6a9a.png

怎么把结果解析成前台认识的页面:

高亮解析:

71bf780d3661c27a2b1946cb88130c9d.png

package cn.wolfcode.luowowo.search.service.impl;import cn.wolfcode.luowowo.domain.Destination;import cn.wolfcode.luowowo.domain.Strategy;import cn.wolfcode.luowowo.domain.Travel;import cn.wolfcode.luowowo.domain.UserInfo;import cn.wolfcode.luowowo.search.query.SearchQuery;import cn.wolfcode.luowowo.search.service.ISearchService;import cn.wolfcode.luowowo.service.IDestinationService;import cn.wolfcode.luowowo.service.IStrategyService;import cn.wolfcode.luowowo.service.ITravelService;import cn.wolfcode.luowowo.service.IUserInfoService;import org.apache.commons.beanutils.BeanUtils;import org.elasticsearch.action.search.SearchResponse;import org.elasticsearch.common.text.Text;import org.elasticsearch.index.query.QueryBuilders;import org.elasticsearch.search.SearchHit;import org.elasticsearch.search.SearchHits;import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;import org.springframework.data.elasticsearch.core.SearchResultMapper;import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;import org.springframework.stereotype.Service;import java.lang.reflect.InvocationTargetException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;@Servicepublic class SearchServiceImpl implements ISearchService {    @Autowired    private IUserInfoService userInfoService;    @Autowired    private IStrategyService strategyService;    @Autowired    private ITravelService travelService;    @Autowired    private IDestinationService destinationService;    @Autowired    private ElasticsearchTemplate template;    //类比:select * from xxx where  title like  %#{keyword}% or subTitle like %#{keyword}%  or summary like %#{keyword}%    //关键字: keyword = 广州    //以title为例:    //原始匹配: "有娃必看,广州长隆野生动物园全攻略"    //高亮显示后:"有娃必看,广州长隆野生动物园全攻略"    @Override    public <T> Page<T> searchWithHighlight(String index, String type, Class<T> clz, SearchQuery qo, String... fields) {        String preTags = "";        String postTags = "";        //需要进行高亮显示的字段对象, 他是一个数组, 个数由搜索的字段个数决定: fields 个数决定        //fields : title subTitle  summary        HighlightBuilder.Field[] fs = new HighlightBuilder.Field[fields.length];        for (int i = 0; i < fs.length; i++) {            //最终查询结果: 广州            fs[i] = new HighlightBuilder.Field(fields[i])                    .preTags(preTags)  //拼接高亮显示关键字的开始的样式                       .postTags(postTags);//拼接高亮显示关键字的结束的样式           }        NativeSearchQueryBuilder searchQuery = new NativeSearchQueryBuilder();        searchQuery.withIndices(index)  //设置搜索索引                .withTypes(type);   // 设置搜索类型        /*"query":{            "multi_match": {                "query": "广州",                "fields": ["title","subTitle","summary"]            }        },*/        searchQuery.withQuery(QueryBuilders.multiMatchQuery(qo.getKeyword(), fields));  //拼接查询条件        /**         "from": 0,         "size":3,         */        searchQuery.withPageable(qo.getPageable());   //分页操作        //高亮显示        /**         "highlight": {         "fields" : {         "title" : {},         "subTitle" : {},         "summary" : {}         }         }         */        searchQuery.withHighlightFields(fs);        //List es = template.queryForList(searchQuery.build(), UserInfoEs.class);        //调用template.queryForPage 实现结果处理        //参数1:DSL语句封装对象        //参数2:返回Page对象中list的泛型        //参数3:SearchResultMapper 全文搜索返回的结果处理对象        //     功能: 将DSL语句执行结果处理成Page 分页对象        return template.queryForPage(searchQuery.build(), clz, new SearchResultMapper() {            ///mapResults  真正处理DSL语句返回结果 方法            //参数1: DSL语句查询结果            //参数2: 最终处理完之后, 返回Page对象中list的泛型            //参数3: 分页对象            @Override            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {                List<T> list = new ArrayList<>();                SearchHits hits = response.getHits(); //结果对象中hist 里面包含全文搜索结果集                SearchHit[] searchHits = hits.getHits();//结果对象中hist的hit 里面包含全文搜索结果集                for (SearchHit searchHit : searchHits) {                    T t = mapSearchHit(searchHit, clazz);                    //必须使用拥有高亮显示的效果字段替换原先的数据                    //参数1: 原始对象(字段中没有高亮显示)                    //参数2:具有高亮显示效果字段, 他是一个map集合, key: 高亮显示字段名, value: 高亮显示字段值对象                    //参数3:需要替换所有字段                    Map<String, String> map = highlightFieldsCopy(searchHit.getHighlightFields(), fields);                    //BeanUtils.copyProperties(map, t);                    /*两个不同包下BeanUtils工具类的区别:                        1.springboot 框架中的BeanUtils类,如果参数是map集合,将无法进行属性的复制                            copyProperties(源, 目标);                        2.Apache 的BeanUtils类,可以对map进行属性的复制                            copyProperties(目标, 源);                    */                    try {                        BeanUtils.copyProperties(t, map);                    } catch (IllegalAccessException e) {                        e.printStackTrace();                    } catch (InvocationTargetException e) {                        e.printStackTrace();                    }                    list.add(t);                }                //将结果集封装成分页对象Page : 参数1:查询数据, 参数2:分页数据, 参数3:查询到总记录数                AggregatedPage<T> result = new AggregatedPageImpl<>(list, pageable, response.getHits().getTotalHits());                return result;            }            @Override            public <T> T mapSearchHit(SearchHit searchHit, Class<T> clz) {                String id = searchHit.getSourceAsMap().get("id").toString();                T t = null;                if (clz == UserInfo.class) {                    t = (T) userInfoService.get(id);                } else if (clz == Travel.class) {                    t = (T) travelService.get(id);                } else if (clz == Strategy.class) {                    t = (T) strategyService.get(id);                } else if (clz == Destination.class) {                    t = (T) destinationService.get(id);                } else {                    t = null;                }                return t;            }        });    }    //fields: title subTitle summary    private Map<String, String> highlightFieldsCopy(Map<String, HighlightField> map, String... fields) {        Map<String, String> mm = new HashMap<>();        //title: "广州小吃名店红黑榜:你还是当年珠江畔那个老字号吗?"        //subTitle: "广州小吃名店红黑榜"        //summary: "企鹅吃喝指南|“城市指南“第4站-广州   小吃篇"        //title subTitle summary        for (String field : fields) {            HighlightField hf = map.get(field);            if (hf != null) {                //获取高亮显示字段值, 因为是一个数组, 所有使用string拼接                Text[] fragments = hf.fragments();                String str = "";                for (Text text : fragments) {                    str += text;                }                mm.put(field, str);  //使用map对象将所有能替换字段先缓存, 后续统一替换                //BeanUtils.setProperty(t,field,  str);  识别一个替换一个            }        }        return mm;    }}
攻略高亮:
    //攻略    private Object searchStrategy(SearchQuery qo) {        //全文搜索        /**         * query:{         *     multi_match{         *         query:"keyword"         *         fields:["title", "intro"]         *     }         * }         */        //高亮显示        Page page = searchService.searchWithHighlight(                StrategyEs.INDEX_NAME,  //索引                StrategyEs.TYPE_NAME,   //类型                Strategy.class,         //攻略对象类型                qo,                     //分页数据                "title", "subTitle", "summary"  //搜索条件列        );        return JsonResult.success(new ParamMap().put("page", page).put("qo", qo));    }
游记高亮:
    //游记    private Object searchTravel(SearchQuery qo) {        Page page = searchService.searchWithHighlight(                TravelEs.INDEX_NAME,  //索引                TravelEs.TYPE_NAME,   //类型                Travel.class,         //攻略对象类型                qo,                     //分页数据                "title", "summary"  //搜索条件列        );        return JsonResult.success(new ParamMap().put("page", page).put("qo", qo));    }
用户高亮:
    //用户    private Object searchUser(SearchQuery qo) {        Page page = searchService.searchWithHighlight(                UserInfoEs.INDEX_NAME,  //索引                UserInfoEs.TYPE_NAME,   //类型                UserInfo.class,         //攻略对象类型                qo,                     //分页数据                "info", "city"  //搜索条件列        );        return JsonResult.success(new ParamMap().put("page", page).put("qo", qo));    }
默认的全部显示:
    //全部    private Object searchAll(SearchQuery qo) {        SearchResultVO result = new SearchResultVO();        Page us = searchService.searchWithHighlight(UserInfoEs.INDEX_NAME, UserInfoEs.TYPE_NAME,                UserInfo.class, qo, "info", "city"        );        Page ts = searchService.searchWithHighlight(TravelEs.INDEX_NAME, TravelEs.TYPE_NAME,                Travel.class, qo, "title", "summary"        );        Page sts = searchService.searchWithHighlight(StrategyEs.INDEX_NAME, StrategyEs.TYPE_NAME,                Strategy.class, qo, "title", "subTitle", "summary"        );        Page ds = searchService.searchWithHighlight(                DestinationEs.INDEX_NAME, DestinationEs.TYPE_NAME, Destination.class,                qo, "name", "info"        );        //返回数据的封装        result.setUsers(us.getContent());        result.setTravels(ts.getContent());        result.setStrategys(sts.getContent());        result.setDests(ds.getContent());        result.setTotal(us.getTotalElements()                + ts.getTotalElements() + sts.getTotalElements() + ds.getTotalElements());        return JsonResult.success(new ParamMap().put("result", result).put("qo", qo));    }

java学途

只分享有用的Java技术资料 

扫描二维码关注公众号

a69c173f43f039ef9ab94c982cb7dd25.png 

笔记|学习资料|面试笔试题|经验分享 

如有任何需求或问题欢迎骚扰。微信号:JL2020aini

或扫描下方二维码添加小编微信

f2647725f0ec1eac14d9b7b865041aaa.png 

小伙砸,欢迎再看分享给其他小伙伴!共同进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值