Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。
Elasticsearch是与名为Logstash的数据收集和日志解析引擎以及名为Kibana的分析和可视化平台一起开发的。这三个产品被设计成一个集成解决方案,称为“Elastic Stack”(以前称为“ELK stack”)。
Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。Elasticsearch是分布式的,这意味着索引可以被分成分片,每个分片可以有0个或多个副本。每个节点托管一个或多个分片,并充当协调器将操作委托给正确的分片。再平衡和路由是自动完成的。相关数据通常存储在同一个索引中,该索引由一个或多个主分片和零个或多个复制分片组成。一旦创建了索引,就不能更改主分片的数量。
Elasticsearch使用Lucene,并试图通过JSON和Java API提供其所有特性。它支持facetting和percolating,如果新文档与注册查询匹配,这对于通知非常有用。另一个特性称为“网关”,处理索引的长期持久性;例如,在服务器崩溃的情况下,可以从网关恢复索引。Elasticsearch支持实时GET请求,适合作为NoSQL数据存储,但缺少分布式事务。
ElasticSearch中有一些新的概念,这里我们对应于MySQL数据库中的一些概念来对其进行讲解,可能会有更好的效果。
MySQL | ElasticSearch | 说明 |
---|---|---|
Table | Index | 索引,就是文档的集合,类似于数据库中的表 |
Row | Document | 文档,就是一条条的数据,类似数据库中的一行,文档都是JSON形式 |
Column | Field | 字段,就是JSON中的字段名,类似数据库中的列 |
Schema | Mapping | Mapping是索引中文档的约束,例如字段类型约束,类似数据库的表结构 |
SQL | DSL | DSL是ElasticSearch提供的JSON风格的请求语句,用于操作ElasticSearch |
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
然后使用elasticsearch的7.12.1版本的镜像,直接pull。
pull elasticsearch:7.12.1
如果需要运行es并进行单点部署,那么命令如下:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
命令解释:
-e "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:前一个是设置初始堆的大小,后一个设置最大堆的大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中-p 9200:9200
:端口映射配置,暴露的HTTP请求的端口-p 9300:9300
:端口映射配置,暴露ElasticSearch互联的端口访问虚拟机地址的9200端口,如果出现以下页面,说明配置成功,
kibana可以给我们提供一个ElasticSearch的可视化界面,方便我们学习,运行如下代码表示运行一个kibana,
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中-p 5601:5601
:端口映射配置之后等待其部署完毕后,访问虚拟机的5601端口,发现乳腺的界面表示启动成功,
在ElasticSearch中,我们常常需要用到分词的操作,英文还好,其自带的就可以进行分词,但是中文,其只会按照逐字的方式对词进行划分,这显然是并不友好的,因此,我们需要安装一个专门的分词器来对中文进行分词。
安装IK分词器的步骤如下:
# 进入容器内部
docker exec -it es /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart es
打开kibana中的目录,找到Dev tools,
在其中输入DSL查询语句进行分词。
# 测试分词器
POST /_analyze
{
"text": "我们ikun不惹事,但也不怕事",
"analyzer": "ik_smart"#分词的模式
}
分词结果如下:
还有一种分词模式是 ik_max_word
,能够按照最细粒度去进行分词,更加占用内存空间。
mapping属性相当于就是数据库的字段约束,主要常用的mapping属性约束如下:
了解了mapping约束后,我们就可以开始创建索引了,创建索引库的语法如下:
PUT /索引库名
创建索引也就是创建每一个字段的约束条件,与数据库类似,我们创建一个名为 ikun
的索引,索引如下:
PUT /ikun
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
执行后显示的结果如下,表明创建成功:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "ikun"
}
查询索引库的语法如下:
GET /索引库名
删除索引库的语法如下:
DELETE /索引库名
需要注意的是,索引库一经创建就不允许进行修改,但是,我们可以对原来的索引库进行新增,语法如下:
PUT /索引库名/_mapping
{ "properties":
{ "新字段名":{
"type": "integer"
}
}
}
在索引库中插入文档相当于在数据库的表结构中增加一行数据。
新增文档的DSL语法如下:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4",
}
}
文档id如果没有指定的话,会随机生成。
比如我们对上面创建的索引库进行新增如下:
POST /ikun/_doc/1
{
"info": "我们ikun不惹事,但也不怕事",
"email": "[email protected]",
"name": {
"firstName": "i",
"lastName": "kun"
}
}
查看文档的语法为:
GET /ikun/_doc/1
删除文档的语法为:
DELETE /ikun/_doc/1
修改文档有两种方法。
一种是全量修改,其会首先找到旧的文档,将旧的文档进行删除,然后将修改的再添加进去。如果旧的文档不存在,这种方法还是会进行新增。语法如下:
PUT /索引库名/_doc/1
{
"字段1": "值1",
"字段2": "值2",
}
还有一种是增量修改,只会修改指定的字段,语法如下:
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新的值"
}
}
比如我们修改上面的文档1的邮箱可以为:
POST /ikun/_update/1
{
"doc": {
"email": "[email protected]"
}
}
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES,这里我们要学习的就是java中调用RestClient。
我们的数据库数据结构如下所示,
故我们构建索引库的代码如下:
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"
},
"starName": {
"type": "keyword"
},
"bussiness": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
在ElasticSearch中,对经纬度专门指定了一个结构 geo_point
,这里面能够存储经度和纬度的结构,除此之外,上面的 copy_to
字段是对字段进行联合索引的时候使用的。比如在上面我们需要对 name
属性和 brand
属性就行搜索,一般是先搜索符合的 name
,再到结果集里面搜索符合条件的 brand
,这样显然非常麻烦,而加入了一个 copy_to
字段后,便可以将该属性复制一份到 all
属性中,当然, all
属性并不存在与索引的 suorce
里面,此后,如果我们需要查询符合条件的 name
和 brand
时,只需要查询 all
属性即可。
那怎么在java中操作RestClient客户端呢?
首先是引入依赖,需要引入如下的依赖:
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.12.1elasticsearch.version>
properties>
<dependencies>
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.12.1version>
dependencies>
由于SpringBoot在父maven中已经定义了ElasticSearch的版本号,所以改版本的时候需要在 properties
标签中覆盖父pom定义的版本号。
之后就是初始化RestClient了。
如果我们对每一个类都要创建和销毁RestClient客户端的话,那就显得太过麻烦了,我们可以将创建和销毁写作一个Ioc切面,在每一个Bean创建之前切入并创建客户端,在每一个Bean执行后切入并销毁客户端,具体代码如下:
public class HotelIndexTest {
private RestHighLevelClient restHighLevelClient;
@BeforeEach
void setup(){
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.59.233:9200")
));
}
@AfterEach
void teardown() throws IOException {
this.restHighLevelClient.close();
}
}
@Test
void createHotelIndex() throws IOException {
//1.创建Request对象,索引坤名称为ikun
CreateIndexRequest request = new CreateIndexRequest("ikun");
//2.准备请求参数,即DSL语句,第一个参数为DSL语句,第二个参数指定为JSON形式
request.source("{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"kunName\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}", XContentType.JSON);
//3.发送请求
restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
}
@Test
void DeleteHotelIndex() throws IOException {
//1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("ikun");
restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
}
@Test
void ExistHotelIndex() throws IOException {
//1.创建Request对象
GetIndexRequest request = new GetIndexRequest("ikun");
boolean exist = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exist);
}
增加文档
如果我们需要用RestClient进行文档的增加,那么首先我们需要的就是类型转换,我们的文档内容肯定是从数据库中进行获取,但是,数据库中的数据与索引库的数据还是有一点不一样的,那就是经纬度。在数据库中,我们定义的是经度以及纬度,但是,在索引库中,我们定义的是一个数据结构 geo_point
,里面包含了经度以及纬度,所以,我们首先定义一个与索引库结构一致的类,如下:
@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();
}
}
之后,便可以读取数据库中的信息对索引库进行文档的增加了,增加的代码如下,
@SpringBootTest
public class HotelIndexTest {
@Autowired
private IHotelService hotelService;
private RestHighLevelClient restHighLevelClient;
@Test
void AddHotelDocument() throws IOException {
//1.根据ID查询酒店数据
Hotel hotel = hotelService.getById(61083L);
//2.转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//3.准备Request对象,其参数只接受String
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
//4.准备JSON文档
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//5.发送请求
restHighLevelClient.index(request, RequestOptions.DEFAULT);
}
@BeforeEach
void setup(){
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.59.233:9200")
));
}
@AfterEach
void teardown() throws IOException {
this.restHighLevelClient.close();
}
}
查询文档
@Test
void FindHotelDocument() throws IOException {
//1.准备Request对象,其参数只接受String
GetRequest request = new GetRequest("hotel", "61083");
//2.发送请求得到响应
GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
//3.解析相应结果,即将source字段解析为json格式的字符串
String json = response.getSourceAsString();
//4.将JSON格式的字符串解析为相应的对象
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
更新文档
@Test
void UpdateHotelDocument() throws IOException {
//1.准备Request对象,其参数只接受String
UpdateRequest request = new UpdateRequest("hotel", "61083");
//2.准备参数,特别注意,这里是逗号,没有冒号!!
request.doc(
"price", "1001",
"startName", "四钻"
);
//3.发送请求
restHighLevelClient.update(request, RequestOptions.DEFAULT);
}
删除文档
@Test
void UpdateHotelDocument() throws IOException {
//1.准备Request对象,其参数只接受String
DeleteRequest request = new DeleteRequest("hotel", "61083");
//2.发送请求
restHighLevelClient.delete(request, RequestOptions.DEFAULT);
}
批量新增数据
@Test
void AddMoreDocument() throws IOException {
//1.批量查询数据库中的信息
List<Hotel> hotels = hotelService.list();
//2.创建Request
BulkRequest request = new BulkRequest();
//3.准备参数,添加多个新增的Request
for(Hotel hotel: hotels){
HotelDoc hotelDoc = new HotelDoc(hotel);
request.add(new IndexRequest("hotel")
.id(hotel.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON));
}
//4.发送请求
restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
}