ES数据库操作入门总结「建议收藏」

2022-08-27 13:17:43 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

elasticsearch总的来说应该算是一个搜索引擎,公司使用一般是作为日志结果查询。 json文档格式,倒排索引的快速以及分布式的特性,使得es可以在大量数据中快速查询到结果。

windows安装和配置可参考官方网址。

代码语言:javascript复制
https://www.elastic.co/guide/en/elasticsearch/reference/current/zip-windows.html

倒排查询可参考这个知乎回答

代码语言:javascript复制
https://zhuanlan.zhihu.com/p/62892586

可以使用浏览器的URL栏输入或者使用postman来输入类似URL形式的代码来操作es,即输入主机名:端口号/索引名/类名/id/_search这种(电脑默认get方法,不用输)。如果要操作可能还是用postman比较好,因为可以很方便的创建json文本数据。不过可以的话,推荐使用kibana这种软件操作。但是由于es比较需要使用大量数据来操作搜索进行练习,因此可以的话,最好用比较方便的软件创建大量测试数据操作。

ES的安装

es的安装非常简单,可以直接在自身主机上安装,并开始使用。不需要集群也可以创建elastic节点。 操作的格式如下

代码语言:javascript复制
GET /索引名/类名/id/方法名 { 
   
						条件
						}

索引名,类名,id可省略,默认为在所有索引中操作。 经过一段时间学习与运用发现es的操作一般只涉及查询,这也符合作为搜索引擎的特性。因此以下都只谈查询。 查询一般使用方法**_search**,下接query如下

代码语言:javascript复制
GET /_search
{ 
   
	"query" : { 
   
		"match_all":{ 
   }
		}
}

全索引查询相关度前十的结果。

查询出来的结果包括许多部分。需要关注的部分以这个为例

