Java项目笔记之首页和全文搜索

原创
10/15 22:28
阅读数 6.5K


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


网站首页


es/mysql/mongodb/redis区别

关系型数据库: MySQL

关系型数据库是一种基于关系的数据库,而关系模型可通过二维表来进行表示,所以数据的存储方式是由行列组成的表,每一列是一个字段,每一行是一个记录。在关系型数据库中通常包含了三个概念:数据库(database)、表(table)、记录(record)。在大部分关系型数据库中,都是适用B+树作为索引,比如MySQL。

  • MySQL也是一种硬盘型数据库,操作数据是IO级别的,它所有的数据都是存放在硬盘中,需要使用的时候才会交换到内存中。因此MySQL能够处理海量的数据,但是数据量很大的时,速度会稍慢。

  • MySQL的使用需要提前建表,不适用于数据结构变换频繁的情况

非关系型数据库:MongoDB、Redis
MongoDB介绍

MongoDB是由c++语言编写的非关系型数据库,是一个基于分布式文件存储的开源数据库系统,其内容存储类似JSON对象,它的字段可以包含其他的文档、数组以及文档数组。MongoDB包含了三个层次概念:数据库(database)、集合(collection)、文档(document)。MongoDB的数据索引是B-树。

  • MongoDB 在创建数据库的时候,会直接在磁盘上面分配一组数据文件,所有的集合、索引和数据库的其他元数据都保存在这些文件中。

  • 在使用MongoDB中,操作系统会通过mmap将进程所需要的所有数据都映射到虚拟内存中,然后在将当前需要处理的数据映射到内存中。当需要访问的数据不在虚拟内存的时候,会触发page fault,然后os就会硬盘中的数据加载到虚拟内存和内存中。而当内存已满时,会触发swap-out操作,将一些数据写回硬盘。所以有了这种内存映射文件的方法,就会有种好像所有需要访问的数据都在内存里一样。

  • MongoDB的特点:

    • 提供面向文档存储,操作简单

    • 扩展性强,第三方支持丰富

    • 具有failover机制(失效转移:一种备份操作模式,当一个系统因为一些故障无法完成工作的时候,另一个系统自动接替已失效系统的工作继续执行)

    • 支持大容量存储,内置GridFS(可用于存放大量的小文件)

    • 在高负载的情况下,可以添加更多的节点,保证服务器性能

  • 缺点

    • 无事务机制(数据库事务(database transaction)对单个的逻辑单元执行一系列的操作,要么完全执行,要么完全不执行)

    • 占用空间过大

    • 没有mysql那样成熟的维护工具

  • 适用场景

    • 适合那种数据格式不明确或者经常变化的模型,比如事件记录、内容管理或者博客平台。

Redis

Redis是一种内存数据库,所有的数据都是放在内存之中,定期写入磁盘中,当内存不够的时候,可选择指定的LRU算法删除数据。Redis是基于哈希字典建立的,因此其索引方式是哈希。

  • 特点

    • 由于数据存放在内存中,因此读写性能高

    • 支持丰富的数据类型,如键值对、集合、列表、散列存储


elasticsearch

1、Elasticsearch和MongoDB/Redis/Memcache一样,是非关系型数据库。是一个接近实时的搜索平台,从索引这个文档到这个文档能够被搜索到只有一个轻微的延迟,企业应用定位:采用Restful API标准的可扩展和高可用的实时数据分析的全文搜索工具。

2、可拓展:支持一主多从且扩容简易,只要cluster.name一致且在同一个网络中就能自动加入当前集群;本身就是开源软件,也支持很多开源的第三方插件。

3、高可用:在一个集群的多个节点中进行分布式存储,索引支持shards和复制,即使部分节点down掉,也能自动进行数据恢复和主从切换。

3、采用RestfulAPI标准:通过http接口使用JSON格式进行操作数据。

4、数据存储的最小单位是文档,本质上是一个JSON 文本;

实际项目开发中,几乎每个系统都会有一个搜索的功能,数据量少时可以直接从主数据库中比如Mysql搜索,但当搜索做到一定程度时,比如系统数据量上了10亿、100亿条的时候,传统的关系型数据库的I/O性能和统计分析性能就难以满足用户需要了。所以很多公司都会把搜索单独做成一个独立的模块,用ElasticSearch等来实现。虽然内存缓存数据库的读写性能很高,但完全把数据放在内存中是不太现实的


需求:使用es做站内搜索


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

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

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

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

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

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

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

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

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

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


关键字搜索

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

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

1:关键词搜索

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

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

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

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

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

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


添加依赖:

         <!--elasticsearch-->         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-data-elasticsearch</artifactId>         </dependency>         <dependency>             <groupId>commons-beanutils</groupId>             <artifactId>commons-beanutils</artifactId>             <version>1.9.3</version>         </dependency>

es的配置:

es中数据的初始化

目的地:

search.domain

search.repository

search.service


