ES - 学习

一、引言


1.1 有了数据库查询为什么还要ElasticSearch?

数据库一般只适合保存搜索结构化的数据,对于非结构化的数据( 比如文章内容),只能通过like%%模糊查询,但是在大量的数据面前,like%%有两个弊端:
1)搜索效率会很差,因为是做一个全表扫描(like%%会让索引失效)
2)搜索没办法通过相关度匹配排序(可能返回的是用户不关心的结果)
ElasticSearch就可以解决这些问题

1.2 什么是全文检索?

全文检索 将非结构化数据中的一部分信息提取出来,重新组织,使其变得具有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引
例如字典的拼音表和部首检字表就相当于字典的索引,通过查找拼音表或者部首检字表就可以快速的查找到我们要查的字。
这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。

1.3 全文检索的流程

1.4 构建索引的过程

索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
获得文档创建文档分析文档索引文档

1.4.1 获得原始文档

原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。

1.4.2 创建文档对象(Document)

获取原始文档的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。

1.4.3 分析文档(分词)

将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词将字母转为小写去除标点符号去除停用词等过程生成最终的语汇单元。

1.4.4 创建索引

创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构

1.5 倒排索引

1.5.1 正向索引

简单来说,正向索引就是根据文件ID找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。

1.5.2 反向(倒排)索引

倒排索引和正向索引刚好相反,是根据内容(词语)找文档

二、ElasticSearch(DB - NoSQL数据库 - 非关系型数据库)


2.1 ElasticSearch简介

ElasticSearch基本概念

2.1.1 索引库(index)

索引库是ElasticSearch存放数据的地方,可以理解为关系型数据库中的一个数据库。事实上,我们的数据被存储和索引在分片(shards)中,索引只是一个把一个或多个分片分组在一起的逻辑空间。

2.1.2 映射类型(type)

映射类型用于区分同一个索引下不同的数据类型,相当于关系型数据库中的表
注意:在 6.0 的index下是无法创建多个type,并且会在 7.0 中完全移除。

2.1.3 文档(documents)

文档是ElasticSearch中存储的实体,类比关系型数据库,每个文档相当于数据库表中的一行数据

2.1.4 字段(fields)

文档由字段组成,相当于关系数据库中列的属性

2.1.5  分片与副本

如果一个索引具有很大的数据量,它的数据量可能会超出单个节点的容量限制(硬盘容量),而且单个节点数据量过大,执行性能也会随之下降,每个搜索请求的执行效率都会降低。 为了解决上述问题, Elasticsearch 提出了分片的概念,索引将划分成多份,称为分片。每个分片都可以创建对应的副本,以便保证服务的高可用性。

2.2 ElasticSearch的安装

1)准备ElasticSearch的docker-compose.yml文件

version: '3.1'
services:
  elasticsearch:
    image: elasticsearch:7.7.1
    restart: always
    container_name: elasticsearch
    ports:
      - 9200:9200
      - 9300:9300
    environment:
      discovery.type: single-node
      TZ: Asia/Shanghai
    volumes:
      - ./es/data:/usr/share/elasticsearch/data:rw
      - ./es/logs:/usr/share/elasticsearch/logs:rw
      - ./es/plugins:/usr/share/elasticsearch/plugins
      - config:/usr/share/elasticsearch/config
  kibana:
    image: kibana:7.7.1
    container_name: kibana
    restart: always
    environment:
      SERVER_NAME: kibana
      ELASTICSEARCH_URL: http://192.168.144.129:9200
    ports:
      - 5601:5601
volumes:
  config:

注意:第23行必须写elasticsearch所在机器的ip地址,不能写127.0.0.1

2)执行docker-compose up -d 命令启动容器

docker-compose up -d

注意:第一次创建容器会失败,需要给.es文件夹赋予权限,执行chmod 777 -R ./es命令,然后重启容器

3)安装中文分词器

进入elasticsearch容器,执行中文分词器相关安装命令

docker exec -it elasticsearch bash
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.5/elasticsearch-analysis-ik-6.8.5.zip

注意:可以访问
Releases · medcl/elasticsearch-analysis-ik · GitHub
查找和当前es匹配的版本

4)手动安装中文分词器

如果第3步安装超时失败,可以尝试进行手动安装,如果第三步成功则跳过该步骤

4.1)手动下载对应的IK分词器版本https://github.com/medcl/elasticsearch-analysis-ik/releases
4.2)直接将分词器压缩包上传到容器的plugins路径(上传到宿主机的容器卷路径即可)
4.3)解压分词器

unzip elasticsearch-analysis-ik-6.8.5.zip -d ./ik-analyze

5)重启容器

docker-compose restart

6)访问kibana服务

7)测试IK分词器

POST _analyze
{
  "analyzer":"ik_smart",
  "text":"歼10系列战斗机"
}
POST _analyze
{
  "analyzer":"ik_max_word",
  "text":"歼10系列战斗机"
}

2.3 索引库(Index)相关操作

2.3.1 概述

ElasticSearch采用Rest风格的API,因此其API就是一次Http请求
请求分为: PUT POST GET DELETE
GET:查询数据
PUT:插入数据
POST:更新数据,实际上很多情况下 es 不是很清晰你到底要作什么,有些时候POST也可用于新增或者查询
DELETE: 删除数据

2.3.2 新增索引库语法

PUT /索引库名称
{
  "settings":{
    "number_of_shards": 3, #分片的数量
    "number_of_replicas": 2 #副本的数量
  }
}

settings:表示索引库的设置
number_of_shards:表示分片的数量
number_of_replicas:副本数量

2.3.3 查询索引信息

GET /索引库名称

结果:
{
"qf" : {
"aliases" : { },
"mappings" : { },
"settings" : {
"index" : {
"creation_date" : "1582083142738",
"number_of_shards" : "3",
"number_of_replicas" : "2",
"uuid" : "RaRHf6zISkqFh3a10FDc6A",
"version" : {
"created" : "6050499"
},
"provided_name" : "qf"
}
}
}
}

2.3.4 判断索引库是否存在

HEAD /索引库名称

2.3.5 删除索引库

DELETE /索引库名称

注意
1)索引库 类似于 MySQL中数据库的概念
2)如果创建索引不指定settings,默认会有5个分片1个副本(7.x之后的版本,默认是1个分片,1个副本)

2.4 映射类型(type)相关操作

2.4.1 新增映射类型语法

PUT /索引库名/_mapping/类型名称
{
    "properties": {
        "字段名1": {
            "type": "类型",
            "index": true,
            "store": true,
            "analyzer": "分词器"
        },
        "字段名2": {
            "type": "类型",
            "index": true,
            "store": true,
            "analyzer": "分词器"
        }
    }
}

type:类型,可以是text、long、date、integer、object、keyword(表示关键字,不能被分词)
index:是否参与索引,默认为true
store:是否参与存储,默认为false
analyzer:分词器,可选 “ik_max_word”或者“ik_smart”,表示使用ik分词器

2.4.2 查看映射类型信息

GET /索引库名称/_mapping/类型名称

2.4.3 字段属性详解

type

