全文搜索引擎Elasticsearch+Kibanas使用教程

1,827 阅读19分钟

highlight: agate

一、什么是Elasticsearch

官网:www.elastic.co/cn/
Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库无论是开源还是私有。

但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。 更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常 复杂。

Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单, 通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。

然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:

  • 一个分布式的实时文档存储,每个字段 可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

Elasticsearch 将所有的功能打包成一个单独的服务,这样你可以通过程序与它提供的简单的 RESTful API 进行通信, 可以使用自己喜欢的编程语言充当 web 客户端,甚至可以使用命令行(去充当这个客户端)。

二、Elasticsearch术语

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

(2) 集群(cluster) 
一个集群就是由一个或多个节点组织在一起,它们共同持有你整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是“elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。在产品环境中显式地设定这个名字是一个好习惯,但是使用默认值来进行测试/开发也是不错的。

(3) 节点(node) 
一个节点是你集群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。和集群类似,一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于Elasticsearch集群中的哪些节点。

一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。

在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何Elasticsearch节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。

(4) 索引(index) 
一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。索引类似于关系型数据库中Database的概念。在一个集群中,如果你想,可以定义任意多的索引。

(5) 类型(type)
在一个索引中,你可以定义一种或多种类型。一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。比如说,我们假设你运营一个博客平台并且将你所有的数据存储到一个索引中。在这个索引中,你可以为用户数据定义一个类型,为博客数据定义另一个类型,当然,也可以为评论数据定义另一个类型。类型类似于关系型数据库中Table的概念。

(6)文档(document) 
一个文档是一个可被索引的基础信息单元。比如,你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。 
在一个index/type里面,只要你想,你可以存储任意多的文档。注意,尽管一个文档,物理上存在于一个索引之中,文档必须被索引/赋予一个索引的type。文档类似于关系型数据库中Record的概念。实际上一个文档除了用户定义的数据外,还包括_index_type_id字段。

