1. 介绍
ElasticSearch,简称ES,是java开发的一个开源的、高扩展的、分布式的全文搜索引擎服务器
所以说它是一个单独的软件,可以提供跟百度、谷歌一样的搜索功能
官网地址:https://www.elastic.co/cn/
1. 为什么使用ES
传统的数据库查询,假如现在有一个商品表
需求1:从 title 获取包含“手机”的数据
select * from goods where title like ‘%手机%’
这个sql存在的问题:
- 使用like时,以‘%’开头,会导致索引失效,这时候全表扫描,效率低
而且,如果表中数据太多,比如:1亿条,假设1秒钟查询十万,也需要1000秒,速度慢
需求2:从 title 获取包含“华为手机”的数据
select * from goods where title like ‘%华为手机%’,这个sql根本就查不出来
总结,使用数据库查询
- 效率低
- 功能弱
ES能解决这些的问题
2. 两种索引
假设有一本古诗集,包含
《静夜思》
窗前明月光,疑是地上霜。
举头望明月,低头思故乡。
《水调歌头》
明月几时有?把酒问青天。
不知天上宫阙,今夕是何年?
.....
《月下独酌四首》
花间一壶酒,独酌无相亲。
举杯邀明月,对影成三人。
.....
需求:找出这本书中包含“明月”的古诗
1. 正向索引
搜索逻辑:先看《静夜思》里有没有“明月”,有就记录,再看《水调歌头》里有没有“明月”,有就记录。。。。
就这样一首一首的判断下去,很明显效率很慢
2. 反向索引
又称:倒排索引,按照规则,对文本内容进行分词,拆分出不同的词条(term)
比如:床前明月光,就可能分成:床前、明月、光、月光 等词
所以它最终的索引结构:
这样就能根据“明月”快速的查询到对应古诗
总结:倒排索引,对文档内容进行分词,最终得出词条和文档唯一标示(文档id)的对应关系
3. ES的存储和搜索原理
ES中存在文档、索引库着两个概念
- 文档经过分词后存到索引库
- ES中的文档是json
- 索引库存储着词条跟文档的对应关系,并向用户提供搜索服务
倒排索引建立的过程:
id | title |
---|---|
1 | 华为 电信3G手机 |
2 | 三星 移动4G手机 |
-
id=1 的数据第一个存进来,分词后
-
然后 id=2 的数据进来,分词后:三星、移动、4G、手机
- 三星、移动、4G是新增的词条,手机是已存在的词条
就这样,文档以倒排索引的方式存放到索引库
同时也会对词条进行处理,形成树形结构,有点儿类似数据库中跟某个字段建立索引,目的是提高查询速度
查询时,如果用户输入“华为手机”
- 先分词为:华为、手机
- 根据这两个词条分别查询
- 最后汇总结果,反馈用户
4. ES和Mysql区别
- mysql有事务,ES没有
- ES没有外键,如果数据一致性要求高,还是要用Mysql
总结:mysql存储数据,ES搜索数据
2. 安装
ES下载地址:https://www.elastic.co/cn/downloads/past-releases#elasticsearch
官网下载的慢,华为开源镜像:https://repo.huaweicloud.com/elasticsearch/
- 下载完成后,把压缩包上传到虚拟机,我这里上传到了 /home/soft 目录
- 执行:tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz 进行解压,我这里也是解压到当前目录
- 修改配置,切换到:/home/soft/elasticsearch-7.8.0/config 目录
编辑 elasticsearch.yml
# ES的集群名称,默认是elasticsearch,建议修改为一个有意义的名字
cluster.name: my-es
# 节点名
node.name: node-1
# 暴露一个IP,供外网访问
network.host: 192.168.56.107
# http访问的端口号
http.port: 9200
# 配置集群时,用来选举master
cluster.initial_master_nodes: ["node-1"]
# 注意:上方配置“:”后都有空格
默认 ES 启动占用 1G 内存,可以通过编辑 config 目录下得 jvm.options 文件修改**(非必须)**
- 执行:/home/soft/elasticsearch-7.8.0/bin/ 下的 elasticsearch文件,启动
会发现报错,因为安全问题,Elasticsearch 不允许root用户运行,所以要创建一个新的用户
5. Centos7增加新用户
[root@localhost bin]# useradd javasm
[root@localhost bin]# passwd javasm
更改用户 javasm 的密码 。
新的 密码:
给新创建的用户授权,
# 设置 elasticsearch-7.8.0 文件见的所有者是 fengxiansheng
chown -R javasm:javasm /home/soft/elasticsearch-7.8.0
结果:
另外,新用户在系统中可创建的文件、进程数、内存等太小,修改下面的配置文件
vi /etc/security/limits.conf
# 文件末尾加上下面 2 行
javasm soft nofile 65536
javasm hard nofile 65536
vi /etc/security/limits.d/20-nproc.conf
# 文件末尾加上下面 3 行
javasm soft nofile 65536
javasm hard nofile 65536
* hard nproc 4096
vi /etc/sysctl.conf
# 文件末尾加上下面 1 行
vm.max_map_count=655360
# 使配置生效,命令行执行:
sysctl -p
- 重新启动,注意需要关闭防火墙
查看防火墙状态: firewall-cmd --state
关闭防火墙: systemctl stop firewalld
开机禁用防火墙: systemctl disable firewalld.service
切换到 javasm 用户:su javasm,
切换到 /home/soft/elasticsearch-7.8.0 目录,执行:bin/elasticsearch,重新启动ES
注意:如果启动不成功,重启虚拟机
浏览器访问:
后台启动:bin/elasticsearch -d
3. Kibana安装
kibana 是 ES的一个可视化平台,用来搜索、查看存储在ES中的数据,可以通过各种图表进行高级数据分析
下载地址:https://repo.huaweicloud.com/kibana/7.8.0/
- 上传到 /home/soft 目录
- 执行:tar -xzf kibana-7.8.0-linux-x86_64.tar.gz,进行解压
- 修改配置:
vi kibana-7.8.0-linux-x86_64/config/kibana.yml
#文件末尾加上
# http 访问端口
server.port: 5601
# 暴露一个IP,供外网访问
server.host: 192.168.56.107
# ES 的访问地址
elasticsearch.hosts: ["http://192.168.56.107:9200"]
# 请求ES的超时时间,单位:毫秒,默认值:30000
elasticsearch.requestTimeout: 99999
- 启动 Kibana,切换到:/home/soft/kibana-7.8.0-linux-x86_64 目录,执行:bin/kibana
提示不建议使用 root 用户启动,如果非要用root,加上 –allow-root 参数
注意:启动会很慢,耐心等待,如果等的时间太长,建议调大虚拟机内存
浏览器访问:http://192.168.56.107:5601/
然后可以看到:
4. 核心概念
操作ES之前需要了解几个概念
- 文档,document
- ES中的最小数据单位,json格式,一个documet相当于数据库,表中的一条数据
- 映射,mapping
- 定义每个字段的类型,相当于数据库中的表结构
- 索引,index
- ES存储数据的地方,一个index就相当于一个数据库表
5. 使用
1. 操作索引
1. 使用postman
- 添加索引,必须使用 put 请求,比如:192.168.56.107:9200/goods_indes
- 查询索引,使用 get 请求,只需要把 put 改成 get 就行
如果有想查询多个索引,逗号分隔,比如:192.168.56.107:9200/goods_index,goods_index2
_all:查询所有索引
- 删除索引,使用 delete 请求
- 关闭索引,表示不想删除也不想让外界访问,post 请求
使用 _close ,比如:
- 打开索引,也是 post 请求,使用_open
2. 使用Kibana
# 查询所有索引,会返回很多默认索引
GET _all
# 创建索引
PUT person
# 查询索引
GET person
# 关闭索引
POST person/_close
# 打开索引
POST person/_open
# 删除索引
DELETE person
2. 操作映射–Mapping
之前说过 mapping 中定义字段的类型,所以先了解一下ES中的数据类型
1. 数据类型
- 字符串
- text:对内容进行分词,得出多个词条
- 比如:“华为手机”分词后,得到两个词条:华为、手机
- 一般存储:网页内容、文章、日志等
- keyword:不会对内容进行分词,将内容当作一个词条
- 一般存储:电话、邮编、邮箱等
- text:对内容进行分词,得出多个词条
- 数值
-
scaled_float 缩放类型的的浮点数
- ⽐如价格只需要精确到分,缩放因⼦为100,price=57.34,存起来就是5734
- 布尔,boolean
- 日期类型,date
- 数组:[],被一对儿中括号扩起来
- 对象:{},被一对儿大括号扩起来
2. 操作映射
相当于定义一个表的结构,我们使用Kibana操作
- 创建映射
# 1. 创建索引
PUT hero
# 2. 创建mapping, 定义两个字段
PUT hero/_mapping
{
"properties":{
"name":{
"type":"keyword"
},
"skill":{
"type":"text"
}
}
}
查询索引可以看到,添加的 mapping
也可以直接查询 mapping
GET hero/_mapping
也可以在创建索引的同时,添加映射
先删除 hero这个索引,然后执行:
# 创建索引,并添加映射
PUT hero
{
"mappings": {
"properties": {
"name":{
"type":"keyword"
},
"skill":{
"type":"text"
}
}
}
}
- 添加字段
# 添加字段
PUT hero/_mapping
{
"properties":{
"skill_num":{
"type":"integer"
}
}
}
3. 操作文档
- 添加文档
# 添加文档
POST hero/_doc
{
"name":"亚瑟",
"skill":"从天而降大宝剑",
"skill_num":3
}
这时候 ID 自动生成
**
**
也可以添加时,指定id
# 添加文档
POST hero/_doc/1
{
"name":"吕布",
"skill":"心中的黑暗,在无限膨胀",
"skill_num":3
}
- 查询文档
# 查询单个文档
GET hero/_doc/1
# 查询一个不存在的文档
GET hero/_doc/2
查询所有文档
# 查询所有文档
GET hero/_search
- 修改文档
# 修改文档,当id存在时:修改,不存在时:新增
POST hero/_doc/1
{
"name":"吕布",
"skill":"心中的黑暗,在无限膨胀,哈哈",
"skill_num":3
}
注意:就算只更新一个字段也需要传整个对象,否则不传的字段会当成空处理
# 修改文档,不传 skill_num,再次查询,skill_num 就没有了
POST hero/_doc/1
{
"name":"吕布",
"skill":"心中的黑暗,在无限膨胀,哈哈"
}
当然也可以只修改某个字段
# 只修改指定字段
POST hero/_update/1
{
"doc": {
"skill_num":4
}
}
再次查询:
- 删除文档
# 根据id删除
DELETE hero/_doc/60NWooABIuAyT-o-ys22
删除不存在的数据返回:not_found
# 删除不存在的数据
DELETE hero/_doc/2
=done&style=none&taskId=ucef98852-a445-4484-b9fb-bf74fe5f2e0&title=&width=402)
4. 分词器
1. 默认分词器
分词器:将一段文字,按照规则,切分成多个词语的工具,比如:华为手机,分词后:华为、手机
ES内置了很多分词器:
- standard : 默认分词器,按词切分,小写处理
- simple: 按照非字母切分(符号被过滤),小写处理,会去除数字
- whitespace: 按照空格切分,不转小写
- stop: 小写处理,停用词过滤(the,a,is)
- keyword: 不分词,直接将输入当作输出
但是这些我们不用,只做了解,因为它们对中文很不友好,比如:
# 默认分词器
POST _analyze
{
"analyzer": "standard",
"text": ["中华任命共和国"]
}
2. IK分词器
ik分词器是java语言开发的一个中文分词器
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.8.0
注意:下载的包一定要和 ElasticSearch 的版本一致
下载后,上传到虚拟机的 /home/soft/elasticsearch-7.8.0/plugins/ik 目录(需要先创建好)
然后执行:** unzip elasticsearch-analysis-ik-7.8.0.zip**
将 config 目录下的文件 复制到 /home/soft/elasticsearch-7.8.0/config/ 目录
执行:cp -R config/ /home/soft/elasticsearch-7.8.0/config/*
最后重启 ES 和 Kibana
3. 基础使用
使用IK有两种方式:ik_smart、ik_max_word
# ik_smart,粗粒度划分,结果会少一些
GET _analyze
{
"analyzer": "ik_smart",
"text": ["中华人民共和国"]
}
# ik_max_word,细粒度划分,结果会多一些
GET _analyze
{
"analyzer": "ik_max_word",
"text": ["中华人民共和国"]
}
4. 操作文档
- 删除之前创建的 hero,重新创建,因为之前用的是默认的 standard 分词器
# 创建索引,指定使用IK分词器
PUT hero
{
"mappings": {
"properties": {
"name":{
"type":"keyword"
},
"skill":{
"type":"text",
"analyzer": "ik_max_word"
}
}
}
}
- 添加文档
# 添加3个文档
PUT hero/_doc/1
{
"name":"亚瑟",
"skill":"王者背负,王者审判,王者不可阻挡!"
}
PUT hero/_doc/2
{
"name":"妲己",
"skill":"羁绊是什么东西"
}
PUT hero/_doc/3
{
"name":"吕布",
"skill":"无法得到那就将他彻底毁掉"
}
- 查询文档
查询分为两种:对关键字分词、不对关键字分词
不对关键字分词,使用“term”
# 通过数据的关键字查询文档,不对关键字分词,相当于数据库查询的“=”
GET hero/_search
{
"query": {
"term": {
"name": {
"value": "妲己"
}
}
}
}
但是查询 skill 字段
可以看到是没有结果了,原因有两个:
- 没有对查询的关键字进行分词
- 我们存储的数据中没有“羁绊是什么东西”这个词条
这时候把查询关键字修改为“羁绊”就能看到结果
对关键字分词,使用“match”
# 对关键分词后再查询
GET hero/_search
{
"query": {
"match": {
"skill": "羁绊是什么"
}
}
}
可以看到查出来了,这是因为“羁绊是什么”分词后的结果是:
5. Java API
1. 操作索引
创建一个普通的Maven项目
pom.xml
<!-- elasticsearch API -->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.8.0</version>
</dependency>
<!-- elasticsearch 的客户端 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
- 创建索引
public static void main(String[] args) throws IOException {
//1. 创建ES连接客户端
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.56.109",9200,"http")));
//2. 创建索引请求
CreateIndexRequest request = new CreateIndexRequest("wangzhe");//fengxiansheng是索引名
//2.1 mapping
String mapping = "{\n" +
" \"properties\":{\n" +
" \"name\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"skill\":{\n" +
" \"type\":\"text\"\n" +
" }\n" +
" }\n" +
"}";
//设置 mapping 的格式是json
request.mapping(mapping, XContentType.JSON);
//2.2 使用client创建索引
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
//3. 返回true,表示创建成功
System.out.println(response.isAcknowledged());
//4. 关闭客户端
client.close();
}
运行后,在 Kibana 上查看结果
- 查询索引
private static void getIndex() throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.109",9200,"http")));
GetIndexRequest request = new GetIndexRequest("wangzhe");//wangzhe 是索引名
GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);
//获取结果
Map<String, MappingMetadata> mappings = response.getMappings();
for(String key : mappings.keySet()){
System.out.println(key + "----" + mappings.get(key).sourceAsMap());
}
client.close();
}
结果
- 删除索引
private static void deleteIndex() throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.109",9200,"http")));
DeleteIndexRequest request = new DeleteIndexRequest("wangzhe");//wangzhe 是索引名
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
client.close();
}
- 判断索引是否存在
private static void existIndex() throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.109",9200,"http")));
GetIndexRequest request = new GetIndexRequest("wangzhe");//fengxiansheng 是索引名
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// true:存在,false:不存在
System.out.println(exists);
client.close();
}
2. 操作文档
创建一个 Hero 类
public class Hero {
private String name;
private String skill;
//get、set 省略
}
- 把对象,存到ES中
private static void createDoc() throws IOException {
Hero hero = new Hero();
hero.setName("张飞");
hero.setSkill("英雄比普通人更不正常");
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.109",9200,"http")));
//组装添加文档的请求
IndexRequest request = new IndexRequest("hero")//hero 是索引名
.id("4")//指定id
.source(JSONObject.toJSONString(hero), XContentType.JSON);//数据内容json格式
//执行添加文档的命令
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
//返回结果
System.out.println(response);
client.close();
}
结果:
Kibana 上查询:
- 修改文档
修改文档跟添加文档的代码一样,id存在就更新,id不存在就添加
private static void updateDOc() throws IOException {
Hero hero = new Hero();
hero.setName("张飞");
hero.setSkill("英雄比普通人更不正常66666");
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.109",9200,"http")));
//组装添加文档的请求
IndexRequest request = new IndexRequest("hero")//hero 是索引名
.id("4")//指定id
.source(JSONObject.toJSONString(hero), XContentType.JSON);//数据内容json格式
//执行添加文档的命令
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 使用UpdateRequest,更新指定字段
// Map<String,Object> map = new HashMap<String, Object>();
// map.put("skill","英雄比普通人更不正常哈哈哈哈");
// UpdateRequest updateRequest = new UpdateRequest("hero","4");
// updateRequest.doc(map);
// UpdateResponse response = client.update(updateRequest, RequestOptions.DEFAULT);
//返回结果
System.out.println(response);
client.close();
}
Kibana 上查询:
- 根据ID查询
private static void getById() throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.109",9200,"http")));
//创建请求对象,两个参数,分别是:索引名、文档ID
GetRequest request = new GetRequest("hero","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
System.out.println(response);
System.out.println(response.getSourceAsMap());
client.close();
}
结果:
- 删除文档
private static void deleteDoc() throws IOException {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("192.168.56.107",9200,"http")));
//创建请求对象,两个参数,分别是:索引名、文档ID
DeleteRequest request = new DeleteRequest("person","5");
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println(response);
client.close();
}
结果: