分布式全文搜索服务Elasticsearch

1、ElasticSearch简介

1.1 什么是ElasticSearch

Elasticsearch,简称为es,是一款开源的高扩展的分布式全文搜索服务,它可以近乎实时的检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。es也是使用Java开发并使用Lucene作为其核心来实现搜索的功能,但是它是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

1.2 ElasticSearch使用案例

  • GitHub:2013年初,GitHub抛弃了Solr,采取ElasticSearch来做PB级的搜索。“GitHub使用ElasticSearch搜索20TB的数据,包括13亿文件和1300亿行代码”。

  • 维基百科:启动以elasticsearch为基础的核心搜索架构。

  • SoundCloud:“SoundCloud使用ElasticSearch为1.8亿用户提供即时而精准的音乐搜索服务”。

  • 百度:百度目前广泛使用ElasticSearch作为文本数据分析,采集百度所有服务器上的各类指标数据及用户自定义数据,通过对各种数据进行多维分析展示,辅助定位分析实例异常或业务层面异常。目前覆盖百度内部20多个业务线(包括casio、云分析、网盟、预测、文库、直达号、钱包、风控等),单集群最大100台机器,200个ES节点,每天导入30TB+数据。

  • 新浪使用ES 分析处理32亿条实时日志。

  • 阿里使用ES 构建自己的日志采集和分析体系。

1.3 为什么不直接用Lucene?

  • 当多个系统都需要用到搜索的时候,需要单独部署一套,共用性差

  • lucene不能做集群,当数据量越来越大,无法动态扩展,扩展性差

1.4 ElasticSearch对比Solr

  • Solr利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能。

  • Solr支持更多格式的数据,而Elasticsearch仅支持json文件格式。

  • Solr官方提供的高级功能更多,例如自带控制台可查询数据,而Elasticsearch更注重于核心功能:索引流程和检索流程。

  • Solr在建立索引库时会产生io阻塞,导致查询性能变差,并且随着数据量的增加,Solr的搜索效率会变得更低,而Elasticsearch却没有明显的变化。综上所述,Solr是传统搜索应用的有力解决方案,而Elasticsearch 更适用于实时搜索应用。

2、ElasticSearch:核心概念

2 .1概述

Elasticsearch面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档进行索引、搜索、排序、过滤。

关系型数据库与ES索引库类比:

2.2 核心概念

近实时 NRT(Near Realtime)

Elasticsearch是一个接近实时的搜索平台。这意味着,从索引一个文档直到这个文档能够被搜索到有一个轻微的延迟(通常是1秒以内)

每隔1s刷新buffer缓存区数据到segment段,对搜索(需要使用倒排索引)可见,因此是近实时

节点 node

节点是一个Elasticsearch实例,本质上就是一个java进程,节点也有一个名称(默认是随机分配的),当然也可以通过配置文件配置,或者在启动的时候,-E node.name=node1指定。此名称对于管理目的很重要,因为您希望确定网络中的哪些服务器对应于ElasticSearch集群中的哪些节点。

在Elasticsearch中,节点的类型主要分为如下几种:

  • master eligible节点:有资格参加选举成为Master的节点,默认为true,可以通过node.master: false设置。

  • data节点:保存数据的节点,负责保存分片数据,默认为true,在数据扩展上起到了至关重要的作用。

  • Coordinating 节点:负责接收客户端请求,将请求发送到合适的节点,最终把结果汇集到一起返回,默认为true。

开发环境中一个节点可以承担多个角色,生产环境中,建议设置单一的角色,可以提高性能等

集群 cluster

一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是“elasticsearch”,这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。

索引库 index(相当于mysql的database实例)

索引是具有某种相似特性的文档集合。例如,您可以拥有客户数据的索引、产品目录的另一个索引以及订单数据的另一个索引。索引由一个名称(必须全部是小写)标识。在单个集群中,您可以定义任意多个索引。每个索引都有自己的mapping定义,用于定义包含文档的字段名和字段类型。可以将其暂时理解为 MySql中的 database。

索引的包含两个部分:

  • mapping:定义文档字段(field)的属性

  • setting:定义分片数和副本数(默认5个分片,每个分片一个副本)

类型 type

相当于mysql的表,但是在一个索引库中不能再创建多个类型,在以后的版本中也将删除类型的整个概念。

文档 document(相当于数据库表中的一行数据)

一个文档是一个可被索引的基础信息单元。比如,你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示。 在一个index/type里面,你可以存储任意多的文档。注意,尽管一个文档,物理上存在于一个索引之中,但文档必须被索引/赋予一个索引的type。

分片

索引库可能存储大量数据,这些数据可能会超出单个节点的硬件限制。例如:占用1TB磁盘空间的10亿个文档的单个索引可能不适合单个节点的磁盘,或者速度太慢,无法单独满足单个节点的搜索请求(把索引库的数据分成多个部分存储在不同的节点上)

为了解决这个问题,ElasticSearch提供了将索引细分为多个片段(称为分片shard)的能力。创建索引时,只需定义所需的分片数量。每个分片本身就是一个完全功能性和独立的"索引",可以托管在集群中的任何节点上。

为什么要分片?

  • 它允许您水平拆分/缩放索引数据。

  • 它允许您跨分片(可能在多个节点上)分布和并行操作,从而提高查询性能/吞吐量。

如何分配分片以及如何将其文档聚合回搜索请求的机制完全由ElasticSearch管理,并且对作为用户的您是透明的。分片数在索引库创建时指定,后续不允许修改,除非重新创建索引。

副本

副本即对分片数据的备份,为什么要有副本?

  • 当分片/节点发生故障时提供高可用性。因此,需要注意的是,副本分片永远不会分配到复制它的原始/主分片所在的节点上

  • 提高搜索的并发量,可以在所有副本上并行执行搜索。

在创建索引时为每个索引定义分片和副本的数量。创建索引后,您还可以随时动态更改副本的数量,但是不能修改分片的数量。建议在创建索引时就考虑好分片和副本的数量。默认情况下,ElasticSearch为每个索引分配5个分片,每个分片1个副本。

其中A、B是节点,a1、a2是分片,b1、b2为副本,把分片和副本交叉放到不同的节点,保证最大程度的利用服务器资源。

倒排索引

  • DocID:出现某单词的文档ID

  • TF(词频):单词在该文档中出现的次数

  • POS:单词在文档中的位置

3.ES的REST风格API:基本CRUD

Elasticsearch提供了Rest风格的API,即http请求接口,也提供了各种语言的客户端API

Rest风格API

文档地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html

1.创建索引

Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求

创建索引的请求格式:

  • 请求方式:PUT

  • 请求路径:/索引库名

  • 请求参数:json格式:

{
    "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 2
      }
}

settings:索引库的设置

  • number_of_shards:分片数量

  • number_of_replicas:副本数量

kibana的控制台,可以对http请求进行简化,示例:

创建goods索引:

相当于是省去了elasticsearch的服务器地址

而且还有语法提示,非常舒服。

2.删除索引

删除索引使用DELETE请求

DELETE 索引库名

3.新增数据

随机生成id

通过POST请求,可以向一个已经存在的索引库中添加数据。

POST /索引库名/类型名
{
    "key":"value"
}

示例:

POST /goods/docs
{
     "title":"小米手机",
    "images":"http://image.leyou.com/12479122.jpg",
    "price":2699.00
}

自定义id

POST /索引库名/类型/id值
{
    ...
}

示例:

POST /goods/docs/2
{
    "title":"大米手机",
    "images":"http://image.leyou.com/12479122.jpg",
    "price":2899.00
}

4.修改数据

把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,

  • id对应文档存在,则修改

  • id对应文档不存在,则新增

比如,我们把id为3的数据进行修改:

PUT /goods/docs/2
{
    "title":"超大米手机",
    "images":"http://image.leyou.com/12479122.jpg",
    "price":3899.00,
    "stock": 100,
    "saleable":true
}

5.删除数据

删除使用DELETE请求,同样,需要根据id进行删除:

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

4、ES的REST风格API

1.查询基本语法

基本语法

GET /索引库名/_search
{
    "query":{
        "查询类型":{
            "查询条件":"查询条件值"
        }
    }
}

这里的query代表一个查询对象,里面可以有不同的查询属性

  • 查询类型:

    • 例如:match_all, matchterm , range 等等

  • 查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解

2.查询所有(match_all)

  • query:代表查询对象

  • match_all:代表查询所有

示例:

GET /goods/_search
{
    "query":{
        "match_all": {}
    }
}

3.匹配查询(match)

匹配查询(会先分词再匹配)

GET /goods/_search
{
  "query": {
    "match": {
      "title": "小米手机"
    }
  }
}

在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是or的关系。

  • and关系

某些情况下,我们需要更精确查找,我们希望这个关系变成and,可以这样做:

GET /heima/_search
{
    "query":{
        "match": {
          "title": {
            "query": "小米电视",
            "operator": "and"
          }
        }
    }
}

本例中,只有同时包含小米电视的词条才会被搜索到。

4.多字段查询(multi_match)

multi_matchmatch类似,不同的是它可以在多个字段中查询

GET /goods/_search
{
  "query": {
    "multi_match": {
      "query": "小米高端大气",
      "fields": ["title","subTitle"]
    }
  }
}

5.词条匹配(term)

term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串

GET /goods/_search
{
  "query": {
    "term": {
      "price": {
        "value": 2500
      }
    }
  }
}

6.多词条精确匹配(terms)

terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:

GET /goods/_search
{
  "query": {
    "terms": {
      "price": [
        4899.0,
        2699.0
      ]
    }
  }
}

7.结果过滤(_source)

默认情况下,elasticsearch在搜索的结果中,会把文档中保存在_source的所有字段都返回。

如果我们只想获取其中的部分字段,我们可以添加_source的过滤

直接指定字段

GET /goods/_search
{
  "query": {
    "term": {
      "price": 4899.0
    }
  },
  "_source": ["title","price"]
}

指定includes和excludes

我们也可以通过:

  • includes:来指定想要显示的字段

  • excludes:来指定不想要显示的字段

二者都是可选的。

includes示例:

GET /goods/_search
{
  "query": {
    "term": {
      "price": 4899.0
    }
  },
  "_source":{
    "includes": ["title","subTitle"]
  }
}

8.布尔组合(bool)

bool把各种其它查询通过must(与)、must_not(非)、should(或)的方式进行组合

GET /goods/_search
{
  "query": {
     "bool": {
       "must": [
         {
           "match": {
             "title": "小米"
           }
         },
         {
           "match": {
             "subTitle": "小米"
           }
         }
       ]
     }
  }
}

9.过滤条件(filter)

所有的查询都会影响到文档的评分及排名。如果我们需要在查询结果中进行过滤,并且不希望过滤条件影响评分,那么就不要把过滤条件作为查询条件来用。而是使用filter方式:

注意:filter通常和bool一起使用(在bool基础上进行过滤)。

GET /goods/_search
{
  "query": {
     "bool": {
       "must": [
         {
           "match": {
             "title": "小米"
           }
         },
         {
           "match": {
             "subTitle": "小米"
           }
         }
       ],
       "filter": {
         "term": {
           "category.keyword": "手机"
         }
       }
     }
  }
}

注意:如果我们使用过滤条件,那么过滤后的结果不会影响原来结果的排序。反之,如果不用过滤,则会影响原来结果排序

5、ES的REST风格API

聚合查询就是分组统计

聚合可以让我们极其方便的实现对数据的统计、分析。例如:

  • 什么品牌的手机最受欢迎?

  • 这些手机的平均价格、最高价格、最低价格?

  • 这些手机每月的销售情况如何?

实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现实时搜索效果。

1)聚合基本概念

Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫,一个叫度量

桶(bucket)

桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个,例如我们根据国籍对人划分,可以得到中国桶英国桶日本桶……或者我们按照年龄段对人进行划分:0~10,10~20,20~30,30~40等。

Elasticsearch中提供的划分桶的方式有很多:

  • Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组

  • Histogram Aggregation:根据数值阶梯分组,与日期类似

  • Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组

  • Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组

  • ……

bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量

 

度量(metrics)

分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量

比较常用的一些度量聚合方式:

  • Avg Aggregation:求平均值

  • Max Aggregation:求最大值

  • Min Aggregation:求最小值

  • Percentiles Aggregation:求百分比

  • Stats Aggregation:同时返回avg、max、min、sum、count等

  • Sum Aggregation:求和

  • Top hits Aggregation:求前几

  • Value Count Aggregation:求总数

  • ……

2)聚合为桶 (Aggragation for Buket)

聚合为桶其实就是分组统计!

首先,我们按照 汽车的颜色color来划分

GET /cars/_search
{
    "size" : 0,
    "aggs" : { 
        "popular_colors" : { 
            "terms" : { 
              "field" : "color"
            }
        }
    }
}

  • size: 查询条数,这里设置为0,因为我们不关心搜索到的数据,只关心聚合结果,提高效率

  • aggs:声明这是一个聚合查询,是aggregations的缩写

    • popular_colors:给这次聚合起一个名字,任意。

      • terms:划分桶的方式,这里是根据词条划分

        • field:划分桶的字段