代码语言:javascript复制
{ 
   
  "took" : 6479,
  "timed_out" : false,
  "num_reduce_phases" : 9,
  "_shards" : { 
   
    "total" : 4260,
    "successful" : 4260,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : { 
   
    "total" : { 
   
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : 1.0,
    "hits" : [
      { 
   
        "_index" : ".data-frame-internal-2",
        "_type" : "_doc",
        "_id" : "data_frame_transform_config-test",
        "_score" : 1.0,
        "_source" : { 
   
          "id" : "test",
          "source" : { 
   
            "index" : [
              "logstash-applog*"
            ],
            "query" : { 
   
              "match_all" : { 
    }
            }
          },
 以下省略一大堆

took:表示你查询耗时,默认是ms, timed_out: 表示你是否查询超过规定的时间,没超过显示false shards下是一些分片信息,像这个就是查了4260个分片全都成功查询,没有跳过或者失败的。 hits:第一个下的total指的是查到的总的信息,value就是匹配的结果有10000个(其实好像是因为默认只允许最多查10000个),relation种的gte就是指value这个数据不准确,准确是eq。看下面这个就是eq

代码语言:javascript复制
{ 
   
    "took": 0,
    "timed_out": false,
    "_shards": { 
   
        "total": 0,
        "successful": 0,
        "skipped": 0,
        "failed": 0
    },
    "hits": { 
   
        "total": { 
   
            "value": 0,
            "relation": "eq"
        },
        "max_score": 0.0,
        "hits": []
    }
}

max_score就是最高的相关性得分,就是和查询条件的匹配度,我这里是从0到1,数字越大匹配度越高,默认是查询结果按照相关性得分从高到低排序。

第二个hits,里面就放的是我查询到的结果。这个一般就包含五个部分,从上到下分别 _index:索引名 _type:类名 _id:id名 _score:相关性得分 _source:这里面我们就能看到我们想要的json数据了。

组合查询

其实指的就是bool查询,写法如下

代码语言:javascript复制
GET /_search
{ 
   
	"query" : { 
   
		"bool" : { 
   
			"must" : { 
   
				"match" : { 
   
					"key" : "value"
					}
				}
			}
		}
	}

上面这个就是在所有索引库查key对应的值和value匹配的所有id表

和must同级的有must_not、should、和filter前面三个涉及相关性得分,后一个只起到过滤作用,不会计算相关性得分。前面两个起到过滤加计分作用,should在没must时候作用和must差不多,有must的话就只起到计分的作用,即不满足条件的也不会被过滤掉。

match同级 的我一般用的就三个,一个是term,要求精确值匹配,即value必须等于value而不是value able里面含有value。还有一个是terms,就是key必须等于value数组或队列(termsQuery可以放队列)中的一个值。还有一个就是range,一个范围内取值,这个看下面例子,gte就是比这个值大,lt就是比这个小。这里的price就是”key“,后面的就是value

代码语言:javascript复制
GET /my_store/products/_search
{ 
   
    "query" : { 
   
            "filter" : { 
   
                "range" : { 
   
                    "price" : { 
   
                        "gte" : 20,
                        "lt"  : 40
                    }
                }            
        }
    }
}

value可以添加boot权重属性来影响相关性的得分比如这个分值越大权重越高。

代码语言:javascript复制
},
            "should": [
                { 
    "match": { 
   
                    "content": { 
   
                        "query": "Elasticsearch",
                        "boost": 3 
                    }

其他的就多和自定义过滤器,分析器有关了。此外7.x的文档添加了许多有用属性,并且string这个映射类型已经被抛弃了,现在用的是text和keyword表示字符串类型。

聚合

聚合其实指的就是group by,即,将查询出的数据以某种方式分支,从而得到想要的度量值。我们先看这个例子 以color这个属性来分组,这个colors其实就是给查询结果取了个名字,你可以随便取

代码语言:javascript复制
GET /cars/transactions/_search
{ 
   
   "size" : 0,
   "aggs": { 
   
      "colors": { 
   
         "terms": { 
   
            "field": "color"
         }
      }
   }
}

查出来像这个

代码语言:javascript复制
。。。这里省略的有hits部分还有上面的shards部分等等,这个例子因为size为0因此第二个hits里面是空的,就是一个[]
"aggregations": { 
   
      "colors": { 
   
         "buckets": [
            { 
   
               "key": "red",
               "doc_count": 4,
。。。               下面省略的都是没关系的

看这个其实就知道,color = red 的有四个,换句话说,去重功能(distinct)已经做出来了。

如果你要做平均值什么的,只需在里面这样添加

代码语言:javascript复制
GET /cars/transactions/_search
{ 
   
   "size" : 0,
   "aggs": { 
   
      "colors": { 
   
         "terms": { 
   
            "field": "color"
         },
         "aggs": { 
    
            "avg_price": { 
    
               "avg": { 
   
                  "field": "price" 
               }
            }
         }
      }
   }
}

注意哈,这个第二个aggs是和上面的terms同一级。field这个单词可以理解为字段,即你选择的那个key

此外聚合允许多重聚合,相当于对一个属性进行group by后对另一个属性再进行group by。 就是在里面再放一个aggs,全称是aggregations。其实你上面就是,只不过这第二个aggs没有创建桶。

代码语言:javascript复制
GET /cars/transactions/_search
{ 
   
   "size" : 0,
   "aggs": { 
   
      "colors": { 
   
         "terms": { 
   
            "field": "color"
         },
         "aggs": { 
   
            "avg_price": { 
    "avg": { 
    "field": "price" }
            },
            "make" : { 
   
                "terms" : { 
   
                    "field" : "make"
                },
                "aggs" : { 
    
                    "min_price" : { 
    "min": { 
    "field": "price"} }, 
                    "max_price" : { 
    "max": { 
    "field": "price"} } 
                }
            }
         }
      }
   }
}

游标查询

游标查询很简单,首先

代码语言:javascript复制
GET/test/_search?scroll=1m 
{

    "query":{
        "match_all":{}
    },
    "size" : 1
}

这一部分就是查询old_index下的所有数据,并且缓存下来,但是一次只向前台输出1条。 输出结果如下

代码语言:javascript复制
{
    "_scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm9aeEY4Q2VPU3IyNXloU2ptb1hhNHcAAAAAAAAAZRZsaHN6YVNqRVIwaWpMWXZvbnZacnF3",
    "took": 47,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nB3cK3wBYbxq_xzH5a0Z",
                "_score": 1.0,
                "_source": {
                    "title": "P908a"
                }
            }
        ]
    }
}

这里面需要关注的只有那个scroll_id 然后如果需要输出这第一条之后的结果的话,我们就要利用这个scroll_id 如下

代码语言:javascript复制
localhost:9200/_search/scroll
{
"scroll" : "1m",
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm9aeEY4Q2VPU3IyNXloU2ptb1hhNHcAAAAAAAAAZxZsaHN6YVNqRVIwaWpMWXZvbnZacnF3"
}

可以看到返回结果 如下

代码语言:javascript复制
{
    "_scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm9aeEY4Q2VPU3IyNXloU2ptb1hhNHcAAAAAAAAAahZsaHN6YVNqRVIwaWpMWXZvbnZacnF3",
    "took": 37,
    "timed_out": false,
    "terminated_early": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nR3dK3wBYbxq_xzHcq3X",
                "_score": 1.0,
                "_source": {
                    "title": "P909a",
                    "stat": "P909a"
                }
            }
        ]
    }
}

返回了第二条数据。 这其中的1m指的是id有效时长为1min,超过了会报如下的bug,因此记得像上面那样持续更新scroll_id的有效时长

代码语言:javascript复制
"error": {
        "root_cause": [
            {
                "type": "search_context_missing_exception",
                "reason": "No search context found for id [106]"
            }
        ],
        "type": "search_phase_execution_exception",
        "reason": "all shards failed",
        "phase": "query",
        "grouped": true,
        "failed_shards": [
            {
                "shard": -1,
                "index": null,
                "reason": {
                    "type": "search_context_missing_exception",
                    "reason": "No search context found for id [106]"
                }
            }
        ],
        "caused_by": {
            "type": "search_context_missing_exception",
            "reason": "No search context found for id [106]"
        }
    },
    "status": 404
}

为什么会存在游标查询呢?这是为了解决如果你的确需要查询超过10000条数据(默认最大10000),那么这时候你显然是做不到一次返回给前台10000条以上,假入使用from,size,那么当你需要用到10000,到第20000条时,你就会在后台再查询一次,很显然会造成的结果就是,前台分页等待时间太长了,用户体验很不好,而游标查询在设定时间内查询到所有的数据并缓存下来,那么当你需要10000~20000条时,不用再查一次,只要读取就ok了。换句话说你只要给后台传送你的scroll_id后台就能准确知道你要哪个地方的数据。(特别适合用在前台滚轮向下查的时候)

分析器和动态映射

这一部分我不会讲你如何设置分析器,而是讲一讲默认的分析器,以及动态映射的一些容易被坑的点。

这个默认分析器主要是在你创建索引和搜索时会被es自动使用,用来对数据内容做分析。 而这个动态映射会在你往索引添加不存在的字段时会采用,用来对数据类型做分析。

分析器默认的是standard分析器,他会对你的text类型的数据进行分析以后再建索引,standard会把这个text字符串中的字母全部切换为小写,并且把空格去掉,还有一些没意义的词,比如(a,an)。记住只作用在text类上。这个小写是个隐藏坑。

那么哪些字段会被当做text呢?除了建索引的时候你自己设置的mapping中将某些字段上设置为的text类数据,还有就是动态映射(dynamic mapping的时候),就是当你往索引插入没有的字段时候调用的东西,他会自动识别,并给这个字段一个类型。而对于一些特定格式的数据会被动态映射定义为text类数据 他的默认规则如下:

来源为es官方文档,dynamic mapping这一节内容

我们默认dynamic是true。这后面三个,就是”2021-09-29″这个字符串可能会被当成date类,“1234”会被当成long类,而“aaa111″会被当成text类,而且带有一个后缀**.keyword**的keyword类。(建议看官方,不过官方文档读起来好累啊。)

再强调一哈,对于这里的text类数据,动态映射会将此数据同时生成一个,字段名.keyword的字段,数据相同但保存为keyword类,而不再是text类了。这里为什么要做这一步呢?这是因为text类数据他会调用分析器!!然后把数据分词,大写字母变小写等等,这造成的直接结果是,你查原来的数据你是查不到的,因为你就没有对这个原来的数据建索引。而keyword类不会调用分析器,因此动态映射帮你保留了一个字段名.keyword的字段,来让你可以精准查询。

代码语言:javascript复制
{
    "took": 115,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nB3cK3wBYbxq_xzH5a0Z",
                "_score": 1.0,
                "_source": {
                    "title": "P908a"
                }
            },
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nR3dK3wBYbxq_xzHcq3X",
                "_score": 1.0,
                "_source": {
                    "title": "P909a",
                    "stat": "P909a"
                }
            }
        ]
    }
}

这里你只要关注source里面,总共有title和stat两个字段。其中title是我事先定义的,而stat是后来添加的,因此,title是会被standard分析器分析后建索引,stat则是在动态映射后,然后分析,再建索引。 由于分析器会将数据变为小写,因此如果这样写

代码语言:javascript复制
{
    "query" : {
        "bool" : {
            "must":{
                "term" : {"title" : "P909a"}
            }
        }
    }
}

你是查不到的。 结果如下

代码语言:javascript复制
{
    "took": 8,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 0,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    }
}

但你如果把里面的内容改成这样

代码语言:javascript复制
"term" : {"title" : "p909a"}  //P切换为小写p

你就会看到

代码语言:javascript复制
"hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 0.6931471,
        "hits": [
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nR3dK3wBYbxq_xzHcq3X",
                "_score": 0.6931471,
                "_source": {
                    "title": "P909a",
                    "stat": "P909a"
                }
            }
        ]
    }
}

我查到了数据。 title是我提前在mapping中定义为了text类,因此不会存在keyword字段。换句话说

代码语言:javascript复制
"term" : {"title.keyword" : "P909a"}

是查不到的

代码语言:javascript复制
"hits": {
        "total": {
            "value": 0,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    }

我们再来看stat这一字段 同理

代码语言:javascript复制
{
    "query" : {
        "bool" : {
            "must":{
                "term" : {"stat" : "p909a"} //注意看p是小写的
            }
        }
    }
}

这样写可以查到数据

代码语言:javascript复制
        "hits": [
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nR3dK3wBYbxq_xzHcq3X",
                "_score": 0.2876821,
                "_source": {
                    "title": "P909a",
                    "stat": "P909a"
                }
            }
            ]

此外,由于title用了动态映射,因此你可以这样精准查询

代码语言:javascript复制
"term" : {"stat.keyword" : "P909a"}
代码语言:javascript复制
"hits": [
            {
                "_index": "test",
                "_type": "_doc",
                "_id": "nR3dK3wBYbxq_xzHcq3X",
                "_score": 0.2876821,
                "_source": {
                    "title": "P909a",
                    "stat": "P909a"
                }
            }
        ]

综上所述,如果你事先定义了text类,而又使用了默认分析器,那么你要记住,你很有可能因为字母大小写,分词等原因无法精准查询(因为你的规则里没有额外给他定义为keyword类),而对于未定义的 那些被动态映射为text的字段,你可以用.keyword字段(这个是keyword类)来精准查询,也可以直接用原字段,根据分析器的规则来查询。

至于如何定制分析器,这里就不叙述了,我目前用不上。还有关于如何定制化映射,比如说在开头或者结尾看到什么字符就将字符串定义为date类什么的,我也一般情况下用不上,因此不叙述了。

建索引,mapping写法如下(mappings)

代码语言:javascript复制
PUT "localhost:9200/bilibili"

{
   "mappings":{
       "properties":{
           "title" : {
               "type" :"text",
               "fields":{
              	 "keyword":{
               		"type":"keyword"
           }
       }      
   } 
}	

这样我就建了个“bilibili”的索引,里面事先定义了一个叫title的text类字段。 如果你看到,mapping里有个_doc记得删去,这个已经被废弃了,不需要加。

High Level Rest Api中一些常用API

我们后台写接口不可能像上面命令行那样操作数据库,因此es给了javaAPI让你能在后台操作es数据库。 一般而言写接口只涉及查询,举个例子,查询title字段为“p909a”

代码语言:javascript复制
traceExceptionSearchRequest.source(new SearchSourceBuilder()
                    .timeout(TimeValue.timeValueSeconds(10))
                    .query(new BoolQueryBuilder()
                            .must(QueryBuilders.termQuery("title", "p909a"))
                            .must(QueryBuilders.rangeQuery(timeField)
                                    .gte(startTime)
                                    .lte(endTime)))
                    .size(0)
);

如果熟悉命令行操作的话,看这个应该很快就能理解。其他的都同理。 然后是调用,得到返回值

代码语言:javascript复制
traceExceptionResponse = restHighLevelClient.search(traceExceptionSearchRequest, RequestOptions.DEFAULT);

至于如何取到返回值里你想要的数据 举个例子,想要取到_source里的字段

代码语言:javascript复制
 List<Map> orderList = new ArrayList<>();
        for (SearchHit hit : busiOrderStatResponse.getHits().getHits()) {
            Map order = hit.getSourceAsMap();
            orderList.add(order);
        }

熟悉命令行应该也能很快看明白。(json数据的返回值中的一种结构,map数据结构,还可以是List,或者Object) 像聚合的操作

代码语言:javascript复制
.aggregation(AggregationBuilders.terms(Name)
                            .size(Integer.MAX_VALUE)
                            .field(Field)));

取返回值

代码语言:javascript复制
				List<String> virtualOrderIds = new ArrayList<>();
				
                Terms virtualOrderAggs = traceExceptionResponse.getAggregations().get(Name);
                for (MultiBucketsAggregation.Bucket virtualOrder : virtualOrderAggs.getBuckets()) {
                    virtualOrderIds.add((String) virtualOrder.getKey());
                }
            }