String类型,又分两种:
text:可分词,不可参与聚合
keyword:不可分词,数据会作为完整字段进行匹配,可以参与聚合
Numerical数值类型,分两类
基本数据类型:long、interger、short、byte、double、float、half_flfloat
浮点数的高精度类型:scaled_float,需要指定一个精度因子,比如10或100,elasticsearch会把真实值乘以这个因子后存储,取出时再还原
Date:日期类型
elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间
boolean:   设置字段类型为boolean后,可以填入的值为:true、false、"true"、"false"
binary:   binary类型接受base64编码的字符串
geo_point:   地理点类型用于存储地理位置的经纬度对
更多类型参考:https://www.elastic.co/guide/en/elasticsearch/reference/6.5/mapping-types.html

index

index影响字段的索引情况
true:字段会被索引,则可以用来进行搜索。默认值就是true
false:字段不会被索引,不能用来搜索
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。但是有些字段是我们不希望被索引的,比如商品的图片信息,就需要手动设置index为false

store

是否将数据进行额外存储。在学习lucene和solr时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。
但是在Elasticsearch中,即便store设置为false,也可以搜索到结果。
原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做 source 的属性中。而且我们可以通过过滤 source 来选择哪些要显示,哪些不显示。而如果设置store为true,就会在 _source 以外额外存储一份数据,多余,因此一般我们都会将store设置为false,事实上,store的默认值就是false。

analyzer

定义的是该字段的分析器,默认的分析器是 standard 标准分析器,这个地方可定义为自定义的分析器。
比如IK分词器为: ik_max_word 或者 ik_smart

boost

激励因子。这个与lucene中一样,我们可以通过指定一个boost值来控制每个查询子句的相对权重。
该值默认为1。一个大于1的boost会增加该查询子句的相对权重
比如:

GET /_search {
    "query": {
        "bool": {
            "must": {
                "match": {
                    "content": {
                        "query": "full text search",
                        "operator": "and"
                    }
                }
            },
            "should": [{
                    "match": {
                        "content": {
                            "query": "Elasticsearch",
                            "boost": 3
                        }
                    }
                },
                {
                    "match": {
                        "content": {
                            "query": "Lucene",
                            "boost": 2
                        }
                    }
                }
            ]
        }
    }
}

注意
1)映射类型(type) 类似于 MySQL数据库中表的概念
2)从ElasticSearch 6.x之后,一个Index下只能有一个type,7.x之后,移除了type的概念,不能再指定type的名称了

2.5 文档相关(document)操作

2.5.1 添加文档

#指定id的添加方式
PUT /索引库名/类型名称/id #id需要自己指定
{
  "field1":"value1",
  "field2":"value2",
  ...
}

#自动生成id的添加方式
POST /索引库名/类型名称  #使用POST无需指定id
{
  "field1":"value1",
  "field2":"value2",
  ...
}

#批量添加文档
PUT /索引库名称/类型名称/_bulk
{"index":{"_id":id值1}}
{"field1":"value1", "field2":"value2"...}
{"index":{"_id":id值2}}
{"field1":"value1", "field2":"value2"...}
....

2.5.2 更新文档

#全局更新,会将所有字段更新,没有指定的字段会自动删除
PUT /索引库名/类型名称/id #需要更新的id,id必须存在,如果不存在就变成了添加
{
  "field1":"value1",
  "field2":"value2",
  ...
}

#局部更新,只更新需要更新的字段
POST /索引库名/类型名称/id/_update
{
  "doc":{
    "field1": "新的value"
  }
}

2.5.3 删除文档

DELETE /索引库名/类型名称/id

2.5.4 查询文档

#查询索引库全部数据
GET /索引库名称/_search

#根据id查询
GET /索引库名称/类型名称/id

#批量查询
GET /_mget
{
    "docs": [
        {
            "_index": "索引库名称1",
            "_type": "映射类型1",
            "_id":"查询文档id1"
        },
        {
       		"_index": "索引库名称2",
       		"_type": "映射类型2",
       		"_id":"查询文档id2"
    	}
    ]
}

注意
1)文档(document)类似于 数据库中表的一条记录
2)当添加的文档中,设置的field,而type中没有时,type会自动的添加该field的映射记录,
这是elasticsearch的自动映射功能

三、SpringBoot操作ElasticSearch(SpringBoot自带的整合包)

3.1 配置SpringBoot

1)添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

注意:springboot选择2.3.0以上的版本,如果是2.3.0以下的版本,建议手动导入elasticsearch-rest-high-level-client依赖,操作elasticsearch

2)配置application.yml

spring:
  elasticsearch:
    rest:
      uris: http://192.168.195.188:9200

3)在业务层注入ES的模板对象

@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;

4)设置实体类

@Data
@Accessors(chain = true)
@Document(indexName = "hotal_index", shards = 1, replicas = 0)
public class Hotal implements Serializable {
  @TableId(type = IdType.AUTO)
  @Field(type = FieldType.Integer, index = false)
  private Integer id;
  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String hotalName;
  @Field(type = FieldType.Keyword, index = false)
  private String hotalImage;
  @Field(type = FieldType.Integer)
  private Integer type;
  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String hotalInfo;
  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String keyword;
  private double lon;
  private double lat;
  @Field(type = FieldType.Integer)
  private Integer star;
  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String brand;
  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String address;
  @DateTimeFormat(pattern = "yyyy-MM-dd")
  @Field(type = FieldType.Date, pattern = "yyyy-MM-dd")
  private Date openTime;
  private Integer cid;
  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String district;
  private Date createTime = new Date();
  private Integer status = 0;
  @TableField(exist = false)
  private City city;
}

注意:实体类工程需要单独添加依赖

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
    <version>4.0.0.RELEASE</version>
</dependency>

5)通过模板对象操作ES

@Autowired
private ElasticsearchRestTemplate restTemplate;
@Override
public boolean createIndex() {
    IndexOperations indexOperations = restTemplate.indexOps(Hotal.class);
    if (!indexOperations.exists()){
        System.out.println("索引库不存在,就进行创建!");
        indexOperations.create();
        //创建索引库的映射类型
        Document document = indexOperations.createMapping();
        indexOperations.putMapping(document);
    }
    return false;
}
@Override
public boolean deleteIndex() {
    IndexOperations indexOperations = restTemplate.indexOps(Hotal.class);
    return indexOperations.delete();
}
@Override
public boolean insertDoc(Hotal hotal) {
    Hotal save = restTemplate.save(hotal);
    return save != null;
}
@Override
public boolean updateDoc(Map<String, Object> params, Integer id) {
    Document document = Document.create();
    params.entrySet().forEach(entry -> {
        document.append(entry.getKey(), entry.getValue());
    });
    UpdateQuery updateQuery = UpdateQuery.builder(id + "")
            .withDocument(document)
            .build();
    UpdateResponse response = restTemplate.update(updateQuery, IndexCoordinates.of("hotal_index"));
    return response.getResult() == UpdateResponse.Result.UPDATED;
}
@Override
public boolean deleteDocById(Integer id) {
    String delete = restTemplate.delete(id + "", Hotal.class);
    System.out.println("删除结果:" + delete);
    return delete != null;
}

三、SpringBoot操作ElasticSearch(elasticsearch-rest-high-level-client)

3.1 配置ElasticSearch

