Java项目笔记之首页和全文搜索
网站首页
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中去把满足条件的数据找到显示出来。
从mongodb中同步条件列数据以及主键id到es中(推荐:因为内存资源宝贵,选择牺牲性能)
先匹配es中条件列搜索满足条件的数据,得到主键id集合,然后以id集合作为条件去mongodb中对应的id数据集合,之后再页面显示;
优点:节省内存空间(数据量小了);
缺点:稍微有损性能(去两个数据库中查询了);
从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
public class DataController {//es相关服务private IDestinationEsService destinationEsService;private IStrategyEsService strategyEsService;private ITravelEsService travelEsService;private IUserInfoEsService userInfoEsService;//mongodb相关服务private IDestinationService destinationService;private IStrategyService strategyService;private ITravelService travelService;private IUserInfoService userInfoService;("/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语句查询到的响应结果:
怎么把结果解析成前台认识的页面:
高亮解析:
代码:
public class SearchServiceImpl implements ISearchService {private IUserInfoService userInfoService;private IStrategyService strategyService;private ITravelService travelService;private IDestinationService destinationService;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>长隆野生动物园全攻略"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 summaryHighlightBuilder.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: 分页对象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;}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 summaryprivate 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 summaryfor (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学途(javaxty)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。






































































