文章目录
ES
从海量数据中快速找到所需内容
ElasticSearch结合kibana,Logstash,Beats,也就是ELK技术栈,被广泛用于日志数据分析(将日志数据可视化的展示出来),实时监控(项目的运行情况)等领域
lucene:Java语言搜索引擎,也就是个jar包,es是基于它做的二次开发
- 易扩展
- 高性能:(基于倒排索引)
概念
**文档:**es是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息
文档数据会被序列化成json格式存在es中
**索引:**相同类型的文档的集合,类似于数据库的表
**映射:**索引中文档的约束,类似于数据库的字段类型,可以看成数据库的表结构
**字段:**json文档中的字段,类似于数据库中的列
**DSL:**es提供的json风格的请求语句,操作es,实现crud
Mysql擅长事务类型操作,确保数据安全和统一性
es擅长海量数据搜索,分析
倒排索引
- 文档:每条数据就是一个文档
- 词条:文档按照语义分成的词语
词条是唯一的,如果数据中包含这个词条,就将它的文档id,记录进去
先找到词条再找到文档,而传统的mysql搜索先找到文档再找词条,所以叫倒排索引
分词器
创建倒排索引时对文档分词
用户搜索时,对输入的内容分词
IK分词器模式:ik_smart
IK分词器拓展词条:ik_max_word
IK分词器停用词条:修改config目录下的IKAnalyzer.cfg.xml
索引库操作
mapping属性
字段的属性
- type:字段数据类型
- 字符串:text(可分词文本),keyword(精确值,如品牌,国家,ip)
- 数值:和java中差不多
- 布尔:boolean
- 日期:date
- 对象:object
- 没有数组类型,但是允许一个字段多个值
- 支持两种地理坐标数据类型:
- geo_point:由纬度和经度确定的一个点,如
“32.231321,64.561551”
- geo_shape:由多个geo_point组成的复杂几何图形
- index:是否创建索引,默认true
- analyzer:使用哪种分词器
- properties:子字段(对象嵌套时使用)
创建索引库
es中通过RestFul请求操作索引库和文档,请求内容用dsl语句表示
示例:
PUT /myes
{
"mappings": {
"properties": {
"info": {
"type":"text",
"analyzer": "ik_smart"
},
"emali": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstName": {
"type":"keyword"
},
"lastName":{
"type":"keyword"
}
}
}
}
}
}
查看删除索引库
get /索引库名
delete /name
es是禁止修改索引库的,因为会基于mapping创建索引,一旦修改,会导致原本索引完全失效但是可以添加新字段
PUT /索引库名/_mapping
{
"properties":{
"新字段": {
"type":""
……
}
}
}
文档操作
添加
POST /索引库名/_doc/文档id (如果不填就会自动生成)
{
"age":12,
"tall":13,
"email":"46461651@qq.com",
"info":"白片白嫖学java,华为云享智能",
"name":{
"firstName":"赵",
"lastName":"云"
}
"字段名":值
}
查看删除
GET /索引库名/_doc/文档id
GET /索引库名/_search
DELETE /索引库名/_doc/文档id
修改
-
全量修改,删除旧文档,添加新文档
PUT /索引库名/_doc/文档id(如果这个id根本不存在,就直接新增) { "age":12, "tall":13, "email":"46461651@qq.com", "info":"白片白嫖学java,华为云享智能", "name":{ "firstName":"赵", "lastName":"云" } "字段名":值 }
-
增量修改,修改指定字段值
POST /索引库名/_update/文档id { "doc": { "要修改的字段":修改的值 "age":30, "tall":99, "email":"zhaoyun@qq.com" } }
RestClient操作索引库
操作es的客户端,本质就是组装dsl语句,通过http请求发送给es
如果用户输入一行数据,希望这一行数据能够根据多个字段搜索,但是明显根据一个字段搜性能更好
为了解决这个问题,es提供了字段拷贝的功能
使用copy_to属性将当前字段拷贝到指定字段,es会根据这个字段创建全新索引
-
引入依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
-
统一es版本和服务端相同
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
-
初始化客户端
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://81.70.144.36:9200") ));
RestClient操作文档
从数据库中查询酒店数据,导入到索引库
DSL查询语法
基本语法
GET /索引库名/_search
{
"query": {
"查询类型": {
“field”:"text",
…………
}
}
}
精确查询
一般查找keyword的字段,不会对搜索条件分词,常见的有
- term:根据词条精确值查询
- range:根据值的范围查询可以是数值,日期的范围
地理查询
根据经纬度查询
-
geo_bounding_box:查询geo_point值落在某个矩形范围的所有文档
-
geo_distance:查询指定中心点小于某个距离值的所有文档
GET /索引库名/_search { "query": { "geo_distance":{ "distance":"5km", "表示经纬度的字段":"31.21,121.5" } } }
复合查询
将简单查询组合起来,实现更复杂的搜索逻辑
FunctionScoreQuery
在match查询中,文档的相关性算分影响文档排名。
算分是消耗性能的操作
function score query:算分函数查询,可以修改文档的相关性算分,得到新的算分排序
BooleanQuery
将多个查询组合在一起形成新的查询
- must:与
- should:或
- must_not:必须不匹配,不参与算分,非
- filter:必须匹配,不参与算分
例如:
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"match": {
"name": "如家"
}}
],
"must_not": [
{"range": {
"FIELD": {
"gte": 400
}
}}
],
"filter": [
{"geo_distance": {
"distance":"10km",
"location":"31.21,121.5"
}}
]
}
}
}
搜索结果处理
排序
默认根据算分来排序,可以排序的字段有keyword类型,数值类型,地理坐标类型,日期类型
如果我们自定义排序方式,es就会放弃打分
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"match": {
"name": "如家"
}}
],
"filter": [
{"geo_distance": {
"distance":"10km",
"location":"31.21,121.5"
}}
]
}
},
"sort": [
{
"price": "asc",
"score": "desc",
"_geo_distance": {
"location": {
"lat": 40,
"lon": -70
},
"order": "asc",
"unit": "km"
}
}
]
}
分页
es默认只返回top10的数据,想要获取更多需要修改分页参数
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990, //分页开始的位置
"size": 10 //期望获得文档总数
}
es分页的底层原理是如上面这个例子,先排序获取前1000条文档,在截取990~1000条文档
对于es集群,他只能将所有数据分片的前1000条文档拿出来,再做一个排序,再截取990~1000的文档,当分片多了的时候,对内存消耗是很大的,所以为了避免搜索页数过深,或者结果集(from+size)过大,es设定结果集查询上限是10000
搜索页数深的解决方案是after search 和 scroll
- from+size:
- 优点:支持随机翻页
- 缺点:深度分页问题,默认from+size上限10000
- after search:
- 无查询上限
- 只能向后逐页查询,不支持随机翻页
- 手机向下滚动翻页
- scroll:
- 无查询上限
- 通过快照,内存消耗大,搜索结果不是实时的
高亮
- 将搜索结果中的关键字用标签标记出来
- 在页面中给标签添加css样式
默认情况下,es搜索字段必须和高亮字段一致
如果不一致(如搜索字段是个all,all中包含了好几个字段,但是要显示高亮的只有一个字段),需要添加require_field_match=false
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "北京"
}
}
},
"highlight": {
"fields": {
"city": {
"pre_tags": "<em>",
"post_tags": "</em>" //这两个不写,默认标签就是em
}
}
}
}
RestClient查询文档
org.elasticsearch.index.query.QueryBuilders中提供了各种各样的查询
request.source()中提供了查询和对搜索结果的处理,可以理解成dsl查询语句的最外层的大json
以match_all查询为例
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchAllQuery());
SearchResponse re = client.search(request, RequestOptions.DEFAULT);
//解析结果
SearchHits hits = re.getHits();
long value = hits.getTotalHits().value;
System.out.println("总条数:"+value);
SearchHit[] res = hits.getHits();//得到结果数组
for (SearchHit hit : res) {
String json = hit.getSourceAsString();
System.out.println(json);
}
数据聚合
聚合可以实现对文档数据的统计,分析,运算,可视化
聚合的字段类型一定是不分词的
常见的有三类
- 桶(bucket)聚合:用来对文档分组
- TermAggregation:按照文档字段分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,一年为一组
- 度量(Metrics)聚合:
- Avg
- Max
- Min
- Stats:同时求max,min,avg,sum等
- 管道聚合:其他聚合的结果为基础做聚合
DSL实现Bucket聚合
# 聚合
GET /索引库名/_search
{
"size": 0, //显示文档数量,为0表示不想看文档,只看聚合结果
"aggs": {
"brands": { //桶聚合名称,任意起
"terms": { //根据字段聚合的字段类型
"field": "brand", //根据哪个字段聚合
"size": 20 //要显示桶聚合的数量
}
}
}
}
聚合结果排序
默认,Bucket聚合会统计Bucket内文档数量,记为_count,并且按照 _count降序排序
可以修改排序方式
GET /hotel/_search
{
"size": 0,
"aggs": {
"brands": {
"terms": {
"field": "brand",
"size": 20,
"order": { //加一个order属性即可
"_count": "asc"
}
}
}
}
}
限定聚合范围
默认情况下,是对索引库中所有文档做聚合,当库中数据特别多时,对内存消耗特别大,我们可以限定聚合文档范围,只要添加query条件即可。
GET /hotel/_search
{
"size": 0,
"query": {
"range": {
"price": {
"gte": 100,
"lte": 200 //只对价格在100~200的文档做聚合
}
}
},
"aggs": {
"brands": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
DSL实现Metrics聚合
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20,
"order": {
"score_stats.avg": "desc" //按照度量聚合的avg排序
}
},
"aggs": { //这里是对每个桶中的score做统计,所以要放到桶里面
"score_stats": { //聚合名称
"stats": { //聚合类型
"field": "score" //根据哪个字段聚合
}
}
}
}
}
}
RestClient实现聚合