1)添加依赖

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.8.5</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.8.5</version>
</dependency>

2)配置application.yml

spring:
  elasticsearch:
    rest:
      uris: http://192.168.195.135:9200

3)在需要的地方注入RestHighLevelClient对象

@Autowired
 private RestHighLevelClient restHighLevelClient;

3.2 使用SpringBoot操作索引库(Index)

@Autowired
    private RestHighLevelClient client;
    /**
     * 创建索引
     * @param indexName
     * @return
     */
    @Override
    public boolean createIndex(String indexName) {
        CreateIndexRequest indexRequest = new CreateIndexRequest(indexName);
        //设置索引库的相关属性
        Settings settings = Settings.builder()
                .put("number_of_shards", 1)//设置分片数量
                .put("number_of_replicas", 0)//设置副本数量
                .build();
        indexRequest.settings(settings);
        try {
            CreateIndexResponse response = client.indices().create(indexRequest, RequestOptions.DEFAULT);
            //返回结果
            return response.isAcknowledged();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    /**
     * 判断索引是否存在
     * @param indexName
     * @return
     */
    @Override
    public boolean isExistsIndex(String indexName) {
        GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
        try {
            boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
            return exists;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    /**
     * 删除索引
     * @param indexName
     * @return
     */
    @Override
    public boolean deleteIndex(String indexName) {
        DeleteIndexRequest request = new DeleteIndexRequest(indexName);
        try {
            AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
            return response.isAcknowledged();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

3.3 使用SpringBoot操作映射类型(type)

/**
     * 添加映射
     * PUT /partform_hotal/_mapping/hotal
     * {
     *   "properties": {
     *     "hotalName":{
     *       "type": "text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "hotalImage":{
     *       "type": "keyword",
     *       "index": false
     *     },
     *     "type":{
     *       "type": "integer"
     *     },
     *     "hotalInfo":{
     *       "type":"text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "keyword":{
     *       "type":"text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "location":{
     *       "type": "geo_point"
     *     },
     *     "star":{
     *       "type": "integer"
     *     },
     *     "brand":{
     *       "type": "text",
     *       "analyzer": "ik_max_word"
     *     },
     *     "address":{
     *       "type": "keyword"
     *     },
     *     "openTime":{
     *       "type": "date",
     *       "format": "yyyy-MM-dd"
     *     },
     *     "cityname":{
     *       "type": "keyword"
     *     },
     *     "regid":{
     *       "type": "text",
     *       "analyzer": "ik_max_word"
     *     }
     *   }
     * }
     *
     *
     * @return
     */
    @Override
    public boolean createMapping(String index) {
        PutMappingRequest putMappingRequest = new PutMappingRequest(index);
        try {
            XContentBuilder builder = JsonXContent.contentBuilder();
            builder
                    .startObject()
                    .startObject("properties")
                    .startObject("hotalName")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()
                    .startObject("hotalImage")
                    .field("type", "keyword")
                    .field("index", "false")
                    .endObject()
                    .startObject("type")
                    .field("type", "integer")
                    .endObject()
                    .startObject("hotalInfo")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()
                    .startObject("keyword")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()
                    .startObject("location")
                    .field("type", "geo_point")
                    .endObject()
                    .startObject("star")
                    .field("type", "integer")
                    .endObject()
                    .startObject("brand")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()
                    .startObject("address")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()
                    .startObject("openTime")
                    .field("type", "date")
                    .field("format", "yyyy-MM-dd")
                    .endObject()
                    .startObject("cityname")
                    .field("type", "keyword")
                    .endObject()
                    .startObject("regid")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .endObject()
                    .endObject().endObject();
            //设置到Request对象中
            putMappingRequest.source(builder);
            client.indices().putMapping(putMappingRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

3.4 使用SpringBoot操作文档(document)

新增文档

/**
     * 给索引库添加文档
     * @param indexName
     * @param hotal
     * @return
     */
    @Override
    public boolean insertDco(String indexName, Hotal hotal) {
        String json = JSON.toJSONString(hotal);
        System.out.println(json);
        IndexRequest indexRequest = new IndexRequest(indexName, "_doc")
                .id(hotal.getId() + "")
                .source(json, XContentType.JSON);
        try {
            IndexResponse index = client.index(indexRequest, RequestOptions.DEFAULT);
            long seqNo = index.getSeqNo();
            String lowercase = index.getResult().getLowercase();
            int status = index.status().getStatus();
            System.out.println("状态:" + status);
            System.out.println("返回:" + lowercase);
            System.out.println("序号:" + seqNo);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

删除文档

/**
     * 根据ID删除
     * @param indexName
     * @param id
     * @return
     */
    @Override
    public boolean deleteDco(String indexName, Integer id) {
        DeleteRequest deleteRequest = new DeleteRequest(indexName, "_doc", id + "");
        try {
            DeleteResponse resp = client.delete(deleteRequest, RequestOptions.DEFAULT);
            int status = resp.status().getStatus();
            System.out.println("结果:" + status);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

更新文档

/**
     * 根据id修改信息
     * @param indexName
     * @param hotal
     * @return
     */
    @Override
    public boolean updateDco(String indexName, Hotal hotal) {
        String json = JSON.toJSONString(hotal);
        System.out.println(json);
//        Map map = new HashMap();
//        map.put("hotalInfo", "xxxx");
        UpdateRequest updateRequest = new UpdateRequest(indexName, "_doc", hotal.getId() + "");
        updateRequest.doc(json, XContentType.JSON);
        try {
            UpdateResponse response = client.update(updateRequest, RequestOptions.DEFAULT);
            int status = response.status().getStatus();
            System.out.println("状态:" + status);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

四、基本查询

4.1 term、terms查询

什么是term查询?

term是代表完全匹配,也就是精确查询,搜索前不会再对搜索词进行分词,所以我们的搜索词必须是文档分词集合中的一个。   比如文档内容为:"美的微波炉",被分词为"美的"和"微波炉",term搜索的关键字必须为"美的"或者"微波炉"才能搜索出这个文档,搜索"美的微波炉"搜索不出来

语法

#term查询
GET /索引库/映射类型/_search
{
  "query": {
    "term": {
      "字段名称": {
        "value": "搜索关键字"
      }
    }
  }
}

#terms查询 - 可以同时查询多个关键词
GET /索引库/映射类型/_search
{
  "query":{
    "terms": {
      "字段名称": [
        "关键字1","关键字2"...
      ]
    }
  }  
}

注意:terms查询 多个关键字之间是或者的关系,也就是说只要符合一个关键字的文档就会被查询出来

4.2 match查询

什么是match查询?

match 查询是高层查询,它们了解字段映射的信息:
1.如果查询 日期(date) 或 整数(integer) 字段,它们会将查询字符串分别作为日期或整数对待。
2.如果查询一个( not_analyzed )未分词的精确值字符串字段, 它们会将整个查询字符串作为单个词项对待。
3.但如果要查询一个( analyzed )已分析的全文字段, 它们会先将查询字符串传递到一个合适的分析器,然后生成一个供查询的词项列表。   一旦组成了词项列表,这个查询会对每个词项逐一执行底层的查询,再将结果合并,然后为每个文档生成一个最终的相关度评分。 match查询其实底层是多个term查询,最后将term的结果合并。

语法

#match_all查询 - 查询指定库的指定类型的所有文档
GET /索引库/映射类型/_search
{
  "query": {
    "match_all": {}
  }
}

#match查询 - 根据关键字查询
GET /索引库/映射类型/_search
{
  "query": {
    "match": {
      "字段名称": "搜索关键字"
    }
  }
}

#布尔match查询
GET /索引库/映射类型/_search
{
  "query": {
    "match": {
      "字段名称": {
        "query": "搜索关键字",
        "operator": "OR或者AND"  
      }
    }
  }
}

注意:operator值为
and表示关键词分词后的结果,必须全部匹配上
or表示需要一个分词匹配上即可,默认为or

#mulit_match查询 - 可以查询多个字段
GET /索引库/映射类型/_search
{
  "query": {
    "multi_match": {
      "query": "搜索关键字",
      "fields": ["字段名称1^2.0", "字段名称2^0.5"],
      "operator": "or"
    }
  }
}

注意:
^2.0表示这个字段在搜索中的权重,值越高权重越大,可以不设置。

#match_phrase查询 - 短语查询
GET /索引库/映射类型/_search
{
  "query": {
    "match_phrase": {
      "字段名称": "关键词1 关键词2"
    }
  }
}

注意:
match_phrase查询,只会匹配关键词1 和关键词2 挨在一起的文档,如果两个关键词分开太远的文档是不会匹配上的

4.3 Ids查询

什么Ids查询?

ids查询是一类简单的查询,它过滤返回的文档只包含其中指定标识符的文档,
该查询默认指定作用在“_id”上面。

语法

GET /索引库/映射类型/_search
{
  "query": {
    "ids": {
      "values": ["1","3","6"...]
    }
  }
}

4.4 prefix前缀查询

什么是prefix查询?

前缀查询,可以使我们找到某个字段以给定前缀开头的文档。最典型的使用场景,一般是在文本框录入的时候的联想功能

语法

GET /索引库/映射类型/_search
{
  "query": {
    "prefix": {
      "字段名称": {
        "value": "前缀"
      }
    }
  }
}

注意:前缀查询并不是和搜索字段的内容前缀匹配,而是和搜索字段的所有分词的前缀匹配,匹配上一个分词后,就会查询出该文档,建议和keyword类型的字段结合使用

4.5 fuzzy查询

什么是fuzzy查询?

fuzzy(模糊)查询是一种模糊查询,term 查询的模糊等价。

语法

GET /索引库/映射类型/_search
{
  "query": {
    "fuzzy": {
      "字段名称": {
        "value": "关键词",
        "fuzziness": "2"
      }
    }
  }
}

注意:
1、fuzzy搜索以后,会自动尝试将你的搜索文本进行纠错,然后去跟文本进行匹配
2、fuzziness属性表示关键词最多纠正的次数, 比如空条 -> 空调,需要纠正一次,fuzziness需要设置为1
3、prefix_length属性表示不能被 “模糊化” 的初始字符数。 大部分的拼写错误发生在词的结尾,而不是词的开始。 例如通过将prefix_length 设置为 3 ,你可能够显著降低匹配的词项数量。

4.6 wildcard查询

什么是wildcard查询?

wildcard(通配符)查询意为通配符查询

GET /索引库/映射类型/_search
{
  "query": {
    "wildcard": {
      "字段名称": {
        "value": "关键词? *"
      }
    }
  }
}

注意:
*表示匹配0或者多个字符
?表示匹配一个字符
wildcard查询不注意查询性能,应尽可能避免使用

4.7 range查询

什么range查询?

range查询既范围查询,可以对某个字段进行范围匹配

GET /索引库/映射类型/_search
{
  "query": {
    "range": {
      "字段名称": {
        "gte": 0,
        "lte": 2000,
      }
    }
  }
}

注意:gte表示>=,lte表示<=,gt表示>,lt表示<

4.8 regexp查询

什么是regexp查询?

正则表达式查询,wildcard和regexp查询的工作方式和prefix查询完全一样。它们也需要遍历倒排索引中的词条列表来找到所有的匹配词条,然后逐个词条地收集对应的文档ID。它们和prefix查询的唯一区别在于它们能够支持更加复杂的模式。

语法

GET /索引库/映射类型/_search
{
  "query": {
    "regexp": {
      "字段名称": "正则表达式"
    }
  }
}

注意:
1、prefix(前缀)wildcard(通配符)以及regexp(正则)查询基于词条进行操作。如果你在一个analyzed字段上使用了它们,它们会检查字段中的每个词条,而不是整个字段。
2、对一个含有很多不同词条的字段运行这类查询是非常消耗资源的。应该避免使用一个以通配符开头的模式(比如,*foo)

4.9 使用JavaAPI实现以上查询

查询的基础结构,通过不同的QueryBuilder对象,可以实现不同的查询

/**
 * 搜索 - 使用Spring封装的API进行搜索
 * @return
 */
@Override
public List<Hotal> search(QueryBuilder queryBuilder) {
    NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryBuilder);
    //分页的设置
    PageRequest pageRequest = PageRequest.of(0, 2);
    nativeSearchQuery.setPageable(pageRequest);
    SearchHits<Hotal> hits = restTemplate.search(nativeSearchQuery, Hotal.class);
    long total = hits.getTotalHits();
    System.out.println("符合条件的文档总数量:" + total);
    float maxScore = hits.getMaxScore();
    System.out.println("最高评分:" + maxScore);
    List<Hotal> hotals = new ArrayList<>();
    hits.getSearchHits().stream().forEach(hotalSearchHit -> {
        //hotalSearchHit - 查询结果
        String id = hotalSearchHit.getId();
        Hotal hotal = hotalSearchHit.getContent();
        hotals.add(hotal);
    });
    return hotals;
}

//原生API的方式进行搜索----------------------------------
@Override
public List<Hotal> queryHotals(String indexName, QueryBuilder queryBuilder) {
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.query(queryBuilder);
        
    SearchRequest searchRequest = new SearchRequest(indexName);
    searchRequest.source(searchSourceBuilder);
    try {
        SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        //循环结果
        for (SearchHit hit : hits) {
            System.out.println("------------------------------------------");
            Map<String, DocumentField> fields = hit.getFields();
            for (Map.Entry<String, DocumentField> entry : fields.entrySet()) {
                System.out.println(entry.getKey() + ":" +  entry.getValue().getValue());
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

//term
TermQueryBuilder termQueryBuilder = 
    		QueryBuilders.termQuery("hotalName", "连锁");
//terms
TermsQueryBuilder termsQueryBuilder = 
    		QueryBuilders.termsQuery("hotalName", "7天", "连锁");
//match
MatchQueryBuilder matchQueryBuilder = 
    		QueryBuilders.matchQuery("hotalName", "爱丽丝");
//matchall
MatchAllQueryBuilder matchAllQueryBuilder = 
    		QueryBuilders.matchAllQuery();
//Ids
IdsQueryBuilder idsQueryBuilder = 
    		QueryBuilders.idsQuery().addIds("2","3");
//prefix
PrefixQueryBuilder prefixQueryBuilder = 
    		QueryBuilders.prefixQuery("hotalName", "连");
//fuzzy
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("regid", "平三区")
                .fuzziness(Fuzziness.TWO)
                .prefixLength(0);
//wildcard
WildcardQueryBuilder wildcardQueryBuilder = 
    		QueryBuilders.wildcardQuery("hotalName", "爱丽*");
//range
RangeQueryBuilder rangeQuery = 
    		QueryBuilders.rangeQuery("star").gte(0).lt(3);
//regexp
RegexpQueryBuilder regexQuery = 
    		QueryBuilders.regexpQuery("hotalName", "\\S{0,}[0-9]{1}.*");

五、复合查询

5.1 bool查询

bool 过滤器。 这是个 复合过滤器(compound fifilter) ,它可以接受多个其他过滤器作为参数,并将这些过滤器结合成各式各样的布尔(逻辑)组合。

语法

GET /索引库/映射类型/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "content": {
              "value": "性价比"
            }
          }
        },{
          "match": {
            "title": "微波炉"
          }
        }
      ],
      "must": [
        {
          "match": {
            "content": "格力造"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gte": 300,
              "lte": 3000
            }
          }
        }
      ],
      "filter": {
        "match": {
          "title": "美的"
        }
      }
    }
  }
}

属性含义

must: 返回的文档必须满足must子句的条件,并且参与计算分值,与 AND 等价
must_not:所有的语句都 不能(must not) 匹配,与 NOT 等价
should: 返回的文档可能满足should子句的条件。在一个Bool查询中,如果没有must或者filter,有一个或者多个should子句,那么只要满足一个就可以返回,与 OR 等价
minimum_should_match:用来指定should至少需要匹配几个语句
filter:返回的文档必须满足filter子句的条件。但是不会像Must一样,参与计算分值

注意

如果查询中没有must语句,那么至少要匹配一个should语句

5.1.1 什么是filter?

filter vs query

filter ,仅仅只是按照搜索条件过滤出需要的数据而已,不计算任何相关度分数,对相关度没有任何影响;
query ,会去计算每个document相对于搜索条件的相关度,并按照相关度进行排序;
一般来说,如果你是在进行搜索,需要将最匹配搜索条件的数据先返回,那么用query;如果你只是要根据一些条件筛选出一部分数据,不关注其排序,那么用filter; 除非是你的这些搜索条件,你希望越符合这些搜索条件的document越排在前面返回,那么这些搜索条件要放在query中;如果你不希望一些搜索条件来影响你的document排序,那么就放在filter中即可;

filter和query的性能对比

filter ,不需要计算相关度分数,不需要按照相关度分数进行排序,同时还有内置的自动cache最常使用filter的数据
query ,相反,要计算相关度分数,按照分数进行排序,而且无法cache结果
所以filter查询性能会高于query

5.3 boosting查询

什么是boosting查询?

该查询用于将两个查询封装在一起,并改变其中一个查询所返回文档的分值。它接受一个positive查询和一个negative查询。只有匹配了positive查询的文档才会被包含到结果集中,但是同时匹配了negative查询的文档会被降低其相关度,通过将文档原本的score和negative_boost参数进行相乘来得到新的score

运用场景

例如,在互联网上搜索"苹果"也许会返回,水果或者各种食谱的结果。但是用户可能只想搜索到苹果手机等电子产品,当然我们可以通过排除“水果 乔木 维生素”和这类单词,结合bool查询中的must_not子句,将结果范围缩小到只剩苹果手机,但是这种做法难免会排除掉那些真的想搜索水果的用户,这时可以通过boosting查询,通过降低“水果 乔木 维生素”等关键词的评分,让苹果等电子产品的排名靠前

语法

GET /索引库/映射类型/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "title": "性价比"
        }
      },
      "negative": {
        "match": {
          "content": "性价比"
        }
      },
      "negative_boost": 0.1
    }
  }
}

5.4 使用JavaAPI实现以上查询

//bool
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .must(....)
                .mustNot(...)
                .should(...)
                .filter(...)
                .minimumShouldMatch(1);
//boosting
BoostingQueryBuilder boostingQueryBuilder = QueryBuilders
                .boostingQuery(..., ...)
                .negativeBoost(0.2f);

六、排序

ElasticSearch默认会有一套相关性分数计算,分数越高,说明文档相关性越大,也就越会排在前面。除了相关性排序之外,开发者也可以通过自己的需要,通过某些规则设置查询文档的排序

语法

GET /索引库/映射类型/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "排序字段1": {
        "order": "asc"
      }
    },
    {
      "排序字段2":{
        "order": "desc"
      }
    }
  ]
}

//创建查询构建器
QueryBuilder queryBuilder = .........
..................
//执行查询
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
     .query(queryBuilder)
      //设置排序
     .sort("star", SortOrder.DESC)
     .sort("type", SortOrder.ASC);

NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryBuilder);
        //分页的设置
//        PageRequest pageRequest = PageRequest.of(0, 2);
//        nativeSearchQuery.setPageable(pageRequest);
        //排序的设置
        nativeSearchQuery.addSort(Sort.by(Sort.Order.asc("id")));

七、高亮

什么是高亮?

许多应用都倾向于在每个搜索结果中 高亮 显示搜索的关键词,比如字体的加粗,改变字体的颜色等.以便让用户知道为何该文档符合查询条件。在 Elasticsearch 中检索出高亮片段也很容易。高亮显示需要一个字段的实际内容。 如果该字段没有被存储(映射mapping没有将存储设置为 true),则加载实际的source,并从source中提取相关的字段。

语法

GET /索引库/映射类型/_search
{
  "query": {
    ....
  },
  "highlight": {
    "fields": {
      "待高亮字段1": {},
      "待高亮字段2": {}
    },
    "post_tags": ["</font>"],
    "pre_tags": ["<font color='red'>"],
    "number_of_fragments": 5,
    "fragment_size": 3
  }
}

参数含义

number_of_fragments: fragment 是指一段连续的文字。返回结果最多可以包含几段不连续的文字。
默认是5。
fragment_size: 某字段的值,长度是1万,但是我们一般不会在页面展示这么长,可能只是展示一部分。设置要显示出来的fragment文本判断的长度,默认是100
noMatchSize: 搜索出来的这个文档这个字段已经显示出高亮的情况,可是其它字段并没有任何显示,设置这个属性可以显示出来。
pre_tags: 标记 highlight 的开始标签。
post_tags: 标记 highlight 的结束标签。

JavaAPI

//设置高亮信息
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder
    .field("title", 100)
    .field("content", 100)
    .preTags("<font color='red'>")
    .postTags("</font>");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
     .query(queryBuilder)
     .highlighter(highlightBuilder)
     .sort("star", SortOrder.DESC)
     .sort("type", SortOrder.ASC);
........     
//获得高亮结果
Map<String, HighlightField> highlightFields 
				= hit.getHighlightFields();
for (Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) {
	System.out.println(entry.getKey() + "--" 
				+ entry.getValue().getFragments()[0].string());
}

//高亮的设置
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder
        .field("hotalName")
        .preTags("<font color='red'>")
        .postTags("</font>");
nativeSearchQuery.setHighlightQuery(new HighlightQuery(highlightBuilder));

八、地理位置搜索

地理位置在ElasticSearch中的字段类型geo-point

8.1 地理位置类型的相关操作

#创建映射类型:
PUT /soufang/_mapping/house
{
  "properties": {
    "name": {
      "type": "text"
    },
    "location": {
      "type": "geo_point"
    }
  }
}

#添加坐标点数据:
PUT /soufang/house/1
{
  "name": "市民中心",
  "location": {
    "lat": 22.54737, #lat代表纬度
    "lon": 114.067531 #lon代表经度
  }
}

8.2 通过geo_distance过滤器搜索坐标

geo_distance:地理距离过滤器( geo_distance )以给定位置为圆心画一个圆,来找出那些地理坐标落在其中的文档

GET /soufang/house/_search
{
  "query": {
    "geo_distance":{
      "location": {
        "lat": 22.551013,
        "lon": 114.065432
      },
      "distance": "1km",
      "distance_type": "arc"
    }
  }
}

distance:中心点的半径距离
distance_type:两点间的距离计算的精度算法
arc - 最慢但是最精确是弧形(arc)计算方式,这种方式把世界当作是球体来处理
plane - 平面(plane)计算方式,把地球当成是平坦的。 这种方式快一些但是精度略逊

8.3 通过geo_bounding_box过滤器搜索坐标

geo_bounding_box: 查找某个长方形区域内的位置

GET /soufang/house/_search
{
  "query": {
    "geo_bounding_box":{
      "location":{
        "top_left": {
          "lat": 22.628427,
          "lon": 114.009234
        },
        "bottom_right": {
          "lat": 22.521103,
          "lon": 114.148939
        }
      }
    }
  }
}

top_left:代表矩形左上角
bottom_right:代表矩形右下角

8.4 通过geo_polygon过滤器搜索坐标

geo_polygon:查找位于多边形内的地点

GET /soufang/house/_search
{
  "query": {
    "geo_polygon": {
      "location":{
        "points": [
          [113.908911, 22.613748],
          [114.056952,22.634298],
          [114.031368,22.575843],
          [114.097196,22.500803],
          [113.9,22.493591]
        ]
      }
    }
  }
}

8.5 过滤结果通过距离排序

GET /soufang/house/_search
{
  "query": {
    "geo_distance":{
      "location": {
        "lat": 22.551013,
        "lon": 114.065432
      },
      "distance": "1km",
      "distance_type": "arc"
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "order": "asc",
        "location": {
          "lat": 22.551013,
          "lon": 114.065432
        },
        "unit": "km",
        "distance_type": "arc"
      }
    }
  ]
}

unit:以 公里(km)为单位,将距离设置到每个返回结果的 sort 键中

8.6 JavaApi的执行方式

//geo_distance查找方式
QueryBuilders.geoDistanceQuery("location")
                    .point(22.55243, 114.044335)
                    .distance(2.8, DistanceUnit.KILOMETERS)
//geo_bounding_box查找方式
QueryBuilders.geoBoundingBoxQuery("location")
        .setCorners(
        new GeoPoint(22.628427, 114.009234), 
        new GeoPoint(22.521103, 114.148939)) 
//geo_polygon查找方式
List<GeoPoint> points = new ArrayList<>();
    points.add(new GeoPoint(22.613748, 113.908911));
    points.add(new GeoPoint(22.634298, 114.056952));
    points.add(new GeoPoint(22.575843,114.031368));
    points.add(new GeoPoint(22.500803,114.097196));
    points.add(new GeoPoint(22.493591,113.9));
QueryBuilders.geoPolygonQuery("location", points)
//根据距离排序 - 原生API
SortBuilders
	.geoDistanceSort("location", 22.586737, 113.960829)
	.order(SortOrder.DESC)
	.unit(DistanceUnit.KILOMETERS)

//设置按距离排序 - Spring封装方法
Sort.Order mylocation =
        new GeoDistanceOrder("mylocation", 
                new GeoPoint(22.634478, 113.841844))
        .with(GeoDistanceOrder.DistanceType.plane)
        .withUnit("km");
nativeSearchQuery.addSort(Sort.by(mylocation));

九、聚合查询

9.1 什么是聚合?

ElasticSearch除了致力于搜索之外,也提供了聚合实时分析数据的功能。透过聚合,我们可以得到一个数据的概览,聚合能做的是分析和总结全套的数据,而不是查找单个文档(这是搜索做的事)

9.2 聚合的概念

9.2.1 桶

桶(Buckets) : 满足特定条件的文档的集合
1)当聚合开始被执行,每个文档会决定符合哪个桶的条件,如果匹配到,文档将放入相应的桶并接着进行聚合操作
2)桶可以被嵌套在其他桶里面
3)Elasticsearch提供了很多种类型的桶,像是时间、最受欢迎的词、年龄区间、地理位置桶等等,不过他们在根本上都是通过同样的原理进行操作,也就是基于条件来划分文档,一个文档只要符合条件,就可以加入那个桶,因此一个文档可以同时加入很多桶