(7) 分片和复制(shards & replicas) 
一个索引可以存储超出单个结点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。 为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。 
分片之所以重要,主要有两方面的原因:

  • 允许你水平分割/扩展你的内容容量
  • 允许你在分片(潜在地,位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量

至于一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由Elasticsearch管理的,对于作为用户的你来说,这些都是透明的。

在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了。这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制。复制之所以重要,主要有两方面的原因:

  • 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。
  • 扩展你的搜索量/吞吐量,因为搜索可以在所有的复制上并行运行

总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制数量,但是不能改变分片的数量。

默认情况下,Elasticsearch中的每个索引被分片5个主分片和1个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有5个主分片和另外5个复制分片(1个完全拷贝),这样的话每个索引总共就有10个分片。一个索引的多个分片可以存放在集群中的一台主机上,也可以存放在多台主机上,这取决于你的集群机器数量。主分片和复制分片的具体位置是由ES内在的策略所决定的。

三、安装与配置Elasticsearch

  1. 拉取镜像
docker pull elasticsearch # 拉取elasticsearch
  1. 运行
docker run -id --name=z_es -p 9200:9200 -p 9300:9300 \
-e ES_JAVA_OPTS="-Xms256m -Xmx256m" \
elasticsearch:5.6.8
  1. 访问ip:9200

image.png
4. 换源

docker cp ./sources.list z_es:/etc/apt/sources.list

sources.list

deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
apt-get update
apt-get install vim
  1. 配置跨域访问

/usr/share/elasticsearch/config/elasticsearch.yml

http.host: 0.0.0.0
  
# Uncomment the following lines for a production cluster deployment
transport.host: 0.0.0.0
#discovery.zen.minimum_master_nodes: 1

cluster.name: my-application
http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 192.168.119.1

  1. 系统参数配置 nofile是单个进程允许打开的最大文件 soft nofile是软限制 hard nofile是硬限制

vim /etc/security/limits.conf

* soft nofile 65536
* hard nofile 65536

限制一个进程可以拥有的VMA(虚拟内存区域)的数量

vim /etc/sysctl.conf

vm.max_map_count=655360

执行下面命令,修改内核参数马上生效

sysctl -p

重启虚拟机再次启动容器

reboot

四、安装head插件

  1. 下载elasticsearch-head插件 github.com/mobz/elasti…
  2. 安装code.js
  3. 将grunt安装为全局命令, Grunt是基于Node.js的项目构建工具
cnpm install -g grunt-cli
  1. 进入elasticsearch-head-master目录启动head
>cnpm install
>grunt server

image.png 5. 连接访问

image.png

五、使用Postman进行Restful接口访问

5.1 创建索引index和映射mapping

使用postman创建索引index

{
    "mappings": {
        "article": {
            "properties": {
                "id": {
                    "type": "long",
                    "store": true,
                    "index": "not_analyzed"
                },
                "title": {
                    "type": "text",
                    "store": true,
                    "index": "analyzed",
                    "analyzer": "standard"
                },
                "content": {
                    "type": "text",
                    "store": true,
                    "index": "analyzed",
                    "analyzer": "standard"
                }
            }
        }
    }
}

image.png

image.png

5.2 删除索引index

image.png

5.3 创建文档document

image.png

image.png

5.4 修改文档document

还是ip3:9200/blog1/article/1,但是修改请求体内容

image.png

5.5 删除文档document

image.png

5.6 查询文档

请求url

-GET ip:9200/blog1/article/1

image.png

5.7 查询文档-querystring查询

请求url

-POST http://ip:9200/blog1/article/_search

请求体

{
    "query": {
        "query_string": {
            "default_field": "title",
            "query": "搜索"
        }
    }
}

image.png

这里搜索换成钢索也是可以搜到的,因为前面tiltle使用的标准分词"analyzer": "standard",分词成搜不到内容但是可以搜索到内容,因此钢索可以搜索到。

5.8 查询文档-term查询

请求url

-POST http://ip:9200/blog1/article/_search

请求体

{
    "query": {
        "term": {
            "title": "搜索"
        }
    }
}

image.png

term查询查搜索是搜索不出来的,搜索或者就可以。term是不可划分的不分词的。

六、 IK分词器使用

6.1 IK分词器简介

IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始,IKAnalyzer已经推出 了3个大版本。最初,它是以开源项目Lucene为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为 面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。

IK分词器3.0的特性如下:

1)采用了特有的“正向迭代最细粒度切分算法“,具有60万字/秒的高速处理能力。
2)采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。
3)对中英联合支持不是很好,在这方面的处理比较麻烦.需再做一次查询,同时是支持个人词条的优化的词典存储,更小的内存占用。
4)支持用户词典扩展定义。
5)针对Lucene全文检索优化的查询分析器IKQueryParser;采用歧义分析算法优化查询关键字的搜索排列组合,能极大的提高Lucene检索的命中率。

6.2 安装IK分词

elasticsearch-analysis-ik-5.6.8.zip下载

1.上传到docker 2.解压 3.改名 4.cp到elasticsearch容器中

# 解压
unzip elasticsearch-analysis-ik-5.6.8.zip
# 改名
mv elasticsearch ik
# 复制到容器中
docker cp ./ik z_es:/usr/share/elasticsearch/plugins

6.2.1 分词测试

ik_smart:最粗粒度的拆分 image.png ik_max_word:最细粒度的拆分

image.png

七、Kibanas使用

7.1 Kibana简介

Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和 Elasticsearch 协作。您可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。您可以很方便的利用图表、表格及地图对数据进行多元化的分析和呈现。

Kibana 可以使大数据通俗易懂。它很简单,基于浏览器的界面便于您快速创建和分享动态数据仪表板来追踪 Elasticsearch 的实时数据变化。

搭建 Kibana 非常简单。您可以分分钟完成 Kibana 的安装并开始探索 Elasticsearch 的索引数据 — 没有代码、不需要额外的基础设施。

7.2 Kibana安装

  1. 拉取镜像
docker pull docker.io/kibana:5.6.8
  1. 启动容器
docker run -it -d -e ELASTICSEARCH_URL=http://ip:9200 --name kibana -p 5601:5601 kibana:5.6.8
  1. 快捷键
ctrl+i # 自动缩进

ctrl+enter # 提交请求

down # 打开自动补全菜单

enter/tab # 选中项自动补全

esc # 关闭补全菜单
  1. 访问测试

image.png

7.3 DSL语句使用

7.3.1 Query DSL结构化查询介绍

查询表达式(Query DSL)是一种非常灵活又富有表现力的 查询语言。 Elasticsearch 使用它可以以简单的 JSON 接口来展现 Lucene 功能的绝大部分。在你的应用中,你应该用它来编写你的查询语句。它可以使你的查询语句更灵活、更精确、易读和易调试。

7.3.2 索引操作

  1. 查询所有索引
GET _cat/indices?v

image.png 2. 删除某个索引

DELETE /blog1
  1. 新增索引
PUT /user
  1. 创建映射
PUT /user/userinfo/_mapping
{
  "properties": {
    "name": {
      "type": "text",
      "analyzer": "ik_smart",
      "search_analyzer": "ik_smart"
    },
    "city": {
      "type": "text",
      "analyzer": "ik_smart",
      "search_analyzer": "ik_smart"
    },
    "age": {
      "type": "long"
    },
    "description": {
      "type": "text",
      "analyzer": "ik_smart",
      "search_analyzer": "ik_smart"
    }
  }
}
  1. 新增文档数据
PUT /user/userinfo/1
{
  "name":"AAA",
  "age":22,
  "city":"shenzhen",
  "descrption":"sz"
}

PUT /user/userinfo/2
{
  "name":"BBB",
  "age":28,
  "city":"wuhan",
  "descrption":"wh"
}

PUT /user/userinfo/3
{
  "name":"CCC",
  "age":36,
  "city":"nanjing",
  "descrption":"nj"
}

PUT /user/userinfo/4
{
  "name":"DDD",
  "age":55,
  "city":"beijing",
  "descrption":"jb"
}

PUT /user/userinfo/5
{
  "name":"EEE",
  "age":13,
  "city":"guangzhou",
  "descrption":"gz",
  "address":"gz"
}

PUT /user/userinfo/6
{
  "name":"FFF",
  "age":66,
  "city":"xiamen",
  "descrption":"xm"
}

PUT /user/userinfo/7
{
  "name":"GGG",
  "age":46,
  "city":"hangzhou",
  "descrption":"hz"
}

PUT /user/userinfo/8
{
  "name":"HHH",
  "age":88,
  "city":"changsha",
  "descrption":"cs"
}

7.3.3 数据查询

  1. 根据id查询
GET /user/userinfo/1
  1. 查询所有数据
GET /user/userinfo/_search
  1. 搜索排序 根据年龄倒序
GET /user/userinfo/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "age": {
        "order": "desc"
      }
    }
  ]
}

根据年龄倒序 from:从下标N开始 size:每页数量

GET /user/userinfo/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "age": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 2
}
  1. term查询
GET _search
{
  "query": {
    "term": {
      "city": {
        "value": "shenzhen"
      }
    }
  }
}

term查询更精确匹配,如果是用mmatch查询,查询深圳武汉能出现深圳武汉的查询结果,而term是不可以的。

  1. match查询
GET _search
{
  "query": {
    "match": {
      "city": "shenzhen"
    }
  }
}
  1. range查询
GET _search
{
  "query": {
    "range": {
      "age": {
        "gte": 20,
        "lte": 60
      }
    }
  }
}
  1. exists查询 exists是指包含某个域的数据检索
GET _search
{
  "query": {
    "exists": {
      "field": "address"
    }
  }
}
  1. 过滤查询
GET /user/userinfo/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "city": "shenzhen"
        }}
      ],
      "filter": {
        "range": {
          "age": {
            "gte": 20,
            "lte": 60
          }
        }
      }
    }
  }
}

image.png

八、Elasticsearch编程操作

8.1 创建索引index

public class ElasticSearchClientTest{

    TransportClient client;

    @Before
    public void init() throws Exception{
        // 1. 配置
        Settings settings = Settings.builder().put("cluster.name", "my-elasticsearch").build();
        // 2. 客户端
        client = new PreBuiltTransportClient(settings);
        client.addTransportAddresses(new InetSocketTransportAddress(InetAddress.getByName("ip"), 9300));

    }

