Elasticsearch
elasticsearch 是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容,可以用来实现搜索、日志统计、分析、系统监控等功能。
一、倒排索引
首先,倒排索引的概念是基于 MySQL 这样的正向索引而言的。

- 倒排索引中有两个非常重要的概念:
- 文档(
Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息 - 词条(
Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
- 文档(
- 创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档 id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如 hash 表结构索引
-
倒排索引的搜索流程如下(以搜索"华为手机"为例)
- 用户输入条件
"华为手机"进行搜索 - 对用户输入内容分词,得到词条:
华为、手机 - 拿着词条在倒排索引中查找,可以得到包含词条的文档 id 有 1、2、3
- 拿着文档 id 到正向索引中查找具体文档
![]()
虽然要先查询倒排索引,再查询正向索引,但是词条和文档id 都建立了索引,查询速度非常快!无需全表扫描。
正向索引是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程
倒排索引则相反,是先找到用户要搜索的词条,根据得到的文档 id 获取该文档。是根据词条找文档的过程
- 用户输入条件
二、索引、映射、文档和字段
-
索引(Index),就是相同类型的文档的集合。
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;
因此,我们可以把索引当做是数据库中的表。
- 数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
- elasticsearch 是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为 json 格式后存储在 elasticsearch。
- JSON 文档中往往包含很多的字段(Field),类似于数据库中的列。
三、mysql和elasticsearch区别
| MySQL | Elasticsearch | 说明 |
|---|---|---|
| Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
| Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
| SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
四、IK分词器
ik_smart:智能切分,粗粒度ik_max_word:最细切分,细粒度- 扩展词词典
五、索引库操作
-
Mapping属性映射mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为 true
- analyzer:使用哪种分词器
- properties:该字段的子字段
PUT /索引库名称 { "mappings": { "properties": { "字段名":{ "type": "text", "analyzer": "ik_smart" }, "字段名2":{ "type": "keyword", "index": "false" }, "字段名3":{ "properties": { "子字段": { "type": "keyword" } } } // ...略 } } }
PUT /xn2001 { "mappings": { "properties": { "info":{ "type": "text", "analyzer": "ik_smart" }, "email":{ "type": "keyword", "index": "false" }, "name":{ "properties": { "firstName": { "type": "keyword" }, "lastName": { "type": "keyword" } } } } } }
![]()
特殊字段说明:
- location:地理坐标,里面包含精度、纬度
- all:一个组合字段,其目的是将多字段的值利用
copy_to合并,提供给用户搜索,
这样一来就只需要搜索一个字段就可以得到结果,性能更好。
PUT /hotel { "mappings": { "properties": { "id": { "type": "keyword" }, "name":{ "type": "text", "analyzer": "ik_max_word", "copy_to": "all" }, "address":{ "type": "keyword", "index": false }, "price":{ "type": "integer" }, "score":{ "type": "integer" }, "brand":{ "type": "keyword", "copy_to": "all" }, "city":{ "type": "keyword", "copy_to": "all" }, "starName":{ "type": "keyword" }, "business":{ "type": "keyword" }, "location":{ "type": "geo_point" }, "pic":{ "type": "keyword", "index": false }, "all":{ "type": "text", "analyzer": "ik_max_word" } } } }
- type:字段数据类型,常见的简单类型有:
-
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping,虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,不会对倒排索引产生影响。
PUT /索引库名/_mapping { "properties": { "新字段名":{ "type": "integer" } } }
-
删除索引库
DELETE /索引库名
-
查询索引库
GET /数据库名
六、DSL文档操作
-
新增文档
POST /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" } // ... }
POST /xn2001/_doc/1 { "info": "我不会Java", "email": "jialna@qq.com", "name": { "firstName": "钟", "lastName": "弟弟" } }
-
修改文档
PUT /{索引库名}/_doc/id { "字段1": "值1", "字段2": "值2", // ... 略 }全量修改是覆盖原来的文档,其本质是:
- 根据指定的 id 删除文档
- 新增一个相同 id 的文档
注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就是变成了新增操作
PUT /xn2001/_doc/1 { "info": "我也不会敲代码", "email": "3300123589@qq.com", "name": { "firstName": "弟弟", "lastName": "钟" } }
POST /{索引库名}/_update/文档id { "doc": { "字段名": "新的值", } }增量修改是只修改指定 id 匹配的文档中的部分字段
POST /xn2001/_update/1 { "doc": { "email": "update@qq.com" } }
-
查询文档
GET /{索引库名称}/_doc/{id} -
删除文档
DELETE /{索引库名}/_doc/{id}
七、RestClint文档操作
-
初始化RestClient
- 引入依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>注意:springBoot默认es版本会覆盖引入如的es依赖版本,需要覆盖默认es版本
- 引入依赖
-
初始化RestHighLevelClient
public class HotelIndexTest { private RestHighLevelClient restHighLevelClient; @Test void testInit(){ System.out.println(this.restHighLevelClient); }
// 初始化 @BeforeEach void init(){ this.restHighLevelClient = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://x.x.x.x:9200") )); } @AfterEach void down() throws IOException { this.restHighLevelClient.close(); } } -
创建索引库
@Test void createHotelIndex() throws IOException { //指定索引库名 CreateIndexRequest hotel = new CreateIndexRequest("hotel"); //写入JSON数据,这里是Mapping映射 hotel.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON); //创建索引库 restHighLevelClient.indices().create(hotel, RequestOptions.DEFAULT); }
public class HotelConstants { public static String MAPPING_TEMPLATE = "{\n" + " \"mappings\": {\n" + " \"properties\": {\n" + " \"id\": {\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"name\":{\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"address\":{\n" + " \"type\": \"keyword\",\n" + " \"index\": false\n" + " },\n" + " \"price\":{\n" + " \"type\": \"integer\"\n" + " },\n" + " \"score\":{\n" + " \"type\": \"integer\"\n" + " },\n" + " \"brand\":{\n" + " \"type\": \"keyword\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"city\":{\n" + " \"type\": \"keyword\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"starName\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"business\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"location\":{\n" + " \"type\": \"geo_point\"\n" + " },\n" + " \"pic\":{\n" + " \"type\": \"keyword\",\n" + " \"index\": false\n" + " },\n" + " \"all\":{\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}"; }
-
删除索引库
@Test void deleteHotelIndex() throws IOException { DeleteIndexRequest hotel = new DeleteIndexRequest("hotel"); restHighLevelClient.indices().delete(hotel,RequestOptions.DEFAULT); }
-
判断索引库
@Test void existHotelIndex() throws IOException { GetIndexRequest hotel = new GetIndexRequest("hotel"); boolean exists = restHighLevelClient.indices().exists(hotel, RequestOptions.DEFAULT); System.out.println(exists); }
-
新增文档
@Test void createHotelIndex() throws IOException { Hotel hotel = hotelService.getById(61083L); HotelDoc hotelDoc = new HotelDoc(hotel); // 1.准备Request对象 IndexRequest hotelIndex = new IndexRequest("hotel").id(hotelDoc.getId().toString()); // 2.准备Json文档 hotelIndex.source(JSON.toJSONString(hotelDoc), XContentType.JSON); // 3.发送请求 restHighLevelClient.index(hotelIndex, RequestOptions.DEFAULT); }
-
查询文档
@Test void testGetDocumentById() throws IOException { // 1.准备Request GetRequest hotel = new GetRequest("hotel", "61083"); // 2.发送请求,得到响应 GetResponse hotelResponse = restHighLevelClient.get(hotel, RequestOptions.DEFAULT); // 3.解析响应结果 String hotelDocSourceAsString = hotelResponse.getSourceAsString(); // 4.json转实体类 HotelDoc hotelDoc = JSON.parseObject(hotelDocSourceAsString, HotelDoc.class); System.out.println(hotelDoc); }
-
删除文档
@Test void testDeleteDocumentById() throws IOException { DeleteRequest hotel = new DeleteRequest("hotel", "61083"); restHighLevelClient.delete(hotel,RequestOptions.DEFAULT); }
-
修改文档:增量修改
@Test void testUpdateDocument() throws IOException { // 1.准备Request UpdateRequest request = new UpdateRequest("hotel", "61083"); // 2.准备请求参数 request.doc( "price", "952", "starName", "四钻" ); // 3.发送请求 restHighLevelClient.update(request, RequestOptions.DEFAULT); }
-
批量导入
八、DSL文档查询
-
查询所有:查询出所有数据,一般测试用。例如:match_all
// 查询所有 GET /indexName/_search { "query": { "match_all": { } } }
-
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query:单字段查询
GET /indexName/_search { "query": { "match": { "FIELD": "TEXT" } } }
GET /hotel/_search { "query": { "match": { "all": "7天酒店" } } }
- multi_match_query:多字段查询,任意一个字段符合条件就算符合查询条件
注意:搜索字段越多,对查询性能影响越大,因此建议采用 copy_to 将多个字段合并为一个,然后使用单字段查询的方式。GET /indexName/_search { "query": { "multi_match": { "query": "TEXT", "fields": ["FIELD1", " FIELD12"] } } }
GET /hotel/_search { "query": { "multi_match": { "query": "7天酒店", "fields": ["brand","name"] } } }
- match_query:单字段查询
-
精确查询:一般是查找 keyword、数值、日期、boolean 等类型字段。所以不会对搜索条件分词。
- term:根据词条精确值查询
// term查询 GET /indexName/_search { "query": { "term": { "FIELD": { "value": "VALUE" } } } }
GET /hotel/_search { "query": { "term": { "brand": { "value": "7天酒店" } } } }
- range:根据值的范围查询
// range查询 GET /indexName/_search { "query": { "range": { "FIELD": { "gte": 10, // 这里的gte代表大于等于,gt则代表大于 "lte": 20 // lte代表小于等于,lt则代表小于 } } } }
- term:根据词条精确值查询
-
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
![]()
function score 的运行流程如下:- 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- 根据过滤条件,过滤文档
- 符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
- 将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
子查询的组合方式有:GET /hotel/_search { "query": { "bool": { "must": [ {"term": {"city": "上海" }} ], "should": [ {"term": {"brand": "皇冠假日" }}, {"term": {"brand": "华美达" }} ], "must_not": [ { "range": { "price": { "lte": 500 } }} ], "filter": [ { "range": {"score": { "gte": 45 } }} ] } } }
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
- 搜索框的关键字搜索,是全文检索查询,使用 must 查询,参与算分
- 其它过滤条件,采用 filter 查询,不参与算分
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
-
相关性算分
在后来的5.1版本升级中,elasticsearch 将算法改进为 BM25 算法,公式如下:![]()
九、搜索结果处理
-
排序:elasticsearch 默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等
GET /indexName/_search { "query": { "match_all": {} }, "sort": [ { "FIELD": "desc" // 排序字段、排序方式ASC、DESC } ] }
-
分页:elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。
- elasticsearch 通过修改 from、size 参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
GET /hotel/_search { "query": { "match_all": {} }, "from": 0, // 分页开始的位置,默认为0 "size": 10, // 期望获取的文档总数 "sort": [ {"price": "asc"} ] }
- 深度分页
注意:elasticsearch 内部分页时,必须先查询 0~1000条,然后截取其中的 990 ~ 1000 的这10条
GET /hotel/_search { "query": { "match_all": {} }, "from": 990, // 分页开始的位置,默认为0 "size": 10, // 期望获取的文档总数 "sort": [ {"price": "asc"} ] }
- 分页查询的常见实现方案以及优缺点
from + size- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
- elasticsearch 通过修改 from、size 参数来控制要返回的分页结果:
-
高亮:我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示
注意:GET /hotel/_search { "query": { "match": { "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询 } }, "highlight": { "fields": { // 指定要高亮的字段 "FIELD": { "pre_tags": "<em>", // 用来标记高亮字段的前置标签 "post_tags": "</em>" // 用来标记高亮字段的后置标签 } } } }
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:
required_field_match=false
十、RestClient文档查询
-
发起查询请求
@Test public void match_All() throws IOException { SearchRequest request = new SearchRequest("hotel"); request.source() .query(QueryBuilders.matchAllQuery()); SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); }
- 第一步,创建
SearchRequest对象,指定索引库名 - 第二步,利用
request.source()构建 DSL,DSL 中可以包含查询、分页、排序、高亮等query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个 match_all 查询的 DSL
request.source(),其中包含了查询、排序、分页、高亮等所有功能QueryBuilders,其中包含 matchAllQuery、match、term、function_score、bool 等各种查询- 第三步,利用
client.search()发送请求,得到响应
- 第一步,创建
-
match查询
public void matchQuery() throws IOException { SearchRequest request = new SearchRequest("hotel"); request.source() .query(QueryBuilders.matchQuery("all","如家")); SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); SearchHits searchHits = response.getHits(); System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { String sourceAsString = hit.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); System.out.println(hotelDoc); } }
-
精确查询、布尔查询
void testBool() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest("hotel"); // 2.准备DSL request.source() .query( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("city", "上海")) .filter(QueryBuilders.rangeQuery("price").lte(300)) ); // 3.发送请求 SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); // 4.解析响应 SearchHits searchHits = response.getHits(); System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { String sourceAsString = hit.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); System.out.println(hotelDoc); } }
-
排序、分页
void testPageAndSort() throws IOException { // 页码,每页大小 int page = 1, size = 5; // 1.准备Request SearchRequest request = new SearchRequest("hotel"); // 2.准备DSL // 2.1.query request.source().query(QueryBuilders.matchAllQuery()); // 2.2.排序 sort request.source().sort("price", SortOrder.ASC); // 2.3.分页 from、size request.source().from((page - 1) * size).size(5); // 3.发送请求 SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); // 4.解析响应 SearchHits searchHits = response.getHits(); System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { String sourceAsString = hit.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); System.out.println(hotelDoc); } }
-
高亮
void testHighlight() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest("hotel"); // 2.准备DSL // 2.1.query request.source().query(QueryBuilders.matchQuery("all", "如家")); // 2.2.高亮 request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false)); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); }
十一、DSL数据聚合
聚合常见的有三类
-
桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
GET /hotel/_search { "size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果 "aggs": { // 定义聚合 "brandAgg": { //给聚合起个名字 "terms": { // 聚合的类型,按照品牌值聚合,所以选择term "field": "brand", // 参与聚合的字段 "size": 20 // 希望获取的聚合结果数量 } } } }
-
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求 max、min、avg、sum 等
GET /hotel/_search { "size": 0, "aggs": { "brandAgg": { "terms": { "field": "brand", "size": 20,
"order": {
"score_stats.avg": "asc" // 按照score_stats.avg升序排列
}
}, "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算 "score_stats": { // 聚合名称 "stats": { // 聚合类型,这里stats可以计算min、max、avg等 "field": "score" // 聚合字段,这里是score } } } } } }
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
十二、RestAPI数据聚合
聚合条件与 query 条件同级别,因此需要使用 request.source() 来指定聚合条件
-
public void testAggregation() throws IOException { SearchRequest request = new SearchRequest("hotel"); request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(20)); SearchResponse response = client.search(request, RequestOptions.DEFAULT); Terms brandAgg = response.getAggregations().get("brandAgg"); List<? extends Terms.Bucket> buckets = brandAgg.getBuckets(); for (Terms.Bucket bucket : buckets) { String key = bucket.getKeyAsString(); System.out.println("key = " + key); } }
十三、数据同步
常见的数据同步方案有三种
- 同步调用
- 异步通知
- 监听 binlog





浙公网安备 33010602011771号