SpringCloud微服务(实用篇二)

一、RabbitMQ

1.初始MQ

1.1同步通讯

同步:如视频电话,一旦接通就可以建立连接

异步:微信,消息发过去,不立马得到回复,时效性差

同步调用存在的问题

微服务间基于Feign的调用就属于同步方式,存在一些问题。

1.代码耦合问题,随着业务功能的增加还需要修改原本代码 

2.性能下降,吞吐量下降(因为是同步调用,支付服务调用订单服务结束后才能调仓储服务)

3.资源浪费,资源利用不充分,支付服务调用业务后只能等待

4.级联失败,仓储服务如果挂了,支付服务调不到,只能卡着,多了就挂了

同步调用的优点

  • 时效性强,可以立即得到结果

同步调用的问题

  • 耦合度高
  • 性能和吞吐能力下降
  • 有额外的资源消耗
  • 有级联失败问题

1.2异步通讯

异步调用方案:

异步调用常见实现就是事件驱动模式

异步通信的优点:

  • 耦合度低
  • 吞吐量提升
  • 故障隔离
  • 流量削峰

异步通信的缺点

  • 依赖于Broker的可靠性、安全性、吞吐能力
  • 架构复杂了,业务没有明显的流程线,不好追踪管理

1.3MQ常见框架

MQ就是消息队列,字面来看就是存放消息的队列,也就是事件驱动架构中的BROKER

2.RabbitMQ快速入门

2.1RabbitMQ概述和安装

docker load -i mq.tar

docker run \
 -e RABBITMQ_DEFAULT_USER=itcast \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 --name mq \
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management

RabbitMQ中的几个概念:

  • channel:操作MQ的工具
  • exchange:路由消息到队列中
  • queue:缓存消息
  • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

2.2常见消息模型

2.3快速入门

publisher:消息发布者,将消息发送到队列queue

queue:消息队列,负责接受并缓存消息

consumer:订阅队列,处理队列中的消息

基本消息队列的消息发送流程:

1.建立connection

2.创建channel

3.利用channel声明队列

4.利用channel向队发送消息

基本消息队列的消息接受流程:

1.建立connection

2.创建channel

3.利用channel声明队列

4.定义consumer的消费行为handleDelivery()

5.利用channel将消费者与队列绑定

为了防止队列不存在,生产者和消费者都声明队列

3.SpringAMQP

3.1Basic Queue 简单队列模型

利用SpringAMQP实现HelloWorld中的基础消息队列功能

第一步:引依赖

因为publisher和consumer服务都需要amqp依赖,因此这里把依赖直接放到父工程mq-demo中

 <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

第二步

1.在publish服务中编写application.yml,添加mq连接信息:

spring:
  rabbitmq:
    host: 106.55.251.245 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: itcast # 用户名
    password: 123321 # 密码
    virtual-host: / # 虚拟主机

2.在publish服务中新建一个测试类,编写测试方法

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue(){
        String queueName = "simple.queue";
        String message = "hello,spring amqp";
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

什么是AMQP?

  • 应用间消息通信的一种协议,与语言和平台无关

SpringAMQP如何发送消息?

  • 引入amqp的starter依赖
  • 配置RabbitMQ地址
  • 利用RabbitTemplate的convertAndSend方法

步骤3:在consumer中编写消费者逻辑,监听simple.queue

1.在consumer服务中编写application.yml添加mq连接信息

spring:
  rabbitmq:
    host: 106.55.251.245 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: itcast # 用户名
    password: 123321 # 密码
    virtual-host: / # 虚拟主机

2.在consumer服务中新建一个类,编写消费逻辑:

@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者接受到simple.queue的消息:[" + msg + "]");
    }
}

SpringAMQP如何接受消息?

  • 引入amqp的starter依赖
  • 配置RabiitMQ地址
  • 定义类,添加@Component注解
  • 类中声明方法,添加@RabbitListener注解,方法参数就时消息

注意:消息一旦消费者就会从队列删除,RabbitMQ没有消息回溯功能

3.2Word Queue 动作队列模型

提高消息处理速度,避免队列消息堆积

案例:模拟WorkQueue,实现一个队列绑定多个消费者

步骤一:生产循环发送消息到simple.queue

在publisher服务中添加一个测试方法,循环发送50条消息到simple.queue队列

 @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2WorkQueue() throws InterruptedException {
        String queueName = "simple.queue";
        String message = "hello,message_";
        for (int i = 1; i <= 50; i++) {
            rabbitTemplate.convertAndSend(queueName, message + i);
            Thread.sleep(20);
        }
    }

步骤2:编写两个消费者,都监听simple.queue

在consumer服务中添加一个消费者,也监听simple.queue:

@RabbitListener(queues = "simple.queue")
    public void listenWork1Queue(String msg) throws InterruptedException {
        System.out.println("消费者1接受到的消息:[" + msg + "]" + LocalTime.now());
        Thread.sleep(20);
    }
    @RabbitListener(queues = "simple.queue")
    public void listenWork2Queue(String msg) throws InterruptedException {
        System.err.println("消费者2.......接受到的消息:[" + msg + "]" + LocalTime.now());
        Thread.sleep(200);
    }

3.3消费预取限制

修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限:

spring:
  rabbitmq:
    host: 106.55.251.245 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: itcast # 用户名
    password: 123321 # 密码
    virtual-host: / # 虚拟主机
    listener:
      simple:
        prefetch: 1 # 每次只能读取一个消息,处理完成才能获取下一个消息

Work模型的使用:

  • 多个消费者绑定到一个队列,同一个消息只会被一个消费者处理
  • 通过设置prefetch来控制消费者预取的消息数量

3.4发布、订阅模型-Fanout

发布订阅模式与之前案例的区别就是允许将同一消息发送个多个消费者。实现方式是加入了exchange(交换机).

注意:exchange负责消息路由,而不是存储,路由失败则消息丢失

利用SpringAMQP演示FanoutExchange的使用

步骤1:在consumer服务声明Exchange、Queue、Bindindm,添加@Configuration注解,并声明FanoutExchange、Queue和绑定关系对象Binding,代码如下:

@Configuration
public class FanoutConfig {
    //1.声明交换机itcast.fanout
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }
    //2.声明队列fanout.queue1
    @Bean
    public Queue fanoutqueue1(){
        return new Queue("fanout.queue1");
    }
    //2.声明队列fanout.queue2
    @Bean
    public Queue fanoutqueue2(){
        return new Queue("fanout.queue2");
    }
    //绑定队列1到交换机
    @Bean
    public Binding fanoutBinding(Queue fanoutqueue1,FanoutExchange fanoutExchange){
        return BindingBuilder
                .bind(fanoutqueue1)
                .to(fanoutExchange);
    }
    //绑定队列2到交换机
    @Bean
    public Binding fanoutBinding2(Queue fanoutqueue2,FanoutExchange fanoutExchange){
        return BindingBuilder
                .bind(fanoutqueue2)
                .to(fanoutExchange);
    }
}

步骤2:在consumer服务声明两个消费者

在consumer服务的springRabbitLister类中,添加连个方法,分别监听fanout.queue1和fanout.queue2:

       @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue(String msg){
        System.out.println("消费者接受到fanout.queue1的消息:[" + msg + "]");
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanout2Queue(String msg){
        System.out.println("消费者接受到fanout.queue2的消息:[" + msg + "]");
    }

步骤3:在pulish服务中的SpringAmqpTest类中添加测试方法:

 @Test
    public void testSendFanoutExchange(){
        // 交换机名称
        String exchangeName = "itcast.fanout";
        //消息
        String message = "hello,everyone";
        rabbitTemplate.convertAndSend(exchangeName,"",message);
    }

交换机的作用是什么?

  • 接受publisher发送的消息
  • 将消息按规则路由到与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • FanoutExchange的会将消息路由到每个绑定的队列

声明队列、交换机、绑定关系的Bean是什么?

  • Queue
  • FanoutExchange
  • Binding

3.5发布、订阅模型-Direc

案例:利用SpringAMQP演示DirectExchange的使用

步骤一:在consumer服务中,编写两个消费者方法,分别监听direct.queue1和queue2,

2.并利用@RabbitListener声明Exchange、Queue、RoutingKey

 @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queu1"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String msg){
        System.out.println("消费者收到direct.queue1的消息:[" + msg + "]");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queu2"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String msg){
        System.out.println("消费者收到direct.queue2的消息:[" + msg + "]");
    }

步骤2:publisher服务发送消息到DirectExchange

 @Test
    public void testSendDirectExchange(){
        // 交换机名称
        String exchangeName = "itcast.direct";
        //消息
        String message = "hello,red";
        rabbitTemplate.convertAndSend(exchangeName,"red",message);
    }

3.6描述下Direct交换机与Fanout交换机的差异?

  • Fanout交换机将消息路由给每个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给哪个队列
  • 如果多个队列具有相同的RoutingKey,则与Fanout功能类似

