栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 前沿技术 > 大数据 > 大数据系统

elasticsearch学习笔记

elasticsearch学习笔记

初识elasticsearch 什么是elasticsearch?

- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

什么是elastic stack(ELK)?

- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch

什么是Lucene?

- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API

倒排索引 倒排索引中有两个非常重要的概念:

- 文档(`document`):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息

- 词条(`Term`):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

创建倒排索引是对正向索引的一种特殊处理,流程如下:

- 将每一个文档的数据利用算法分词,得到一个个词条

- 创建表,每行数据包括词条、词条所在文档id、位置等信息

- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引

倒排索引的搜索流程如下(以搜索"华为手机"为例):

1)用户输入条件`"华为手机"`进行搜索。

2)对用户输入内容**分词**,得到词条:`华为`、`手机`。

3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。

4)拿着文档id到正向索引中查找具体文档。

那么为什么一个叫做正向索引,一个叫做倒排索引呢?

正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。 -而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。

那么两者方式的优缺点是什么呢?

**正向索引**:

- 优点:

- 可以给多个字段创建索引

- 根据索引字段搜索、排序速度非常快

- 缺点:

- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

**倒排索引**:

- 优点: - 根据词条搜索、模糊搜索时,速度非常快

- 缺点: - 只能给词条创建索引,而不是字段

- 无法根据字段做排序

文档和字段

elasticsearch是面向**文档(document)**存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch 而Json文档中往往包含很多的

**字段(Field)**,类似于数据库中的列。

索引和映射

索引(Index),就是相同类型的文档的集合。

例如: - 所有用户文档,就可以组织在一起,称为用户的索引;

- 所有商品的文档,可以组织在一起,称为商品的索引;

- 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表

数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。

因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。

mysql与elasticsearch

 

- Mysql:擅长事务类型操作,可以确保数据的安全和一致性

- Elasticsearch:擅长海量数据的搜索、分析、计算 因此在企业中,往往是两者结合使用:

- 对安全性要求较高的写操作,使用mysql实现

- 对查询性能要求较高的搜索需求,使用elasticsearch实现 - 两者再基于某种方式,实现数据的同步,保证一致性

分词器的作用是什么?

- 创建倒排索引时对文档分词

- 用户搜索时,对输入的内容分词

IK分词器有几种模式?

- ik_smart:智能切分,粗粒度

- ik_max_word:最细切分,细粒度

IK分词器如何拓展词条?如何停用词条?

- 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典

- 在词典中添加拓展词条或者停用词条

索引库操作 mapping映射属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

- type:字段数据类型,常见的简单类型有:

- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)

- 数值:long、integer、short、byte、double、float、

- 布尔:boolean

- 日期:date

- 对象:object

- index:是否创建索引,默认为true

- analyzer:使用哪种分词器

- properties:该字段的子字段

例如下面的json文档:

{
    "age": 21,
    "weight": 52.1,
    "isMarried": false,
    "info": "啊波次的额佛哥",
    "email": "11@.com",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "三 ",
        "lastName": "张 "
    }
}

对应的每个字段映射(mapping):

- age:类型为 integer;参与搜索,因此需要index为true;无需分词器

- weight:类型为float;参与搜索,因此需要index为true;无需分词器

- isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器

- info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart

- email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器

- score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器

- name:类型为object,需要定义多个子属性

- name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器

- name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器

索引库的CRUD

创建索引库和映射
PUT /zxq
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "email":{
        "type": "keyword",
        "index": "falsae"
      },
      "name":{
        "properties": {
          "firstName": {
            "type": "keyword"
          }
        }
      },
      // ... 略
    }
  }
}
查询索引库
GET /索引库名

删除索引库
DELETe /索引库名

修改索引库

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。 虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}
文档操作 新增文档

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}
查询文档

根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。

GET /{索引库名称}/_doc/{id}
删除文档
DELETE /{索引库名}/_doc/id值
修改文档

修改有两种方式:

- 全量修改:直接覆盖原来的文档

- 增量修改:修改文档中的部分字段

 

全量修改是覆盖原来的文档,其本质是:

- 根据指定的id删除文档

- 新增一个相同id的文档

PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}

增量修改是只修改指定id匹配的文档中的部分字段。

POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

RestAPI

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html 其中的Java Rest Client又包括两种: - Java Low Level Rest Client

- Java High Level Rest Client

在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。 分为三步

引入es的RestHighLevelClient依赖


    org.elasticsearch.client
    elasticsearch-rest-high-level-client

因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本


    1.8
    7.12.1

初始化RestHighLevelClient:

RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")
));
创建索引库