结果:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 8,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "popular_colors": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "red",
          "doc_count": 4
        },
        {
          "key": "blue",
          "doc_count": 2
        },
        {
          "key": "green",
          "doc_count": 2
        }
      ]
    }
  }
}

 

  • hits:查询结果为空,因为我们设置了size为0

  • aggregations:聚合的结果

  • popular_colors:我们定义的聚合名称

  • buckets:查找到的桶,每个不同的color字段值都会形成一个桶

    • key:这个桶对应的color字段的值

    • doc_count:这个桶中的文档数量

# 聚合查询(分组统计)

# AGG_TYPE: 聚合类型

# 常见的聚合类型
# terms: 按数量聚合(统计),类似SQL的count(*)
# avg: 按均值聚合(统计),类似SQL的avg(age)
# max: 按最大值聚合(统计),类似SQL的max(age)
# min: 按最小聚合(统计),类似SQL的min(age)
# sum: 按求和聚合(统计),类似SQL的sum(score)

#统计不同颜色的汽车分别有多少部?

GET /cars/_search
{
 "aggs": {
   "colorAgg": {
     "terms": {
       "field": "color"
     }
   }
 } 
  
}

# 统计所有汽车的均价

GET /cars/_search
{
 "aggs": {
   "priceAgg": {
     "avg": {
       "field": "price"
     }
   }
 }
  
}

#查询honda的汽车的均价?

GET /cars/_search
{
  "query": {
    "term": {
      "make": {
        "value": "honda"
      }
    }
  },
  "aggs": {
   "priceAgg": {
     "avg": {
       "field": "price"
     }
   }
 }
  
}


# 统计不同颜色的汽车的均价分别是多少?

GET /cars/_search
{
 "aggs": {
   "colorAgg": {
     "terms": {
       "field": "color"
     },
     "aggs": {
       "priceAgg": {
         "avg": {
           "field": "price"
         }
       }
     }
   }
 } 
  
}
 

6、SpringDataElasticsearch

1.SpringDataElasticsearch简介

Spring Data Elasticsearch是Spring Data项目下的一个子模块。

查看 Spring Data的官网:http://projects.spring.io/spring-data/

特征:

  • 支持Spring的基于@Configuration的java配置方式,或者XML配置方式

  • 提供了用于操作ES的便捷工具类ElasticsearchTemplate。包括实现文档到POJO之间的自动智能映射。

  • 利用Spring的数据转换服务实现的功能丰富的对象映射

  • 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式

  • 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询

2.搭建SpringDataElasticsearch环境

 

创建项目及导入依赖

pom依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itheima</groupId>
    <artifactId>spring-data-es</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <!--spring data es-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    
</project>

application.yml配置

spring:
  data:
    elasticsearch:
      cluster-name: elasticsearch # 集群名称
      cluster-nodes: 127.0.0.1:9300 # 节点地址

编写启动类

package com.it;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 *
 */
@SpringBootApplication
public class ESApplication {
    public static void main(String[] args) {
        SpringApplication.run(ESApplication.class,args);
    }
}

实体类及注解

映射

Spring Data通过注解来声明字段的映射属性,有下面的三个注解:

  • @Document 作用在类,标记实体类为文档对象,一般有四个属性

    • indexName:对应索引库名称

    • type:对应在索引库中的类型

    • shards:分片数量,默认5

    • replicas:副本数量,默认1

  • @Id 作用在成员变量,标记一个字段作为id主键

  • @Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:

    • type:字段类型,取值是枚举:FieldType

    • index:是否索引,布尔类型,默认是true

    • store:是否存储,布尔类型,默认是false

    • analyzer:分词器名称:ik_max_word

package com.it.pojo;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
 *  @Document: 映射索引库中的文档
 *    indexName: 索引库名称
 *    type: 类型
 *    shards:分片
 *    replicas:副本
 *  @Id: 映射文档ID
 *  @Field
 *    type: 字段的类型
 *         Text: 文本类型,分词的
 *         Keyword: 文本类型,不分词的
 *         Integer、Long、Float、Double: 数值类型,必须不分词的
 *         Boolean:布尔类型,必须不分词的
 *         Date: 日期类型,必须不分词的
 *         Object: 自定义对象或集合(List,Set,Map等),所有对象里面的属性都是索引和分词的
 *    index:
 *         该字段是否索引  默认为true
 *    analyzer
 *         指定分词器的算法
 *            如:ik分词器的算法
 *                 ik_smart: 最小分词    我是程序员 ->  我  是  程序员
 *                 ik_max_word: 最细分词   我是程序员 ->  我  是  程序员  程序 员
 */
@Data
@Document(indexName = "goods",type = "docs" ,shards = 1,replicas = 1)
public class Item {
    @Id
    Long id;

    @Field(type = FieldType.Text,analyzer ="ik_max_word" )
    String title; //标题
    
    @Field(type = FieldType.Keyword)
    String category;// 分类
    @Field(type = FieldType.Keyword)
    String brand; // 品牌
    @Field(type = FieldType.Double)
    Double price; // 价格
    @Field(type = FieldType.Keyword,index = false)
    String images; // 图片地址
}

7、SpringDataElasticsearch:基本CRUD

1.编写Repository接口

Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。

我们只需要定义接口,然后继承它就OK了。

package com.it.repository;

import com.it.pojo.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

/**
 * Dao接口
 * 泛型一:操作的实体类类型
 * 泛型二:实体类的ID类型
 */
public interface ItemRepository extends ElasticsearchRepository<Item,Long>{
}

2.增删改查

package com.it;

import com.it.pojo.Item;
import com.it.repository.ItemRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 *新增文档
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class EsDemo1 {
    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void save(){
        Item item = new Item(1L, "小米手机7", "手机",
                "小米", 3499.00, "http://image.leyou.com/13123.jpg");
        itemRepository.save(item);
    }


   /**
  * 批量新增索引
  */
@Test
    public void testBatchSave(){
        List<Item> list = new ArrayList<>();
        list.add(new Item(2L, "坚果手机R1", "手机","锤子", 3699.00, "http://image.leyou.com/13124.jpg"));
        list.add(new Item(3L, "华为META10", "手机","华为", 4499.00, "http://image.leyou.com/13125.jpg"));
        list.add(new Item(4L, "小米电视1", "电视","小米", 5499.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(5L, "小米手机8", "手机","小米", 2199.00, "http://image.leyou.com/13124.jpg"));
        itemRepository.saveAll(list);
    }



/**
 * 修改索引
 */
@Test
public void testUpdate(){
    Item item = new Item(1L, "小米手机8", "手机",
            "小米", 4499.00, "http://image.leyou.com/13124.jpg");
    itemRepository.save(item);
}



  /**
     * 查询所有
     */
    @Test
    public void testFindAll(){
        Iterable<Item> list = itemRepository.findAll();
        list.forEach(System.out::println);
    }
    /**
     * 根据id查询
     */
    @Test
    public void testFindById(){
        Item item = itemRepository.findById(1L).get();
        System.out.println(item);
    }




/**
 * 删除索引
 */
@Test
public void testDelete(){
    Item item = new Item();
    item.setId(1L);
    itemRepository.delete(item);
}




}




8、SpringDataElasticsearch:高级查询(重点)

如果要完成更加复杂的查询,同时包含条件,分页,高亮,聚合等操作,需要用到ElasticsearchTemplate对象

8.1基本条件查询

# 基本查询
GET /goods/_search
{
  "query": {
    "match": {
      "title": "小米"
    }
  }
}

@Test
    public void test1(){
        //1.创建本地查询构造器对象: 用于封装所有查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //添加query条件
        //注意:所有的Query条件都是使用QueryBuilders类构造的
        queryBuilder.withQuery(QueryBuilders.matchQuery("title","小米"));


        //2.执行查询(执行本地查询),获取结果
        /**
         * 参数一:本地查询对象
         * 参数二:需要封装数据的实体类
         */
        List<Item> items = esTemplate.queryForList(queryBuilder.build(), Item.class);

        //3.处理结果
        items.forEach(System.out::println);
    }

8.2分页查询

# 分页
# from: 起始索引行号,从0开始
# size: 页面大小
GET /goods/_search
{
  "query": {
    "match": {
      "title": "小米"
    }
  },
  "from": 0 ,
  "size": 2
}

 /**
     * 2)分页条件查询
     */
    @Test
    public void testPageQuery(){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //1. 添加Query条件
        queryBuilder.withQuery( QueryBuilders.matchQuery("title","小米") );


        //实际页面传递的参数
        int page = 1;//页面传递的当前页码
        int size = 2;//页面大小


        //2.添加分页参数
        /**
         * page: ES的当前页码(page从0开始计算)
         * size: 页面大小
         */
        queryBuilder.withPageable(PageRequest.of(page-1,size));

        //Page: 用于封装分页查询结果
        Page<Item> pageBean = esTemplate.queryForPage(queryBuilder.build(),Item.class);

        System.out.println("总记录数:"+pageBean.getTotalElements());
        System.out.println("总页数:"+pageBean.getTotalPages());
        pageBean.getContent().forEach(System.out::println);
    }

8.3布尔组合+过滤查询(*)

# 布尔+过滤

GET /goods/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "小米"
          }
        }
      ],
      "filter": {
        "term": {
          "category": "手机"
        }
      }
    }
  }
}

 /**
     * 3)布尔+过滤
     */
    @Test
    public void testBoolQueryAndFilter(){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //1.1 添加Query条件
        //1)创建布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //2)往布尔查询中添加must条件
        boolQueryBuilder.must( QueryBuilders.matchQuery("title","小米")  );
        //3)往布尔查询中添加filter过滤
        boolQueryBuilder.filter( QueryBuilders.termQuery("category","手机") );

        queryBuilder.withQuery( boolQueryBuilder );


        List<Item> items = esTemplate.queryForList(queryBuilder.build(), Item.class);

        items.forEach(System.out::println);

    }