9.2.2 指标

指标(Metrics) : 对桶内的文档进行统计计算
1)桶能让我们划分文档到有意义的集合, 但是最终我们需要的是对这些桶内的文档进行一些指标的计算
2)指标通常是简单的数学运算(像是min、max、avg、sum),而这些是通过当前桶中的文档的值来计算的,利用指标能让你计算像平均薪资、最高出售价格等这样的数据

9.3 聚合语法

GET /索引库/_search
{
    "query": { //查询条件 },
    "size": 0,
    "aggs": {
        "自定义名称1": {  //aggs后面接著的是一个自定义的name
            "桶/指标": { ... }  //再来才是接桶
        },
        "自定义名称2": {  //一个aggs裡可以有很多聚合
            "桶/指标": { ... }
        },
        "自定义名称3": {
            "桶": {
               .....
            },
            "aggs": {  //aggs可以嵌套在别的aggs裡面
                "嵌套自定义名称": { //记得使用aggs需要先自定义一个name
                    "桶/指标": { ... } //该桶作用的文档是上一级桶的结果
                }
            }
        }
    }
}

注意:
1、当query和aggs一起存在时,会先执行query的主查询,主查询query执行完后会搜出一批结果,而这些结果才会被拿去aggs做聚合
2、如果有些情况不在意查询结果是什麽,而只在意aggs的结果,可以把size设为0,如此可以让返回的hits结果集是0,加快返回的速度
3、一个aggs裡可以有很多个聚合,每个聚合彼此间都是独立的,因此可以一个聚合拿来统计数量、一个聚合拿来分析数据、一个聚合拿来计算标准差...
4、aggs可以嵌套在其他的aggs裡面,而嵌套的桶能作用的文档集范围,是外层的桶所输出的结果集

9.4 常用的指标聚合( cardinality、avg、stats)

#对所有文档的star字段进行去重统计操作
GET /hotal_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "star_cardinality": {
      "cardinality": {
        "field": "star"
      }
    }
  }
}
#对所有文档的star字段求平均值
GET /hotal_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "star_avg": {
      "avg": {
        "field": "star"
      }
    }
  }
}
#求所有文档star字段的 数量、最小值、最大值、平均值、总和
GET /hotal_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "star_stats": {
      "stats": {
        "field": "star"
      }
    }
  }
}