代码分为三步:

- 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。

- 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。

- 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。

public class HotelConstants {
    public static final String MAPPING_TEMPLATE = "{n" +
            "  "mappings": {n" +
            "    "properties": {n" +
            "      "id": {n" +
            "        "type": "keyword"n" +
            "      },n" +
            "      "name":{n" +
            "        "type": "text",n" +
            "        "analyzer": "ik_max_word",n" +
            "        "copy_to": "all"n" +
            "      },n" +
            "      "address":{n" +
            "        "type": "keyword",n" +
            "        "index": falsen" +
            "      },n" +
            "      "price":{n" +
            "        "type": "integer"n" +
            "      },n" +
            "      "score":{n" +
            "        "type": "integer"n" +
            "      },n" +
            "      "brand":{n" +
            "        "type": "keyword",n" +
            "        "copy_to": "all"n" +
            "      },n" +
            "      "city":{n" +
            "        "type": "keyword",n" +
            "        "copy_to": "all"n" +
            "      },n" +
            "      "starName":{n" +
            "        "type": "keyword"n" +
            "      },n" +
            "      "business":{n" +
            "        "type": "keyword"n" +
            "      },n" +
            "      "location":{n" +
            "        "type": "geo_point"n" +
            "      },n" +
            "      "pic":{n" +
            "        "type": "keyword",n" +
            "        "index": falsen" +
            "      },n" +
            "      "all":{n" +
            "        "type": "text",n" +
            "        "analyzer": "ik_max_word"n" +
            "      }n" +
            "    }n" +
            "  }n" +
            "}";
}
@Test
void createHotelIndex() throws IOException {
    // 1.创建Request对象
    CreateIndexRequest request = new CreateIndexRequest("hotel");
    // 2.准备请求的参数:DSL语句
    request.source(MAPPING_TEMPLATE, XContentType.JSON);
    // 3.发送请求
    client.indices().create(request, RequestOptions.DEFAULT);
}
删除索引库
@Test
void testDeleteHotelIndex() throws IOException {
    // 1.创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    // 2.发送请求
    client.indices().delete(request, RequestOptions.DEFAULT);
}

判断索引库是否存在

判断索引库是否存在,本质就是查询,对应的DSL是:

GET /hotel
@Test
void testExistsHotelIndex() throws IOException {
    // 1.创建Request对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    // 2.发送请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    // 3.输出
    System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
RestClient操作文档

为了与索引库操作分离,我们再次参加一个测试类,做两件事情: - 初始化RestHighLevelClient - 我们的酒店数据在数据库,需要利用IHotelService去查询,所以注入这个接口

新增文档
@Test
void testAdddocument() throws IOException {
    // 1.根据id查询酒店数据
    Hotel hotel = hotelService.getById(61083L);
    // 2.转换为文档类型
    HotelDoc hotelDoc = new HotelDoc(hotel);
    // 3.将HotelDoc转json
    String json = JSON.toJSonString(hotelDoc);

    // 1.准备Request对象
    IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
    // 2.准备Json文档
    request.source(json, XContentType.JSON);
    // 3.发送请求
    client.index(request, RequestOptions.DEFAULT);
}

查询文档
@Test
void testGetdocumentById() throws IOException {
    // 1.准备Request
    GetRequest request = new GetRequest("hotel", "61082");
    // 2.发送请求,得到响应
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    // 3.解析响应结果
    String json = response.getSourceAsString();

    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    System.out.println(hotelDoc);
}
删除文档
@Test
void testDeletedocument() throws IOException {
    // 1.准备Request
    DeleteRequest request = new DeleteRequest("hotel", "61083");
    // 2.发送请求
    client.delete(request, RequestOptions.DEFAULT);
}

修改文档
@Test
void testUpdatedocument() throws IOException {
    // 1.准备Request
    UpdateRequest request = new UpdateRequest("hotel", "61083");
    // 2.准备请求参数
    request.doc(
        "price", "952",
        "starName", "四钻"
    );
    // 3.发送请求
    client.update(request, RequestOptions.DEFAULT);
}

批量导入文档
@Test
void testBulkRequest() throws IOException {
    // 批量查询酒店数据
    List hotels = hotelService.list();

    // 1.创建Request
    BulkRequest request = new BulkRequest();
    // 2.准备参数,添加多个新增的Request
    for (Hotel hotel : hotels) {
        // 2.1.转换为文档类型HotelDoc
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 2.2.创建新增文档的Request对象
        request.add(new IndexRequest("hotel")
                    .id(hotelDoc.getId().toString())
                    .source(JSON.toJSonString(hotelDoc), XContentType.JSON));
    }
    // 3.发送请求
    client.bulk(request, RequestOptions.DEFAULT);
}

DSL查询文档

DSL查询分类

Elasticsearch提供了基于JSON的DSL([Domain Specific Language]来定义查询。常见的查询类型包括:

**查询所有**:查询出所有数据,一般测试用。例如:match_all

**全文检索(full text)查询**:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。

例如:

- match_query

- multi_match_query

**精确查询**:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。

例如:

- ids - range

- term

**地理(geo)查询**:

根据经纬度查询。

例如:

- geo_distance

- geo_bounding_box

**复合(compound)查询**:复合查询可以将上述各种查询条件组合起来,合并查询条件。

例如:

- bool

- function_score

// 查询所有
GET /indexName/_search
{
  "query": {
    "match_all": {
    }
  }
}
全文检索查询

全文检索查询的基本流程如下:

- 对用户搜索的内容做分词,得到词条

- 根据词条去倒排索引库中匹配,得到文档id

- 根据文档id找到文档,返回给用户 比较常用的场景包括:

- 商城的输入框搜索

- 百度输入框搜索

基本语法

- match:根据一个字段查询

- multi_match:根据多个字段查询,参与查询字段越多,查询性能越差

match查询语法如下

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "外滩如家"
    }
  }
}

mulit_match语法如下:

GET /hotel/_search
{
  "query": {
    "multi_match": {
      "query": "外滩如家",
      "fields": ["brand","business","name"]
    }
  }
}
精准查询

- term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段

- range查询:根据数值范围查询,可以是数值、日期的范围

term查询

因为精确查询的字段搜是不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。

GET /hotel/_search
{
  "query": {
    "term": {
      "city": {
        "value": "上海"
      }
    }
  }
}
range查询

范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 1000,// 这里的gte代表大于等于,gt则代表大于
        "lte": 2000 // lte代表小于等于,lt则代表小于
      }
    }
  }
}
地理坐标查询

所谓的地理坐标查询,其实就是根据经纬度查询

常见的使用场景包括:

- 携程:搜索我附近的酒店

- 滴滴:搜索我附近的出租车

- 微信:搜索我附近的人

矩形范围查询

矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:

查询时,需要指定矩形的**左上**、**右下**两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。

// geo_bounding_box查询
GET /indexName/_search
{
  "query": {
    "geo_bounding_box": {
      "FIELD": {
        "top_left": { // 左上点
          "lat": 31.1,
          "lon": 121.5
        },
        "bottom_right": { // 右下点
          "lat": 30.9,
          "lon": 121.7
        }
      }
    }
  }
}
附近查询

附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。

换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:

//distance

GET /hotel/_search
{
  "query": {
    "geo_distance":{
      "distance":"2km",
      "location":"31.21,121.5"
    }
  }
}

复合查询

复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。

常见的有两种:

- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名

- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索

算分函数查询

function score 查询中包含四部分内容:

- 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)

- 过滤条件:filter部分,符合该条件的文档才会重新算分

- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数

- weight:函数结果是常量

- field_value_factor:以文档中的某个字段值作为函数结果

- random_score:以随机数作为函数结果

- script_score:自定义算分函数算法

- 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:

- multiply:相乘

- replace:用function score替换query score

- 其它,例如:sum、avg、max、min

 

function score的运行流程如下:

- 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)

- 2)根据过滤条件,过滤文档

- 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)

- 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。

GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {  .... }, // 原始查询,可以是任意条件
      "functions": [ // 算分函数
        {
          "filter": { // 满足的条件,品牌必须是如家
            "term": {
              "brand": "如家"
            }
          },
          "weight": 2 // 算分权重为2
        }
      ],
      "boost_mode": "sum" // 加权模式,求和
    }
  }
}
function score query定义的三要素是什么?

- 过滤条件:哪些文档要加分

- 算分函数:如何计算function score

- 加权方式:function score 与 query score如何运算

布尔查询

布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。

子查询的组合方式有:

- must:必须匹配每个子查询,类似“与”

- should:选择性匹配子查询,类似“或”

- must_not:必须不匹配,不参与算分,类似“非”

- filter:必须匹配,不参与算分

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"city": "上海" }}
      ],
      "should": [
        {"term": {"brand": "皇冠假日" }},
        {"term": {"brand": "华美达" }}
      ],
      "must_not": [
        { "range": { "price": { "lte": 500 } }}
      ],
      "filter": [
        { "range": {"score": { "gte": 45 } }}
      ]
    }
  }
}
搜索结果处理

排序 普通字段排序