    @Test
    public void createIndex() throws Exception{
        client.addTransportAddresses(new InetSocketTransportAddress(InetAddress.getByName("xxx.xxx.xxx.xxx"), 9300));
        // 3. 使用api创建索引
        client.admin().indices().prepareCreate("index_hello3").get();

        // 4. 关闭client
        client.close();
    }

image.png

image.png

8.2 创建映射Mapping

@Test
public void setMappings() throws Exception{

    XContentBuilder builder = XContentFactory.jsonBuilder()
            .startObject()
            .startObject("article")
            .startObject("properties")
            .startObject("id")
            .field("type", "integer").field("store","yes")
            .endObject()
            .startObject("title")
            .field("type", "string").field("store","yes").field("analyzer","ik_smart")
            .endObject()
            .startObject("content")
            .field("type", "string").field("store","yes").field("analyzer","ik_smart")
            .endObject()
            .endObject()
            .endObject()
            .endObject();

    // 3. 使用api创建mapping
    client.admin().indices().preparePutMapping("index_hello2").setType("article").setSource(builder).get();

    // 4. 关闭client
    client.close();
}

8.3 建立文档document

8.3.1 通过XContentBuilder

@Test
public void testAddDocument() throws Exception{
    //创建文档对象
    XContentBuilder builder = XContentFactory.jsonBuilder()
            .startObject()
                .field("id",21)
                .field("title","今日天气预报")
                .field("content","今日天气28摄氏度")
            .endObject();
    //把文档对象添加到索引库
    client.prepareIndex()
            //索引名
            .setIndex("index_hello")
            .setType("article")
            .setId("2")
            // 上面写的文档对象
            .setSource(builder)
            //执行操作
            .get();

    client.close();
}

image.png

8.3.2 通过Jackson转换实体

@Data
public class Article {
    private Integer id;
    private String title;
    private String content;
}
@Test
public void testAddDocument2() throws Exception{
    Article article = new Article();
    article.setId(31);
    article.setTitle("今日解说");
    article.setContent("今日解说电影血战钢锯岭");
    // 把article对象转成json字符串
    ObjectMapper objectMapper = new ObjectMapper();
    String jsonDocument = objectMapper.writeValueAsString(article);
    System.out.println(jsonDocument);
    // 使用client对象把文档写入索引库
    client.prepareIndex("index_hello","article","3")
            .setSource(jsonDocument, XContentType.JSON)
            .get();

    client.close();
}

8.4 查询文档操作

8.4.1 重构复用代码

private void search(QueryBuilder queryBuilder) {
    // 执行查询
    SearchResponse searchResponse = client.prepareSearch("index_hello")
            .setTypes("article")
            .setQuery(queryBuilder)
            .get();

    // 查询结果
    SearchHits searchHits = searchResponse.getHits();
    // 取查询结果的总记录数
    System.out.println("查询结果总记录数" + searchHits.getTotalHits());
    // 查询结果列表
    Iterator<SearchHit> iterator = searchHits.iterator();
    while (iterator.hasNext()) {
        SearchHit searchHit = iterator.next();
        // 打印文档对象 以json格式输出
        System.out.println(searchHit.getSourceAsString());
        System.out.println("-------文档属性");
        Map<String, Object> document = searchHit.getSource();
        System.out.println(document.get("id"));
        System.out.println(document.get("title"));
        System.out.println(document.get("content"));
    }
    client.close();
}

8.4.2 term查询

@Test
public void testQueryByTerm() throws Exception{
    QueryBuilder queryBuilder = QueryBuilders.termQuery("title","今日");

    // 执行查询
    search(queryBuilder);
}

8.4.3 Match查询

@Test
public void testQueryByMatchQuery() throws Exception{
    // Match可以查出进入今日 而term查不出
    QueryBuilder queryBuilder = QueryBuilders.matchQuery("title","进入今日");
    search(queryBuilder);

}

Match可以查出进入今日 而term查不出

8.4.4 Querystring查询

@Test
public void testQueryByQueryString() throws Exception{
    QueryBuilder queryBuilder = QueryBuilders.queryStringQuery("进入今日").defaultField("title");

    // 执行查询
    search(queryBuilder);
}

QueryString可以查出进入今日 而term查不出

8.4.5 根据Id查询

@Test
public void testQueryById() throws Exception{
    QueryBuilder queryBuilder = QueryBuilders.idsQuery().addIds("2","3");
    search(queryBuilder);

}

8.4.6 分页查询

@Test
public void testQueryByMatchAll() throws Exception{
    QueryBuilder queryBuilder = QueryBuilders.matchAllQuery();

    // 执行查询
    SearchResponse searchResponse = client.prepareSearch("index_hello")
            .setTypes("article")
            .setQuery(queryBuilder)
            .setFrom(0)
            .setSize(5)
            .get();

    // 查询结果
    SearchHits searchHits = searchResponse.getHits();
    // 取查询结果的总记录数
    System.out.println("查询结果总记录数" + searchHits.getTotalHits());
    // 查询结果列表
    Iterator<SearchHit> iterator = searchHits.iterator();
    while (iterator.hasNext()) {
        SearchHit searchHit = iterator.next();
        // 打印文档对象 以json格式输出
        System.out.println(searchHit.getSourceAsString());
        System.out.println("-------文档属性");
        Map<String, Object> document = searchHit.getSource();
        System.out.println(document.get("id"));
        System.out.println(document.get("title"));
        System.out.println(document.get("content"));
    }

    client.close();
}

image.png

8.5 高亮显示

@Test
public void testQueryByHighlight() throws Exception{
    QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "今日");