9.5 常用的桶聚合(filter、range、terms)

#按照“城市名称”进行分组,最多显示10组的内容
GET /hotal_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "my_terms": {
      "terms": {
        "field": "cityname",
        "size": 10
      }
    }
  }
}
#按照star类型进行范围分组,第一组*~100,第二组100~200, 第三组200~*
GET /hotal_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "my_range": {
      "range": {
        "field": "star",
        "ranges": [
          {
            "to": 100
          },
          {
            "from": 100,
            "to": 200
          },
          {
            "from": 200
          }  
        ]
      }
    }
  }
}
#使用filter对桶分组进行过滤
GET /hotal_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "my_range": {
      "terms": {
        "field": "cityname",
        "size": 10
      },
      "aggs": {
        "myfilter": {
          "filter": {
            "match": {
              "hotalName": "希尔顿"
            }
          }
        }
      }
    }
  }
}

9.6 JavaAPI的方式设置聚合

MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
        NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(matchAllQueryBuilder);
        //设置聚合
        TermsAggregationBuilder cityName = new TermsAggregationBuilder("myterms", ValueType.STRING).field("cityname").size(10);
        nativeSearchQuery.addAggregation(cityName);
        SearchHits<Hotal> search = elasticsearchRestTemplate.search(nativeSearchQuery, Hotal.class);
        //获得聚合结果
        Aggregations aggregations = search.getAggregations();
        Aggregation myterms = aggregations.get("myterms");
        ParsedStringTerms parsedStringTerms = (ParsedStringTerms) myterms;
        List<? extends Terms.Bucket> buckets = parsedStringTerms.getBuckets();
        buckets.stream().forEach(bucket -> {
            Object key = bucket.getKey();
            long docCount = bucket.getDocCount();
            System.out.println(key + ":" + docCount);
        });

