文章目录
ES
ES概述
ES和LUCENS
一个很好的搜索库Lucene,但是很复杂,ES就封装了Lucene,支持了很多东西
- 分布式
ES核心概念
- 近实时
- 两方面
- 写入数据时,过1秒才会被搜索到,因为内部再分词和录入索引
- es搜索时,搜索和分析数据需要秒级出结果
- 两方面
- Cluster集群
- 包含一个或多个启动这es实例的机器群时,通常一个机器起es实例,同一网络下,多个es自动集群,负载均衡
- Node节点
- 每个es实例
- Document文档
- es的最小数据单元
- 格式为json格式
- 多个document存储于一个索引中
- index索引
- 包含一堆相似结构的文档数据
- 和数据库的表一样
- Field字段
- 相当于mysql的列
- type
- 是index的逻辑分类,将要删除
- shard分片
- index过大时,就是表数据量过大时,可以分为多个表
- 减轻单节点压力
- 充分利用性能
- 方便集群拓展
- 副本replica shard
- 为了容错,就是为了备份,像redis的slave主从
- 高可用,吞吐量 0
es和mysql对比
mysql | es |
---|---|
database库 | index索引 |
table | index索引(原来为type) |
数据行Row | 文档document |
数据列column | 字段field |
约束schema | 映射mapping |
ES目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3sDyQo7-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009095533897.png)]
- bin
- 启动脚本
- config
- 配置文件
- jdk
- 就是一些jar包
- logs
- 日志
- modules
- 模块包
主要配置文件
-
elasticesearch.yml
-
配置格式yaml格式
-
cluster.name: my-application #集群名称 基本都是默认设置 node.name: node-1 #节点名字 node.attr.rack: r1 #机架
-
两个端口
- 9200 对外端口
- 9300 集群节点之间通信端口
-
-
jvm.options
- jvm设置
- -Xms2g 启动时多少g
- -Xmx2g 最大运行
-
log4j
- 日志设置
es在windos要配一点东西才可以
node.name: node-1
cluster.initial_master_nodes: ["node-1"]
xpack.ml.enabled: false
http.cors.enabled: true
http.cors.allow-origin: /.*/
启动
/bin/elasticsearch.bat
kibana下载
默认5601端口
- i18n.locale:
- 语言设置
- 启动
- /bin/kibana.bat
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HkGDCNO4-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009105611242.png)]
Es快速入门
文档documnt
json格式
案例图书管理案例
- 增删改查crud
ES中Api
- GET /_cat/health?v
- 节点信息
- GET /_cat/indices?v
- 索引,也就是表
- PUT /demo_index?pretty
- 创建索引
- DELETE /demo_index
- 删除索引
Crud操作
创建book索引
插入数据
- PUT /索引库/type/id
PUT /book
// /索引库/表/id
PUT /book/_doc/1
{
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发css框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个css,不受浏览器限制的精美界面css效果。",
"studymodel": "201002",
"price":38.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "bootstrap", "dev"]
}
PUT /book/_doc/2
{
"name": "java编程思想",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price":68.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "java", "dev"]
}
PUT /book/_doc/3
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price":88.6,
"timestamp":"2019-08-24 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "spring", "java"]
}
获取数据
-
GET /索引库/type/id
GET /book/_doc/1 GET /book/_doc/2?_source_includes=name,price
修改数据
修改全部
-
PUT /book/_doc/1
{
全部数据
}
PUT /book/_doc/1
{
"name": "Bootstrap开发教程1",
"description": "Bootstrap是由Twitter推出的一个前台页面开发css框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个css,不受浏览器限制的精美界面css效果。",
"studymodel": "201002",
"price":38.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "bootstrap", "开发"]
}
修改部分
POST /索引库/type/id/_update
{
"doc":{
"name": "bootstrap开发教程高级"
}
}
POST /book/_update/1/
{
"doc": {
"name": " Bootstrap开发教程高级"
}
}
删除
DELETE /索引库/type/id
默认自带字段解析
_index
库
_type
表
_id
id主键,可以自动生成
创建索引时,不同数据放入不同索引。
生成文档ID
直接POST
POST /index/_doc
PUT /test_index/_doc/1
{
"test_field": "test"
}
- 自动ID
- 长度为20,URL安全,base64,GUID,分布式生成不冲突
_source
原始数据
定制获取相应字段,不用返回全部字段
GET /book/_doc/2?_source_includes=name,price
全量替换
就是我们刚刚的修改全部的哪里
PUT /index/type/1
{
}
内部实现
并不会一次次地去删除,比如全局修改时,他会在整个数据上面加上一个字段delete标志为删除,version为1
新的文档version为2。当删除的文档很多时,也就是是delete很多时,es会自动批量删除
旧文档的内容不会立即删除,只是标记为deleted。适当的时机,集群会将这些文档删除。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9wOkvz3N-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009150514705.png)]
java全局替换会先让es返回这条数据的对象,然后把要修改的值修改了再插入进去
强制创建
为防止覆盖原有数据,我们在新增时,设置为强制创建,不会覆盖原有文档
PUT /test_index/_doc/1/_create
{
"test_field": "test"
}
就是已经存在的话,就不会覆盖
删除
不会立刻删除,只是delete,es批量删除
lazy delete
局部替换
POST /index/_doc/id/_update
{
要修改的字段
}
内部原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mLkCAn9A-1666522442625)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009151629408.png)]
优点
- 减少网络请求次数
- 减少网络开销
- 减少并发冲突
使用脚本更新
内置脚本
es6后就不支持了,因为耗内存,不安全远程注入漏洞
脚本操作
ctx就是context上下文
_source
num+=1
POST /test_index/_doc/6/_update
{
"script" : "ctx._source.num+=1"
}
这样的话就可以一次请求就加一了。不用先获取看看他多少再加一,减少了网络传输
下面这个是比较复杂的内置脚本
GET /test_index/_search
{
"script_fields": {
"my_doubled_field": {
"script": {
"lang": "expression",
"source": "doc['num'] * multiplier",
"params": {
"multiplier": 2
}
}
}
}
}
es并发问题
在高并发情况下,仍会有并发
- 悲观锁
- 锁线程
- 乐观锁
- 版本管理
es的锁是乐观锁,他本身就有一个版本号。
每次修改都会version+1,删除也是在version+1的版本里,此时新增的话就在delete的version+1
比如
version = 3,修改 version = 4 ,删除 version = 5 ,加上delete标识,添加version = 6,没有delete标识
es的后台主从同步时异步多线程,所以说,多个请求时是乱序的 ,就是他是打乱发送的,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvfReC7e-1666522442626)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221009155403919.png)]
javaAPI
es都是rest,很方便
es的rest api很多
在maven工程中
注意这个版本一定要对应到你的es版本
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.6</version>
<exclusions>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.17.6</version>
</dependency>
测试的demo
public class TestDemo {
public static void main(String[] args) throws IOException {
//1,获取连接客户端
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost",9000,"http")
));
//2,构建请求
GetRequest getRequest = new GetRequest("book", "2");
//3,执行
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(response.getId());
System.out.println(response.getVersion());
System.out.println(response.getSourceAsString());
}
}
springboot工程实现
配置类注入restClient
@Component
public class EsConfig {
@Value("${heima.elasticsearch.hostlist}")
private String hostList;
//es自带的close方法
@Bean(destroyMethod = "close")
public RestHighLevelClient restHighLevelClient(){
String[] split = hostList.split(",");
HttpHost[] httpHosts = new HttpHost[split.length];
for (int i = 0; i < httpHosts.length; i++) {
String item = split[i];
httpHosts[i] = new HttpHost(item.split(":")[0],Integer.parseInt(item.split(":")[1]),"http");
}
return new RestHighLevelClient(RestClient.builder(
httpHosts
));
}
}
下面直接引入就可以了
获取信息的API
@Autowired
private RestHighLevelClient restHighLevelClient;
@Test
void contextLoads() throws IOException {
//1,构建请求
GetRequest book = new GetRequest("book", "2");
//2,执行
GetResponse response = restHighLevelClient.get(book, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString());
}
上面这种方式是查询所有的字段的,查询全字段效率低,我们可以查询自己需要的字段
透过两个String和FetchSourceContext来进行辨别
@Test
void test2() throws IOException {
GetRequest getRequest = new GetRequest("book", "2");
//想要的字段
String[] includes = new String[]{"name", "price"};
//不想要的字段
String[] excludes = Strings.EMPTY_ARRAY;
FetchSourceContext fetchSourceContext = new FetchSourceContext(true, includes, excludes);
getRequest.fetchSourceContext(fetchSourceContext);
GetResponse response = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString());
}
使用异步查询
ActionListener<GetResponse> listener = new ActionListener<GetResponse>() {
//成功
@Override
public void onResponse(GetResponse getResponse) {
System.out.println(getResponse.getSourceAsString());
}
//失败
@Override
public void onFailure(Exception e) {
e.printStackTrace();
}
};
多种返回String,Map,byte数组
@Test
void contextLoads() throws IOException {
//1,构建请求
GetRequest book = new GetRequest("book", "3");
//2,执行
GetResponse response = restHighLevelClient.get(book, RequestOptions.DEFAULT);
if (response.isExists()){
System.out.println(response.getSourceAsMap());
}else {
System.out.println("不存在");
}
}
文档新增操作
PUT
这里只学3种方式
@Test
void testAdd() throws IOException {
// PUT /test_index/_doc/3/_create
// {
// "test_field": "test"
// }
IndexRequest request = new IndexRequest("test_index", "_doc");
// request.id("1");
//4种构建文档数据的方式
//方法1 使用json字符串
String jsonString = " {\n" +
" \"test_field\": \"test\"\n" +
" }";
request.source(jsonString, XContentType.JSON);
//方法2 使用map
// HashMap<String, Object> map = new HashMap<>();
// map.put("test_field","test");
// request.source(map);
//方法3 不说了优点麻烦
//方法4
// request.source("test_index","test");
//设置超时时间
request.timeout("1s");
//手动维护版本号
// request.version(2);
// request.versionType(VersionType.EXTERNAL);
IndexResponse index = restHighLevelClient.index(request, RequestOptions.DEFAULT);
System.out.println(index.getId());
}
- Json格式
- Map格式
- 直接source一个个Json
插入成功的判断
IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
//如果创建成功
if (response.getResult() == DocWriteResponse.Result.CREATED){
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}else if (response.getResult() == DocWriteResponse.Result.UPDATED){
//全文覆盖的情况
}else {
//失败
}
分片管理
ReplicationResponse.ShardInfo shardInfo = response.getShardInfo();
if (shardInfo.getTotal() != shardInfo.getSuccessful()){
System.out.println("处理成功的分片数少于总分片数");
}
if (shardInfo.getFailed() > 0){
for (ReplicationResponse.ShardInfo.Failure failure:
shardInfo.getFailures()) {
System.out.println(failure.reason());
}
}
异步新增
ActionListener<IndexResponse> listener = new ActionListener<IndexResponse>() {
@Override
public void onResponse(IndexResponse response) {
System.out.println(response.getIndex());
}
@Override
public void onFailure(Exception e) {
System.out.println("新增失败");
}
};
restHighLevelClient.indexAsync(request,RequestOptions.DEFAULT,listener);
更新文档
@Test
void testPost() throws IOException {
// POST /test_index/_doc/3/_update
// {
// "doc":{
// "test_field":"test2"
// }
// }
UpdateRequest request = new UpdateRequest("test_index", "_doc", "3");
//和新增的3种方式一样
request.doc("test_field","test2");
request.timeout("1s");
//重试次数
request.retryOnConflict(3);
UpdateResponse update = restHighLevelClient.update(request, RequestOptions.DEFAULT);
System.out.println(update.getResult());
//如果创建成功
if (response.getResult() == DocWriteResponse.Result.CREATED){
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}else if (response.getResult() == DocWriteResponse.Result.UPDATED){
//全文覆盖的情况
System.out.println("update");
}else if (response.getResult() == DocWriteResponse.Result.DELETED){
System.out.println("delete");
}else if (response.getResult() == DocWriteResponse.Result.NOOP){
System.out.println("没有操作");
}
}
}
也可以异步,弄个监听器就行
批量插入
@Test
void testBulk() throws IOException {
BulkRequest request = new BulkRequest("test_index", "_doc");
request.add(new IndexRequest("post").id("6").source(XContentType.JSON,"test_field","test3"));
request.add(new IndexRequest("post").id("7").source(XContentType.JSON,"test_field","test3"));
request.add(new IndexRequest("post").id("8").source(XContentType.JSON,"test_field","test3"));
request.add(new DeleteRequest("post").id("7"));
request.add(new DeleteRequest("post").id("8"));
BulkResponse bulk = restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
//成功条数
System.out.println("成功条数" + bulk.getIngestTook());
//获取每个插入的Response
for (BulkItemResponse res:
bulk) {
DocWriteResponse response = res.getResponse();
switch (res.getOpType()){
case INDEX:
IndexResponse indexResponse = (IndexResponse)response;
System.out.println("put普通插入或者全局替换");
break;
case CREATE:
IndexResponse indexResponse1 = (IndexResponse)response;
System.out.println("强制创建");
break;
case UPDATE:
UpdateResponse updateResponse = (UpdateResponse)response;
System.out.println("修改");
break;
case DELETE:
DeleteResponse deleteResponse = (DeleteResponse)response;
System.out.println("删除");
break;
}
}
}
POST /_bulk
功能总结
- delete
- 删除
- create
- 强制创建
- index
- 普通put,也可以全量替换
- update
- 局部更新
ES的分布式架构
- 1,复杂分布式分片
- 负载,分片,副本,平均分配分片
- 2, 新的es实例,自动加入集群
- 自动加入es集群,自动副本
- 3,扩容方案
- 垂直扩容
- 比如1T数据到8T数据,换机器,买个大的
- 水平扩容
- 直接新加入,原来的还继续用
- 一般情况下使用水平扩容
- 垂直扩容
- 4,rebalence
- 有时有些服务器负载重或者轻,es集群会将某些分片会自动转移获取移走
- 5,master节点
- 管理es集群的元数据,创建删除索引,维护索引的元数据
- 默认下,es会自动选一台机器作为master
- 6,节点对等分布式
- 请求的数据在各个分片的情况 都有可能
图解shard,副本机制
- 每个shard节点,其实都封装了Lucene实例,完整的建立索引
- 每个index包含一个或这个shard
- shard自动负载均衡
- shard和其副本,数据同步
- 副本负责容错,代替shard的备胎,同时也可以负责读的一些操作,因为它是和其shard是数据同步的
- 主分片创建索引就固定了,副本则可以改
- 默认共有2个分片,1个主分片,1个副本分片
- 主分片和副本分片不可以在同一节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3r9z9XSH-1666522442626)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010100052367.png)]
单一个节点下的机制图解
一个node就是一台机器
可以自行设置分片数和副本数,比如先新创一个index,3个主分片一个副本分片
但是副本是使用不了的,因为上面说到副本和分片不能再一个机器节点,所以副本失效,状态为yellow,一旦宕机全部失效
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iCefXaN-1666522442626)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010100720016.png)]
两个节点机制
两个节点下,创建分片数3,副本数3
因为副本和主分片不在同一机器,所以有可能一个节点放主分片,一个节点放副本
读取时,负载均衡,在主和副读都可以
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o4rzYXK6-1666522442627)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101022227.png)]
横向扩容
这样的话,可以分担写的请求,不用都在一台机器上操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hy8xdj9y-1666522442627)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101445330.png)]
若再加一个台机器,横向会自动分布主分片和节点数量,使各个机器均衡分布
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PBIlwN1O-1666522442628)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101633989.png)]
这样,同时也是让读是3台机器,写也是3台
如果机器足够多的话,会自动给副本再加副本,读的话9台,写的话3台
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8UVwXHF-1666522442628)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010101921717.png)]
Master选举
这个和redis的是基本一样的,有一个节点挂了,会去找另一个节点作为master,当宕机的重启会作为子节点,新master的副本会代替原来master的主分片
- 情况1
- node1宕机,P0shard没有了,所有主分片不是全active,集群状态red
- 容错第一步
- 重新选master节点
- 容错第二步
- 新master会将丢失的主分片的某个副本提升为主分片,集群状态为yellow(副本分片丢失)
- 容错的第三步
- 宕机的机器重启,作为子节点
- 将确实的副本分片copy一份到新机器上(部分替换)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BG7tR1tT-1666522442628)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010131652570.png)]
文档的存储机制
一个文档只会再一个分片上面
数据路由
一个文档会只落在一个分片上
路由算法
路由公式
shard = harh(routing) % number_of_primary_shards
1%3 = 1放在1号分片,取得时候也很简单,获取到这个位置直接取这个分片找
手动指定
PUT /test_index/_doc/15?routing=tom
{
"username":"zs"
}
这种一般用于同种属性得数据放于同一分片,方便管理
但是容易数据倾斜,所以不同文档尽量放到不同的索引,剩下得交给es
主分片数量不可变
因为路由的情况,在路由时已经确定好,如果改变会破坏路由的规则,导致寻找数据失败。
比如
存储id = 8
主分片数 = 3
hash(8) = 8 % 3 = 2 放到2号分片,如果现在分片数变了 + 1 主分片数为4
hash(8) = 8 % 4 = 0 到0号库找这条数据,但是插入的时候在2号库所以找不到
增删改机制
分为4步走
比如现在3分片1副本
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mr3E8d9T-1666522442629)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221010134213787.png)]
新增id = 1的数据
- 1,java客户端发送请求,选择一个node作为协调节点
- 2,协调节点进行路由计算,然后把请求转发到对应node
- 3,实际node上的主分片进行数据处理,然后该节点的主分片会发一个请求到其副本的节点进行数据同步
- 4,协调节点如果发现分片和副本都成功,则返回结构
查询机制
查询id = 1数据
- 1,java客户端发送请求,选择一个node作为协调节点
- 2,协调节点进行路由计算,然后把请求转发到对应node,此时会使用round-robin轮询随机算法,在主分片及其所有的副本中随机选一个进行负载均衡
- 实现减轻查询负担
- 3,接收的node返回document给协调节点
- 4,协调节点返回客户端
- 5,特殊情况:
- document在建立索引过程,主分片已建立,但是副本还没有,此时导致可能无法读取到数据,当建立索引完毕后,就一定查询到。
bulk api的json格式
传统json格式节点需要把不同操作分割为json数组,然后进行路由分配,再将其序列化,这样会消耗更多内存,jvm gc的开销
现在的格式,天然形成的分配,插入和修改会占用两个元素
POST /_bulk
{ "delete": { "_index": "test_index", "_id": "5" }} \n
{ "create": { "_index": "test_index", "_id": "14" }}\n
{ "test_field": "test14" }\n
{ "update": { "_index": "test_index", "_id": "2"} }\n
{ "doc" : {"test_field" : "bulk test"} }\n
Mapping映射入门
mapping映射概念
自动或手动为index中的_doc建立的一种数据结构的相关配置,坚持mapping映射
先插入几条数据
PUT /website/_doc/1
{
"post_date": "2019-01-01",
"title": "my first article",
"content": "this is my first article in this website",
"author_id": 11400
}
PUT /website/_doc/2
{
"post_date": "2019-01-02",
"title": "my second article",
"content": "this is my second article in this website",
"author_id": 11400
}
PUT /website/_doc/3
{
"post_date": "2019-01-03",
"title": "my third article",
"content": "this is my third article in this website",
"author_id": 11400
}
对于上面条数据,我们并没有创建index就可以直接对website进行插入,因为系统帮我们创建了。额对于mysql则是必须创建表才可以插入。
比如在mysql中需要指定特定的数据类型
create table website(
post_date date,
title varchar(50),
content varchar(100),
author_id int(11)
);
在es中:
会进行动态映射,根据自动创建索引index,以及对于mapping,mapping中包含了每个field的数据类型,以及分词设置
这个就是系统自动创建的动态映射
{
"website" : {
"mappings" : {
"properties" : {
"author_id" : {
"type" : "long"
},
"content" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"post_date" : {
"type" : "date"
},
"title" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
搜全部
GET index/_search
GET index/_search?q=查询条件
GET index/_search?q=third
#这样是在全部属性都查
GET index/_search?q=属性:值
GET index/_search?q=title:third
在不同的data type的分词和搜索行为不一样,有可能搜不到,比如2019搜不了,2019-01-01能搜到。
精确匹配和全文检索的对比分析
exact value精确匹配
就是全部输入2019-01-01才能搜到,2019搜不出
full text全文搜索
用户只输入关键词输入2019就可以,如笔记电脑,内部会进行分词笔记 电脑两个词
2019-01-01 对于2019 01 01分为3个词
- 缩写自动全称
- cn vs china
- 格式转化
- like vs likes
- 大小写
- tom vs Tom
- 同义词
- like vs love
NPL自然语言处理底层。
全文检索下的倒排索引
doc1:I really liked my small dogs, and I think my mom also liked them.
doc2:He never liked any dogs, so I hope that my mom will not expect me to liked him
分词,建立倒排索引
term | doc1 | doc2 |
---|---|---|
I | * | * |
really | * | |
liked | * | * |
my | * | * |
small | * | |
dogs | * | |
and | * | |
think | * | |
mom | * | * |
also | * | |
them | * | |
He | * | |
never | * | |
any | * | |
so | * | |
hope | * | |
that | * | |
will | * | |
not | * | |
expect | * | |
me | * | |
to | * | |
him | * |
mother like little dog,不可能有任何结果
mother
like
little
dog
这不是我们想要的结果。同义词mom\mother在我们人类看来是一样。想进行标准化操作,没有进行同义词转化
重建倒排索引
就是上面时态,单复数,同义词,大小写转化
mom ―> mother
liked ―> like
small ―> little
dogs ―> dog
重新建立倒排索引,加入normalization,再次用mother liked little dog搜索,就可以搜索到了
word | doc1 | doc2 | normalization |
---|---|---|---|
I | * | * | |
really | * | ||
like | * | * | liked ―> like |
my | * | * | |
little | * | small ―> little | |
dog | * | dogs ―> dog | |
and | * | ||
think | * | ||
mother | * | * | mom ―> mother |
also | * | ||
them | * | ||
He | * | ||
never | * | ||
any | * | ||
so | * | ||
hope | * | ||
that | * | ||
will | * | ||
not | * | ||
expect | * | ||
me | * | ||
to | * | ||
him | * |
这样的话就能搜索到了
分词器
作用:切分词语在进行正规化操作
就是把句子分词,然后再进行语态同义词等进行添加。
分词过程
- 1,预处理,把表签去掉,比如过滤html标签
- 2,进行分词
- 3,正规化处理,比如同义词,大小写转换,同时把禁用词去掉,无语义的词也去掉
通过上面,才会进行建立倒排索引。
分词器分类
- 标准分词
- 简单分词器
- 空格分词器,更简单的分词器
- 特定语言分词器,比如中文,英文进行分词
- 等等
对于字段的分词策略
对于不同的数据类型分词策略也不一样,日期data 的话一般是精确匹配,对于文本text一般是全文检索
我们可以指定分词器进行分词
GET /_analyze
{
"analyzer": "standard",
"text": "Text to analyze 80"
}
动态自动映射
回顾
- 1,插入数据,自动建立索引
- 2,自动定义数据类型
- 3,不同数据类型,匹配规则不一样,精确匹配,全文检索
- 4,在全文检索建立倒排索引前,会预处理,分词,正规化
- 5,精确匹配是直接将整个放入倒排索引
核心数据类型
- text,keyword
- string
- 地理位置信息
- ip
- 数组
- 基本类型
- 多个域
- 等等
动态推测
根据值推测
true 就是 boolean
2019-01-01 date
查看索引映射
GET /index/_mapping
手动管理映射
手动创建映射,动态映射可能不太好,我们就需要自己去定义映射
PUT book
PUT book/_mapping
{
"properties": {
"name": {
"type": "text"
},
"description": {
"type": "text",
"analyzer":"english",
"search_analyzer":"english"
},
"pic":{
"type":"text",
"index":false
},
"studymodel":{
"type":"text"
}
}
}
PUT /book/_doc/1
{
"name":"Bootstrap开发框架",
"description":"Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"studymodel":"201002"
}
Get /book/_search?q=name:开发
Get /book/_search?q=description:开发
Get /book/_search?q=pic:group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg
Get /book/_search?q=studymodel:201002
通过测试发现:name和description都支持全文检索,pic不可作为查询条件。
- properties{}
- 属性 :{}
- 属性的类型
- 属性的分词器
- 属性搜索的分词器
- index是否进行索引,就是整个倒排索引,比如URL就不用建立倒排索引
- 如果是日期还可以指定format格式
- 属性 :{}
keyword关键词
创建映射时不需要倒排索引,一般用户身份证,手机号码等等,不需要分词的
date日期
这个也默认不建立倒排索引
选用基本数据类
尽量选用小的提高效率
浮点数
尽量使用比例因子,比如单位元,按分,按百
不可以修改映射,只可以新增
PUT /book/_mapping/
{
"properties" : {
"new_field" : {
"type" : "text",
"index": "false"
}
}
}
一些复杂的数据类型
multivalue field
{"tag":["tag1","tag2"]}
empty field
null , [] , [null]
对象
像这个地址就是一个对象
PUT /company/_doc/1
{
"address": {
"country": "china",
"province": "guangdong",
"city": "guangzhou"
},
"name": "jack",
"age": 27,
"join_date": "2019-01-01"
}
{
"address": {
"country": "china",
"province": "guangdong",
"city": "guangzhou"
},
"name": "jack",
"age": 27,
"join_date": "2017-01-01"
}
{
"name": [jack],
"age": [27],
"join_date": [2017-01-01],
"address.country": [china],
"address.province": [guangdong],
"address.city": [guangzhou]
}
对象数组
{
"authors": [
{ "age": 26, "name": "Jack White"},
{ "age": 55, "name": "Tom Jones"},
{ "age": 39, "name": "Kitty Smith"}
]
}
{
"authors.age": [26, 55, 39],
"authors.name": [jack, white, tom, jones, kitty, smith]
}
索引
我们之前都是直接PUT index/_doc/1创建索引的,这种动态映射有可能出错
所以,我们需要自己去定制
自定义创建索引
创建索引的时候把映射上
PUT /index
{
"settings": { ... any settings ... },
"mappings": {
"properties" : {
"field1" : { "type" : "text" }
}
},
"aliases": {
"default_index": {}
}
}
- settings
- 配置分片数
- mappings
- properties
- 属性
- type,分词器等等
- 属性
- properties
- aliases别名
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"field1":{
"type": "text"
},
"field2":{
"type": "text"
}
}
},
"aliases": {
"default_index": {}
}
}
put /my_index/_doc/1
{
"field1":"java",
"field2":"js"
}
GET my_index/_doc/1
#获取索引信息
GET my_index
GET my_index/_mapping
GET my_index/_settings
DELETE my_index
之前以及说过映射不可以修改,只可以添加,同时分片数也不可修改
定制分词器
默认分词器standard
standard,其中分词的三个组件,
-
character filter
-
tokenizer
-
token filter
-
standard tokenizer:以单词边界进行切分
-
standard token filter:什么都不做
-
lowercase token filter:将所有字母转换为小写
-
stop token filer(默认被禁用):移除停用词,比如a the it等等
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"es_std":{
"type":"standard",
"stopwords":"_english_"
}
}
}
}
}
定制化自己的分词器
PUT /my_index
{
"settings": {
"analysis": {
#& 转化为and
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": ["&=> and"]
}
},
#定义声明停用词
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": ["the", "a"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
#使用上面的和html标签过滤
"char_filter": ["html_strip", "&_to_and"],
"tokenizer": "standard",
#使用上面的去掉停用词
"filter": ["lowercase", "my_stopwords"]
}
}
}
}
}
使用我们自己的分词器进行测试分词
GET /my_index2/_analyze
{
"analyzer": "my_analyzer"
, "text": "tom & jerry are a friend in the house <a>,HAHA!!"
}
就是使用自己的分词器制定分词策略
type弃用的原因
PUT my_index/_doc/1
底层都是没有type类型
es储存不同type的机制,就是把它们都合起来
{
"goods": {
"mappings": {
"electronic_goods": {
"properties": {
"name": {
"type": "string",
},
"price": {
"type": "double"
},
"service_period": {
"type": "string"
}
}
},
"fresh_goods": {
"properties": {
"name": {
"type": "string",
},
"price": {
"type": "double"
},
"eat_period": {
"type": "string"
}
}
}
}
}
}
变成下面这样
{
"goods": {
"mappings": {
"_type": {
"type": "string",
"index": "false"
},
"name": {
"type": "string"
}
"price": {
"type": "double"
}
"service_period": {
"type": "string"
},
"eat_period": {
"type": "string"
}
}
}
}
没有得那些属性就为""
如果不弃用得话,就会还记录type,会浪费内存
定制动态映射
true:遇到陌生字段,就进行dynamic mapping
false:新检测到得字段将被忽略。这些字段将不会被索引,因此将无法搜索,但仍将出现在返回点击的源字段中。这些字段不会添加到映射中,
必须显式添加新字段
strict:遇陌生字段,就报错
主要有一些
- 陌生字段映射
- 类型探测
- 进行自定义匹配规则dynamic_templates
创建mapping
PUT my_index3
{
"mappings": {
"dynamic": true
, "properties": {
"title":{
"type": "text"
},
"address":{
"type": "object",
"dynamic":true
}
}
}
}
这个address就是一个对象,里面有可能有未知道的属性
PUT /my_index
{
"mappings": {
"dynamic": "false",
"properties": {
"title": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
插入title,content,address数据,content为未知数据,不分词放入倒排索引,
PUT /my_index
{
"mappings": {
"dynamic": "strict",
"properties": {
"title": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
如果为strict就直接报错了
日期探测
比如现在有一个属性是2019-01-01如果直接插入的话,动态映射会是一个data类型,如果我们希望他是一个字符=串类型,
就需要把date_detection设置为false
PUT /my_index
{
"mappings": {
"date_detection": false,
"properties": {
"title": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
自定义日期格式
PUT my_index
{
"mappings": {
"dynamic_date_formats": ["MM/dd/yyyy"]
}
}
数字探测
numeric_detection
PUT my_index
{
"mappings": {
"numeric_detection": true
}
}
PUT my_index/_doc/1
{
"my_float": "1.0",
"my_integer": "1"
}
定制动态模板
PUT /my_index
{
"mappings": {
"dynamic_templates": [
{
"en": {
"match": "*_en",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "english"
}
}
}
]
}
}
定制en动态模板
-
match 是匹配 _en结尾的
-
match_mapping_type 匹配字符串String
-
如果满足上面两个,则自动映射为text 分词器为英语
-
mappings
- dynamic_templates
- 名字
- 匹配规则match
- 映射为什么mapping
- 匹配规则match
- 名字
- dynamic_templates
PUT /my_index/_doc/1
{
"title": "this is my first article"
}
PUT /my_index/_doc/2
{
"title_en": "this is my first article"
}
GET my_index/_search?q=first
#这个被模板检测到的话,因为英语的分词器is去掉,所以搜不到2文档
GET my_index/_search?q=is
PUT my_index
{
"mappings": {
"dynamic_templates": [
{
"integers": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
},
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}
- 看见long类型的就匹配,转为integer
- 看见string就转为text,还有另一个field
所以
-
match
- long_*
-
unmatch
- 不匹配这样的
-
match_mapping_type
- 类型
-
path_match 路劲
- name.*
-
path_unmatch
-
*.middle
-
"match_pattern": "regex" "match": "正则表达式"
使用场景
1,结构化搜索
映射为关键字,就只搜那个关键字
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
2,仅搜索
将内容全文检索搜索
{
"strings_as_text": {
"match_mapping_type": "string",
"mapping": {
"type": "text"
}
}
}
3,不关心评分norms
norms是指时间的评分因素,如不关心评分,例如不对文档评分进行排序,则可以在索引中禁用评分因子储存来节省空间。
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"norms": false,
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
“norms”: false
零停机重建索引
当插入一条数据,类型对不上你的索引库,不可能修改field类型,那么一个重新按照新的mapping,建立一个index
在系统建立索引过程中,是停机状态,影响生产。
PUT /my_index/_doc/1
{
"title": "2019-09-10"
}
PUT /my_index/_doc/2
{
"title": "2019-09-11"
}
上面会自动动态生成mapping
PUT /my_index/_doc/3
{
"title": "my first article"
}
上面属性对不上自动创建的index会报错
同时对于索引属性的类型不可改
我们需要定制一个新的mapping
PUT /my_index_new
{
"mappings": {
"properties": {
"title": {
"type": "text"
}
}
}
}
但是这样做会停机。
停机后,我们这样把旧的数据全部查出,然后批量插入新的索引
PUT /my_index/_alias/prod_index
#取出所有数据
GET /my_index/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 1
}
#批量插入
POST /_bulk
{ "index": { "_index": "my_index_new", "_id": "1" }}
{ "title": "2019-09-10" }
#别名切换
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index", "alias": "prod_index" }},
{ "add": { "index": "my_index_new", "alias": "prod_index" }}
]
}
#重新执行插入到新的
PUT /my_index_new/_doc/3
{
"title": "my first article"
}
#通过使用索引别名访问
GET /prod_index/_doc/3
这样的话我们就可以无缝切入到my_index_new
过程
- 当旧的index出现类型出错
- 先新创建一个符合类型的索引
- 查询旧的索引的全部数据,批量插入到新索引中
- 把旧索引的别名指向新索引
- 把出错的数据插入到新索引
所以在生产中,我们都是对索引起别名,因为出现类型错误时要0停机创建索引。然后别名指向新索引,使用别名查询代码依旧可以进行使用
- 陌生字段动态映射
- 日期和数字探测
- 动态模板
IK分词器
因为es的标志分词器不对中文支持,我们需要按照IK按中文分词
中文分词器IK
基础知识
- ik_max_word
- 最细粒度区别
- ik_smart
- 最粗粒度区别
GET /_analyze
{
"analyzer": "ik_max_word"
,"text": "中华人民共和国大会堂"
}
我们可以创建索引的时候指定分词器
PUT /my_index_new
{
"mappings": {
"properties": {
"text":{
"type": "text"
, "analyzer": "ik_max_word"
, "search_analyzer": "ik_smart"
}
}
}
}
存储用ik_max_word,搜索用ik_smart,这是固定的
PUT /my_index_new/_doc/1
{
"text":"中华人民共和国大会堂"
}
GET /my_index_new/_search?q=text:会堂
ik分词的配置文件
-
ik配置文件地址:es/plugins/ik/config目录
-
IKAnalyzer.cfg.xml:
-
用来配置自定义词库
-
main.dic:
-
ik原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在一起
-
preposition.dic: 介词
-
quantifier.dic:放了一些单位相关的词,量词
-
suffix.dic:放了一些后缀
-
surname.dic:中国的姓氏
-
stopword.dic:英文停用词 ,ik原生最重要的两个配置文件
-
stopword.dic:包含了英文的停用词
停用词,stopword a the and at but一般,像停用词,会在分词的时候,直接被干掉,不会建立在倒排索引中
自定义词组
因为在每年都有会一些流行词,所以我们需要自定义词汇,把一些新词放入main.dic
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DMmVrAdk-1666522442629)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221011104955818.png)]
然后到配置文件把文件设置好就行
使用mysql热更新词库
为什么我们要热更新?
虽然在拓展dic中添加拓展词汇,但是需要重启es,很不方便,不可能使用人工更新
热更新方案
请求远程接口
就是在配置文件中的
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
我们可以直接修改源码调用,手动支持从mysql中热更新
修改源代码
1、下载源码
https://github.com/medcl/elasticsearch-analysis-ik/releases
ik分词器,是个标准的java maven工程,直接导入eclipse就可以看到源码
2、修改源
org.wltea.analyzer.dic.Dictionary类,160行Dictionary单例类的初始化方法,在这里需要创建一个我们自定义的线程,并且启动它
org.wltea.analyzer.dic.HotDictReloadThread类:就是死循环,不断调用Dictionary.getSingleton().reLoadMainDict(),去重新加载词典
Dictionary类,399行:this.loadMySQLExtDict(); 加载mymsql字典。
Dictionary类,609行:this.loadMySQLStopwordDict();加载mysql停用词
config下jdbc-reload.properties。mysql配置文件
3、mvn package打包代码
target\releases\elasticsearch-analysis-ik-7.3.0.zip
4、解压缩ik压缩包
将mysql驱动jar,放入ik的目录下
5、修改jdbc相关配置
6、重启es
观察日志,日志中就会显示我们打印的那些东西,比如加载了什么配置,加载了什么词语,什么停用词
7、在mysql中添加词库与停用词
8、分词实验,验证热更新生效
JAVAAPI实现索引操作
新增
@Test
public void testCreateIndex() throws IOException {
//1,创建请求
CreateIndexRequest request = new CreateIndexRequest("user_index_new1");
//2,执行操作
//2.1,settings操作
request.settings(Settings.builder()
.put("number_of_shards","1")
.put("number_of_replicas","1")
);
//2.2 添加mapping
request.mapping("{\n" +
" \"properties\": {\n" +
" \"field1\":{\n" +
" \"type\": \"text\"\n" +
" },\n" +
" \"field2\":{\n" +
" \"type\": \"text\"\n" +
" }\n" +
" }\n" +
" }", XContentType.JSON);
//3,设置别名
request.alias(new Alias("my_alias_new"));
//4,额外参数
//4.1 超时时间为两分钟
request.setTimeout(TimeValue.timeValueMinutes(2));
//设置主节点超时时间
request.setMasterTimeout(TimeValue.timeValueMinutes(1));
//在创建索引API返回响应之前等待的活动分片副本的数量,以int形式表示
request.waitForActiveShards(ActiveShardCount.from(2));
request.waitForActiveShards(ActiveShardCount.DEFAULT);
IndicesClient indices = restHighLevelClient.indices();
CreateIndexResponse response = indices.create(request, RequestOptions.DEFAULT);
//是否成功
boolean isSuccess = response.isAcknowledged();
//得到响应 指示是否在超时前为索引中的每个分片启动了所需数量的碎片副本
boolean isShardsAcknowledged = response.isShardsAcknowledged();
System.out.println(isSuccess);
System.out.println(isShardsAcknowledged);
}
//异步
@Test
public void testAysnc() throws IOException {
//1,创建请求
CreateIndexRequest request = new CreateIndexRequest("user_index_new2");
//2,执行操作
//2.1,settings操作
request.settings(Settings.builder()
.put("number_of_shards","1")
.put("number_of_replicas","1")
);
//2.2 添加mapping
request.mapping("{\n" +
" \"properties\": {\n" +
" \"field1\":{\n" +
" \"type\": \"text\"\n" +
" },\n" +
" \"field2\":{\n" +
" \"type\": \"text\"\n" +
" }\n" +
" }\n" +
" }", XContentType.JSON);
//3,设置别名
request.alias(new Alias("my_alias_new"));
//4,额外参数
//4.1 超时时间为两分钟
request.setTimeout(TimeValue.timeValueMinutes(2));
//设置主节点超时时间
request.setMasterTimeout(TimeValue.timeValueMinutes(1));
//在创建索引API返回响应之前等待的活动分片副本的数量,以int形式表示
request.waitForActiveShards(ActiveShardCount.from(2));
request.waitForActiveShards(ActiveShardCount.DEFAULT);
ActionListener<CreateIndexResponse> listener = new ActionListener<CreateIndexResponse>() {
@Override
public void onResponse(CreateIndexResponse response) {
//是否成功
boolean isSuccess = response.isAcknowledged();
//得到响应 指示是否在超时前为索引中的每个分片启动了所需数量的碎片副本
boolean isShardsAcknowledged = response.isShardsAcknowledged();
System.out.println(isSuccess);
System.out.println(isShardsAcknowledged);
}
@Override
public void onFailure(Exception e) {
}
};
IndicesClient indices = restHighLevelClient.indices();
indices.createAsync(request, RequestOptions.DEFAULT,listener);
}
删除索引
//删除索引库
@Test
public void testDeleteIndex() throws IOException {
//删除索引对象
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("ydlclass_book2");
//操作索引的客户端
IndicesClient indices = restHighLevelClient.indices();
//执行删除索引
AcknowledgedResponse delete = indices.delete(deleteIndexRequest, RequestOptions.DEFAULT);
//得到响应
boolean acknowledged = delete.isAcknowledged();
System.out.println(acknowledged);
}
//异步删除索引库
@Test
public void testDeleteIndexAsync() throws IOException {
//删除索引对象
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("ydlclass_book2");
//操作索引的客户端
IndicesClient indices = restHighLevelClient.indices();
//监听方法
ActionListener<AcknowledgedResponse> listener =
new ActionListener<AcknowledgedResponse>() {
@Override
public void onResponse(AcknowledgedResponse deleteIndexResponse) {
System.out.println("!!!!!!!!删除索引成功");
System.out.println(deleteIndexResponse.toString());
}
@Override
public void onFailure(Exception e) {
System.out.println("!!!!!!!!删除索引失败");
e.printStackTrace();
}
};
//执行删除索引
indices.deleteAsync(deleteIndexRequest, RequestOptions.DEFAULT, listener);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
查看索引
// Indices Exists API
@Test
public void testExistIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("ydlclass_book");
request.local(false);//从主节点返回本地信息或检索状态
request.humanReadable(true);//以适合人类的格式返回结果
request.includeDefaults(false);//是否返回每个索引的所有默认设置
boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
开启和关闭索引
// Indices Open API
@Test
public void testOpenIndex() throws IOException {
OpenIndexRequest request = new OpenIndexRequest("ydlclass_book");
OpenIndexResponse openIndexResponse = restHighLevelClient.indices().open(request, RequestOptions.DEFAULT);
boolean acknowledged = openIndexResponse.isAcknowledged();
System.out.println("!!!!!!!!!"+acknowledged);
}
// Indices Close API
@Test
public void testCloseIndex() throws IOException {
CloseIndexRequest request = new CloseIndexRequest("index");
AcknowledgedResponse closeIndexResponse = restHighLevelClient.indices().close(request, RequestOptions.DEFAULT);
boolean acknowledged = closeIndexResponse.isAcknowledged();
System.out.println("!!!!!!!!!"+acknowledged);
}
搜素入门
无条件搜素
查全部
GET book/_search
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "book",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"name" : "java编程思想",
"description" : "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel" : "201001",
"price" : 68.6,
"timestamp" : "2019-08-25 19:11:35",
"pic" : "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags" : [
"java",
"dev"
]
}
},
{
"_index" : "book",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "spring开发基础",
"description" : "spring 在java领域非常流行,java程序员都在用。",
"studymodel" : "201001",
"price" : 88.6,
"timestamp" : "2019-08-24 19:11:35",
"pic" : "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags" : [
"spring",
"java"
]
}
}
]
}
}
字段信息
- took
- 花费时间
- timeout
- 超时
- shard
- 分片信息
- hits
- 数据信息
- max_source
- 匹配度分数
传参
查名字
GET book/_search?q=name:java
按名字查并且按金额排序
GET book/_search?q=name:java&sort=price
图解timeout机制
超时就把这个时间内查到的数据返回,比如有100条数据,10ms内只查到20条,就把20条返回
-
丢失超时部分,设置timeout时间
-
GET book/_search?q=name:java&sort=price&timeout=10ms
夜可以设置全局
-
多索引搜索
相当于多表
GET book,book1/_search
GET book*/_search
有时需要*,因为有时日期分表分库,根据日期创建索引
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5gyuZ44-1666522442630)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221011171838244.png)]
每个分片都有一部分数据,每个主分片都搜索,有副本可以到副本去搜索,
分页搜素
size,from进行分页
从0开始,拿3条数据
GET book/_search?from=0&size=3
深度分页
根据相关度评分倒排序,所以分页过深,协调节点会将大量数据聚合分析。
-
1消耗网络带宽,因为所搜过深的话,各 shard 要把数据传递给 coordinate node,这个过程是有大量数据传递的,消耗网络。
-
2消耗内存,各 shard 要把数据传送给 coordinate node,这个传递回来的数据,是被 coordinate node 保存在内存中的,这样会大量消耗内存。
-
3消耗cup,coordinate node 要把传回来的数据进行排序,这个排序过程很消耗cpu。 所以:鉴于deep paging的性能问题,所有应尽量减少使用
es如何进行分页
搜9990开始要10条数据,那么就是到10000,然后3个节点共3万条数据,进行大到小排序,然后在内存中的三万数据中的9990到10000取出这10条数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgPV0Ke6-1666522442630)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221011174259419.png)]
query string
name中不包含java的
GET book/_search?q=-name:java
包含java的
GET book/_search?q=+name:java
只要有java都搜出来
GET book/_search?q=java
它会将进行一次全量分词,就是一次全部放进去,放到all field中,没有指定时,就在_all搜索。
DSL入门
query string后面参数越来越多,搜索越来越复杂,不能满足需求,就是带上请求体。
query string基础语法
GET /book/_search?q=name:java
GET /book/_search?q=+name:java
GET /book/_search?q=-name:java
_all metadata的原理和作用
分词的时候就把一个完整的进去倒排索引,这样所有字段都能覆盖搜索
GET /book/_search?q=java
query DSL
简单固定语法
GET /test_index/_search
{
"query": {
"match": {
"test_field": "test"
}
}
}
- query
- match
- field
- sort
- 排序
- from size
- 分页
- _source
- 返回具体字段
- match
查全部
GET /book/_search
{
"query": { "match_all": {} }
}
查name和对priice排序
GET /book/_search
{
"query" : {
"match" : {
"name" : " java"
}
},
"sort": [
{ "price": "desc" }
]
}
分页
GET /book/_search
{
"query": { "match_all": {} },
"from": 0,
"size": 1
}
返回具体字段
GET /book/_search
{
"query": { "match_all": {} },
"_source": ["name", "studymodel"]
}
复杂完整语法
GET /website/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "elasticsearch"
}
}
],
"should": [
{
"match": {
"content": "elasticsearch"
}
}
],
"must_not": [
{
"match": {
"author_id": 111
}
}
]
}
}
}
- query
- bool
- must 必须的
- match
- should 可有的
- match
- must_not 不等于
- match
- must 必须的
- bool
select * from test_index where name='tom' or (hired =true and (personality ='good' and rude != true ))
GET /test_index/_search
{
"query": {
"bool": {
"must": { "match":{ "name": "tom" }},
"should": [
{ "match":{ "hired": true }},
{ "bool": {
"must":{ "match": { "personality": "good" }},
"must_not": { "match": { "rude": true }}
}}
],
"minimum_should_match": 1
}
}
}
_source
结果是按照匹配分数排序的,所以,我们来看看这个分数是怎么得出来得
其实很简单,你的输入条件,它会根据分词策略进行分词,看看文档里对于搜索条件分词的频率。
1、建立索引时, description字段 term倒排索引
java 2,3
程序员 3
2、搜索时,直接找description中含有java的文档 2,3,并且3号文档含有两个java字段,一个程序员,所以得分高,排在前面。2号文档含有一个java,排在后面。
匹配规则
match_all
匹配全部
GET /book/_search
{
"query": {
"match_all": {}
}
}
match
对字段全文检索
GET /book/_search
{
"query": {
"match": {
"description": "java程序员"
}
}
}
multi_match
匹配多字段
GET /book/_search
{
"query": {
"multi_match": {
"query": "java程序员",
"fields": ["name", "description"]
}
}
}
range query 范围查询
范围查询
GET /book/_search
{
"query": {
"range": {
"price": {
"gte": 80,
"lte": 90
}
}
}
}
term query
字段为key word时搜索和储存不进行分词
GET /book/_search
{
"query": {
"term": {
"description": "java程序员"
}
}
}
terms query
和上面一样就是多个字段,直接查,不用分词
GET /book/_search
{
"query": { "terms": { "tag": [ "search", "full_text", "nosql" ] }}
}
exist query
查有该字段的文档
GET /_search
{
"query": {
"exists": {
"field": "join_date"
}
}
}
Fuzzy query
智能搜索
GET /book/_search
{
"query": {
"fuzzy": {
"description": {
"value": "jave"
}
}
}
}
IDS
根据id查
GET /book/_search
{
"query": {
"ids" : {
"values" : ["1", "4", "100"]
}
}
}
前缀
GET /book/_search
{
"query": {
"prefix": {
"description": {
"value": "spring"
}
}
}
}
正则查
GET /book/_search
{
"query": {
"regexp": {
"description": {
"value": "j.*a",
"flags" : "ALL",
"max_determinized_states": 10000,
"rewrite": "constant_score"
}
}
}
}
Filter过滤器
query和filter的例子
查询despcrition中有java程序员的,并且加个大于50小于90的
query的
GET /book/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "java程序员"
}
},
{
"range": {
"price": {
"gte": 80,
"lte": 90
}
}
}
]
}
}
}
使用filter
GET /book/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "java程序员"
}
}
],
"filter": {
"range": {
"price": {
"gte": 80,
"lte": 90
}
}
}
}
}
}
对比
- query对文档分数排序,所以要处理文档的分数评分
- filter只根据条件过滤,不处理分数
性能
filter不参与分数评分排序,同时内置缓存,更快
定位错误语法
GET /book/_validate/query?explain
{
"query": {
"mach": {
"description": "java程序员"
}
}
}
错误提示
{
"valid" : false,
"error" : "org.elasticsearch.common.ParsingException: no [query] registered for [mach]"
}
写得多的时候可以用这种方法验证
语法,_validate/query?explain
定制排序规则
两种
- 默认使用query,分数排序
- fliter,然后定制
使用过滤器的
GET /book/_search
{
"query": {
"constant_score": {
"filter" : {
"term" : {
"studymodel" : "201001"
}
}
}
},
"sort": [
{
"price": {
"order": "asc"
}
}
]
}
正常的
GET book/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "java程序员"
}
}
]
}
}
}
对text排序
如果对一个text field进行排序,结果往往不准确,因为分词后是多个单词,再排序就不是我们想要的结果了。
通常解决方案是,将一个text field建立两次索引,一个分词,用来进行搜索;一个不分词,用来进行排序。、
索引给加多一个属性,就是fields加一个keyword,查的时候直接field.keyword
PUT /website
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"content": {
"type": "text"
},
"post_date": {
"type": "date"
},
"author_id": {
"type": "long"
}
}
}
}
PUT /website/_doc/1
{
"title": "first article",
"content": "this is my second article",
"post_date": "2019-01-01",
"author_id": 110
}
PUT /website/_doc/2
{
"title": "second article",
"content": "this is my second article",
"post_date": "2019-01-01",
"author_id": 110
}
PUT /website/_doc/3
{
"title": "third article",
"content": "this is my third article",
"post_date": "2019-01-02",
"author_id": 110
}
搜索字段的不分词哪里keyword
GET /website/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"title.keyword": {
"order": "desc"
}
}
]
}
Scroll分批查询
由于数据量过大,不能一次查出,内部分多次查,查到全部一起返回。
GET /book/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 3
}
{
"_scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAMOkWTURBNDUtcjZTVUdKMFp5cXloVElOQQ==",
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
]
}
}
但是不这么做,一般定制mapping
用于系统内部操作,0停机改变索引,数据转移
JAVA搜索API
简单搜素
@Test
public void testGetAll() throws IOException {
//1,构建请求
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
//匹配全部
builder.query(QueryBuilders.matchAllQuery());
//获取某些字段 第一个数组是想要,第二个数组是排除
builder.fetchSource(new String[]{"name"}, new String[]{});
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//所有数据
SearchHits hits = response.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit:
hits1) {
Map<String, Object> map = hit.getSourceAsMap();
System.out.println(map.get("name"));
System.out.println(map.get("price"));
System.out.println(map.get("description"));
System.out.println(hit);
}
}
分页搜素
from size
@Test
public void testPage() throws IOException {
// GET book/_search
// {
// "query": {
// "match_all": {}
// },
// "from": 0,
// "size": 2
// }
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchAllQuery());
builder.from(0);
builder.size(1);
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit:
hits1) {
System.out.println(hit);
}
}
根据ID搜
@Test
public void testIDS() throws IOException {
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.idsQuery().addIds("2"));
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit:
hits1) {
System.out.println(hit);
}
}
全文检索
@Test
public void testMatch() throws IOException {
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchQuery("description", "java"));
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit :
hits1) {
System.out.println(hit);
}
}
多条件全文检索
@Test
public void multiMatch() throws IOException {
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.multiMatchQuery("java", "description","name"));
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit :
hits1) {
System.out.println(hit);
}
}
使用bool查询
@Test
public void testBool() throws IOException {
// GET /book/_search
// {
// "query": {
// "bool": {
// "must": [
// {
// "multi_match": {
// "query": "java程序员",
// "fields": ["name","description"]
// }
// }
// ],
// "should": [
// {
// "match": {
// "studymodel": "201001"
// }
// }
// ]
// }
// }
// }
//bool复杂搜索
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("java程序", "name", "description");
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("studymodel", "201001");
boolQueryBuilder.must(multiMatchQueryBuilder);
boolQueryBuilder.should(matchQueryBuilder);
builder.query(boolQueryBuilder);
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit :
hits1) {
System.out.println(hit);
}
}
使用过滤
@Test
public void testBoolAndFitler() throws IOException {
// GET /book/_search
// {
// "query": {
// "bool": {
// "must": [
// {
// "multi_match": {
// "query": "java程序员",
// "fields": ["name","description"]
// }
// }
// ],
// "should": [
// {
// "match": {
// "studymodel": "201001"
// }
// }
// ],
// "filter": {
// "range": {
// "price": {
// "gte": 50,
// "lte": 90
// }
// }
//
// }
// }
// }
// }
//bool加上filter复杂搜索
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("java程序", "name", "description");
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("studymodel", "201001");
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(50).lte(90);
boolQueryBuilder.must(multiMatchQueryBuilder);
boolQueryBuilder.should(matchQueryBuilder);
boolQueryBuilder.filter(rangeQueryBuilder);
builder.query(boolQueryBuilder);
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit :
hits1) {
System.out.println(hit);
}
}
排序
@Test
public void testSort() throws IOException {
// GET /book/_search
// {
// "query": {
// "bool": {
// "must": [
// {
// "multi_match": {
// "query": "java程序员",
// "fields": ["name","description"]
// }
// }
// ],
// "should": [
// {
// "match": {
// "studymodel": "201001"
// }
// }
// ],
// "filter": {
// "range": {
// "price": {
// "gte": 50,
// "lte": 90
// }
// }
//
// }
// }
// },
// "sort": [
// {
// "price": {
// "order": "asc"
// }
// }
// ]
// }
//bool加上filter复杂搜索
SearchRequest request = new SearchRequest("book");
SearchSourceBuilder builder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("java程序", "name", "description");
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("studymodel", "201001");
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price").gte(50).lte(90);
boolQueryBuilder.must(multiMatchQueryBuilder);
boolQueryBuilder.should(matchQueryBuilder);
boolQueryBuilder.filter(rangeQueryBuilder);
//排序
builder.sort("price", SortOrder.DESC);
builder.query(boolQueryBuilder);
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit :
hits1) {
System.out.println(hit);
}
}
TFIDF算法
评分机制
算法介绍
分析要素
- 出现频率,
- 多的分数高
- 逆序出现频率
- 整个文档中越多出现的分数越低
- 长度
- field长度越长,相关度越弱
score怎么算出来
- 1,对用户输入进行分词
- 2,对每个分词计算tf和idf值
- 3,综合每个分词的tf和idf值,利用公式计算每个文档评分
DOC value
搜索时使用倒排索引,排序时使用正排索引
DOC value就是正排索引
比如对price进行排序,或者对字符串进行排序
对text field排序,不准确,因为分词后是多个单词,再排序就不是我们的结果
- 方案一
- fielddata:true
- 方案二
- 使用正排索引,和数据库差不多,一条一条的存
query phase
在返回数据前的阶段,就是把查到的数据先只返回两个id和score字段到协调节点
fetch phase
获取数据阶段,按照上面阶段获取到的id,多次批量获取
preference
跳越结果情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-liiSu8fl-1666522442630)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221012143111772.png)]
- 情况一
- 3号文档为同步到R0,查P0和P1则拿到132
- 到R0和P1拿则拿到12
- 情况二
- P0和P1的读取顺序,P0快时,13 然后2
- P1快时,2 然后13
决定了哪些shard会被用来执行搜索操作
_primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz, _shards:2,3
解决方案就是将preference设置为一个字符串,比如说user_id,让每个user每次搜索的时候,都使用同一个replica shard去执行,就不会看到bouncing results
ES的聚合
像mysql的聚合函数,求总数,最大值,最小值,平均值等等。
注意,只有不建立分词倒排的才可以进行聚合操作
PUT /book/_mapping/
{
"properties": {
"tags": {
"type": "text",
"fielddata":true
}
}
}
可以通过上述操作,或者在创建mapping的时候加多个field弄个keyword
aggs其实就是聚合
terms求个数
1、需求:计算每个studymodel下的商品数量
select studymodel,count(*) from book group by studymodel
size0是为了跳过具体的数据
GET /book/_search
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"group_by_model": {
"terms": { "field": "studymodel" }
}
}
}
2、需求:计算每个tags下的商品数量
GET /book/_search
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"group_by_tags": {
"terms": { "field": "tags" }
}
}
}
3,需求:加上搜索条件,计算每个tags下的商品数量
GET /book/_search
{
"size": 0,
"query": {
"match": {
"description": "java程序员"
}
}
, "aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
}
}
}
}
4,需求:先分组,再算每组的平均值,计算每个tag下的商品的平均价格
GET /book/_search
{
"size": 0,
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
}
, "aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
5,需求:计算每个tag下的商品的平均价格,并且按照平均价格降序排序
es的语法
聚合里可以直接调用聚合的结果进行排序
GET /book/_search
{
"size": 0,
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags",
"order": {
"avg_price": "desc"
}
}
, "aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
6,需求:1,按照指定的价格范围区间进行分组,2,然后在每组内再按照tag进行分组,3最后再计算每组的平均价格
GET /book/_search
{
"size": 0,
"aggs": {
"group_by_price": {
"range": {
"field": "price",
"ranges": [
{
"from": 0,
"to": 40
},
{
"from": 40,
"to": 60
},
{
"from": 60,
"to": 80
}
]
},
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
, "order": {
"average_price": "desc"
}
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}}
关于两个概率
bucket
一个数据分组。按照城市分桶,
select city,count(*) from table group by city
metric
聚合函数操作,比如求平均值,最小值,最大值
聚合电视的案例
建立index
PUT /tvs
PUT /tvs/_mapping
{
"properties": {
"price": {
"type": "long"
},
"color": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"sold_date": {
"type": "date"
}
}
}
4个属性
- price
- 价格
- color颜色
- brand品牌
- sold_data 销售日期
插入数据
POST /tvs/_bulk
{ "index": {}}
{ "price" : 1000, "color" : "红色", "brand" : "长虹", "sold_date" : "2019-10-28" }
{ "index": {}}
{ "price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "2019-11-05" }
{ "index": {}}
{ "price" : 3000, "color" : "绿色", "brand" : "小米", "sold_date" : "2019-05-18" }
{ "index": {}}
{ "price" : 1500, "color" : "蓝色", "brand" : "TCL", "sold_date" : "2019-07-02" }
{ "index": {}}
{ "price" : 1200, "color" : "绿色", "brand" : "TCL", "sold_date" : "2019-08-19" }
{ "index": {}}
{ "price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "2019-11-05" }
{ "index": {}}
{ "price" : 8000, "color" : "红色", "brand" : "三星", "sold_date" : "2020-01-01" }
{ "index": {}}
{ "price" : 2500, "color" : "蓝色", "brand" : "小米", "sold_date" : "2020-02-12" }
需求
需求1 统计哪种颜色的电视销量最高
桶的第一个就是
GET /tvs/_search
{
"size": 0,
"aggs": {
"popular_color": {
"terms": {
"field": "color"
}
}
}
}
统计每种颜色电视平均价格
GET /tvs/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
每个颜色下,平均价格及每个颜色下,每个品牌的平均价格
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"color_avg_price": {
"avg": {
"field": "price"
}
},
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"brand_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}
#各种颜色的最大,最小,平均,总价格
#各种颜色的最大,最小,平均,总价格
GET /tvs/_search
{
"size": 0,
"aggs": {
"all_colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {"avg":{"field": "price"}},
"sum_price":{ "sum":{"field": "price"}},
"min_price":{ "min":{"field": "price"}},
"max_price":{ "max":{"field": "price"}}
}
}
}
}
#划分范围 histogram
#划分范围 histogram
GET /tvs/_search
{
"aggs": {
"price_score": {
"histogram": {
"field": "price",
"interval": 2000
},
"aggs": {
"sum_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
#按照日期分组聚合
#按照日期分组聚合
GET /tvs/_search
{
"size": 0,
"aggs": {
"data_scope": {
"date_histogram": {
"field": "sold_date",
"interval": "month",
"format": "yyyy-MM-dd",
"extended_bounds": {
"min" : "2019-01-01",
"max" : "2020-12-31"
}
}
}
}
}
#需求7 统计每季度每个品牌的销售额
#需求7 统计每季度每个品牌的销售额
GET /tvs/_search
{
"size": 0,
"aggs": {
"data_scope": {
"date_histogram": {
"field": "sold_date",
"interval": "quarter",
"min_doc_count": 0,
"format": "yyyy-MM-dd",
"extended_bounds": {
"min" : "2019-01-01",
"max" : "2020-12-31"
}
},
"aggs": {
"all_brands": {
"terms": {
"field": "brand"
},
"aggs": {
"price_3mon": {
"sum": {
"field": "price"
}
}
}
},
"total_sum_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
#搜索与聚合结合,查询某个品牌按颜色销量
#搜索与聚合结合,查询某个品牌按颜色销量
GET /tvs/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "小米"
}
}
},
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
}
}
}
}
#global bucket:单个品牌与所有品牌销量对比 global为全局桶
#global bucket:单个品牌与所有品牌销量对比 global为全局桶
GET /tvs/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "小米"
}
}
},
"aggs": {
"one_brand": {
"avg": {
"field": "price"
}
},
"all_brand":{
"global": {},
"aggs": {
"all_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
#过滤+聚合:统计价格大于1200的电视平均价格
#过滤+聚合:统计价格大于1200的电视平均价格
GET /tvs/_search
{
"size": 0,
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 1200
}
}
}
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
#bucket filter:统计品牌最近一个月的平均价格
GET /tvs/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "小米"
}
}
},
"aggs": {
"recent_150d": {
"filter": {
"range": {
"sold_date": {
"gte": "now-150d"
}
}
},
"aggs": {
"recent_150d_avg_price": {
"avg": {
"field": "price"
}
}
}
},
"recent_140d": {
"filter": {
"range": {
"sold_date": {
"gte": "now-140d"
}
}
},
"aggs": {
"recent_140d_avg_price": {
"avg": {
"field": "price"
}
}
}
},
"recent_130d": {
"filter": {
"range": {
"sold_date": {
"gte": "now-130d"
}
}
},
"aggs": {
"recent_130d_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
#排序:按每种颜色的平均销售额降序排序
#排序:按每种颜色的平均销售额降序排序
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"avg_price": "asc"
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
#排序:按每种颜色的每种品牌平均销售额降序排序
#排序:按每种颜色的每种品牌平均销售额降序排序
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"group_by_brand": {
"terms": {
"field": "brand",
"order": {
"avg_price": "desc"
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}
javaAPI实现聚合
//需求一:按照颜色分组,计算每个颜色卖出的个数
//需求一:按照颜色分组,计算每个颜色卖出的个数
// GET /tvs/_search
// {
// "size": 0,
// "query": {"match_all": {}},
// "aggs": {
// "group_by_color": {
// "terms": {
// "field": "color"
// }
// }
// }
// }
@Test
public void test1() throws IOException {
SearchRequest request = new SearchRequest("tvs");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.size(0);
builder.query(QueryBuilders.matchAllQuery());
TermsAggregationBuilder aggsBuild = AggregationBuilders.terms("group_by_color").field("color");
builder.aggregation(aggsBuild);
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//获取aggs
Aggregations aggregations = response.getAggregations();
//分组
Terms group_by_color = aggregations.get("group_by_color");
List<? extends Terms.Bucket> buckets = group_by_color.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println("key:"+key);
long docCount = bucket.getDocCount();
System.out.println("docCount:"+docCount);
System.out.println("=================================");
}
}
// #需求二:按照颜色分组,计算每个颜色卖出的个数,每个颜色卖出的平均价格
@Test
public void testAggsAndAvg() throws IOException {
// GET /tvs/_search
// {
// "size": 0,
// "query": {"match_all": {}},
// "aggs": {
// "group_by_color": {
// "terms": {
// "field": "color"
// },
// "aggs": {
// "avg_price": {
// "avg": {
// "field": "price"
// }
// }
// }
// }
// }
// }
//1 构建请求
SearchRequest searchRequest=new SearchRequest("tvs");
//请求体
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("group_by_color").field("color");
//terms聚合下填充一个子聚合
AvgAggregationBuilder avgAggregationBuilder = AggregationBuilders.avg("avg_price").field("price");
termsAggregationBuilder.subAggregation(avgAggregationBuilder);
searchSourceBuilder.aggregation(termsAggregationBuilder);
//请求体放入请求头
searchRequest.source(searchSourceBuilder);
//2 执行
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//3 获取结果
// {
// "key" : "红色",
// "doc_count" : 4,
// "avg_price" : {
// "value" : 3250.0
// }
// }
Aggregations aggregations = searchResponse.getAggregations();
Terms group_by_color = aggregations.get("group_by_color");
List<? extends Terms.Bucket> buckets = group_by_color.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println("key:"+key);
long docCount = bucket.getDocCount();
System.out.println("docCount:"+docCount);
Aggregations aggregations1 = bucket.getAggregations();
Avg avg_price = aggregations1.get("avg_price");
double value = avg_price.getValue();
System.out.println("value:"+value);
System.out.println("=================================");
}
}
// #需求四:按照售价每2000价格划分范围,算出每个区间的销售总额 histogram
@Test
public void testAggsAndHistogram() throws IOException {
// GET /tvs/_search
// {
// "size" : 0,
// "aggs":{
// "by_histogram":{
// "histogram":{
// "field": "price",
// "interval": 2000
// },
// "aggs":{
// "income": {
// "sum": {
// "field" : "price"
// }
// }
// }
// }
// }
// }
//1 构建请求
SearchRequest searchRequest=new SearchRequest("tvs");
//请求体
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
HistogramAggregationBuilder histogramAggregationBuilder = AggregationBuilders.histogram("by_histogram").field("price").interval(2000);
SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("income").field("price");
histogramAggregationBuilder.subAggregation(sumAggregationBuilder);
searchSourceBuilder.aggregation(histogramAggregationBuilder);
//请求体放入请求头
searchRequest.source(searchSourceBuilder);
//2 执行
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//3 获取结果
// {
// "key" : 0.0,
// "doc_count" : 3,
// income" : {
// "value" : 3700.0
// }
// }
Aggregations aggregations = searchResponse.getAggregations();
Histogram group_by_color = aggregations.get("by_histogram");
List<? extends Histogram.Bucket> buckets = group_by_color.getBuckets();
for (Histogram.Bucket bucket : buckets) {
String keyAsString = bucket.getKeyAsString();
System.out.println("keyAsString:"+keyAsString);
long docCount = bucket.getDocCount();
System.out.println("docCount:"+docCount);
Aggregations aggregations1 = bucket.getAggregations();
Sum income = aggregations1.get("income");
double value = income.getValue();
System.out.println("value:"+value);
System.out.println("=================================");
}
}
// #需求五:计算每个季度的销售总额
@Test
public void testAggsAndDateHistogram() throws IOException {
// GET /tvs/_search
// {
// "size" : 0,
// "aggs": {
// "sales": {
// "date_histogram": {
// "field": "sold_date",
// "interval": "quarter",
// "format": "yyyy-MM-dd",
// "min_doc_count" : 0,
// "extended_bounds" : {
// "min" : "2019-01-01",
// "max" : "2020-12-31"
// }
// },
// "aggs": {
// "income": {
// "sum": {
// "field": "price"
// }
// }
// }
// }
// }
// }
//1 构建请求
SearchRequest searchRequest=new SearchRequest("tvs");
//请求体
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
DateHistogramAggregationBuilder dateHistogramAggregationBuilder = AggregationBuilders.dateHistogram("date_histogram").field("sold_date").calendarInterval(DateHistogramInterval.QUARTER)
.format("yyyy-MM-dd").minDocCount(0).extendedBounds(new ExtendedBounds("2019-01-01", "2020-12-31"));
SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("income").field("price");
dateHistogramAggregationBuilder.subAggregation(sumAggregationBuilder);
searchSourceBuilder.aggregation(dateHistogramAggregationBuilder);
//请求体放入请求头
searchRequest.source(searchSourceBuilder);
//2 执行
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//3 获取结果
// {
// "key_as_string" : "2019-01-01",
// "key" : 1546300800000,
// "doc_count" : 0,
// "income" : {
// "value" : 0.0
// }
// }
Aggregations aggregations = searchResponse.getAggregations();
ParsedDateHistogram date_histogram = aggregations.get("date_histogram");
List<? extends Histogram.Bucket> buckets = date_histogram.getBuckets();
for (Histogram.Bucket bucket : buckets) {
String keyAsString = bucket.getKeyAsString();
System.out.println("keyAsString:"+keyAsString);
long docCount = bucket.getDocCount();
System.out.println("docCount:"+docCount);
Aggregations aggregations1 = bucket.getAggregations();
Sum income = aggregations1.get("income");
double value = income.getValue();
System.out.println("value:"+value);
System.out.println("====================");
}
}
// #需求三:按照颜色分组,计算每个颜色卖出的个数,以及每个颜色卖出的平均值、最大值、最小值、总和。
@Test
public void testAggsAndMore() throws IOException {
// GET /tvs/_search
// {
// "size" : 0,
// "aggs": {
// "group_by_color": {
// "terms": {
// "field": "color"
// },
// "aggs": {
// "avg_price": { "avg": { "field": "price" } },
// "min_price" : { "min": { "field": "price"} },
// "max_price" : { "max": { "field": "price"} },
// "sum_price" : { "sum": { "field": "price" } }
// }
// }
// }
// }
//1 构建请求
SearchRequest searchRequest=new SearchRequest("tvs");
//请求体
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("group_by_color").field("color");
//termsAggregationBuilder里放入多个子聚合
AvgAggregationBuilder avgAggregationBuilder = AggregationBuilders.avg("avg_price").field("price");
MinAggregationBuilder minAggregationBuilder = AggregationBuilders.min("min_price").field("price");
MaxAggregationBuilder maxAggregationBuilder = AggregationBuilders.max("max_price").field("price");
SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("sum_price").field("price");
termsAggregationBuilder.subAggregation(avgAggregationBuilder);
termsAggregationBuilder.subAggregation(minAggregationBuilder);
termsAggregationBuilder.subAggregation(maxAggregationBuilder);
termsAggregationBuilder.subAggregation(sumAggregationBuilder);
searchSourceBuilder.aggregation(termsAggregationBuilder);
//请求体放入请求头
searchRequest.source(searchSourceBuilder);
//2 执行
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//3 获取结果
// {
// "key" : "红色",
// "doc_count" : 4,
// "max_price" : {
// "value" : 8000.0
// },
// "min_price" : {
// "value" : 1000.0
// },
// "avg_price" : {
// "value" : 3250.0
// },
// "sum_price" : {
// "value" : 13000.0
// }
// }
Aggregations aggregations = searchResponse.getAggregations();
Terms group_by_color = aggregations.get("group_by_color");
List<? extends Terms.Bucket> buckets = group_by_color.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println("key:"+key);
long docCount = bucket.getDocCount();
System.out.println("docCount:"+docCount);
Aggregations aggregations1 = bucket.getAggregations();
Max max_price = aggregations1.get("max_price");
double maxPriceValue = max_price.getValue();
System.out.println("maxPriceValue:"+maxPriceValue);
Min min_price = aggregations1.get("min_price");
double minPriceValue = min_price.getValue();
System.out.println("minPriceValue:"+minPriceValue);
Avg avg_price = aggregations1.get("avg_price");
double avgPriceValue = avg_price.getValue();
System.out.println("avgPriceValue:"+avgPriceValue);
Sum sum_price = aggregations1.get("sum_price");
double sumPriceValue = sum_price.getValue();
System.out.println("sumPriceValue:"+sumPriceValue);
System.out.println("=================================");
}
}
ES7新特性
可以使用sql的语法进行查询
get /_sql?format=txt
{
"query": "select color,avg(price),min(price) from tvs group by color"
}
使用javaapi实现sql
-
1,必要有白金会员的功能
-
2,导入坐标
导入依赖 <dependency> <groupId>org.elasticsearch.plugin</groupId> <artifactId>x-pack-sql-jdbc</artifactId> <version>7.3.0</version> </dependency> 远程创库 <repositories> <repository> <id>elastic.co</id> <url>https://artifacts.elastic.co/maven</url> </repository> </repositories>
3,api
@Test
public void sqlTestJDBC(){
//1,创建连接
try {
Connection connection = DriverManager.getConnection("jdbc:es://http://localhost:9000");
//2,创建statement
Statement statement = connection.createStatement();
//3,执行sql
ResultSet resultSet = statement.executeQuery("select * from tvs");
//4,获取结果
while (resultSet.next()){
System.out.println(resultSet.getString(1));
System.out.println(resultSet.getString(2));
System.out.println(resultSet.getString(3));
System.out.println(resultSet.getString(4));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
Logstash
什么是logstash?
它是一个数据抽取工具,转移数据。
-
input
-
output
-
filter
下载
https://www.elastic.co/cn/downloads/logstash
配置文件
input {
#输入插件
}
filter {
#过滤匹配插件
}
output {
#输出插件
}
启动
logstash.bat -e 'input{stdin{}} output{stdout{}}'
在生产中input{stdin{}},这里是输入环境,也就是你的数据,比如数据库中的表或者某个日志文件
output{stdout{}}输出环境,也就是我们的es
我们可将它物化到一个文件中
我们在一个配置文件中这样写下这些配置文件
输入在控制台输入,输出也是在控制台,
然后启动
logstash.bat -f …/config/test1.conf
input{
stdin{
}
}
filter{
#过滤匹配插件
}
output{
stdout{
codec=>rubydebug
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhtM3uiq-1666522442631)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221013114000571.png)]
输入插件
https://www.elastic.co/guide/en/logstash/current/input-plugins.html
可以从各个地方中获取数据
比如这些,还有很多很多
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VcCpvStB-1666522442631)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221013114430807.png)]
读取文件信息
input {
file {
path => ["/var/*/*"]
start_position => "beginning"
}
}
output {
stdout{
codec=>rubydebug
}
}
这样的话,我们可以实现分布式日志读到一个文件的实现
读取TCP网络信息
input {
tcp {
port => "1234"
}
}
filter {
grok {
match => { "message" => "%{SYSLOGLINE}" }
}
}
output {
stdout{
codec=>rubydebug
}
}
所以我们也可以取读jdbc,也就是mysql的,
过滤插件
https://www.elastic.co/guide/en/logstash/current/filter-plugins.html
Grok 正则捕获
grok是一个十分强大的logstash filter插件,他可以通过正则解析任意文本,将非结构化日志数据弄成结构化和方便查询的结构。他是目前logstash 中解析非结构化日志数据最好的方式。
Grok 的语法规则是:
%{语法: 语义}
例如输入的内容为:
172.16.213.132 [07/Feb/2019:16:24:19 +0800] "GET / HTTP/1.1" 403 5039
%{IP:clientip}匹配模式将获得的结果为:clientip: 172.16.213.132 %{HTTPDATE:timestamp}匹配模式将获得的结果为:timestamp: 07/Feb/2018:16:24:19 +0800 而%{QS:referrer}匹配模式将获得的结果为:referrer: “GET / HTTP/1.1”
下面是一个组合匹配模式,它可以获取上面输入的所有内容:
%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}
通过上面这个组合匹配模式,我们将输入的内容分成了五个部分,即五个字段,将输入内容分割为不同的数据字段,这对于日后解析和查询日志数据非常有用,这正是使用grok的目的。
例子:
input{
stdin{}
}
filter{
grok{
match => ["message","%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}"]
}
}
output{
stdout{
codec => "rubydebug"
}
}
输入内容:
172.16.213.132 [07/Feb/2019:16:24:19 +0800] "GET / HTTP/1.1" 403 5039
# 2、时间处理(Date)
date插件是对于排序事件和回填旧数据尤其重要,它可以用来转换日志记录中的时间字段,变成LogStash::Timestamp对象,然后转存到@timestamp字段里,这在之前已经做过简单的介绍。 下面是date插件的一个配置示例(这里仅仅列出filter部分):
filter {
grok {
match => ["message", "%{HTTPDATE:timestamp}"]
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
}
}
# 3、数据修改(Mutate)
# (1)正则表达式替换匹配字段
gsub可以通过正则表达式替换字段中匹配到的值,只对字符串字段有效,下面是一个关于mutate插件中gsub的示例(仅列出filter部分):
filter {
mutate {
gsub => ["filed_name_1", "/" , "_"]
}
}
这个示例表示将filed_name_1字段中所有"/“字符替换为”_"。
# (2)分隔符分割字符串为数组
split可以通过指定的分隔符分割字段中的字符串为数组,下面是一个关于mutate插件中split的示例(仅列出filter部分):
filter {
mutate {
split => ["filed_name_2", "|"]
}
}
这个示例表示将filed_name_2字段以"|"为区间分隔为数组。
# (3)重命名字段
rename可以实现重命名某个字段的功能,下面是一个关于mutate插件中rename的示例(仅列出filter部分):
filter {
mutate {
rename => { "old_field" => "new_field" }
}
}
这个示例表示将字段old_field重命名为new_field。
# (4)删除字段
remove_field可以实现删除某个字段的功能,下面是一个关于mutate插件中remove_field的示例(仅列出filter部分):
filter {
mutate {
remove_field => ["timestamp"]
}
}
这个示例表示将字段timestamp删除。
# (5)GeoIP 地址查询归类
filter {
geoip {
source => "ip_field"
}
}
# 综合例子:
- 输入
- 控制台输入
- 过滤
- grok正则插件
- 删除message
- 将timestamp格式
- response状态码改为float类型
- 在referrer中的\改为空
- clientip以.分割为数组
- 输出
- 控制台输出
input {
stdin {}
}
filter {
grok {
match => { "message" => "%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}" }
remove_field => [ "message" ]
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
}
mutate {
convert => [ "response","float" ]
rename => { "response" => "response_new" }
gsub => ["referrer","\"",""]
split => ["clientip", "."]
}
}
output {
stdout {
codec => "rubydebug"
}
四、Logstash输出插件(output)
https://www.elastic.co/guide/en/logstash/current/output-plugins.html
output是Logstash的最后阶段,一个事件可以经过多个输出,而一旦所有输出处理完成,整个事件就执行完成。 一些常用的输出包括:
- file: 表示将日志数据写入磁盘上的文件。
- elasticsearch:表示将日志数据发送给Elasticsearch。Elasticsearch可以高效方便和易于查询的保存数据。
1、输出到标准输出(stdout)
output {
stdout {
codec => rubydebug
}
}
2、保存为文件(file)
统一输出实现分布式集中管理日志
output {
file {
path => "/data/log/%{+yyyy-MM-dd}/%{host}_%{+HH}.log"
}
}
3、输出到elasticsearch
output {
elasticsearch {
host => ["192.168.1.1:9200","172.16.213.77:9200"]
index => "logstash-%{+YYYY.MM.dd}"
}
}
- host:是一个数组类型的值,后面跟的值是elasticsearch节点的地址与端口,默认端口是9200。可添加多个地址。
- index:写入elasticsearch的索引的名称,这里可以使用变量。Logstash提供了%{+YYYY.MM.dd}这种写法。在语法解析的时候,看到以+ 号开头的,就会自动认为后面是时间格式,尝试用时间格式来解析后续字符串。这种以天为单位分割的写法,可以很容易的删除老的数据或者搜索指定时间范围内的数据。此外,注意索引名中不能有大写字母。
- manage_template:用来设置是否开启logstash自动管理模板功能,如果设置为false将关闭自动管理模板功能。如果我们自定义了模板,那么应该设置为false。
- template_name:这个配置项用来设置在Elasticsearch中模板的名称。
# 五、综合案例
- 输入 监听ngix日志文件
- 过滤和上面的一样
- 输出到es保存
- 往那个索引输入,接上年月日,更方便按时间看日志
input {
file {
path => ["D:/ES/logstash-7.3.0/nginx.log"]
start_position => "beginning"
}
}
filter {
grok {
match => { "message" => "%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}" }
remove_field => [ "message" ]
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
}
mutate {
rename => { "response" => "response_new" }
convert => [ "response","float" ]
gsub => ["referrer","\"",""]
remove_field => ["timestamp"]
split => ["clientip", "."]
}
}
output {
stdout {
codec => "rubydebug"
}
elasticsearch {
host => ["localhost:9200"]
index => "logstash-%{+YYYY.MM.dd}"
}
}
select content from message_1 where content like “%今天不开心%”;
select * from message_1 where content in
(select content from message_1 where content like “%今天不开心%”);
集群部署
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esRYaYag-1666522442632)(C:\Users\86135\AppData\Roaming\Typora\typora-user-images\image-20221013152652923.png)]
结点的三个角色
主结点:master节点主要用于集群的管理及索引 比如新增结点、分片分配、索引的新增和删除等。 数据结点:data 节点上保存了数据分片,它负责索引和搜索操作。 客户端结点:client 节点仅作为请求客户端存在,client的作用也作为负载均衡器,client 节点不存数据,只是将请求均衡转发到其它结点。
通过下边两项参数来配置结点的功能:
node.master: #是否允许为主结点
node.data: #允许存储数据作为数据结点
node.ingest: #是否允许成为协调节点
四种组合方式:
master=true,data=true:即是主结点又是数据结点
master=false,data=true:仅是数据结点
master=true,data=false:仅是主结点,不存储数据
master=false,data=false:即不是主结点也不是数据结点,此时可设置ingest为true表示它是一个客户端。
协调节点仅仅接收请求,不存数据。
打印日志的收集实例
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml" />
<logger name="org.springframework.web" level="INFO"/>
<logger name="org.springboot.sample" level="TRACE" />
<!-- 日志输出格式:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<!--
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
-->
<!-- 开发、测试环境 -->
<springProfile name="dev,test">
<!--日志存放路径-->
<property name="log.path" value="C:/tjScience/logs" />
<!--日志文件错误日志保存位置-->
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 30天 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收指定级别的(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root lever="INFO">
<appender-ref ref="file_error" />
</root>
<logger name="org.springframework.web" level="INFO"/>
<logger name="org.springboot.sample" level="INFO" />
<logger name="cn.tj.controller" level="DEBUG" />
<logger name="cn.tj.service" level="DEBUG" />
</springProfile>
<!-- 生产环境 -->
<springProfile name="pro">
<!--日志存放路径-->
<property name="log.path" value="/usr/local/tjScience/logs" />
<!--日志文件错误日志保存位置-->
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 30天 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收指定级别的(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root lever="ERROR">
<appender-ref ref="file_error" />
</root>
<logger name="org.springframework.web" level="ERROR"/>
<logger name="org.springboot.sample" level="ERROR" />
<logger name="cn.tj" level="ERROR" />
</springProfile>
</configuration>
/**
* creste by ydlclass.ydl
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestLog {
private static final Logger LOGGER= LoggerFactory.getLogger(TestLog.class);
@Test
public void testLog(){
Random random =new Random();
while (true){
int userid=random.nextInt(10);
LOGGER.info("userId:{},send:{}",userid,"hello world.I am "+userid);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然后我们可以使用logstash到这个日志文件去收集日志到es
写这个logstash很难,我们可以去ibana开启grok debugger模式,解析失败就过滤掉
实现搜索引擎
1、mysql导入course_pub表
# 2、创建索引xc_course
# 3、创建映射
PUT /xc_course
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"description" : {
"analyzer" : "ik_max_word",
"search_analyzer": "ik_smart",
"type" : "text"
},
"grade" : {
"type" : "keyword"
},
"id" : {
"type" : "keyword"
},
"mt" : {
"type" : "keyword"
},
"name" : {
"analyzer" : "ik_max_word",
"search_analyzer": "ik_smart",
"type" : "text"
},
"users" : {
"index" : false,
"type" : "text"
},
"charge" : {
"type" : "keyword"
},
"valid" : {
"type" : "keyword"
},
"pic" : {
"index" : false,
"type" : "keyword"
},
"qq" : {
"index" : false,
"type" : "keyword"
},
"price" : {
"type" : "float"
},
"price_old" : {
"type" : "float"
},
"st" : {
"type" : "keyword"
},
"status" : {
"type" : "keyword"
},
"studymodel" : {
"type" : "keyword"
},
"teachmode" : {
"type" : "keyword"
},
"teachplan" : {
"analyzer" : "ik_max_word",
"search_analyzer": "ik_smart",
"type" : "text"
},
"expires" : {
"type" : "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"pub_time" : {
"type" : "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"start_time" : {
"type" : "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"end_time" : {
"type" : "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
# 4、logstash创建模板文件
Logstash的工作是从MySQL中读取数据,向ES中创建索引,这里需要提前创建mapping的模板文件以便logstash使用。
在logstach的config目录创建xc_course_template.json,内容如下:
{
"mappings" : {
"doc" : {
"properties" : {
"charge" : {
"type" : "keyword"
},
"description" : {
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"type" : "text"
},
"end_time" : {
"format" : "yyyy-MM-dd HH:mm:ss",
"type" : "date"
},
"expires" : {
"format" : "yyyy-MM-dd HH:mm:ss",
"type" : "date"
},
"grade" : {
"type" : "keyword"
},
"id" : {
"type" : "keyword"
},
"mt" : {
"type" : "keyword"
},
"name" : {
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"type" : "text"
},
"pic" : {
"index" : false,
"type" : "keyword"
},
"price" : {
"type" : "float"
},
"price_old" : {
"type" : "float"
},
"pub_time" : {
"format" : "yyyy-MM-dd HH:mm:ss",
"type" : "date"
},
"qq" : {
"index" : false,
"type" : "keyword"
},
"st" : {
"type" : "keyword"
},
"start_time" : {
"format" : "yyyy-MM-dd HH:mm:ss",
"type" : "date"
},
"status" : {
"type" : "keyword"
},
"studymodel" : {
"type" : "keyword"
},
"teachmode" : {
"type" : "keyword"
},
"teachplan" : {
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"type" : "text"
},
"users" : {
"index" : false,
"type" : "text"
},
"valid" : {
"type" : "keyword"
}
}
}
},
"template" : "xc_course"
}
# 5、logstash配置mysql.conf
1、ES采用UTC时区问题
ES采用UTC 时区,比北京时间早8小时,所以ES读取数据时让最后更新时间加8小时
where timestamp > date_add(:sql_last_value,INTERVAL 8 HOUR)
2、logstash每个执行完成会在/config/logstash_metadata记录执行时间下次以此时间为基准进行增量同步数据到索引库。
# 6、启动
.\logstash.bat -f ..\config\mysql.conf
# 7、后端代码
(1)Controller
@RestController
@RequestMapping("/search/course")
public class EsCourseController {
@Autowired
EsCourseService esCourseService;
@GetMapping(value="/list/{page}/{size}")
public QueryResponseResult<CoursePub> list(@PathVariable("page") int page, @PathVariable("size") int size, CourseSearchParam courseSearchParam) {
return esCourseService.list(page,size,courseSearchParam);
}
}
(2)
@Service
public class EsCourseService {
@Value("${heima.course.source_field}")
private String source_field;
@Autowired
RestHighLevelClient restHighLevelClient;
//课程搜索
public QueryResponseResult<CoursePub> list(int page, int size, CourseSearchParam courseSearchParam) {
if (courseSearchParam == null) {
courseSearchParam = new CourseSearchParam();
}
//1创建搜索请求对象
SearchRequest searchRequest = new SearchRequest("xc_course");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//过虑源字段
String[] source_field_array = source_field.split(",");
searchSourceBuilder.fetchSource(source_field_array, new String[]{});
//创建布尔查询对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//搜索条件
//根据关键字搜索
if (StringUtils.isNotEmpty(courseSearchParam.getKeyword())) {
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeyword(), "name", "description", "teachplan")
.minimumShouldMatch("70%")
.field("name", 10);
boolQueryBuilder.must(multiMatchQueryBuilder);
}
if (StringUtils.isNotEmpty(courseSearchParam.getMt())) {
//根据一级分类
boolQueryBuilder.filter(QueryBuilders.termQuery("mt", courseSearchParam.getMt()));
}
if (StringUtils.isNotEmpty(courseSearchParam.getSt())) {
//根据二级分类
boolQueryBuilder.filter(QueryBuilders.termQuery("st", courseSearchParam.getSt()));
}
if (StringUtils.isNotEmpty(courseSearchParam.getGrade())) {
//根据难度等级
boolQueryBuilder.filter(QueryBuilders.termQuery("grade", courseSearchParam.getGrade()));
}
//设置boolQueryBuilder到searchSourceBuilder
searchSourceBuilder.query(boolQueryBuilder);
//设置分页参数
if (page <= 0) {
page = 1;
}
if (size <= 0) {
size = 12;
}
//起始记录下标
int from = (page - 1) * size;
searchSourceBuilder.from(from);
searchSourceBuilder.size(size);
//设置高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font class='eslight'>");
highlightBuilder.postTags("</font>");
//设置高亮字段
// <font class='eslight'>node</font>学习
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
searchSourceBuilder.highlighter(highlightBuilder);
searchRequest.source(searchSourceBuilder);
QueryResult<CoursePub> queryResult = new QueryResult();
List<CoursePub> list = new ArrayList<CoursePub>();
try {
//2执行搜索
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//3获取响应结果
SearchHits hits = searchResponse.getHits();
long totalHits=hits.getTotalHits().value;
//匹配的总记录数
// long totalHits = hits.totalHits;
queryResult.setTotal(totalHits);
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
CoursePub coursePub = new CoursePub();
//源文档
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
//取出id
String id = (String) sourceAsMap.get("id");
coursePub.setId(id);
//取出name
String name = (String) sourceAsMap.get("name");
//取出高亮字段name
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null) {
HighlightField highlightFieldName = highlightFields.get("name");
if (highlightFieldName != null) {
Text[] fragments = highlightFieldName.fragments();
StringBuffer stringBuffer = new StringBuffer();
for (Text text : fragments) {
stringBuffer.append(text);
}
name = stringBuffer.toString();
}
}
coursePub.setName(name);
//图片
String pic = (String) sourceAsMap.get("pic");
coursePub.setPic(pic);
//价格
Double price = null;
try {
if (sourceAsMap.get("price") != null) {
price = (Double) sourceAsMap.get("price");
}
} catch (Exception e) {
e.printStackTrace();
}
coursePub.setPrice(price);
//旧价格
Double price_old = null;
try {
if (sourceAsMap.get("price_old") != null) {
price_old = (Double) sourceAsMap.get("price_old");
}
} catch (Exception e) {
e.printStackTrace();
}
coursePub.setPrice_old(price_old);
//将coursePub对象放入list
list.add(coursePub);
}
} catch (IOException e) {
e.printStackTrace();
}
queryResult.setList(list);
QueryResponseResult<CoursePub> queryResponseResult = new QueryResponseResult<CoursePub>(CommonCode.SUCCESS, queryResult);
return queryResponseResult;
}
}