Elasticsearch
一:es 与 mysql对比
mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
正向索引和倒排索引
简言之,由文档查询关键字的过程,为正排索引
与正排索引相反,由关键字查询文档的过程,为倒排索引
elasticsearch 采用倒排索引:
-
文档(document):每条数据就是一个文档
-
词条(term):文档按照语义分成的词语
二:分词器
es 在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。
-
分词器的作用
-
创建倒排索引时对文档分词
-
用户搜索时就,对输入的内容分词
-
-
ik分词器:ik_smart和ik_max_word
-
ik_smart:最少切分
-
ik_max_word:最细切分
-
POST /_analyze { "analyzer": "standard / ik_smart", # 分词 "text": "你干嘛" }
语法说明:
-
POST:请求方式
-
/analyzes:请求路径,这里省略了http://192.168.200.130:5601/,由kibana为我们提供
-
请求参数,json风格:
-
analyzer:分词器类型,这里是默认的standard
-
text:要分词的类容
-
三:索引库操作
1. mapping属性
mapping 是对索引库中文档的约束,常见的mapping属性包括:
-
type:字段数据类型,常见的类型有:
-
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
-
数值:long、integer、short、byte、double、float
-
布尔:boolean
-
日期:date
-
对象:object
-
-
index:是否创建索引,默认为true
-
analyzer:使用哪种分词器,只有text使用
-
properties:该字段的子字段
-
ES 中支持两种地理坐标数据类型:
-
geo_point:由纬度 和 经度确定的一个点 "32.58859, 120.34343"
-
geo_shape:有多个geo_point 组成的复杂几何图形。例如一条直线,"LINESTRING(-77.0333 38.45454, -77.000949 38.88998)"
-
-
字段拷贝可以使用 copy_to 属性将当前字段拷贝到指定字段。
"all": { "type": "text", "analyzer": "ik_max_word" }, "brand": { "type": "keyword", "copy_to": "all" }
2. 创建索引库
ES 中通过 Restful 请求操作索引库、文档。请求内容用DSL语句表示。创建索引库和mapping的DSL语法如下:
PUT /索引库名称 { "mappings": { "properties": { "字段名": { "type": "text", "analyzer": "ik_smart" }, "字段名2": { "type": "keyword", "index": "false" }, "字段名3": { "properties" : { "子字段": { "type": "keyword" } } }, //...略 } } }
3. 查看、删除索引库
查看索引库语法:
GET /索引库名 // 查看索引库结构 GET /索引库名/_mapping
删除索引库的语法:
DELETE /索引库名
改索引库:
索引库 和 mapping 一旦创建无法修改,但是可以添加新的字段,如下:
PUT /索引库名/_mapping { "properteis": { "新字段名": { "type": "integer" } } }
4. 文档的增删查
-
新增文档的 DSL 语法如下:
POST /索引库名/_doc/文档id { "字段1": "值1", "字段2”: "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" }, //... }
-
查看文档
GET /索引库名/_doc/文档id GET /hotel/_doc/_search #查询所有数据
-
删除文档
DELETE /索引库名/_doc/文档id
5. 修改文档
-
方式一:全量修改,会删除旧文档,添加新文档
PUT /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2", //... }
-
方式二:增量修改,修改指定字段值
POST /索引库名/_update/文档id { "doc": { "字段名": "新的值", } }
四:RestClient 操作索引库
1. 初始化
-
引入es的 RestHighLevelClient 依赖:
<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:
RestHignLevelClient client = new RestHignLevelClient(RestClient.builder(HttpHost.create("http://47.108.239.188:9200")))
2. 索引库操作的基本步骤
-
初始化 RestHighLevelClient
-
创建XxxIndexRequest。xxxclient.indices().exists(request, RequestOptions.DEFAULT);是CREATE、Get、Delete
-
准备 DSL (CREATE 时需要)
-
发送请求。调用 RestHighLevelClient.indices().xxx方法
3. 利用 javaRestClient 批量导入数据到es
-
利用 JavaRestClient 中的 Bulk 批处理,实现批量新增文档
// 1、批量查询数据库数据 List<Hotel> list = hotelService.list(); // 2、创建 request 对象 BulkRequest request = new BulkRequest(); // 3、转换文档格式 for (Hotel hotel : list) { HotelDoc hotelDoc = new HotelDoc(hotel); request.add(new IndexRequest("hotel") .id(hotel.getId().toString()) .source(JSON.toJSONString(hotelDoc), XContentType.JSON)); } // 4、发送请求 client.bulk(request, RequestOptions.DEFAULT);
五:DSL查询文档
Elasticsearch 提供了基于JSON的DSL 来定义查询。常见的查询类型包括:
-
查询所有:查询出所欲的数据,例如:match_all
-
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
-
match_query
-
multi_match_query
-
-
精确查询:根据精确词条值查找数据,一般是 查找keyword、数值、日期、boolean等类型字段。例如:
-
ids
-
range
-
term
-
-
地理(geo)查询:根据经纬度查询。例如:
-
geo_distance
-
geo_bounding_box
-
-
复合(compound)查询:将上述各种查询条件组合起来,合并查询条件。例如:
-
bool
-
function_score
-
1. DSL Query基本语法
查询的基本语法如下:
GET /indexName/_search { "query": { "查询类型": { "查询条件": "条件值" } } }
-
查询所有
GET /indexName/_search { "query": { "match_all": { } } }
2. 全文检索查询
全文检索查询,会对用户输入内容分词,常用于搜索框搜索
2.1 math查询
全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:
GET /indexName/_search { "query": { "match": { "FIELD": "TEXT" } } }
2.2 multi_match 查询
GET /indexName/_search { "query": { "multi_match": { "query": "北京如家", "fields": ["brand", "name", "business"] } } }
match 和 multi_match 的区别:
-
match:根据一个字段查询
-
multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
3. 精确查询
根据精确词条值查找数据,一般是 查找keyword、数值、日期、boolean等类型字段。
-
term:根据词条精确值查询
-
range:根据值得范围查询
3.1 term查询
GET /indexName/_search { "query": { "term": { "FIELD": { "value": "值" } } } }
3.2 range查询
GET /indexName/_search { "query": { "range": { "FIELD": { "gte": 10, #大于等于(gt 大于) "lte": 20 #小于等于(lt 小于) } } } }
4. 地理查询
根据经纬度查询
-
geo_bounding_box:查询geo_point 值落在某个矩形范围得所有文档
GET /indexName/_search { "query": { "geo_bounding_box": { "FIELD": { "top_left": { "lat": 31.1, "lon": 121.5 }, "bottom_right": { "lat": 30.9, #lat 经度 "lon": 121.7 #lon 纬度 } } } } }
-
geo_distance:查询到指定中心点小于某个距离值的所有文档
GET /indexName/_search { "query": { "geo_distance": { "distance": "15km", "FIELD": "31.21, 121.5" } } }
5. 复合查询
复合查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑,例如:
-
function score:算分函数查询,可以控制文档相关性算分,控制文档排名
当我们利用match 查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
es 中的相关性打分算法:
-
TF-IDF:在es5.0之前,会随着词频增加而越来越大
-
BM25:在es5.0之后,会随着词频增加而增大,但增长曲线会趋于水平
5.1 Function Score Query
使用 function score query,可以修改文档的相关性算法,根据新得到的算分排序
GET /hotel/_search { "query": { "function_score": { "query": { "match": { "all": "外滩" } }, "functions": [ { "filter": { "term": { "brand": "如家" } }, "weight": 10 } ], "boost_mode": "sum" } } }
5.2 function score query 定义的三要素
-
过滤条件:哪些文档要加分
-
算分函数:如何计算function score
-
加权方式:function score 与 query score 如何运算
5.3 Boolean Query
布尔查询是一个或多个查询子句的组合。子查询的组合方式有:
-
must:必须匹配每个子查询,类似"与"
-
should:选择性匹配子查询,类似"或"
-
must_not:必须不匹配,不参与算分,类似"非"
-
filter:必须匹配,不参与算分
GET /hotel/_search { "query": { "bool": { "must": [ {"term": {"city": "上海"}} ], "should": [ {"term": { "brand": "xxx" }} ] } } }
六:搜索结果处理
1. 排序
es 支持对搜索结果排序,默认是根据相关度算法(——xcore)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
GET /indexName/_search { "query": { "match_all": {} "sort":[ { "FIELD": "desc" //asc、desc } ] } }
地理排序:
GET /indexName/_search { "query": { "match_all": {} "sort":[ { "_geo_distance": { "FIELD": "经度,纬度", //"FIELD": { "lat": 经度 "lon": 纬度 } "order": "asc", "unit": "km" } } ] } }
2. 分页
es 默认情况下只返回top10的数据。而如果要查询更多数据就要修改分页参数。
es 中通过修改from、size参数来控制要返回的分页结果。
GET /indexName/_search { "query": { "match_all": {} }, "from": 10, //分页开始的位置,默认为0 "size": 10, //期望获取的文档总数 "sort":[ {"price": "asc"} ] }
-
深度分页问题
正在上传…重新上传取消
针对深度分页,es 提供了两种解决方案:
-
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。
-
scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
3. 分页总结
from + size:
-
优点:支持随机翻页
-
缺点:深度分页问题,默认查询上限(from + size)是10000
-
场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search:
-
优点:没有查询上限(单次查询的size不超过10000)
-
缺点:只能向后逐页查询,不支持随机翻页
-
场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:
-
优点:没有查询上限(单次查询的size不超过10000)
-
缺点:会有额外内存消耗,并且搜索是非实时的
-
场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案
4. 高亮
高亮:就是在搜索结果中把搜索关键字突出显示。
原理:
-
将搜索结果中的关键字用标签标记出来
-
在页面中给标签添加css样式
语法:
# 高亮查询,默认情况下,es搜索字段必须与高亮字段一致 GET /hotel/_search { "query": { "match": { "FIELD": "TEXT" } }, "highlight": { "fields": { "FIELD": { //指定要高亮的字段 "pre_tags": "<em>", //用来标记高亮字段的前置标签 "post_tags": "</em>", //用来标记高亮字段的后置标签 "require_field_match": "false" //是否需要与搜索字段匹配 } } } }
七:RestClient 查询文档
1. 快速入门
// 1.准备Request SearchRequest request = new SearchRequest("hotel"); // 2.组织DSL参数 request.source().query(QueryBuilders.matchAllQuery()); // 3.发送请求,得到响应结果 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // ...解析响应结果 // 4.解析结果 SearchHits searchHits = response.getHits(); // 4.1 查询的总条数 long total = searchHits.getTotalHits().value; // 4.2 查询的结果数组 SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { // 4.3 得到source String json = hit.getSourceAsString(); // 4.4 打印 System.out.println(json); }
2. 全文检索查询
全文检索的match 和 multi_match 查询与match_all的API基本一致。差别是查询条件,也就是query的部分。
// 单字段查询 QueryBuilders.matchQuery("all", "如家"); // 多字段查询 QueryBuilders.multiMatchQuery("如家", "name", "business");
3. 精确查询
// 词条查询 QueryBuilders.termQuery("city", "杭州"); // 范围查询 QueryBuilders.rangeQuery("price").get(100).lte(150);
4. 复合查询
4.1 boolean query
// 创建布尔查询 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 添加must条件 boolQuery.must(QueryBuilders.termQuery("city", "杭州")); // 添加filter条件 boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
5. 排序和分页
搜索结果的排序和分页是与query同级的参数,对应的API如下:
// 分页 request.source().from(0).size(5); // 价格排序 request.source().sort("price", SortOrder.ASC); //分页排序 request.source().sort(SortBuilders .geoDistanceSort("location", new GeoPoint("33.21, 132.1")) .order(SortOrder.ASC) .unit(DistanceUnit.KILOMETERS) );
6. 高亮
高亮API包括请求DSL构建和结果解析两部分。
// DSL构建 request.source().highlighter(new HighlightBuilder().field("name").requireFieldMathch(false));
// 结果解析 // 获取source HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class); // 处理高亮 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if (!CollectionUtils.isEmpty(highlightFields)) { // 获取高亮字段结果 HighlightField highlightField = highlightFields.get("name"); if (highlightField != null) { // 取出高亮结果数组中的第一个,就是名称 String name = highlightField.getFragments()[0].string(); hotelDoc.setName(name); } }
7. 组合查询
// function score FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery( QueryBuilders.matchQuery("name", "外滩"), new FunctionScoreQueryBuilder.FilterFunctionBuilder[]( new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery("brand", "如家"), scoreFunctionBuilders.weightFactorFunction(5) ) ) ); request.source.query(functionScoreQueryBuilder);
八. 数据聚合
聚合 可以实现对文档数据的统计、分析、运算。。
参与聚合的字段类型必须是:
-
keyword
-
数值
-
日期
-
布尔
1. 聚合的分类
-
桶(Bucket)聚合:用来对文档分组
-
TermAggregation:按照文档字段值分组
-
Date Histogram:按照日期阶梯分组,例如一周为一组,一月为一组
-
-
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值
-
Avg:求平均值
-
Max:求最大值
-
Min:求最小值
-
Stats:同时求max、min、avg、sum 等
-
-
管道(Pipeline)聚合:其它聚合的结果为基础做聚合
2. 聚合
2.1 DSL 实现Bucket聚合
-
Bucket 聚合-限定聚合范围
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围。只要添加query条件即可:
GET /hotel/_search { "query": { "range": { "price": { "lte": 200 //只对200元以下的文档聚合 } } } "size": 0, //设置size为0,结果中不包含文档,只包含聚合结果 "aggs": { //定义聚合 "brandAgg": { //给聚合起个名字 "terms": { //聚合的类型,按照品牌值聚合,所以选择term "field": "brand", "order": { "_count": "asc" //按照_count升序排序 }, "size": 20 //希望获取的聚合结果数量 } } } }
2.2 DSL 实现Metrics 聚合
"size": 0, "aggs": { "brandAgg": { "terms": { "field": "brand", "size": 20 }, "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算 "score_stats": { // 聚合名称 "stats": { //聚合类型,这里stats 可以计算min、max、avg等 field": "score" //聚合字段,这里是score } } } } } }
3. RestAPI 实现聚合
request.source().size(0); request.source().aggregation( AggregationBuilders .terms("brand_agg") .field("brand") .size(20) ); request.source().aggregation( AggregationBuilders .terms("star_agg") .field("star") .size(20) );
//解析聚合结果 Aggregations aggregations = response.getAggregations(); //根据名称获取聚合结果 Terms brandTerms = aggregation.get("brand_agg"); //获取桶 List<? extends Terms.Bucket> buckets = brandTerms.getBuckets(); //遍历 for (Terms.Bucket bucket: buckets) { //获取key,也就是品牌信息 String brandName = bucket.getKeyAsString(); }
九. 自动补全
1. 分词器
es 中的分词器(analyzer) 的组成包含三部分:
-
character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
-
tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
-
tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
-
拼音分词器
安装插件
-
自定义分词器
我们可以在创建索引库时,通过settings 来配置自定义的analyzer(分词器):
PUT /test { "settings": { "analysis": { "analyzer": { // 自定义分词器 "my_analyzer": { // 分词器名称 "tokenizer": "ik_max_word", "filter": "pinyin" } } } } }
进一步定制:
PUT /test { "settings": { "analysis": { "analyzer": { // 自定义分词器 "my_analyzer": { // 分词器名称 "tokenizer": "ik_max_word", "filter": "py" } }, "filter": { //自定义tokenizer filter "py": { //过滤器名称 "type": "pinyin", //过滤器类型,这里是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 } } } } }
字段在创建倒排索引时应该用my_analyzer 分词器;字段在搜索时应该使用ik_smart 分词器,避免搜索文字时搜出同音的词,如"狮子",搜出"狮子"和"虱子"
PUT /test { "settings": { "analysis": { "analyzer": { // 自定义分词器 "my_analyzer": { // 分词器名称 "tokenizer": "ik_max_word", "filter": "py" } }, "filter": {...} }, "mappings": { "properties": { "name": { "type": "text", "analyzer": "my_analyzer", "search_analyzer": "ik_smart" } } } }
2. 自动补全查询
completion suggester 查询
es 提供了 Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
-
参与补全查询的字段必须是 completion类型。
-
字段的内容一般是用来补全的多个词条形成的数组。
// 创建索引库 PUT test { "mappings": { "properties": { "title": { "type": "completion" } } } } // 示例数据 POST test/_doc { "title": ["Sony", "wh-1000XM3"] } POST test/_doc { "title": ["SK-II", "PITERA"] } POST test/_doc { "title": ["Nintendo", "switch"] }
查询语法如下
// 自动补全查询 GET /test/_search { "suggest": { "title_suggest": { //自定义名字 "text": "s", //关键字 "completion": { "field": "title", // 补全查询的字段 "skip_duplicates": true, //跳过重复的 "size": 10 } } } }
3. RestAPI 实现自动补全
SearchRequest request = new SearchRequest("hotel"); request.source() .suggest(new SuggestBuilder().addSuggestion( "mySuggestion", SuggestBuilders .completionSuggestion("suggestion") .prefix("h") .skipDuplicates(true) .size(10) )); SearchResponse response = client.search(request, RequestOptions.DEFAULT); //解析结果 Suggest suggest = response.getSuggest(); CompletionSuggestion suggestion = suggest.getSuggestion("mySuggestion"); for (CompletionSuggestion.Entry.Option option: suggestion.getOptions()) { String text = option.getText().string(); }
十. 数据同步
1. 数据同步问题分析
es 中的数据来自mysql数据库,因此mysql数据发生改变时,es也必须跟着改变,这个就是es与mysql之间的数据同步。
方案一:同步调用
方案二:异步通知
方案三:监听binlog
总结
方式一:同步调用
-
优点:实现简单,粗暴
-
业务耦合度高
方式二:异步通知
-
优点:低耦合,实现难度一般
-
缺点:依赖mq的可靠性
方式三:监听binlog
-
优点:完全解除服务间耦合文章来源:https://uudwc.com/A/E00m
-
缺点:开启binlog增加数据库负担、实现复杂度高文章来源地址https://uudwc.com/A/E00m