    HighlightBuilder highlightBuilder = new HighlightBuilder();
    highlightBuilder.field("title");
    highlightBuilder.preTags("<em>");
    highlightBuilder.postTags("</em>");

    // 执行查询
    SearchResponse searchResponse = client.prepareSearch("index_hello")
            .setTypes("article")
            .setQuery(queryBuilder)
            .highlighter(highlightBuilder)
            .get();

    // 查询结果
    SearchHits searchHits = searchResponse.getHits();
    // 取查询结果的总记录数
    System.out.println("查询结果总记录数" + searchHits.getTotalHits());
    // 查询结果列表
    Iterator<SearchHit> iterator = searchHits.iterator();
    while (iterator.hasNext()) {
        SearchHit searchHit = iterator.next();
        // 打印文档对象 以json格式输出
        System.out.println(searchHit.getSourceAsString());
        System.out.println("-------高亮结果");
        Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
        for (Map.Entry<String, HighlightField> entry :
                highlightFields.entrySet()) {
            System.out.println(entry.getKey() + ":\t" + Arrays.toString(entry.getValue().fragments()));
        }
    }
    client.close();
}

image.png

九、Spring Data ElasticSearch

9.1 简介

9.1.1 Spring Data简介

Spring Data 的作用是为数据访问提供熟悉且一致的、基于 Spring 的编程模型,同时仍保留底层数据存储的特殊特征。

它使使用数据访问技术、关系和非关系数据库、map-reduce 框架和基于云的数据服务变得容易。这是一个总括项目,其中包含许多特定于给定数据库的子项目。这些项目是通过与这些令人兴奋的技术背后的许多公司和开发人员合作开发的。

特征

  • 强大的存储库和自定义对象映射抽象
  • 从存储库方法名称派生的动态查询
  • 提供基本属性的实现域基类
  • 支持透明审计(创建、上次更改)
  • 集成自定义存储库代码的可能性
  • 通过 JavaConfig 和自定义 XML 命名空间轻松集成 Spring
  • 与 Spring MVC 控制器的高级集成
  • 对跨商店持久性的实验性支持

Spring Data官网

9.1.2 Spring Data ElasticSearch简介

Spring Data for Elasticsearch 是 Spring Data 伞形项目的一部分,该项目旨在为新数据存储提供熟悉且一致的基于 Spring 的编程模型,同时保留特定于存储的特性和功能。

Spring Data Elasticsearch 项目提供与 Elasticsearch 搜索引擎的集成。Spring Data Elasticsearch 的关键功能领域是以 POJO 为中心的模型,用于与 Elastichsearch 文档交互并轻松编写存储库样式的数据访问层。