十、function_score自定义文档相关性

10.1 什么是function_score?

在使用ES进行全文搜索时,搜索结果默认会以文档的相关度进行排序,而这个 "文档的相关度",是可以通过 function_score 自己定义的,也就是说我们可以透过使用function_score,来控制 "怎么样的文档相关度更高" 这件事

10.2 文档相关度评分默认大概规则

1、关键词词频越高,评分越高
2、关键词在所有文档中出现的频率越高,评分越低
3、搜索的关键词与目标文档中分词匹配个数越多,评分越高
4、匹配的字段权重越高,评分越高

10.3 function_score的基本用法 (原始评分、加强评分、评分合并)

10.3.1 function_score提供的加强_score的函数

1、weight:设置权重提升值,可以用于任何查询
2、field_value_factor: 将某个字段的值乘上old_score
3、random_score : 为每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的
4、衰减函数 (linear、exp、guass) : 以某个字段的值为基准,距离某个值越近得分越高
5、script_score : 当需求超出以上范围时,可以用自定义脚本完全控制评分计算,不过因为还要额外维护脚本不好维护,因此尽量使用ES提供的评分函数,需求真的无法满足再使用script_score

10.3.2 function_score其他辅助的参数

boost_mode

决定 old_score 和 加强score 如何合并
可选值:
multiply(默认) : new_score = old_score * 加强score
sum : new_score = old_score + 加强score
min : old_score 和 加强score 取较小值,new_score = min(old_score, 加强score)
max : old_score 和 加强score 取较大值,new_score = max(old_score, 加强score)
replace : 加强score直接替换掉old_score,new_score = 加强score

