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


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


网站首页


es/mysql/mongodb/redis区别

关系型数据库: MySQL

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

  • MySQL也是一种硬盘型数据库,操做数据是IO级别的,它全部的数据都是存放在硬盘中,须要使用的时候才会交换到内存中。所以MySQL可以处理海量的数据,可是数据量很大的时,速度会稍慢。html

  • MySQL的使用须要提早建表,不适用于数据结构变换频繁的状况前端

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

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

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

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

  • MongoDB的特色:c++

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

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

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

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

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

  • 缺点

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

    • 占用空间过大

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

  • 适用场景

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

Redis

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

  • 特色

    • 因为数据存放在内存中,所以读写性能高

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


elasticsearch

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

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

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

三、采用RestfulAPI标准:经过http接口使用JSON格式进行操做数据。

四、数据存储的最小单位是文档,本质上是一个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源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索