keyword、数值、日期类型排序的语法基本一致。

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "FIELD": "desc"  // 排序字段、排序方式ASC、DESC
    }
  ]
}
地理坐标排序
//升序排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 31.034661,
          "lon": 121.612282
        }, 
        "unit": "km", 
        "order": "asc"
      }
    }
  ]
}

分页

elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

- from:从第几个文档开始

- size:总共查询几个文档

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}
深度分页问题

现在,我要查询990~1000的数据,查询逻辑要这么写:

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 990, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

这里是查询990开始的数据,也就是 第990~第1000条 数据。 不过,elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:

那如果我要查询9900~10000的数据呢?

是不是要先查询TOP10000呢?那每个节点都要查询10000条?

汇总到内存中?

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页,ES提供了两种解决方案,[官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html):

- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。

- scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。

高亮

我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示

高亮显示的实现分为两步:

- 1)给文档中的所有关键字都添加一个标签,例如``标签

- 2)页面给``标签编写CSS样式

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  },
  "highlight": {
    "fields": { 
      "name": {
        "require_field_match": "false"
      }
    }
  }
}

RestClient查询文档

文档的查询同样适用昨天学习的 RestHighLevelClient对象,基本步骤包括:

- 1)准备Request对象

- 2)准备请求参数

- 3)发起请求

- 4)解析响应

查询的基本步骤是:

1. 创建SearchRequest对象

2. 准备Request.source(),也就是DSL。

        ① QueryBuilders来构建查询条件

        ② 传入Request.source() 的 query() 方法

3. 发送请求,得到结果

4. 解析结果(参考JSON结果,从外到内,逐层解析)

 

@Test
void testMatchAll() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    request.source()
        .query(QueryBuilders.matchAllQuery());
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    // 4.解析响应
    handleResponse(response);
}

private void handleResponse(SearchResponse response) {
    // 4.解析响应
    SearchHits searchHits = response.getHits();
    // 4.1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 4.2.文档数组
    SearchHit[] hits = searchHits.getHits();
    // 4.3.遍历
    for (SearchHit hit : hits) {
        // 获取文档source
        String json = hit.getSourceAsString();
        // 反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println("hotelDoc = " + hotelDoc);
    }
}
match查询
@Test
void testMatchAll() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    request.source()
            .query(QueryBuilders.matchAllQuery());
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    // 4.解析响应
    handleResponse(response);
}

精确查询
@Test
void testMatch() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    request.source()
            .query(QueryBuilders.matchQuery("all", "如家"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}

布尔查询
@Test
void testBool() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1.准备BooleanQuery
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    // 2.2.添加term
    boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
    // 2.3.添加range
    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

    request.source().query(boolQuery);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}
排序、分页
@Test
void testPageAndSort() throws IOException {
    // 页码,每页大小
    int page = 1, size = 5;

    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1.query
    request.source().query(QueryBuilders.matchAllQuery());
    // 2.2.排序 sort
    request.source().sort("price", SortOrder.ASC);
    // 2.3.分页 from、size
    request.source().from((page - 1) * size).size(5);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}

高亮
@Test
void testHighlight() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1.query
    request.source().query(QueryBuilders.matchQuery("all", "如家"));
    // 2.2.高亮
    request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

private void handleResponse(SearchResponse response) {
    // 4.解析响应
    SearchHits searchHits = response.getHits();
    // 4.1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 4.2.文档数组
    SearchHit[] hits = searchHits.getHits();
    // 4.3.遍历
    for (SearchHit hit : hits) {
        // 获取文档source
        String json = hit.getSourceAsString();
        // 反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        // 获取高亮结果
        Map highlightFields = hit.getHighlightFields();
        if (!CollectionUtils.isEmpty(highlightFields)) {
            // 根据字段名获取高亮结果
            HighlightField highlightField = highlightFields.get("name");
            if (highlightField != null) {
                // 获取高亮值
                String name = highlightField.getFragments()[0].string();
                // 覆盖非高亮结果
                hotelDoc.setName(name);
            }
        }
        System.out.println("hotelDoc = " + hotelDoc);
    }
}

数据聚合

聚合常见的有三类:

- **桶(Bucket)**聚合:用来对文档做分组

        - TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组

        - DateHistogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

- **度量(Metric)**聚合:用以计算一些值,比如:最大值、最小值、平均值等

        - Avg:求平均值

        - Max:求最大值

        - Min:求最小值

        - Stats:同时求max、min、avg、sum等

- **管道(pipeline)**聚合:其它聚合的结果为基础做聚合

Bucket聚合语法

现在,我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是Bucket聚合。