基于@RabbitListener注解声明队列和交换机有哪些常见注解?

@Queue

@Exchange

3.7发布、订阅模型-Topic

步骤一:在consumer声明Exchange、Queue

1.在consumer服务中,编写两个消息者方法,分别监听topic.queue1和topic.queue2

2.并利用@RabbitListener声明Exchange、Queue、RoutingKey

 @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange =  @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenTopicQueue1(String msg){
        System.out.println("消费者收到topic.queue1的消息:[" + msg + "]");
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange =  @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listenTopicQueue2(String msg){
        System.out.println("消费者收到topic.queue2的消息:[" + msg + "]");
    }

步骤二:在publish服务发送消息到TopicExchange

 @Test
    public void testSendTopicExchange(){
        // 交换机名称
        String exchangeName = "itcast.topic";
        //消息
        String message = "天气好";
        rabbitTemplate.convertAndSend(exchangeName,"china.weather",message);
    }

3.8消息转换器

在SpringAMQP的发送方法中,接受消息的类型时Obiect也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮助我们化为字节后发送。

@Bean
    public Queue objectQueue(){
        return new Queue("object.queue");
    }
@Test
    public void testSendMap() throws InterruptedException{
        Map<String,Object> msg = new HashMap<>();
        msg.put("name","Jack");
        msg.put("age",21);
        rabbitTemplate.convertAndSend("object.queue", msg);
    }

Spring的对消息处理默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化

如果要修改只需要定义一个MessageConverter类型的Bean即可。推荐使用JSON序列化,步骤如下:

更改发送消息为JSONx序列化

<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

在配置类上注入bean

 @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

更改接收消息

引依赖同上

注入Bean同上

消费者代码

@RabbitListener(queues = "object.queue")
    public void listenObjectQueue(Map<String, Object> msg){
        System.out.println("消费者收到object.queue的消息:[" + msg + "]");

    }

二、elasticsearch基础功能

1.初识elasticsearch

1.1了解ES

elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容.

elasticsearch结合kibana、Logstash、Bears,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域

elasticsearch是elastic stack的核心,负责存储、搜索、分析数据.

什么是elasticsearch?

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

1.2倒排索引

 

1.3es的一些概念

文档

elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。

文档数据会被序列化为JSON格式后存储在elasticsearch中

索引(Index)

  • 索引:相同类型的文档的集合
  • 映射:索引中文档的字段约束信息,类似表的结构约束

架构

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

Elasticsearch:擅长海量数据的搜索、分析、计算

1.4安装es 、kibana

因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:

docker network create es-net

es容器配置

docker run -d \
	--name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1

kibana容器配置

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1

分词器

es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。

离线安装ik插件

安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:

docker volume inspect es-plugins
[
    {
        "CreatedAt": "2022-05-06T10:06:34+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
        "Name": "es-plugins",
        "Options": null,
        "Scope": "local"
    }
]

上传到es容器的插件数据卷中/var/lib/docker/volumes/es-plugins/_data

重启容器

docker restart es

测试:

IK分词器包含两种模式:

* `ik_smart`:最少切分

* `ik_max_word`:最细切分

2.索引库操作

mapping映射属性

 索引库的CRUD

# 创建索引库
PUT /heima
{
  "mappings": {
    "properties": {
      "info": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "email": {
        "type": "keyword",
        "index": false
      },
      "name": {
        "type": "object",
        "properties": {
           "firstName": {
              "type": "keyword"
            },
            "lastName": {
              "type": "keyword"
            }
        }
      }
  }
}
  
}

查看、删除索引库

修改索引库

3.文档操作

3.1新增文档

3.2查询文档
3.3删除文档

3.4修改文档

4.RestClient操作索引库

4.1 创建索引库

4.2 删除索引库

# 酒店的mapping
PUT /hotel
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "address": {
        "type": "keyword",
        "index": false
      },
      "price": {
        "type": "integer"
      },
      "score": {
        "type": "integer"
      },
      "brand": {
        "type": "keyword",
        "copy_to": "all"
      },
      "city": {
        "type": "keyword"
      },
      "startName": {
        "type": "keyword"
      },
      "business": {
        "type": "keyword",
        "copy_to": "all"
      },
      "location": {
        "type": "geo_point"
      },
      "pic": {
        "type": "keyword",
        "index": false
      },
      "all": {
        "type": "text"
      }
    }
  }
}

步骤三