特征

  • Spring 配置支持使用基于 Java 的@Configuration类或 ES 客户端实例的 XML 命名空间。
  • ElasticsearchTemplate帮助类提高执行常见 ES 操作的生产力。包括文档和 POJO 之间的集成对象映射。
  • 与 Spring 的转换服务集成的功能丰富的对象映射
  • 基于注释的映射元数据,但可扩展以支持其他元数据格式
  • 接口的自动实现,Repository包括对自定义查找器方法的支持。
  • 对存储库的 CDI 支持

9.2 入门案例

9.2.1 创建springboot工程,添加Spring Data ElasticSearch

image.png

9.2.2 配置application.yml

spring:
  data:
    elasticsearch:
      cluster-name: my-elasticsearch
      cluster-nodes: ip:9300

9.2.3 创建实体

@Document(indexName = "zjl_blog", type = "article")
@Data
public class Article {
    @Id
    @Field(type = FieldType.Long, store = true)
    private long id;
    @Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
    private String title;
    @Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
    private String content;
}

9.2.3 创建接口继承ElasticsearchRepository

public interface ArticleDao extends ElasticsearchRepository<Article, Long> {

}

9.2.4 测试

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

    @Autowired
    private ArticleDao dao;

    @Autowired
    private ElasticsearchTemplate template;

    @Test
    public void createIndex() {
        template.createIndex(Article.class);
    }

    @Test
    public void testAddDocument3() throws Exception{
        for (int i = 1; i < 20; i++) {
            Article article = new Article();
            article.setId(i);
            article.setTitle("今日解说" + i);
            article.setContent("今日解说电影血战钢锯岭" + i);
            dao.save(article);
        }

    }
    

    @Test
    public void findAll(){
        dao.findAll().forEach(System.out::println);
    }

}

image.png image.png

image.png

9.2 聚合查询

9.2.1 DSL语句实现

根据车的颜色查询

GET /car_index/car/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "group_by_color": {
      "terms": {
        "field": "color"
      }
    }
  }
}

image.png

根据车的颜色查询在根据平均价格查询

GET /car_index/car/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "group_by_color": {
      "terms": {
        "field": "color"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

image.png

9.2.2 Java实现


@Test
public void testQuerySelfAggs(){
    // 查询条件的构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchAllQuery());
    //排除所有的字段查询
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));

    //添加聚合条件
    queryBuilder.addAggregation(AggregationBuilders.terms("group_by_color").field("color"));
    //执行查询,把查询结果直接转为聚合page
    AggregatedPage<Car> aggPage = (AggregatedPage<Car>) carDao.search(queryBuilder.build());

    //从所有的聚合中获取对应名称的聚合
    StringTerms agg = (StringTerms) aggPage.getAggregation("group_by_color");

    //从聚合的结果中获取所有的桶信息
    List<StringTerms.Bucket> buckets = agg.getBuckets();

    for (StringTerms.Bucket bucket : buckets) {
        String color = bucket.getKeyAsString();
        long docCount = bucket.getDocCount();

        System.out.println("color = " + color + "总数:" + docCount);
    }

}

image.png

嵌套聚合

@Test
public void testQueryBySubAggs(){
    // 查询条件的构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchAllQuery());
    //排除所有的字段查询
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));

    //添加聚合条件
    queryBuilder.addAggregation(AggregationBuilders.terms("group_by_color").field("color")
            .subAggregation(AggregationBuilders.avg("avg_price").field("price")));
    //执行查询,把查询结果直接转为聚合page
    AggregatedPage<Car> aggPage = (AggregatedPage<Car>) carDao.search(queryBuilder.build());

    //从所有的聚合中获取对应名称的聚合
    StringTerms agg = (StringTerms) aggPage.getAggregation("group_by_color");

    //从聚合的结果中获取所有的桶信息
    List<StringTerms.Bucket> buckets = agg.getBuckets();

    for (StringTerms.Bucket bucket : buckets) {
        String color = bucket.getKeyAsString();
        long docCount = bucket.getDocCount();

        //取得内部聚合
        InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("avg_price");

        System.out.println("color = " + color + "总数:" + docCount + " 价格 = " + avg);
    }

}

image.png