GET /hotel/_search
{
  "size": 0,  // 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": { // 定义聚合
    "brandAgg": { //给聚合起个名字
      "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
        "field": "brand", // 参与聚合的字段
        "size": 20 // 希望获取的聚合结果数量
      }
    }
  }
}
Metric聚合语法

现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。 这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。

GET /hotel/_search
{
  "size": 0, 
  "aggs": {
    "brandAgg": { 
      "terms": { 
        "field": "brand", 
        "size": 20
      },
      "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
        "score_stats": { // 聚合名称
          "stats": { // 聚合类型,这里stats可以计算min、max、avg等
            "field": "score" // 聚合字段,这里是score
          }
        }
      }
    }
  }
}
aggs代表聚合,与query同级,此时query的作用是?

        - 限定聚合的的文档范围

聚合必须的三要素:

        - 聚合名称

        - 聚合类型

        - 聚合字段

聚合可配置属性有:

        - size:指定聚合结果数量

        - order:指定聚合结果排序方式

        - field:指定聚合字段

RestAPI实现聚合

 

 

自动补全 拼音分词器
POST /_analyze
{
  "text": "如家酒店还不错",
  "analyzer": "pinyin"
}

自定义分词器

elasticsearch中分词器(analyzer)的组成包含三部分:

- character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符

- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart

- tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

 

 

 

总结:

如何使用拼音分词器?

- ①下载pinyin分词器

- ②解压并放到elasticsearch的plugin目录

- ③重启即可

如何自定义分词器?

- ①创建索引库时,在settings中配置,可以包含三部分

- ②character filter

- ③tokenizer

- ④filter

拼音分词器注意事项?

- 为了避免搜索到同音字,搜索时不要使用拼音分词器

自动补全查询

- 参与补全查询的字段必须是completion类型。

- 字段的内容一般是用来补全的多个词条形成的数组。

 

 

 

 

数据同步

elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的**数据同步**。

常见的数据同步方案有三种:

        - 同步调用

        - 异步通知

        - 监听binlog

同步调用

- hotel-demo对外提供接口,用来修改elasticsearch中的数据

- 酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口,

 

异步通知

- hotel-admin对mysql数据库数据完成增、删、改后,发送MQ消息

- hotel-demo监听MQ,接收到消息后完成elasticsearch数据修改

监听binlog

- 给mysql开启binlog功能

- mysql完成增、删、改操作都会记录在binlog中

- hotel-demo基于canal监听binlog变化,实时更新elasticsearch中的内容

 

 

方式一:同步调用

- 优点:实现简单,粗暴

- 缺点:业务耦合度高

方式二:异步通知

- 优点:低耦合,实现难度一般

- 缺点:依赖mq的可靠性

方式三:监听binlog

- 优点:完全解除服务间耦合

- 缺点:开启binlog增加数据库负担、实现复杂度高

实现数据同步

引入依赖


    org.springframework.boot
    spring-boot-starter-amqp

声明队列交换机名称

public class MqConstants {
    
    public final static String HOTEL_EXCHANGE = "hotel.topic";
    
    public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
    
    public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
    
    public final static String HOTEL_INSERT_KEY = "hotel.insert";
    
    public final static String HOTEL_DELETE_KEY = "hotel.delete";
}

声明队列交换机

@Configuration
public class MqConfig {
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
    }

    @Bean
    public Queue insertQueue(){
        return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
    }

    @Bean
    public Queue deleteQueue(){
        return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
    }

    @Bean
    public Binding insertQueueBinding(){
        return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
    }

    @Bean
    public Binding deleteQueueBinding(){
        return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
    }
}
发送MQ消息

接收MQ消息

首先在hotel-demo的`cn.itcast.hotel.service`包下的`IHotelService`中新增新增、删除业务

void deleteById(Long id);
void insertById(Long id);

给hotel-demo中的`cn.itcast.hotel.service.impl`包下的HotelService中实现业务:

@Override
public void deleteById(Long id) {
    try {
        // 1.准备Request
        DeleteRequest request = new DeleteRequest("hotel", id.toString());
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

@Override
public void insertById(Long id) {
    try {
        // 0.根据id查询酒店数据
        Hotel hotel = getById(id);
        // 转换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);

        // 1.准备Request对象
        IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
        // 2.准备Json文档
        request.source(JSON.toJSonString(hotelDoc), XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

编写监听器

@Component
public class HotelListener {

    @Autowired
    private IHotelService hotelService;

    
    @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
    public void listenHotelInsertOrUpdate(Long id){
        hotelService.insertById(id);
    }

    
    @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
    public void listenHotelDelete(Long id){
        hotelService.deleteById(id);
    }
}

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号