1.引入es的RestHighLevelClient依赖(默认版本7.6.2)和安装版本保持一致

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

2.初始化RestHighLevelClient

import java.io.IOException;

public class HotelIndexTest {

    private RestHighLevelClient client;
    @Test
    void testInit(){
        System.out.println(client);
    }
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://106.55.251.245:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

步骤4:创建索引库

 @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);
    }

将创建索引库的代码定义为常量MAPPING_TEMPLATE

4.3 判断索引库是否存在

删除索引库代码

@Test
    void deleteHotelIndex() throws IOException {
        //1.创建Request对象
        DeleteIndexRequest request = new DeleteIndexRequest("hotel");
        //2.发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

判断索引库是否存在

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

5.RestClient操作文档

5.1新增文档

@SpringBootTest
public class HotelDocumentTest {

    @Autowired
    private IHotelService hotelService;

    private RestHighLevelClient client;

    @Test
    void testAddDocument() throws IOException {
        Hotel hotel = hotelService.getById(61083L);
        //转换为文档类型
        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);
    }
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://106.55.251.245:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

5.2查询文档

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

        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println(hotelDoc);

    }

5.3删除文档

  @Test
    void testDeleteDocument() throws IOException {
        //1.准备Request
        DeleteRequest request = new DeleteRequest("hotel","61083");
        //2.发送请求
        client.delete(request, RequestOptions.DEFAULT);

    }

5.4修改文档

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

5.5批量导入文档

 @Test
    void testBulkRequest() throws IOException {
        //批量查询酒店数据
        List<Hotel> hotels = hotelService.list();

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

elasticsearch搜索功能

1DSL查询分类

  •  查询所有:查询出所有数据,一般测试用。例如:match_all
  • 全文检索查询:利用分词器对用户的输入内容分词,然后去倒排索引库中匹配。例如
    • match_query
    •  match_match_query
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如
    • ids
    • range
    • term
  • 地理查询:根据经纬度查询。例如:
    • geo_distance
    • geo_bounding_box
  • 复合查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
    •        bool
    •        function_score

2全文检索查询

对用户输入内容分词,常用于搜索框搜索:

3精准查询

精确查询一般是查找keyword、数值、日期、bolean等类型字段。所有不会对搜索条件分词。常见的有:

  • term:根据词条精值查询
  • range:根据值的范围查询

# term查询
GET /hotel/_search
{
  "query": {
    "term": {
      "city": {
        "value": "上海"
      }
    }
  }
}
# range查询
GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 100,
        "lte": 300
      }
    }
  }
}
 

4地理坐标查询

# range查询
GET /hotel/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km",
      "location": "31.21, 121.5"
    }
  }
}

5组合查询

s

Function Score Query

Boolean Query

       

2.搜索结果处理

排序

案例一:对酒店数据按照用户评价降序排序,评价相同或的按照价格升序排序

#排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score": "desc"
    },
    {
      "price": "asc"
    }
  ]
}

案例二:实现对酒店数据按照到你的位置坐标的距离升序排序

# 找到116.397128,39.916527距离非酒店,升序排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 39.916527,
          "lon": 116.397128
        },
        "order": "asc",
        "unit": "km"
      }
    }
  ]
}

分页

elasticsearch默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了.

elasticsearch中通过修改from、size参数来控制要返回的分页结果:

# 分页查询
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": "asc"
    }
  ],
  "from": 20,
  "size": 20
}

问题:elasticsearch的数据结构:获取990开始的十个文档,排序,获取前1000数据,截取990-1000条文档

ES是分布式的,所以会面临深度分页问题。例如按price排序后,获取from=990,size=10的数据:

1.首先在每个数据分片上都排序并查询当前1000条文档。

2.然后将所有节点的结果聚合,在内存中重新选出前1000t条文档

3.最后从这1000条文档中,选取从990开始的10条文档

如果搜索页数过深,或者结果集(from+ size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000

from+size:

优点:支持随机翻页

缺点:深度分页问题,默认查询上限(from+size)是10000

场景:百度、京东、谷歌、

高亮

在搜索结果中把搜索关键字突出显示。

原理是:

  • 将搜索结果中的关键字用标签标记出来
  • 在页面中给标签添加css样式

语法:

# 高亮查询,默认情况下,ES字段必须与高亮字段一致
GET /hotel1/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "require_field_match": "false"
      }
    }
  }
}

3RestClient查询文档

快速入门

match

public class HotelSearchTest {

    private RestHighLevelClient client;

    @Test
    void testMatchAll() throws IOException {
        //1.准备Request
        SearchRequest request = new SearchRequest("hotel1");
        //2.准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        //3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //4.解析响应
        SearchHits searchHits = response.getHits();
        //4.1获取总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("共搜索到:" + total + "条数据");
        //4.2文档数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits){
            //获取文档source
            String json = hit.getSourceAsString();
            //反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println(hotelDoc);
        }
        System.out.println(response);
    }
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://106.55.251.245:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

核心API:

source提供了排序、分页、高亮

QueryBuilders提供了各种各样的查询如:match match_all bool term等

精确查询

常见的有term查询和rang查询,同样使用QueryBuilders实现

复合查询

 @Test
    void testBool() throws IOException {
        //1.准备Request
        SearchRequest request = new SearchRequest("hotel1");
        //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);
        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();
        for (SearchHit hit : hits){
            //获取文档source
            String json = hit.getSourceAsString();
            //反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println(hotelDoc);
        }
        System.out.println(response);
    }

排序、分页

搜索结果的排序和分页是是与query同级的参数,对应得API如下:

@Test
    void testPageAndSort() throws IOException {
        //页码、每页大小
        int page = 1, size = 5;
        //1.准备Request
        SearchRequest request = new SearchRequest("hotel1");
        //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);
        handleResponse(response);
    }

高亮

@Test
    void testHighlight() throws IOException {
        //页码、每页大小
        int page = 1, size = 5;
        //1.准备Request
        SearchRequest request = new SearchRequest("hotel1");
        //2.准备DSL
        //2.1.query
        request.source().query(QueryBuilders.matchQuery("all", "如家"));
        //2.2高亮
        request.source().highlighter(new HighlightBuilder().field("city").requireFieldMatch(false));
        //3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResponse(response);
    }

尝试运行后发现需要高亮的字段并没有效果,原来结果解析出现问题,原本的结果解析只获取的是source,高亮和souce同级,所以还需要更改一下结果解析

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();
        for (SearchHit hit : hits){
            //获取文档source
            String json = hit.getSourceAsString();
            //反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            // 获取高亮结果
            Map<String, HighlightField> 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);
        }
        System.out.println(response);
    }

4黑马旅游案例

酒店搜索和分页

案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页

先实现其中的关键字搜索功能,实现步骤如下:

1.定义实体类,接收前端请求

@Data
public class RequestParams {
    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;
}

2.定义controller接口,接收页面请求,调用IHotelService的search方法

  • 请求方式:Post
  • 请求路径:/hotel/list
  • 请求参数:对象,类型为RequestParam
  • 返回值:PageResult,包含两个属性
    • Long total:总条数
    • List<HotelDoc> hotels:酒店数据

将RestHighLevelClient注为Bean

 @Bean
    public RestHighLevelClient client(){
        return new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://106.55.251.245:9200")
        ));
    }

定义返回给前端的类

@Data
public class PageResult {
    private Long total;
    private List<HotelDoc> hotels;

    public PageResult() {
    }
    public PageResult(Long total, List<HotelDoc> hotels) {
        this.total = total;
        this.hotels = hotels;
    }
}

定义controller接口,注入service

@RestController
@RequestMapping("/hotel")
public class HotelController {

    @Autowired
    private IHotelService iHotelService;

    @PostMapping("list")
    public PageResult search(@RequestBody RequestParams params){

    return iHotelService.search(params);
    }
}

3.定义IHotelService中的search方法,利用match查询实现根据关键字搜索酒店信息

注入bean、准备Request、查询、分页、解析结果

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
    @Autowired
    private RestHighLevelClient client;

    @Override
    public PageResult search(RequestParams params){

        try {
            //1.准备Request
            SearchRequest request = new SearchRequest("hotel1");
            //2.准备DSL
            //2.1query
            String key = params.getKey();
            if(key == null || "".equals(key)){
                request.source().query(QueryBuilders.matchAllQuery());
            }else {
                request.source().query(QueryBuilders.matchQuery("all", key));
            }
            int size = params.getSize();
            int page = params.getPage();
            //2.2分页
            request.source().from((page - 1 ) * size).size(size);

            //3.发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            handleResponse(response);

            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private PageResult 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();
        List<HotelDoc> hotels = new ArrayList<>();
        for (SearchHit hit : hits){
            //获取文档source
            String json = hit.getSourceAsString();
            //反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            hotels.add(hotelDoc);

        }
        return new PageResult(total,hotels);
    }
}

酒店结果过滤

案例2:添加品牌、城市、星级、价格等过滤功能

步骤:

1.修改RequestParams类,添加brand、city、startName、minPrice、maxPrice等参数

@Data
public class RequestParams {
    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;
    private String city;
    private String startName;
    private Integer minPrice;
    private Integer maxPrice;
}

2.修改search方法的实现,在关键资搜索时,如果brand参数存在,对其做过滤

过滤条件判断包括:

  • city精确匹配
  • brand精确匹配
  • startName精确匹配
  • price范围过滤

注意事项:

  • 多个条件之间是AND关系,组合多个条件用BooleanQuery
  • 参数存在才需要过滤,做好非空判断
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
    @Autowired
    private RestHighLevelClient client;
    @Override
    public PageResult search(RequestParams params){

        try {
            //1.准备Request
            SearchRequest request = new SearchRequest("hotel1");
            //2.准备DSL
            //2.1query
            //构建BooleanQuery
            buildBasicQuery(params,request);

            int size = params.getSize();
            int page = params.getPage();
            //2.2分页
            request.source().from((page - 1 ) * size).size(size);

            //3.发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //4.解析响应
            handleResponse(response);
            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private void buildBasicQuery(RequestParams params, SearchRequest request) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //关键字搜索
        String key = params.getKey();
        if(key == null || "".equals(key)){
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());
        }else {
            boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));
        }
        //条件过滤
        //城市条件
        if(params.getCity() != null && !params.getCity().equals("")){
            boolQueryBuilder.filter(QueryBuilders.termQuery("city", params.getCity()));
        }
        //品牌条件
        if(params.getBrand() != null && !params.getCity().equals("")){
            boolQueryBuilder.filter(QueryBuilders.termQuery("brand", params.getBrand()));
        }
        //星级条件
        if(params.getStartName() != null && !params.getCity().equals("")){
            boolQueryBuilder.filter(QueryBuilders.termQuery("startName", params.getStartName()));
        }
        //价格
        if(params.getMinPrice() != null && params.getMaxPrice() != null){
            boolQueryBuilder.filter(QueryBuilders
                    .rangeQuery("price")
                    .gte(params.getMinPrice())
                    .lte(params.getMaxPrice()));
        }
        request.source().query(boolQueryBuilder);
    }

我周边的酒店

案例3:我附近的酒店

前端页面点击定位后,会将  所在位置发送到后台:

根据这个坐标,将酒店结果按照到这个点的距离升序排序

 实现思路如下:

  • 修改RequestParams参数,接受location字段
  • 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序功能

//2.3排序
            String location = params.getLocation();
            if(location != null && !"".equals(location)){
                request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(location))
                        .order(SortOrder.ASC)
                        .unit(DistanceUnit.KILOMETERS));
            }

重启后发现不显示距离

如图所示:source和sort同级,结果解析中并没添加该字段,在返回结果中添加location字段,并在结果解析中或者sort

 private PageResult 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();
        List<HotelDoc> hotels = new ArrayList<>();
        for (SearchHit hit : hits){
            //获取文档source
            String json = hit.getSourceAsString();
            //反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            //获取排序值
            Object[] sortValues = hit.getSortValues();
            if(sortValues.length > 0){
                Object sortValue = sortValues[0];
                hotelDoc.setDistance(sortValue);
            }
            hotels.add(hotelDoc);

        }
        return new PageResult(total,hotels);
    }

酒店竞价排名

案例4:让指定的酒店在搜索结果中排名置顶

需要给置顶的酒店文档添加一个标记。然后利用function score给带有标记的文档增加权重》

实现步骤分析:

1.给HotelDoc类型添加isAD字段,Boolean类型

2.挑选几个酒店。给他的文档数据添加isAD字段,值为true

# 添加isAD字段
POST /hotel1/_update/234719711
{
  "doc": {
    "isAD": true
  }
}

3.修改search方法,添加function score功能,给isAD值为true的酒店增加权重

private Boolean isAD;
 //2.算分控制
        FunctionScoreQueryBuilder functionScoreQuery
                = QueryBuilders.functionScoreQuery(
                        //原始查询,相关性算分的查询
                        boolQueryBuilder,
                //function score的数组
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        //其中的一个function score元素
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                //过滤条件
                                QueryBuilders.termQuery("isAD",true),
                                //算分函数
                                ScoreFunctionBuilders.weightFactorFunction(10)
                )
        });

        request.source().query(functionScoreQuery);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值