elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
例如:
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
倒排索引的概念是基于MySQL这样的正向索引而言的。
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:
如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
1)用户搜索数据,条件是title符合"%手机%"
2)逐行获取数据,比如id为1的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
倒排索引中有两个非常重要的概念:
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条创建倒排索引是对正向索引的一种特殊处理,流程如下:
如图:
倒排索引的搜索流程如下(以搜索"华为手机"为例):
1)用户输入条件"华为手机"
进行搜索。
2)对用户输入内容分词,得到词条:华为
、手机
。
3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。
如图:
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引:
倒排索引:
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
elasticsearch是面向**文档(Document)**存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:
而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
索引(Index),就是相同类型的文档的集合。
例如:
因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
我们统一的把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 |
是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长支出:
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
mapping是对索引库中文档的约束,常见的mapping属性包括:
例如下面的json文档:
{"age": 21,"weight": 52.1,"isMarried": false,"info": "Java讲师","email": "zy@itcast.cn","score": [99.1, 99.5, 98.9],"name": {"firstName": "云","lastName": "赵"}
}
对应的每个字段映射(mapping):
这里我们统一使用Kibana编写DSL的方式来演示。
格式:
PUT /索引库名称
{"mappings": {"properties": {"字段名":{"type": "text","analyzer": "ik_smart"},"字段名2":{"type": "keyword","index": "false"},"字段名3":{"properties": {"子字段": {"type": "keyword"}}},// ...略}}
}
PUT /heima
{"mappings": {"properties": {"info":{"type": "text","analyzer": "ik_smart"},"email":{"type": "keyword","index": false},"name":{"type": "object","properties": {"firstName": {"type":"keyword"},"lastName": {"type":"keyword"}}}}}
}
基本语法:
请求方式:GET
请求路径:/索引库名
请求参数:无
格式:
GET /索引库名
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
PUT /索引库名/_mapping
{"properties": {"新字段名":{"type": "integer"}}
}
例子
PUT /heima/_mapping
{"properties":{"age":{"type":"integer"}}
}
示例:
语法:
请求方式:DELETE
请求路径:/索引库名
请求参数:无
格式:
DELETE /索引库名
在kibana中测试:
索引库操作有哪些?
语法:
POST /索引库名/_doc/文档id
{"字段1": "值1","字段2": "值2","字段3": {"子属性1": "值3","子属性2": "值4"},// ...
}
示例:
POST /heima/_doc/1
{"info": "Java讲师","email": "zy@itcast.cn","name": {"firstName": "云","lastName": "赵"}
}
响应:
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引库名称}/_doc/{id}
通过kibana查看数据:
GET /heima/_doc/1
```在这里插入图片描述**查看结果:**

## 3.3.删除文档删除使用DELETE请求,同样,需要根据id进行删除:**语法:**```js
DELETE /{索引库名}/_doc/id值
示例:
# 根据id删除数据
DELETE /heima/_doc/1
结果:
修改有两种方式:
全量修改是覆盖原来的文档,其本质是:
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
PUT /{索引库名}/_doc/文档id
{"字段1": "值1","字段2": "值2",// ... 略
}
示例:
PUT /heima/_doc/1
{"info": "高级Java讲师","email": "zy@itcast.cn","name": {"firstName": "云","lastName": "赵"}
}
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id
{"doc": {"字段名": "新的值",}
}
示例:
POST /heima/_update/1
{"doc": {"email": "ZhaoYun@itcast.cn"}
}
文档操作有哪些?
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种:
我们学习的是Java HighLevel Rest Client客户端API
首先导入课前资料提供的数据库数据:
数据结构如下:
CREATE TABLE `tb_hotel` (`id` bigint(20) NOT NULL COMMENT '酒店id',`name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',`address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',`price` int(10) NOT NULL COMMENT '酒店价格;例:329',`score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',`brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',`city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',`star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',`business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',`latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',`longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',`pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
然后导入课前资料提供的项目:
项目结构如图:
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
其中:
来看下酒店数据的索引库结构:
# 酒店的mapping
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"},"startName":{"type": "keyword"},"business":{"type": "keyword","copy_to": "all"},"location":{"type": "geo_point"},"pic":{"type": "keyword","index": false},"city":{"type": "keyword","copy_to": "all"},"all":{"type": "text","analyzer": "ik_max_word"}}}
}
几个特殊字段说明:
地理坐标说明:
copy_to说明:
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
分为三步:
1)引入es的RestHighLevelClient依赖:
org.elasticsearch.client elasticsearch-rest-high-level-client
2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
1.8 7.12.1
3)初始化RestHighLevelClient:
初始化的代码如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")
));
这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:
public class HotelIndexTest {private RestHighLevelClient client;@Testvoid testInit(){System.out.println(client);}@BeforeEachvoid setUp(){this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.88.169:9200")));}@AfterEachvoid tearDown() throws IOException{this.client.close();}
}}
创建索引库的API如下:
代码分为三步:
在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
public class HotelConstants {public static final 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" +" \"startName\":{\n" +" \"type\": \"keyword\"\n" +" },\n" +" \"business\":{\n" +" \"type\": \"keyword\",\n" +" \"copy_to\": \"all\"\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" +"}";
}
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现创建索引:
// 创建索引库@Testvoid createHotelIndex(){// 1.创建request对象CreateIndexRequest request = new CreateIndexRequest("hotel");// 2.准备请求的参数,DSL语句request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);// 3.发送请求try {IndicesClient indices = client.indices();indices.create(request, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();}}
删除索引库的DSL语句非常简单:
DELETE /hotel
与创建索引库相比:
所以代码的差异,注意体现在Request对象上。依然是三步走:
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现删除索引:
// 删除索引库@Testvoid deleteHotelIndex(){// 1.创建request对象DeleteIndexRequest request = new DeleteIndexRequest("hotel");// 2.发送请求try {IndicesClient indices = client.indices();indices.delete(request, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();}}
判断索引库是否存在,本质就是查询,对应的DSL是:
GET /hotel
因此与删除的Java代码流程是类似的。依然是三步走:
// 判断索引库是否存在@Testvoid existsHotelIndex(){// 1.创建request对象GetIndexRequest request = new GetIndexRequest("hotel");// 2.发送请求try {IndicesClient indices = client.indices();boolean exists = indices.exists(request, RequestOptions.DEFAULT);// 3.输出System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");} catch (IOException e) {e.printStackTrace();}}
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:
我们要将数据库的酒店数据查询出来,写入elasticsearch中。
数据库查询后的结果是一个Hotel类型的对象。结构如下:
@Data
@TableName("tb_hotel")
public class Hotel {@TableId(type = IdType.INPUT)private Long id;private String name;private String address;private Integer price;private Integer score;private String brand;private String city;private String starName;private String business;private String longitude;private String latitude;private String pic;
}
与我们的索引库结构存在差异:
因此,我们需要定义一个新的类型,与索引库结构吻合:
package cn.itcast.hotel.pojo;import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
public class HotelDoc {private Long id;private String name;private String address;private Integer price;private Integer score;private String brand;private String city;private String starName;private String business;private String location;private String pic;public HotelDoc(Hotel hotel) {this.id = hotel.getId();this.name = hotel.getName();this.address = hotel.getAddress();this.price = hotel.getPrice();this.score = hotel.getScore();this.brand = hotel.getBrand();this.city = hotel.getCity();this.starName = hotel.getStarName();this.business = hotel.getBusiness();this.location = hotel.getLatitude() + ", " + hotel.getLongitude();this.pic = hotel.getPic();}
}
新增文档的DSL语句如下:
POST /{索引库名}/_doc/1
{"name": "Jack","age": 21
}
对应的java代码如图:
可以看到与创建索引库类似,同样是三步走:
变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。
我们导入酒店数据,基本流程一致,但是需要考虑几点变化:
因此,代码整体步骤如下:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
// 新增文档// 新增文档@Testvoid testAddDocument() {// 根据id查询酒店数据Hotel hotel = hotelService.getById(61083l);HotelDoc hotelDoc = new HotelDoc(hotel);// 1.准备Request对象IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());// 2.准备Json文档request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);// 3.发送请求try {client.index(request, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();}}
查询的DSL语句如下:
GET /hotel/_doc/{id}
非常简单,因此代码大概分两步:
不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:
可以看到,结果是一个JSON,其中文档放在一个_source
属性中,因此解析就是拿到_source
,反序列化为Java对象即可。
与之前类似,也是三步走:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
// 查询文档@Testvoid testGetDocumentById() {// 1.准备Request对象GetRequest request = new GetRequest("hotel").id("61083");try {// 2.发送请求,得到结果GetResponse response = client.get(request, RequestOptions.DEFAULT);// 3.解析结果String json = response.getSourceAsString();HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);System.out.println(hotelDoc);} catch (IOException e) {e.printStackTrace();}}
删除的DSL为是这样的:
DELETE /hotel/_doc/{id}
与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
// 删除文档@Testvoid testDeleteDocument() {// 1.准备RequestDeleteRequest request = new DeleteRequest("hotel", "61083");// 2.发送请求try {client.delete(request, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();}}
修改我们讲过两种方式:
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
这里不再赘述,我们主要关注增量修改。
代码示例如图:
与之前类似,也是三步走:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
// 修改文档@Testvoid testUpdateDocument() {// 1.准备Request对象UpdateRequest request = new UpdateRequest("hotel", "61083");try {// 2.发送请求,得到结果request.doc("price", "952","starName", "四钻");// 3.发送请求client.update(request, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();}}
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
步骤如下:
利用mybatis-plus查询酒店数据
将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
利用JavaRestClient中的BulkRequest批处理,实现批量新增文档
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:
可以看到,能添加的请求包括:
因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:
其实还是三步走:
我们在导入酒店数据时,将上述代码改造成for循环处理即可。
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
// 批量添加文档@Testvoid testBulkRequest() {// 批量查询酒店数据List hotels = hotelService.list();// 装换为文档类型HotelDoc// 1.准备Request对象BulkRequest request = new BulkRequest();// 2.准备参数,添加多个新增的Requestfor (Hotel hotel : hotels) {HotelDoc hotelDoc = new HotelDoc(hotel);// 创建为文档类型的HotelDocrequest.add(new IndexRequest("hotel").id(hotelDoc.getId().toString()).source(JSON.toJSONString(hotelDoc), XContentType.JSON));}try {// 3.发送请求client.bulk(request, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();}}
查询索引库
GET /hotel/_search
文档操作的基本步骤: