大家好,又见面了,我是你们的朋友全栈君。
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