score_mode

决定functions里面的加强score们怎么合并,会先合并加强score们成一个总加强score,再使用总加强score去和old_score做合并,换言之就是会先执行score_mode,再执行boost_mode
可选值:
multiply (默认):将所有加强score相乘
sum:求和
avg:取平均值
first : 使加强首个函数(可以有filter,也可以没有)的结果作为最终结果
max:取最大值
min:取最小值

max_boost

限制加强函数的最大效果,就是限制加强score最大能多少,但要注意不会限制old_score

10.3.3 function_score语法

单加强函数语法

GET /索引库/映射类型/_search
{
    "query": {
        "function_score": {
        	//主查询,查询完后这裡自己会有一个评分,就是old_score
            "query": {.....}, 
            //在old_score的基础上,给他加强其他字段的评分,这裡会产生一个加强score,如果只有一个加强function时,直接将加强函数名写在query下面就可以了
            "field_value_factor": {...}, 
            //指定用哪种方式结合old_score和加强score成为new_score
            "boost_mode": "multiply", 
            //限制加强score的最高分,但是不会限制old_score
            "max_boost": 1.5 
        }
    }
}

多加强函数语法

GET /索引库/映射类型/_search
{
    "query": {
        "function_score": {
            "query": {.....},
            "functions": [
                //可以有多个加强函数(或是filter+加强函数),每一个加强函数会产生一个加强score,因此functions会有多个加强score
                { "field_value_factor": ... },
                { "gauss": ... },
                { "filter": {...}, "weight": ... }
            ],
            //决定加强score们怎么合并
            "score_mode": "sum", 
            //决定总加强score怎么和old_score合并
            "boost_mode": "multiply" 
        }
    }
}

weight加强函数用法

GET /shop/goods/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {"filter": {
          "range": {
            "price": {
              "gte": 1000,
              "lte": 3000
            }
          }
        }, "weight": 3}
      ],
      "boost_mode": "sum"
    }
  }
}

解析:查询所有文档,如果某个文档的价格在1000~3000范围内,文档评分就会*3,并且new_score会和old_score相加得到最终评分

random_score加强函数使用案例

GET /shop/goods/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {"random_score": {
          "seed": 2
        }}
      ]
    }
  }
}

解析:不同的用户,可以设置不同的seed值(比如用户的id号),实现随机排序的效果,但是对同一个用户排序结果又是恒定的

field_value_factor加强函数使用案例

GET /shop/goods/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {"field_value_factor": {
          "field": "price"
        }}
      ]
    }
  }
}

解析:查询所有文档,并且将所有文档的old_score,乘以本身的价格,得到new_score,默认将new_score * old_score,得到最终评分

10.3.4 衰减函数评分

什么是衰减函数?

以某一个范围为基准,距离这个范围越远,评分越低。 比如以100为基准,那么大于100,或者小于100评分都将变得越来越低。

为什么需要衰减函数?

在一次搜索中,某个条件并不一定是线性增长或者递减来影响最终结果评分的。比如搜索商品,并不是价格越低就意味着越好,用户就会越感兴趣,往往可能维系在一个价格区间的用户会更感兴趣一些。比如原来做过一个调查,如果买车大概会买什么价位的,最后有40%的人选择的是10~15w之间的车型。所以我们会发现,价格这个因素,对用户来说并不是越高越好,同时也不意味着越低越好,而衰减函数就是为了对这一类的数据进行评分的

衰减函数的分类

linear、exp 和 gauss,三种衰减函数的差别只在于衰减曲线的形状,在DSL的语法上的用法完全一样
linear : 线性函数是条直线,一旦直线与横轴相交,所有其他值的评分都是0
exp : 指数函数是先剧烈衰减然后变缓
gauss(最常用) : 高斯函数则是钟形的,他的衰减速率是先缓慢,然后变快,最后又放缓