不解释,自行理解。 还有游标查询

代码语言:javascript复制
searchRequest.scroll(TimeValue.timeValueMinutes(1L));
            searchRequest.source(searchSourceBuilder);   //searchSourceBuilder实现省略,里面放的是查询条件
 response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);//第一次scroll查询
 
 String id = response.getScrollId();
 
SearchScrollRequest scrollRequest = new SearchScrollRequest(id);//后续的scroll查询
            scrollRequest.scroll(TimeValue.timeValueMinutes(1L));  //1L 应该就是1m,不确定,自行查找确认

这里提一提,因为索引id一般会在第二次调用接口的时候使用,而不是查了第一次后立马在这次调用中再查询第二次,因此,你要记得保存下来,然第二次调用接口的时候可以取到。

补充1:

代码语言:javascript复制
.fetchSource(new String[]{ 
   "LogTime", "Info"}, null)
                            .sort("@timestamp", SortOrder.ASC)

fetchSource 对标 dsl中的_source,用来选择以及排除查询到的结果将返回给前台的字段。

dsl中写法如下

代码语言:javascript复制
{ 
   
    "_source":{ 
   
        "includes":["title","url","id"],
        "excludes":["desc"]
    }
}

includes指代选择的,excludes指代排除的。两者关系为与

补充2: dsl中的排序写法如下

代码语言:javascript复制
"sort": { 
    "date": { 
    "order": "desc" }}

注意:日期格式的排序只支持UTC 如下例 “2999-11-16T02:59:52.000000Z”,如果格式不正确的话,将不会有任何返回值

对应的api为

代码语言:javascript复制
.sort("date", SortOrder.DESC)

补充3: java的api访问es步骤

1 引入es的api包

代码语言:javascript复制
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.4.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.elasticsearch</groupId>
                    <artifactId>elasticsearch</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>7.4.0</version>
        </dependency>

2 生成一个restHighLevelClient的bean来访问es 写法如下

代码语言:javascript复制
@Configuration
public class ElasticsearchConfig { 
   

    private static final String SEPARATOR_A = ",";
    private static final String SEPARATOR_B = ":";

    @Value("${spring.elasticsearch.rest.uris}")
    public String esUrl; 例如127.92.37.111:56821,180.48.28.190:52990

    /** * format: http://10.45.61.51:52900 * <p> * 超时时长改成2min * * @return RestHighLevelClient */

    @Value("${spring.elasticsearch.jest.username}")
    public String esUserName = "*****";

    @Value("${spring.elasticsearch.jest.password}")
    public String esPassWord = "******";

    @Bean
    public RestHighLevelClient highLevelClient() { 
   

        List<HttpHost> httpHosts = new ArrayList<>();
        String[] clients = StringUtils.split(esUrl, SEPARATOR_A);

        for (String client:clients) { 
   
            String[] urlFragments = StringUtils.split(client, SEPARATOR_B);

            int port = Integer.parseInt(urlFragments[1]);
            String host = urlFragments[0];

            httpHosts.add(new HttpHost(host, port, "http"));
        }
        //上面这些部分是用来对esUrl的主机ip和端口号进行拆分的,并生成一个list<HttpHost>类



        HttpHost[] httpHostArr = httpHosts.toArray(new HttpHost[0]);


        RestClientBuilder restClientBuilder = RestClient.builder(httpHostArr);此处输入ip地址和端口号

        final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(esUserName, esPassWord));//此处输入账号和密码


        restClientBuilder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() { 
   

            @Override
            public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { 
   
                httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);

                return httpClientBuilder;
            }
        });

        return new RestHighLevelClient(restClientBuilder);
    }
}

3 配置文件 一般bean,都使用配置文件修改配置,比如这里的ip,host,username,password,创建一个名为application.yml

代码语言:javascript复制
spring:
	elasticsearch:
    	rest:
      		uris: 127.92.37.111:56821,180.48.28.190:52990
    	jest:
      		username: *****
      		password: *****

这样就生成了可访问es的类了

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/145932.html原文链接:https://javaforall.cn

0 人点赞