攻略:

其他组件是一样的,拷贝替换就好;


游记:

其他组件是一样的,拷贝替换就好;


用户:

其他组件是一样的,拷贝替换就好;


初始化controller
 @RestController public 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<Strategy> sts = strategyService.list();         for (Strategy st : sts) {             StrategyEs es = new StrategyEs();             BeanUtils.copyProperties(st, es);             strategyEsService.save(es);         }         //游记需要存到es中的数据初始化         List<Travel> ts = travelService.list();         for (Travel t : ts) {             TravelEs es = new TravelEs();             BeanUtils.copyProperties(t, es);             travelEsService.save(es);         }          //用户需要存到es中的数据初始化         List<UserInfo> uf = userInfoService.list();         for (UserInfo u : uf) {             UserInfoEs es = new UserInfoEs();             BeanUtils.copyProperties(u, es);             userInfoEsService.save(es);         }           //目的地需要存到es中的数据初始化         List<Destination> dests = destinationService.list();         for (Destination d : dests) {             DestinationEs es = new DestinationEs();             BeanUtils.copyProperties(d, es);             destinationEsService.save(es);         }          return "ok";     } }

查到数据放到es中:

同理可得,剩下的拷贝。


启动服务器:检查head中的数据是否按要求创建好了索引了:

索引信息一定要和配置的一致


打开mongodb数据库:必须要保证所有的数据是合法合规的,把自己加的错误的坏的数据删了。


之后再进行数据的初始化:发出初始化数据的请求,刚刚设置的controller

查看初始化完成的数据是否正确:



关键字搜索

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


目的地关键词搜索

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

如果目的找不到, 显示:


前端代码:

查看首页前台代码引用了rip-website\js\vue\common.js:

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


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

一个方法中完成不同的搜索目的,如何区分?

怎么将这些不同的搜索类型区分开:用switch语句

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

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


目的地关键词搜索:

system/search/searchDest.js

页面html:

trip-website\views\search\searchDest.html

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


后台:

JPA中定义的方法ByXxx()要去检查一下es中是否有Xxx属性,否则报错:

去哪一个数据库查询数据给前台?由前台需要显示的数据来决定。es能不能满足页面所有的显示的数据。


其他的三个查询方式相同;


定义封装result数据的类型:

用result封装数据:

返回结果


测试查看查询的数据:

查不到数据:

get请求的时候:会将中文字符进行编码了,

后台需要解码,才能转换成中文:

再测试:

看页面少了引用:

报错:找不到用户昵称,查看数据有没有到后天,查看前台有没有按要求封装数据;

头像没了:打印后台传过来的数据,发现没有头像信息;

测试,0条的0没有显示:或者在SearchResultVO中设置total默认值为0;



攻略全文搜索:

仅仅对攻略进行全文搜索

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

拷贝接口:

拷贝实现类:

修改BeanUtils

为什么这么写:因为查询高亮的接口的定义,对比如下:


测试:

查看攻略查询结果正不正常,有没有高亮显示关键字;


攻略

游记

用户


全部

默认情况下查询全部显示:

数据的封装:


测试:




全文搜索方法设计的解释:

EQL语句全文检索:

方法设计:

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

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


方法中需要做什么:

EQL语句查询到的响应结果:

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

高亮解析:


代码:

 @Service public 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为例:     //原始匹配: "有娃必看,广州长隆野生动物园全攻略"     //高亮显示后:"有娃必看,<span style="color:red;">广州</span>长隆野生动物园全攻略"     @Override     public <T> Page<T> searchWithHighlight(String index, String type, Class<T> clz, SearchQuery qo, String... fields) {         String preTags = "<span style='color:red;'>";         String postTags = "</span>";          //需要进行高亮显示的字段对象, 他是一个数组, 个数由搜索的字段个数决定: fields 个数决定         //fields : title subTitle  summary         HighlightBuilder.Field[] fs = new HighlightBuilder.Field[fields.length];         for (int i = 0; i < fs.length; i++) {             //最终查询结果: <span style="color:red;">广州</span>             fs[i] = new HighlightBuilder.Field(fields[i])                     .preTags(preTags)  //拼接高亮显示关键字的开始的样式   <span style="color:red;">                     .postTags(postTags);//拼接高亮显示关键字的结束的样式   </span>         }          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<UserInfoEs> 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: "<em>广州</em>小吃名店红黑榜:你还是当年珠江畔那个老字号吗?"         //subTitle: "<em>广州</em>小吃名店红黑榜"         //summary: "企鹅吃喝指南|“城市指南“第4站-<em>广州</em>   小吃篇"          //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;     }  }






java学途

只分享有用的Java技术资料 

扫描二维码关注公众号

 


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

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

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

 




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




本文分享自微信公众号 - java学途(javaxty)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
2
8 收藏
分享
加载中
更多评论
打赏
0 评论
8 收藏
2
分享
返回顶部
顶部