8.4聚合查询

# 统计不同品牌的手机分别有多少台?
# 聚合查询

GET /goods/_search
{
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand"
      }
    }
  }
}

 /**
     * 4)聚合查询
     */
    @Test
    public void testAggQuery(){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //1.添加聚合条件
        //注意:聚合的条件都是由AggrationBuilders构建而来的
        queryBuilder.addAggregation( AggregationBuilders.terms("brandAgg").field("brand")  );

        //注意:执行聚合查询必须使用queryForPage方法,否则无法获取聚合结果

        //AggregatedPage是Page的子类,Page只是分页查询结果,AggregatedPage既封装了分页结果,也封装了聚合查询
        AggregatedPage<Item> aggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Item.class);

        //只取出聚合结果
        Aggregations aggregations = aggregatedPage.getAggregations();

        //根据聚合别名取出聚合结果
        Terms terms = aggregations.get("brandAgg");

        //取出所有Bucket
        List<? extends Terms.Bucket> buckets = terms.getBuckets();

        //遍历Bucket
        for(Terms.Bucket bucket:buckets){
            String brandName = bucket.getKeyAsString();
            long count = bucket.getDocCount();
            System.out.println(brandName+"\t"+count);
        }
    }

8.5高亮查询

DSL语句:

# 高亮显示

# 注意:如果要某字段高亮显示,该字段必须是参与搜索,且可以分词

GET /goods/_search
{
   "query": {
     "match": {
       "title": "小米"
     }
   },
   "highlight": {
     "pre_tags": "<font color='red'>",
     "post_tags": "</font>",
     "fields": {
       "title": {}
     }
   }
}
 

/**
     * 5)高亮查询
     */
    @Test
    public void testHighlightQuery(){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //1.添加Query条件(title要高亮,所以title参与搜索)
        queryBuilder.withQuery( QueryBuilders.matchQuery("title","小米") );

        //2.添加highlight高亮字段
        HighlightBuilder.Field field = new HighlightBuilder.Field("title");
        //设置前缀和后缀
        field.preTags("<font color='red'>");
        field.postTags("</font>");
        queryBuilder.withHighlightFields(field);

        //3.执行查询
        //SearchResultMapper: 用于自行封装搜索结果
        AggregatedPage<Item> pageBean = esTemplate.queryForPage(queryBuilder.build(), Item.class, new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                List<T> list = new ArrayList<>();

                SearchHits hits = response.getHits();
                for(SearchHit hit:hits){
                    //取出原来的文档内容
                    String json = hit.getSourceAsString();
                    //把json字符串转换对象
                    Item item = JsonUtils.toBean(json, Item.class);

                    //自行取出高亮title的内容
                    HighlightField titleField = hit.getHighlightFields().get("title");
                    if(titleField!=null){
                        //赋值给Item对象的title
                        item.setTitle(titleField.getFragments()[0].toString());
                    }

                    list.add((T)item);
                }

                AggregatedPage<T> aggregatedPage = new AggregatedPageImpl<>(list);
                return aggregatedPage;
            }
        });

        pageBean.getContent().forEach(System.out::println);
    }

