赞
踩
全文检索(搜索)引擎:汇合了网络爬虫技术、检索排序技术、网页处理技术、大数据处理技术、自然语言处理技术等综合性的学科
检索引擎分类:Lucene、Nutch、Solr、Elasticsearch
下面Elasticsearch以7.0+版本做介绍
Elasticsearch(简写es), Elasticsearch是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别(1PB=1024T=1048576G)的数据。
Elasticsearch使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,Lucene非常复杂,而es通过简单的RESTful API隐藏Lucene的复杂性,可理解为简化使用Lucene进行开发
es的定位是:专注于搜索的非关系型数据库,其严重“偏科”,相比其他数据库,其搜索的性能甩几条街,但是对应的DML操作就非常复杂且低性能,因此,使用es就是海量的数据的DQL操作,而DML操作则需要对应权衡
一级分类 | 二级分类 | 具体类型 |
---|---|---|
核心类型 | 字符串类型 | string,text,keyword |
核心类型 | 整数类型 | integer,long,short,byte |
核心类型 | 浮点类型 | double,float,half_float,scaled_float |
核心类型 | 逻辑类型 | boolean |
核心类型 | 日期类型 | date |
核心类型 | 范围类型 | range |
核心类型 | 二进制类型 | binary |
复合类型 | 数组类型 | array |
复合类型 | 对象类型 | object |
复合类型 | 嵌套类型 | nested |
地理类型 | 地理坐标类型 | geo_point |
地理类型 | 地理地图 | geo_shape |
PUT /my_index
{
"settings": {
"number_of_shards": 5, //设置5个片区
"number_of_replicas": 1 //设置1个备份
}
}
创建映射 PUT /索引名(可以和索引一起创建,映射就是对应的文档类型)
PUT /user { "mappings": { "properties":{ "id":{ "type":"long" }, "name":{ "type":"keyword" }, "age":{ "type":"integer" } } } } #或者 PUT /user/_doc/1 { "name":{ "type":"keyword" }, "age":{ "type":"integer" } }
每个文档都有一个ID,如果插入的时候没有指定ID的话,ElasticSearch会自动生成一个字符串_id。
DELETE /user/user/10
注意:这里的删除并且不是真正意义上的删除,仅仅是清空文档内容,并且标记该文档的状态为删除而已。如果后续有数据新增进来则会替换它的位置,如果一直没有数据替换则定时删除
PUT /user/user/10
{
"name":"lll",
"age":12
}
注意:如果有其它字段没有指定的话会清空该字段值,如果仅是更新某些字段可以如此
POST /user/user/10
{
"doc":{
"name":"lll",
"age":12
}
}
GET /user/_doc/10 //根据id查询
GET /索引名/_doc/search //查询所有
#批量查询
GET /user/_doc
{
"docs":[
{"_id":"1"},
{"_id":"11"},
{"_id":"111"}
]
}
GET /索引名/_search { "query":{ "match":{ "field":"value" //按模糊值匹配属性名“模糊“查询,其还会自动按照匹配度自高向低排序 } } } GET /索引名/_search { "query": { "multi_match": { "query": value, "fields": [field1, field2, ...] } } }
"query":{
"multi_match": {
"query": "广州", //关键词
"fields": ["title","subTitle","summary"] //对应关键词检索字段
}
},
es的分词器把文本内容按照一定标准进行切分,默认使用standard分词器,该分词器按单词(注意是单词不是字母)、文字(单拆)匹配查询
es支持额外的分词器插件IK分词器
IK分词器:
ik_smart 粗粒度分词
会做最粗粒度的拆分,即尽可能长地拆分。比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
ik_max_word 细粒度分词
字段尽可能短地拆分,且会从短到更短地拆分。比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
正排索引(模糊查询):所谓正排就是在模糊匹配的时候,扫描索引库中的所有文档,找出所有包含关键词的文档,再根据打分模型进行打分(即匹配度高低),说白了就是找到所有文档再筛选。MySQL的索引在使用模糊匹配时即失效,并且索引没有全局性。
倒排索引则是根据映射直接定点索引(查询)
这也是es为什么如此快搜索的原因,具体什么是倒排索引我们需要先了解es搜索的完整过程:
①es新增文档的时候,使用事先指定的分词器对文档中的每个内容进行拆词
②将拆词之后的到的每一个Term (词根),保存在一张倒排索引列表中(一个main.dic文件,也即Term Dictionary字典),然后会建立词根与文档id(新增的时候就有指定了文档id(也就是_id),所以是直接对应)的一对多映射(一个词根对应很可能有多个id)
③到了搜索时,分词器会对搜索的内容进行分词,然后对应也得到词根
④根据词根到字典中进行匹配文档id列
⑤获得文档id(一般将表id列作为文档id列,为了后面查询方便)之后进行综合处理,然后对其进行热度排序,数据封装成一个集合返回给搜索者(正排索引是全表查询后筛选)
总结:
像通过id查找表中的某行就是正排索引,而倒排索引就是通过某个列或者某些列甚至全表列(具体看你的关键词需要在哪部分出现)中的某个关键词(也即模糊词)到表中查找某行;一般我们从mysql中预热数据到es的时候,都会选择关键词会出现的列作为es的keyword,这将是提高es效率关键的一步。
es只有text类型的数据会分词,而keyword类型的数据是直接建立索引的
所以实际的倒排列表中并不只是存了文档ID那么简单,还有一些其它的信息,比如:词频(Term出现的次数)、偏移量(offset)等。而上述过程也反映了对应的DML操作的复杂性,特别是UPDATE操作,特别消耗性能(update其实就是delete+put)如 当用户在主页上搜索关键词“华为手机”时,假设只存在正向索引(forward index),那么就需要扫描索引库中的所有文档,找出所有包含关键词“华为手机”的文档,再根据打分模型进行打分,排出名次后呈现给用户。
我们进行全文搜索的时候,涉及到的问题无非是:
注意:我们在初始化数据的时候,除了需要需要所以要明确字段外,其次更重要的原因是,es只是做搜索的,主库中的数据会随时更新,那对应的es也要更新,但是由于es“偏科“,所以我们一般会周期性并且避峰进行更新数据到es(比如凌晨1点)
数据准备,从mysql中初始化数据到es,下面以全文检索目的地/攻略/游记/用户为例
使用es的时候要开启终端bat
mgsire/DataController
@RestController public class DataController { //es服务 @Autowired private IDestinationEsService destinationEsService; @Autowired private IStrategyEsService strategyEsService; @Autowired private ITravelEsService travelEsService; @Autowired private IUserInfoEsService userInfoEsService; //mysql服务 @Autowired private IDestinationService destinationService; @Autowired private IStrategyService strategyService; @Autowired private ITravelService travelService; @Autowired private IUserInfoService userInfoService; @GetMapping("/dataInit") public Object dataInit() { //把mysql中的目的地/攻略/游记/用户数据备份到es(对应的只取关键词检索会出现的字段) //攻略 List<Strategy> sts = strategyService.list(); for (Strategy st : sts) { StrategyEs es = new StrategyEs(); BeanUtils.copyProperties(st, es); strategyEsService.save(es); } //游记 List<Travel> ts = travelService.list(); for (Travel t : ts) { TravelEs es = new TravelEs(); BeanUtils.copyProperties(t, es); travelEsService.save(es); } //用户 List<UserInfo> uf = userInfoService.list(); for (UserInfo u : uf) { UserInfoEs es = new UserInfoEs(); BeanUtils.copyProperties(u, es); userInfoEsService.save(es); } //目的地 List<Destination> dests = destinationService.list(); for (Destination d : dests) { DestinationEs es = new DestinationEs(); BeanUtils.copyProperties(d, es); destinationEsService.save(es); } return "ok"; } }
本项目在core的pom和properties中操作
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
#elasticsearch端口
spring.elasticsearch.rest.uris = localhost:9200
其余相关domain差不多,区别一下检索字段即可(就是你的关键词检索内容的位置)(跟mongodb一样,使用es的时候最好分search或elasticsearch包区分,对应的domain后缀名用ES、Es)
/** * 目的地搜索对象 */ @Getter @Setter @Document(indexName = "destination") public class DestinationEs implements Serializable { public static final String INDEX_NAME = "destination"; @Id //@Field 每个文档的字段配置(store是否存储、index是否分词、type类型,analyzer、searchAnalyzer分词器) @Field(store = true, index = false, type = FieldType.Long) private Long 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; }
public interface DestinationEsRepository extends ElasticsearchRepository<DestinationEs,String> {
//...
}
@GetMapping("/search") public JsonResult queryDestination(SearchQueryObject qo)throws UnsupportedEncodingException{ //前端传参的时候可能发生编码转换 String keyWord=URLDecoder.decode(qo.getKeyword(),"utf-8");qo.setKeyword(keyWord); //根据目的地/攻略/游记/用户全文检索,其余domain跟目的地一样,只是区别一下检索字段 return this.searchAll(qo); } private JsonResult searchAll(SearchQueryObject qo){ SearchResultVO vo=new SearchResultVO(); //获取全文查找的集合 List<Destination> destinationList=this.createDestinationPage(qo).getContent(); List<Strategy> strategyList=this.createStrategyPage(qo).getContent(); List<Travel> travelList=this.createTravelPage(qo).getContent(); List<UserInfo> userInfoList=this.createUserInfoPage(qo).getContent(); //结果对象封装 vo.setDests(destinationList); vo.setStrategys(strategyList); vo.setTravels(travelList); vo.setUsers(userInfoList); //结果数据条数封装 vo.setTotal((long)destinationList.size()+strategyList.size()+travelList.size()+userInfoList.size()); //ParamMap就是手写的小工具类,即new HashMap<String,Object>,当然也可以直接new map对象即可 return JsonResult.success(ParamMap.newInstance().put("result",vo).put("qo",qo)); } private Page<Strategy> createStrategyPage(SearchQueryObject qo){ //攻略全文查找,根据攻略标题、副标题、简介 return searchService.searchWithHighlight(StrategyEs.INDEX_NAME,Strategy.class,qo,"title","subTitle","summary"); } private Page<Travel> createTravelPage(SearchQueryObject qo){ //游记全文查找,根据标题、简介 Page<Travel> page=searchService.searchWithHighlight(TravelEs.INDEX_NAME,Travel.class,qo,"title","summary"); //游记需要对author关联 for(Travel travel:page){ travel.setAuthor(userInfoService.getById(travel.getAuthorId())); } return page; } private Page<UserInfo> createUserInfoPage(SearchQueryObject qo){ //用户全文查找,根据昵称、城市、简介 return searchService.searchWithHighlight(UserInfoEs.INDEX_NAME,UserInfo.class,qo,"info","city"); } private Page<Destination> createDestinationPage(SearchQueryObject qo){ return searchService.searchWithHighlight(DestinationEs.INDEX_NAME,Destination.class,qo,"name","info"); }
SearchResultVO结果集封装
@Setter
@Getter
public class SearchResultVO implements Serializable{
private Long total = 0L; //检索数量
private List<Strategy> strategys = new ArrayList<>();
private List<Travel> travels = new ArrayList<>();
private List<UserInfo> users = new ArrayList<>();
private List<Destination> dests = new ArrayList<>();
}
全文检索及高亮显示,所谓高亮显示,就是在检索的时候,对出现的关键词加颜色或者高亮区别其他非关键词,虽然实现很简单,但确是友好对待用户的基本。
es中的分页api和mongodb类似(毕竟同样继承spring-data依赖,可以说是完全一样了)
@Override public<T> Page<T> searchWithHighlight(String index,Class<T> clz,SearchQueryObject qo,String...fields){ SearchRequest searchRequest=new SearchRequest(index); SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder(); //我们需要做的就是通过java的方式将es语法条件拼接起来 //高亮显示 /*"query":{ "multi_match": { "query": "广州", "fields": ["title","subTitle","summary"] } },*/ MultiMatchQueryBuilder queryBuilder=QueryBuilders.multiMatchQuery(qo.getKeyword(),fields); HighlightBuilder highlightBuilder = new HighlightBuilder(); // 生成高亮查询器 for(String field:fields){ highlightBuilder.field(field);// 高亮查询字段 } highlightBuilder.requireFieldMatch(false); // 如果要多个字段高亮,这项要为false highlightBuilder.preTags("<span style='color:red'>"); // 高亮设置 highlightBuilder.postTags("</span>"); highlightBuilder.fragmentSize(800000); // 最大高亮分片数 highlightBuilder.numOfFragments(0); // 从第一个分片获取高亮片段 /** 分页显示 "from": 0, "size":3, */ Pageable pageable = PageRequest.of( qo.getCurrentPage()-1, qo.getPageSize(), Sort.Direction.ASC,"_id" );// 设置分页参数 //构建条件,也即条件拼接 NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(queryBuilder) // match查询 .withPageable(pageable) .withHighlightBuilder(highlightBuilder) // 设置高亮 .build(); SearchHits<T> searchHits = template.search(searchQuery,clz,IndexCoordinates.of(index)); List<T> list = new ArrayList(); for ( SearchHit<T> searchHit : searchHits){ // 获取搜索到的数据 T content = this.parseType(clz,searchHit.getId()); // 处理高亮 Map<String, String> map = highlightFieldsCopy(searchHit.getHighlightFields(),fields); //1:spring 框架中BeanUtils 类,如果是map集合是无法进行属性复制 // copyProperties(源, 目标) //2: apache BeanUtils 类 可以进map集合属性复制 // copyProperties(目标, 源) try{ BeanUtils.copyProperties(content,map); }catch(IllegalAccessException e){ e.printStackTrace(); }catch(InvocationTargetException e){ e.printStackTrace(); } list.add(content); } Page page=new PageImpl(list,pageable,searchHits.getTotalHits()); return page; } /** * 从es中查询到的_id(因为domain和预热数据的时候就将id作为_id,所以es文档_id即表id) * 通过反射获取对象,根据id找到mysql中的数据 */ private<T> T parseType(Class<T> clz,String id){ Long lId = 0L; if(StringUtils.hasLength(id)){ lId=Long.valueOf(id); } T t = null; if (clz == UserInfo.class){ t = (T)userInfoService.getById(lId); } else if(clz == Travel.class){ t = (T)travelService.getById(lId); } else if(clz == Strategy.class){ t = (T)strategyService.getById(lId); } else if(clz == Destination.class){ t = (T)destinationService.getById(lId); } else{ t = null; } return t; } //fields: title subTitle summary private Map<String, String> highlightFieldsCopy(Map<String, List<String>>map,String...fields){ Map<String, String> mm=new HashMap<>(); //title: "有娃必看,<span style='color:red;'>广州</span>长隆野生动物园全攻略" //subTitle: "<span style='color:red;'>广州</span>长隆野生动物园" //summary: "如果要说动物园,楼主强烈推荐带娃去<span style='color:red;'>广州</span>长隆野生动物园 //title subTitle summary for(String field:fields){ List<String> hfs=map.get(field); if(hfs!=null&&!hfs.isEmpty()){ //获取高亮显示字段值, 因为是一个数组, 所有使用string拼接 StringBuilder sb=new StringBuilder(); for(String hf : hfs){ sb.append(hf); } mm.put(field,sb.toString());//使用map对象将所有能替换字段先缓存, 后续统一替换 } } return mm; } }
QueryBuilders:高亮条件构建
PageRequest:分页条件构建
NativeSearchQuery:整合条件构建
SearchHits:template.search的条件检索结果
es从mysql(或者其他关系型数据库)初始化数据有两种方式:同步更新、异步更新
所谓同步更新就是代码对mysql数据进行增删改(下面统称更新)操作之后,紧接着对es数据更新。
抛开es性能不说,这个方法存在一个弊端,就是mysql支持事务,如果mysql在更新完成之后,紧接着es更新数据出现异常,按照事务回滚来说,本该更新完成的数据却因为外界而导致无法更新成功,它们之间互相影响显然不是一个好结果。因此该方案是不可行的。
异步更新有两种方式,一是使用数据库中间件方式(数据库中间件canal将在后面补充),一种是定时器更新方式。
定时器更新即使用定时器从mysql中获取数据到es中,es更新性能很低,所以对应的时间应该设置在凌晨或者用户访问量较低的时间段
mysql作为主库,存储核心数据,其数据关系可以很复杂,因此对应支持复杂联表条件查询。非关系型数据库中的数据都是从关系型数据库获取的,无论后者的数据如何,都是从前者中引申、或者备份而来的。
redis是非关系型数据库,严格来说定位是缓存。用于存储具有时效性、海量的数据。时效性如存储登录用户信息(类session)、短信验证码。redis的读写性能优于mysql,性能大概是mysql的1.0x10^6倍,而其缺点就是断电即失效,毕竟是内存操作,所以对应的关键数据(相比之下短时效的数据是没关系的)需要定期持久化到关系型数据库中。
**mongodb作为关系型数据库和非关系型数据库之间,其可以单独实现如redis的内存操作、mysql的持久化。将mongodb作为类似redis、memcache来做缓存db,为mysql提供服务,或是后端日志收集分析。 **考虑到mongodb属于nosql型数据库,sql语句与数据结构不如mysql那么亲和 ,也会有很多时候将mongodb做为辅助mysql而使用的类redis、memcache 之类的缓存db来使用。 **亦或是仅作日志收集分析。**或者是存储某文章相关的评论,n方级别的数据(sql是中间表的形式,数据量太大)。说白了就是
总结:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。