衰减函数的支持参数

origin : 中心点,或是字段可能的最佳值,落在原点(origin)上的文档评分score为满分1.0,支持数值、时间 以及 "经纬度地理座标点"等类型字段
offset : 从 origin 为中心,为他设置一个偏移量offset覆盖一个范围,在此范围内所有的评分_score也都是和origin一样满分1.0
scale : 衰减率,即是一个文档从origin下落时,_score改变的速度

衰减函数案例

GET /mytest/doc/_search
{
    "query": {
        "function_score": {
            "functions": [
                //第一个gauss加强函数,决定距离的衰减率
                {
                    "gauss": {
                        "location": {
                            "origin": {  //origin点设成酒店的经纬度座标
                                "lat": 51.5,
                                "lon": 0.12
                            },
                            "offset": "2km", //距离中心点2km以内都是满分1.0,2km外开始衰减
                            "scale": "3km"  //衰减率
                        }
                    }
                },
                //第二个gauss加强函数,决定价格的衰减率,因为用户对价格更敏感,所以给了这个gauss						加强函数2倍的权重
                {
                    "gauss": {
                        "price": {
                            "origin": "500", 
                            "offset": "100",
                            "scale": "20"
                        }
                    },
                    "weight": 2
                }
            ]
        }
    }
}

JavaAPI设置评分

/**
* 自定义评分 
*/
@Test
public void functionScore() throws IOException {
    List<FunctionScoreQueryBuilder.FilterFunctionBuilder> list 
    	= new ArrayList<>();
   	list.add(new FunctionScoreQueryBuilder.
   		FilterFunctionBuilder(ScoreFunctionBuilders.
   			gaussDecayFunction("location", new GeoPoint(22.586203, 114.031687), 			"6km", "5km")));
    SearchRequest searchRequest = new SearchRequest("soufang").types("house");
    searchRequest.source().query(
        QueryBuilders.functionScoreQuery(QueryBuilders.matchAllQuery(), 					list.toArray(new FunctionScoreQueryBuilder.FilterFunctionBuilder[0]))
            .boostMode(CombineFunction.REPLACE));
    SearchResponse response = 
    	restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    SearchHits hits = response.getHits();
    for (SearchHit hit : hits) {
        System.out.println("查询结果:" + hit.getSourceAsString() + " 评分:" + hit.getScore());
    }
}

十一、ElasticSearch集群搭建

11.1 ElasticSearch集群的角色

1)Master节点

主节点,该节点不和应用创建连接,每个节点都保存了集群状态,master节点不占用磁盘IO和CPU,内存使用量一般。
作用:master节点控制整个集群的元数据。只有Master Node节点可以修改节点状态信息及元数据的处理,比如索引的新增、删除、分片路由分配、所有索引和相关 Mapping 、Setting 配置等等。
配置方式:
node.master = true
node.data = false

2)Data Node节点

数据节点,该节点和索引应用创建连接、接收索引请求,该节点真正存储数据,ES集群的性能取决于该节点的个数,data节点会占用大量的CPU、IO和内存
作用:data节点的分片执行查询语句获得查询结果后将结果反馈给Coordinating节点,在查询的过程中非常消耗硬件资源。
配置方式:
node.master = false
node.data = true

3)Coordinating Node节点

协调节点,该节点和检索应用创建连接、接受检索请求,但其本身不负责存储数据,可当负载均衡节点。
作用:协调节点接受客户端搜索请求后将请求转发到与查询条件相关的多个data节点的分片上,然后多个data节点的分片执行查询语句或者查询结果再返回给协调节点,协调节点把各个data节点的返回结果进行整合、排序等一系列操作后再将最终结果返回给用户请求
配置方式:
协调节点,是一种角色,而不是真实的Elasticsearch的节点,你没有办法通过配置项来配置哪个节点为协调节点。集群中的任何节点,都可以充当协调节点的角色。当一个节点A收到用户的查询请求后,会把查询子句分发到其它的节点,然后合并各个节点返回的查询结果,最后返回一个完整的数据集给用户。在这个过程中,节点A扮演的就是协调节点的角色。毫无疑问,协调节点会对CPU、Memory要求比较高。

11.2 集群的健康状态

Red,表示有主分片没有分配,某些数据不可用。
Yellow,表示主分片都已分配,数据都可用,但是有复制分片没有分配。
Green,表示主分片和复制分片都已分配,一切正常。

11.3 使用Docker搭建ElasticSearch 7.x的集群

1、创建基本目录/usr/local/es-cluster


2、在master/conf/elasticsearch.yml添加如下内容

bootstrap.memory_lock: false
cluster.name: "es-cluster"
node.name: es-master
node.master: true
node.data: false
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.seed_hosts: ["es-master:9300"]
cluster.initial_master_nodes: ["es-master"]
path.logs: /usr/share/elasticsearch/logs
http.cors.enabled: true
http.cors.allow-origin: "*"
xpack.security.audit.enabled: true

3、在node1&node2/conf/elasticsearch.yml添加如下内容

cluster.name: "es-cluster"
node.name: node1 #这里注意替换
node.master: false
node.data: true
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.seed_hosts: ["es-master:9300"]
path.logs: /usr/share/elasticsearch/logs

4、编写docker-compose.yml

version: '3.1'
services:
  es-master:
    image:  elasticsearch:7.7.1
    container_name: es-master
    restart: always
    volumes:
      - ./master/data:/usr/share/elasticsearch/data:rw
      - ./master/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./master/logs:/user/share/elasticsearch/logs:rw
      - ./master/plugins:/usr/share/elasticsearch/plugins
    ports:
      - 9200:9200
      - 9300:9300
    networks:
      - es-network
  es-node1:
    image:  elasticsearch:7.7.1
    container_name: es-node1
    restart: always
    ports:
      - 9201:9200
      - 9301:9300
    networks:
      - es-network
    volumes:
      - ./node1/data:/usr/share/elasticsearch/data:rw
      - ./node1/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./node1/logs:/user/share/elasticsearch/logs:rw
      - ./node1/plugins:/usr/share/elasticsearch/plugins
  es-node2:
    image:  elasticsearch:7.7.1
    container_name: es-node2
    restart: always
    ports:
      - 9202:9200
      - 9302:9300
    networks:
      - es-network
    volumes:
      - ./node2/data:/usr/share/elasticsearch/data:rw
      - ./node2/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./node2/logs:/user/share/elasticsearch/logs:rw
      - ./node2/plugins:/usr/share/elasticsearch/plugins
  es-head:
    image: mobz/elasticsearch-head:5
    container_name: es-head
    restart: always
    ports:
      - 9100:9100
    networks:
      - es-network
  kibana:
    image: kibana:7.7.1
    restart: always
    container_name: kibana
    environment:
      SERVER_NAME: kibana
      ELASTICSEARCH_URL: http://192.168.195.188:9200
    ports:
      - 5601:5601
    networks:
      - es-network
networks:
  es-network:

5、启动docker-compose.yml

#授权
chmod 777 -R master node1 node2
#启动
docker-compose up -d

PS:安装中遇到问题

问题:max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
解决:在宿主机执行sysctl -w vm.max_map_count=262144,重启docker容器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值