9.ElasticSearch多节点集群搭建与使用

搭建

  • 复制es节点三份,并且删除data目录

  • 修改三个节点上的信息,内容如下:

    • 三个节点端口号: http端口号(9201、9202、9203) tcp端口号(9301、9302、9303)

    • 第一个节点配置(elasticsearch.yml)

# 集群的名字,保证唯一,所有都必须一致 (17行)
cluster.name: cluster-es
# 节点名称,必须不一样 (23行)
node.name: node-1
# 必须为本机的ip地址 (55行)
network.host: 127.0.0.1
# 服务端口,在同一机器下必须不一样 (59行)
http.port: 9201
# 集群间通讯端口号,在同一机器下必须不一样 (60行)
transport.tcp.port: 9301
# 设置集群自动发现机器ip:port集合,采用广播模式 (70行)
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
# 防止脑裂。声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1 (75行)
discovery.zen.minimum_master_nodes: 2

# 允许跨域 (92、93行)
http.cors.enabled: true
http.cors.allow-origin: "*"

第二个节点配置(elasticsearch.yml)

# 集群的名字,保证唯一,所有都必须一致 (17行)
cluster.name: cluster-es
# 节点名称,必须不一样 (23行  改)
node.name: node-2
# 必须为本机的ip地址 (55行)
network.host: 127.0.0.1
# 服务端口,在同一机器下必须不一样 (59行 改)
http.port: 9202
# 集群间通讯端口号,在同一机器下必须不一样 (60行 改)
transport.tcp.port: 9302
# 设置集群自动发现机器ip:port集合,采用广播模式 (70行)
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
# 防止脑裂。声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1 (75行)
discovery.zen.minimum_master_nodes: 2

# 允许跨域 (92、93行)
http.cors.enabled: true
http.cors.allow-origin: "*"

第三个节点配置(elasticsearch.yml)

# 集群的名字,保证唯一,所有都必须一致 (17行)
cluster.name: cluster-es
# 节点名称,必须不一样 (23行 改)
node.name: node-3
# 必须为本机的ip地址 (55行)
network.host: 127.0.0.1
# 服务端口,在同一机器下必须不一样 (59行 改)
http.port: 9203
# 集群间通讯端口号,在同一机器下必须不一样 (60行 改)
transport.tcp.port: 9303
# 设置集群自动发现机器ip:port集合,采用广播模式 (70行)
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
# 防止脑裂。声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1 (75行)
discovery.zen.minimum_master_nodes: 2

# 允许跨域 (92、93行)
http.cors.enabled: true
http.cors.allow-origin: "*"

  • 分别启动三个es节点

  • 通过header-master查看,连接任意一个节点都可以出现下面的效果,说明集群成功。

使用

  • 连接集群

// 1. 创建Settings配置信息对象(主要配置集群名称)
// 参数一: 集群key (固定不变)
// 参数二:集群环境名称,默认的ES的环境集群名称为 "elasticsearch"
Settings settings = Settings.builder()
    .put("cluster.name", "cluster-es").build();
// 2. 创建ES传输客户端对象
TransportClient transportClient = new PreBuiltTransportClient(settings);
// 2.1 添加传输地址对象(集群环境多个)
// 参数一:主机
// 参数二:端口
transportClient.addTransportAddress(new
        TransportAddress(InetAddress.getByName("127.0.0.1"), 9301));
transportClient.addTransportAddress(new
        TransportAddress(InetAddress.getByName("127.0.0.1"), 9302));
transportClient.addTransportAddress(new
        TransportAddress(InetAddress.getByName("127.0.0.1"), 9303));

设置分片与副本

// 3. 创建索引库(index)
// 获取创建索引库请求构建对象
CreateIndexRequestBuilder cirb = transportClient.admin().indices()
    .prepareCreate("blog1");
// 3.1 创建Map集合封装分片与副本设置信息
Map<String, Integer> source = new HashMap<String, Integer>();
// 3.2 设置分片数量
source.put("number_of_shards", 3);
// 3.3 设置副本数量
source.put("number_of_replicas", 1);
// 3.4 设置map集合,执行请求
cirb.setSettings(source).get();

 

Tips:测试关闭一个es节点,整个集群还是可以正常访问。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值