搜索页面中的品牌,城市等信息不应该在页面写死,应该通过聚合索引库中数据得来
而且,在真正实现聚合时,往往需要添加一些过滤条件来限制聚合的范围,例如,索引库中文档所属city有上海,杭州,北京,但是此时用户搜索条件为价格区间为100~300,假设城市为北京的根本没有符合这个价格条件的,搜索时就没必要将北京显示在过滤条件中
自动补全
拼音分词器
安装方式和IK分词器一样
- 解压
- 上传到es的plugin目录
- 重启es
自定义分词器
拼音分词器会将词一个字一个字的转成拼音,而且没有中文,所以我们还要修改拼音分词器的配置
es中的分词器组成包含三部分
- character filters:在tokenizer之前对文本进行处理,例如删除字符,替换字符
- tokenizer:将文本按照一定规则切割成词条
- tokenizer filter:将tokenizer输出的词条做进一步处理,例如大小写转化,同义词处理,拼音处理等
可以在创建分词库时,通过setting来配置自定义的analyzer(分词器)
PUT /test
{
"settings": {
"analysis": {
"analyzer": { //自定义分词器
"my_ana":{ //自定义分词器的名称
"tokenizer": "ik_max_word",
"filter":"py"
}
},
"filter": { //自定义filter
"py":{ //filter名称
"type":"pinyin", //过滤器类型
"keep_full_pinyin":false, //下面都是对这个过滤器自定义的配置
"keep_joined_full_pinyin":true,
"keep_original":true,
"limit_first_letter_length":16,
"remove_duplicated_term":true,
"none_chinese_pinyin_tokenize":false
}
}
}
}
}
但是拼音分词器只适合在创建索引库时使用,为了避免搜索时搜到同音字,搜索一般用ik分词器,所以我们还需要指定搜索分词器
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "my_ana",
"search_analyzer": "ik_smart" //指定搜索分词器
}
}
}
DSL自动补全查询
es中提供了Completion Suggester
查询来实现自动补全功能,这个查询会匹配用户输入内容开头的词条并返回,为了提高补全查询效率,对于文档中字段的类型有一些约束
- 参与补全查询的字段类型必须是completion类型
- 字段值得是多词条的数组
// 自动补全查询
GET /索引库/_search
{
"suggest": {
"title_suggest": {
"text": "s", // 关键字
"completion": {
"field": "title", // 补全字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
数据同步
解决es和mysql数据一致性问题
微服务应用中,es搜索和修改数据库数据肯定在不同的服务模块中
- 方案一:同步调用
es搜索服务将接口暴露,在管理服务模块数据库新增数据完成后,调用es搜索服务更新es- 缺点:耦合,如果一个环节出现问题,整个接口都会报错
- 方案二:异步通知,结合MQ
管理服务新增完数据后,向mq发送消息,es搜索服务监听到消息,更新es- 依赖mq的可靠性
- 方案三:监听binlog
mysql中的binlog默认关闭,一旦开启,每当mysql数据变化,都会记录在binlog中,在用一个中间件监听mysql的binlog,数据发生变更就通知es搜索服务更新es- 实现复杂,且开启mysqlbinlog对mysql压力增大