微服务[学成在线] day10:课程发布、ElasticSearch

微服务[学成在线] day10:课程发布、ElasticSearch

😎 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day10 的内容

  •  课程发布功能开发
  •  ElasticsSearch 安装部署
  •  ElasticsSearch 快速入门、IK 分词器、映射、索引。

目录

内容会比较多,小伙伴们可以根据目录进行按需查阅。

一、课程发布

0x01 需求分析

课程发布后将生成正式的课程详情页面,课程发布后用户即可浏览课程详情页面,并开始课程的学习。课程发布生成课程详情页面的流程与课程预览业务流程相同,如下:

1、用户进入教学管理中心,进入某个课程的管理界面

2、点击课程发布,前端请求到课程管理服务

3、课程管理服务远程调用 CMS 生成课程发布页面,CMS 将课程详情页面发布到服务器

4、课程管理服务修改课程发布状态为 “已发布”,并向前端返回发布成功

5、用户在教学管理中心点击 “课程详情页面” 链接,查看课程详情页面内容

流程图如下

0x02 CMS一键发布接口

1、需求分析

根据需求分析内容,需要在 cms 服务增加页面发布接口供课程管理服务调用,此接口的功能如下:

1、接收课程管理服务发布的页面信息

2、将页面信息添加到 数据库**(mongodb)**

3、对页面信息进行静态化

4、将页面信息发布到服务器

2、接口定义

1、创建响应结果类型

页面发布成功cms返回页面的 url

页面 Url = cmsSite.siteDomain + cmsSite.siteWebPath + cmsPage.pageWebPath + cmsPage.pageName

@Data
@NoArgsConstructor
public class CmsPostPageResult extends ResponseResult {
    String pageUrl;
    public CmsPostPageResult(ResultCode resultCode,String pageUrl){
        super(resultCode);
        this.pageUrl = pageUrl;
    }
}

2、在 api 工程定义页面发布接口

/**
   * 一键发布页面
*/
@ApiOperation("一键发布页面")
public CmsPostPageResult postPageQuick(CmsPage cmsPage);

3、Dao

/**
 * 继承MongoDB自带的一些Repository
 */
public interface CmsSiteRepository extends MongoRepository<CmsSite,String{

}

4、Service

/**
 * 一键发布页面
 * @param cmsPage
 * @return
 */
public CmsPostPageResult postPageQuick(CmsPage cmsPage) {
    //添加页面,这里直接调用我们在做预览页面时候开发的保存页面方法
    CmsPageResult cmsPageResult = this.saveCmsPage(cmsPage);
    if(!cmsPageResult.isSuccess()){
        //添加页面失败
        return new CmsPostPageResult(CommonCode.FAIL,null);
    }

    CmsPage saveCmsPage = cmsPageResult.getCmsPage();
    String pageId = saveCmsPage.getPageId();

    //发布页面,通知cms client发布页面
    ResponseResult responseResult = this.postPage(pageId);
    if(!responseResult.isSuccess()){
        //发布失败
        return new CmsPostPageResult(CommonCode.FAIL,null);
    }

    //得到页面的url,页面url=站点域名+站点webpath+页面webpath+页面名称
    //所以这里我们需要获取站点信息
    String siteId = saveCmsPage.getSiteId();
    Optional<CmsSite> cmsSiteOptional = cmsSiteRepository.findById(siteId);
    if(!cmsSiteOptional.isPresent()){
        //获取站点异常
        return new CmsPostPageResult(CommonCode.FAIL,null);
    }
    CmsSite cmsSite = cmsSiteOptional.get();
    //站点和页面的信息
    String siteDomain = cmsSite.getSiteDomain();
    String siteWebPath = cmsSite.getSiteWebPath();
    String pageWebPath = saveCmsPage.getPageWebPath();
    String pageName = saveCmsPage.getPageName();
    //发布页面的访问地址
    String pageUrl = siteDomain + siteWebPath + pageWebPath + pageName;
    return new CmsPostPageResult(CommonCode.SUCCESS, pageUrl);
}

2、页面发布方法

5、Controller

/**
 * 一键发布页面
 * @param cmsPage
 * @return
 */
@Override
@PostMapping("/postPageQuick")
public CmsPostPageResult postPageQuick(@RequestBody CmsPage cmsPage) {
    return pageService.postPageQuick(cmsPage);
}

0x03 课程发布接口

1、API接口

@ApiOperation("课程发布")
public CoursePublishResult CoursePublish(String courseId);

2、创建Feign Client

定义一个远程调用的方法,用于远程调用刚才我们在 CMS 服务中定义的一键发布接口。

@FeignClient(value = "XC-SERVICE-MANAGE-CMS")
public interface CmsPageClient {
    @PostMapping("/cms/page/postPageQuick")
    CmsPostPageResult postPageQuick(@RequestBody CmsPage cmsPage);
}

3、Service

1、配置课程发布页面参数

新增课程详情页面的站点信息,如果已增加课程详情页面的站点则忽略此步骤。

mongodbcms_site 中新增如下信息

{ 
    "_id" : ObjectId("5b30b052f58b4411fc6cb1cf"), 
    "_class" : "com.xuecheng.framework.domain.cms.CmsSite", 
    "siteName" : "课程详情站点", 
    "siteDomain" : "http://www.xuecheng.com", 
    "sitePort" : "80", 
    "siteWebPath" : "/", 
    "siteCreateTime" : ISODate("2018-02-03T02:34:19.113+0000")
}

application.yml 中配置

course-publish:
  siteId: 5b30b052f58b4411fc6cb1cf
  templateId: 5e93d2e3d79e7d6ed1009b95
  previewUrl: http://www.xuecheng.com/cms/preview/
  pageWebPath: /course/detail/
  pagePhysicalPath: G:/job/code/Project/XueChengOnline/xcEduUI01/xuecheng/static/course/detail/
  dataUrlPre: http://localhost:31200/course/preview/model/
  • siteId:站点id

  • templateId:模板id

  • dataurlPre:数据url的前缀

  • pageWebPath: 页面的 web 访问路径

  • pagePhysicalPath: 这个指的是页面要发布到 nginx 服务器的物理路径,我们在配置文件中定义,可以动态调整,便于扩展。

2、service 方法如下

//拼装页面信息
private CmsPage setPageInfo(String courseId){
    //获取课程信息
    CourseBase courseBaseById = this.findCourseBaseById(courseId);

    //拼装页面基本信息
    CmsPage cmsPage = new CmsPage();
    cmsPage.setDataUrl(publish_dataUrlPre + courseId);
    cmsPage.setPagePhysicalPath(publish_page_physicalpath);
    cmsPage.setPageWebPath(publish_page_webpath);
    cmsPage.setSiteId(publish_siteId);
    cmsPage.setTemplateId(publish_templateId);
    cmsPage.setPageName(courseId + ".html");

    //页面别名
    cmsPage.setPageAliase(courseBaseById.getName());
    return cmsPage;
}

/**
 * 课程详细页面发布
 */
@Transactional
public CoursePublishResult coursePublish(String courseId){
    //拼装页面信息
    CmsPage cmsPage = setPageInfo(courseId);

    //发布课程详细页面
    CmsPostPageResult cmsPostPageResult = cmsPageClient.postPageQuick(cmsPage);
    if(!cmsPostPageResult.isSuccess()){
        return new CoursePublishResult(CommonCode.FAIL,null);
    }

    //更新课程状态
    CourseBase courseBaseById = this.findCourseBaseById(courseId);
    courseBaseById.setStatus("202002");
    courseBaseRepository.save(courseBaseById);

    //课程索引...

    //课程缓存...

    //页面url
    String pageUrl = cmsPostPageResult.getPageUrl();
    return new CoursePublishResult(CommonCode.SUCCESS,pageUrl);
}

4、Controller

/**
 * 课程发布
 * @param courseId
 * @return
 */
@Override
@PostMapping("/publish/{id}")
public CoursePublishResult CoursePublish(@PathVariable("id") String courseId) {
    return courseService.coursePublish(courseId);
}

5、接口测试

0x04 测试CMS一键发布接口

测试前准备工作

1、启动RabbitMQ服务
2、启动 cmscourse 服务
3、启动 cms_client,注意配置 routingKey 和队列名称,routingKey 为所使用的站点id

4、这里我们分别在 coursecms、cms client 三个服务内对应的地方打上断点,观察发布的流程

course 服务断点,我们设置在远程调用 cms 一键发布接口返回结果之后

cms 服务断点,在 一键发布接口 开始执行的地方我们打上断点

当 cms 客户端接收到 cms 通过 rabbitMQ 发送的消息后,会调用该方法将页面保存到 nginx 服务器的物理路径内

准备工作做完了,我们在 course 服务生成的 sawgger-ui 进行测试

发送请求后,我们在 idea 中可以看到,断点已经跑到了 cms 服务的一键发布接口

继续执行的话,cms 服务会调用 this.postPage 方法,获取该页面的 数据模型、以及模板数据,将数据静态化后得到完整的页面数据,并且写入到 GridFS 中;

写入 GridFS后通过 rabbitMQ 发送消息到 cms client ,消息的内容包含一个页面id,入下图,断点已经走到了 cms client 服务的 savePageToServerPath 方法中

``cms client接收到pageId后,获取该页面数据的 fileId,通过该id到GridFS` 中找到该页面的数据,并写入到页面的物理路径内,写入完成后,回到 cms 服务内拼装发布后的页面地址,再返回到 course 服务

course 服务接收到 cms 的响应,取出 pageUrl 返回到前端,如下图

回到 swagger ui,我们可以看到已经返回了一个发布成功的url

访问页面看下效果

0x05 前端页面开发与测试

1、页面开发

<el-card class="box-card">
  <div slot="header" class="clearfix">
    <span>课程发布</span>
  </div>
  <div class="text item">
    <div v-if="course.status == '202001'">
      状态:制作中<br/>
      <el-button type="primary"  @click.native="publish" >新课程发布</el-button>
    </div>
    <div v-else-if="course.status == '202003'">
      状态:已下线
      <br/><br/>
      <span><a :href="'http://www.xuecheng.com/course/detail/'+this.courseid+'.html'" target="_blank">点我查看课程详情页面 </a> </span>
    </div>
    <div v-else-if="course.status == '202002'">
      状态:已发布<br/>
      <el-button type="primary"  @click.native="publish" >修改发布</el-button>
      <br/><br/>
      <span><a :href="'http://www.xuecheng.com/course/detail/'+this.courseid+'.html'" target="_blank">点我查看课程详情页面 </a> </span>
    </div>
  </div>
</el-card>

2、获取课程状态

getCourseView(){
  courseApi.findCourseView(this.courseid).then(res=>{
    if(res){
        //获取课程状态
        this.course.status = res.status;
    }
  })
}

在页面初始化完成其前执行

mounted(){
  //课程id
  this.courseid = this.$route.params.courseid;
  //查询课程信息
  this.getCourseView();
}

3、页面发布接口

api接口定义

/*发布课程*/
export const publish = id => {
  return http.requestPost(apiUrl+'/course/publish/'+id);
}

api接口实现

publish(){
  //课程发布
  courseApi.publish(this.courseid).then(res=>{
      if(res.success){
          this.$message.success("发布成功,请点击下边的链接查询课程详情页面")
          this.getCourseView();
      }else{
        this.$message.error(res.message)
      }

  })
},

测试

二、初识 Elastic Search

0x01 是什么?为什么?

1、简历

百度百科

官方网址:https://www.elastic.co/cn/products/elasticsearch

Github :https://github.com/elastic/elasticsearch

总结:

1、elasticsearch是一个基于Lucene的高扩展的分布式搜索服务器,支持开箱即用。
2、elasticsearch 隐藏了 Lucene 的复杂性,对外提供 Restful 接口来操作索引、搜索。

突出优点:

  1. 扩展性好,可部署上百台服务器集群,处理PB级数据。
  2. 近实时的去索引数据、搜索数据。

市面上的同类型的产品有 solr ,那么 essolr 选择哪个?

如果你公司现在用的 solr 可以满足需求就不要换了。

如果你公司准备进行全文检索项目的开发,建议优先考虑 elasticsearch,因为像 Github这样大规模的搜索都在用它。

2、原理于应用

索引结构

下图是 ElasticSearch 的索引结构,下边黑色部分是物理结构,上边黄色部分是逻辑结构,逻辑结构也是为了更好的去描述 ElasticSearch 的工作原理及去使用物理结构中的索引文件。

逻辑结构部分是一个倒排索引表:

1、将要搜索的文档内容分词,所有不重复的词组成分词列表。

2、将搜索的文档最终以 Document 方式存储起来。

3、每个词和 docment 都有关联。

如下:

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

正排索引:根据词与文章内容的关系进行索引。

倒排索引:根据词与文章的关系进行索引,需要提前对词和文章进行关联。

3、如何使用es?

Elasticsearch 提供 RESTful Api 接口进行索引、搜索,并且支持多种客户端。

下图是es在项目中的应用方式:

1)用户在前端搜索关键字

2)项目前端通过 http 方式请求项目服务端

3)项目服务端通过 Http RESTful 方式请求 ES 集群进行搜索

4)ES 集群从索引库检索数据。

1、安装

安装配置:

1、新版本要求至少 jdk1.8 以上。

2、支持tarziprpm等多种安装方式。在 windows 下开发建议使用ZIP安装方式。

3、支持 docker 方式安装

详细参见:https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html

注意视频教程中用的是6.2.1的,为了适应较新的版本这里我这里下载 6.8.8 的版本作为演示

截至现在官方已经更新 到了 7.7.*,从语义化版本规范来看,6和7应该有架构上的重大变化,所以我还是选择6系的最新一个版本避免踩坑,以后有需求再去对比7的差异。

https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.8.8.zip

其他版本下载:https://www.elastic.co/cn/downloads/past-releases

下载完成后我们解压 elasticsearch-*.*.*.zip

bin:脚本目录,包括:启动、停止等可执行脚本

config:配置文件目录

data:索引目录,存放索引文件的地方

logs:日志目录

modules:模块目录,包括了es的功能模块

plugins :插件目录,es支持插件机制

2、配置文件

ES的配置文件的地址根据安装形式的不同而不同:

1、使用 ziptar 安装,配置文件的地址在安装目录的 config 下。

2、使用 RPM 安装,配置文件在 /etc/elasticsearch 下。

3、使用 MSI 安装,配置文件的地址在安装目录的 config 下,并且会自动将 config 目录地址写入环境变量
ES_PATH_CONF

本教程使用的 zip 包安装,配置文件在ES安装目录的config下。

配置文件如下:

  • elasticsearch.yml : 用于配置Elasticsearch运行参数

  • jvm.options : 用于配置Elasticsearch JVM设置

  • log4j2.properties: 用于配置Elasticsearch日志

elasticsearch.yml

配置格式是 YAML,可以采用如下两种方式:

方式1:层次方式

path: 
  data: /var/lib/elasticsearch
  logs: /var/log/elasticsearch
  

方式2:属性方式

path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch

本项目采用方式2,例子如下:

cluster.name: xuecheng
node.name: xc_node_1
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
node.master: true
node.data: true
#discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301", "0.0.0.0:9302"]
discovery.zen.minimum_master_nodes: 1
bootstrap.memory_lock: false
node.max_local_storage_nodes: 1
path.data: D:\ElasticSearch\elasticsearch‐6.2.1\data
path.logs: D:\ElasticSearch\elasticsearch‐6.2.1\logs
http.cors.enabled: true
http.cors.allow‐origin: /.*/

注意 path.datapath.logs 路径配置正确。

常用的配置项如下:

配置字段描述
cluster.name配置 elasticsearch 的集群名称,默认是 elasticsearch。建议修改成一个有意义的名称。
node.name节点名,通常一台物理服务器就是一个节点,es 会默认随机指定一个名字,建议指定一个有意义的名称,方便管理一个或多个节点组成一个 cluster 集群,集群是一个逻辑的概念,节点是物理概念,后边章节会详细介绍。
path.conf设置配置文件的存储路径,tar或zip包安装默认在 es 根目录下的 config 文件夹,rpm 安装默认在 /etc/
path.logs设置日志文件的存储路径,默认是es根目录下的 logs 文件夹
path.plugins设置插件的存放路径,默认是 es 根目录下的 plugins 文件夹
bootstrap.memory_locktrue 设置为 true 可以锁住 ES 使用的内存,避免内存与 swap 分区交换数据。
elasticsearch path.data设置索引数据的存储路径,默认是 es 根目录下的 data 文件夹,可以设置多个存储路径,用逗号隔开。 path.logs: 设置日志文件的存储路径,默认是es根目录下的 logs 文件夹
network.host设置绑定主机的 ip 地址,设置为 0.0.0.0 表示绑定任何 ip,允许外网访问,生产环境建议设置为具体的 ip
http.port9200 设置对外服务的 http 端口,默认为 9200
transport.tcp.port9300 集群结点之间通信端口
node.master指定该节点是否有资格被选举成为 master 结点,默认是true,如果原来的master宕机会重新选举新的master。
node.data指定该节点是否存储索引数据,默认为 true
discovery.zen.ping.unicast.hosts["host1:port", "host2:port", "..."] 设置集群中 master 节点的初始列表。
discovery.zen.ping.timeout3s 设置 ES 自动发现节点连接超时的时间,默认为3秒,如果网络延迟高可设置大些。
discovery.zen.minimum_master_nodes主结点数量的最少值 ,此值的公式为:(master_eligible_nodes / 2) + 1 ,比如:有3个符合要求的主结点,那么这里要设置为2。
node.max_local_storage_nodes单机允许的最大存储结点数,通常单机启动一个结点建议设置为1,开发环境如果单机启动多个节点可设置大于1.

jvm.options

设置最小及最大的JVM堆内存大小:

jvm.options 中设置 -Xms-Xmx

1) 两个值设置为相等

2) 将 Xmx 设置为不超过物理内存的一半。

log4j2.properties

日志文件设置,ES使用 log4j,注意日志级别的配置。

系统配置

linux上根据系统资源情况,可将每个进程最多允许打开的文件数设置大些。

su limit -n 查询当前文件数

使用命令设置 limit:
先切换到 root,设置完成再切回 elasticsearch 用户。

sudo su 
ulimit ‐n 65536
su elasticsearch

也可通过下边的方式修改文件进行持久设置
/etc/security/limits.conf 将下边的行加入此文件:

elasticsearch  ‐  nofile  65536

3、启动ES

进入 bin 目录,在 cmd 下运行:elasticsearch.bat

浏览器输入:http://localhost:9200

显示结果如下(配置不同内容则不同)说明 ES 启动成功:

{
  "name" : "xc_node_1",
  "cluster_name" : "xuecheng",
  "cluster_uuid" : "J18wPybJREyx1kjOoH8T‐g",
  "version" : {
    "number" : "6.2.1",
    "build_hash" : "7299dc3",
    "build_date" : "2018‐02‐07T19:34:26.990113Z",
    "build_snapshot" : false,
     "lucene_version" : "7.2.1",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

4、安装 head 插件

head 插件是 ES 的一个可视化管理插件,用来监视ES的状态,并通过 head 客户端和 ES 服务进行交互,比如创建映射、创建索引等,head 的项目地址在 https://github.com/mobz/elasticsearch-head

从ES6.0 开始,head 插件支持使得 node.js 运行。

1、安装 node.js

2、下载 head 并运行

git clone git://github.com/mobz/elasticsearch-head.git 
cd elasticsearch-head 
npm install 
npm run start open

访问 http://本地主机:9100/

3、运行

打开浏览器调试工具发现报错:

Origin null is not allowed by Access-Control-Allow-Origin.

原因是:head插件作为客户端要连接ES服务(localhost:9200),此时存在跨域问题,elasticsearch默认不允许跨域访问。

解决方案:

设置 elasticsearch 允许跨域访问

# 跨域配置
http.cors.enabled: true
http.cors.allow-origin: /.*/

跨域访问允许的域名地址,(允许所有域名) 以上使用正则 http.cors.allow-origin: /.*/

注意:将 config/elasticsearch.yml 另存为utf-8编码格式。

成功连接 ES

三、ES 快速入门

ES 作为一个索引及搜索服务,对外提供丰富的 REST 接口,快速入门部分的实例使用 head 插件来测试,目的是对 ES 的使用方法及流程有个初步的认识。

0x01 创建索引库

ES 的索引库是一个逻辑概念,它包括了分词列表及文档列表,同一个索引库中存储了相同类型的文档。它就相当于 MySQL 中的表,或相当于 Mongodb 中的集合。

关于索引这个语:

索引(名词):ES 是基于 Lucene 构建的一个搜索服务,它要从索引库搜索符合条件索引数据。

索引(动词):索引库刚创建起来是空的,将数据添加到索引库的过程称为索引。

下边介绍两种创建索引库的方法,它们的工作原理是相同的,都是客户端向 ES 服务发送命令。

1)使用 postman 或 curl 这样的工具创建:

put http://localhost:9200/xc_course 索引库名称

{
  "settings":{
  "index":{
      "number_of_shards":1,
      "number_of_replicas":0
   }    
  }
}

number_of_shards:设置分片的数量,在集群中通常设置多个分片,表示一个索引库将拆分成多片分别存储不同的结点,提高了 ES 的处理能力和高可用性,入门程序使用单机环境,这里设置为 1。

number_of_replicas:设置副本的数量,设置副本是为了提高 ES 的高可靠性,单机环境设置为 0.

如下是创建的例子,创建 xc_course 索引库,共 1 个分片,0 个副本:

使用 postman 发送 put 请求

2)使用head插件创建

效果如下

0x02 创建映射

1、概念说明

在索引中每个文档都包括了一个或多个 field,创建映射就是向索引库中创建 field 的过程,下边是documentfield 与关系数据库的概念的类比:

文档(Document)---------------- Row 记录

字段(Field)------------------- Columns

注意:6.0 之前的版本有 type(类型)概念,type 相当于关系数据库的表,ES 官方将在 ES9.0 版本中彻底删除 type

上边讲的创建索引库相当于关系数据库中的数据库还是表?

1、如果相当于数据库就表示一个索引库可以创建很多不同类型的文档,这在 ES 中也是允许的。

2、如果相当于表就表示一个索引库只能存储相同类型的文档,ES 官方建议 在一个索引库中只存储相同类型的文档。

2、创建映射

我们要把课程信息存储到 ES 中,这里我们创建课程信息的映射,先来一个简单的映射,如下:

发送:post http://localhost:9200/索引库名称/类型名称/_mapping

创建类型为 xc_course 的映射,共包括三个字段:namedescriptionstudymondel

由于 ES6.0 版本还没有将 type 彻底删除,所以暂时把 type 起一个没有特殊意义的名字。

发送 post 请求:http://localhost:9200/xc_course/doc/_mapping

xc_course 索引库下的 doc 类型下创建映射。doc 是类型名,可以自定义,在 ES6.0 中要弱化类型的概念,给它起一个没有具体业务意义的名称。

body

{
    "properties": {
        "name": {
            "type": "text"
        },
        "description": {
            "type": "text"
        },
        "studymodel": {
            "type": "keyword"
        }
    }
}

映射创建成功,查看 head 界面:

0x03 创建文档

ES中的文档相当于MySQL数据库表中的记录。

发送:put 或 Post 到http://localhost:9200/xc_course/doc/id值(如果不指定id值ES会自动生成ID)

http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000

{
    "name":"Bootstrap开发框架",
    "description":"Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
    "studymodel":"201001"
}

使用 postman 测试:

通过 head 查询数据:

0x04 搜索文档

1、根据课程id查询文档
发送:get http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000

使用 postman 测试:

2、查询所有记录

发送 get http://localhost:9200/xc_course/doc/_search

3、查询名称中包括 bootstrap 关键字的的记录
发送:get http://localhost:9200/xc_course/doc/_search?q=name:bootstrap

4、查询学习模式为 201001 的记录
发送 get http://localhost:9200/xc_course/doc/_search?q=studymodel:201001

查询结果分析

分析上边查询结果:

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.2876821,
        "hits": [
            {
                "_index": "xc_course",
                "_type": "doc",
                "_id": "4028e58161bcf7f40161bcf8b77c0000",
                "_score": 0.2876821,
                "_source": {
                    "name": "Bootstrap开发框架",
                    "description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
                    "studymodel": "201001"
                }
            }
        ]
    }
}
字段名称描述
took本次操作花费的时间,单位为毫秒。
timed_out请求是否超时
_shards说明本次操作共搜索了哪些分片
hits搜索命中的记录
hits.total符合条件的文档总数 hits.hits :匹配度较高的前N个文档
hits.max_score文档匹配得分,这里为最高分
_score每个文档都有一个匹配度得分,按照降序排列。
_source显示了文档的原始内容。

四、IK分词器

0x01 测试ES默认的分词器

在添加文档时会进行分词,索引中存放的就是一个一个的词(term),当你去搜索时就是拿关键字去匹配词,最终找到词关联的文档。

测试当前索引库使用的分词器:

POST 请求:http://localhost:9200/_analyze

{"text":"分词器"}

结果如下:

{
    "tokens": [
        {
            "token": "分",
            "start_offset": 0,
            "end_offset": 1,
            "type": "<IDEOGRAPHIC>",
            "position": 0
        },
        {
            "token": "词",
            "start_offset": 1,
            "end_offset": 2,
            "type": "<IDEOGRAPHIC>",
            "position": 1
        },
        {
            "token": "器",
            "start_offset": 2,
            "end_offset": 3,
            "type": "<IDEOGRAPHIC>",
            "position": 2
        }
    ]
}

从响应的结果来看,ES 的默认分词器会将我们提交的内容中的每一个词进行分割,这样显然不够智能,下面我们来看一下 IK 分词器。

0x02 安装IK分词器

使用IK分词器可以实现对中文分词的效果。
下载IK分词器:(Github地址:https://github.com/medcl/elasticsearch-analysis-ik)

这里要注意的是,IK分词器的版本的ES的版本的相对应的,也就是说,如果我们 ES 的版本用的是 v6.8.8 那么IK分词器的版本也要对应使用 v6.8.8。

解压,并将解压的文件拷贝到 ES 安装目录的 plugins 下的ik目录下

测试分词效果:

POST请求: http://localhost:9200/_analyze

{"text":"测试分词器,后边是测试内容:springcloud实战","analyzer":"ik_max_word"}

请求结果

{
    "tokens": [
        {
            "token": "测试",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "分词器",
            "start_offset": 2,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "分词",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 2
        },
		//...省略一部分结果
    ]
}

从结果可以看出,在我们引入 IK 插件之后,分词器能识别出我们提交的内容中的词语,细心的老铁会注意到我们在 analyzer 字段中引入了 ik_max_word ,这是 IK 插件中的一个分词模式,下面我们来仔细介绍一下 IK 插件中的几种分词模式。

0x03 两种分词模式

ik分词器有两种分词模式:ik_max_wordik_smart 模式,下面我们来测试这两种模式。

1、ik_max_word

会将文本做最细粒度的拆分,比如会将 “中华人民共和国人民大会堂 ” 拆分为“ 中华人民共和国中华人民中华华人人民共和国人民共和国大会堂大会会堂等词语

发送 post 请求: http://localhost:9200/_analyze

{"text":"中华人民共和国人民大会堂","analyzer":"ik_max_word"}

请求结果

{
    "tokens": [
        {
            "token": "中华人民共和国",
            "start_offset": 0,
            "end_offset": 7,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "中华人民",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "中华",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 2
        },
        {
            "token": "华人",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 3
        },
        //省略部分结果...
    ]
}

2、ik_smart

会做最粗粒度的拆分,比如会将 “中华人民共和国人民大会堂” 拆分为 中华人民共和国人民大会堂

{"text":"中华人民共和国人民大会堂","analyzer":"ik_smart"}

请求结果

{
    "tokens": [
        {
            "token": "中华人民共和国",
            "start_offset": 0,
            "end_offset": 7,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "人民大会堂",
            "start_offset": 7,
            "end_offset": 12,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}

0x04 自定义词库

如果要让分词器支持一些专有词语,可以自定义词库。
iK 分词器自带一个 main.dic 的文件,此文件为词库文件。

我们在上边的目录中新建一个 my.dic 文件(注意文件格式为 utf-8,不要选择utf-8 BOM
可以在其中自定义词汇

定义 my.dic , 以每行为单位

桂北汇

编辑 config 中的配置文件 IKAnalyzer.cfg.xml 中配置 my.dic

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
	<entry key="ext_dict">my.dic</entry>
	 <!--用户可以在这里配置自己的扩展停止词字典-->
	<entry key="ext_stopwords"></entry>
	<!--用户可以在这里配置远程扩展字典 -->
	<!-- <entry key="remote_ext_dict">words_location</entry> -->
	<!--用户可以在这里配置远程扩展停止词字典-->
	<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

在适自定义配置生效之前,我们先来看一下未自定义之前的效果

发送:post http://localhost:9200/_analyze

{"text":"桂北汇", "analyzer":"ik_max_word" }
{
    "tokens": [
        {
            "token": "桂北",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "汇",
            "start_offset": 2,
            "end_offset": 3,
            "type": "CN_CHAR",
            "position": 1
        }
    ]
}

可以看到,在自定义的分词未生效之前,ES还是将我们的提交的 “桂北汇” 拆分了,因为常用的词语组合。

我们重启 ES,使刚才修改的自定义配置生效再次测试分词效果:

{
    "tokens": [
        {
            "token": "桂北汇",
            "start_offset": 0,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "桂北",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "汇",
            "start_offset": 2,
            "end_offset": 3,
            "type": "CN_CHAR",
            "position": 2
        }
    ]
}

从结果可以看出,我们在 my.dic 中添加的 “桂北汇” 分词被识别出来了,由于我们使用的是 ik_max_word 分词模式,所以 ESIK 插件会进行更 细粒度 的识别。

五、映射

上边章节安装了 ik 分词器,如果在索引和搜索时去使用 ik 分词器呢?如何指定其它类型的 field,比如日期类型、数值类型等。本章节学习各种映射类型及映射维护方法。

0x01 映射维护方法

1、查询所有索引的映射

GET 请求: http://localhost:9200/_mapping

2、创建映射

POST 请求:http://localhost:9200/xc_course/doc/_mapping

{
    "properties": {
        "name": {
        	"type": "text"
        },
        "description": {
        	"type": "text"
        },
        "studymodel": {
        	"type": "keyword"
        }
    }
}

这里要注意得是,映射只能 创建,不能删除,以及类型也不能修改,因为如果这个映射一旦创建后,你可能会关联了大量的数据,所以修改是不可取的。

0x02 常用映射类型

下图是ES6.2核心的字段类型如下:

1、text 文本字段

字符串包括 textkeyword 两种类型

analyzer 属性

通过 analyzer 属性指定分词器。

下边指定 name 的字段类型为 text,使用 ik 分词器的 ik_max_word 分词模式。

"name": {
    "type": "text",
    "analyzer":"ik_max_word"
}

上边指定了 analyzer 是指在索引和搜索都使用 ik_max_word,如果单独想定义搜索时使用的分词器则可以通过
search_analyzer 属性。对于 ik 分词器建议是索引时使用 ik_max_word 将搜索内容进行细粒度分词,搜索时使用 ik_smart 提高搜索精确性 。

"name": {
    "type": "text",
    "analyzer":"ik_max_word",
    "search_analyzer":"ik_smart"
}

index 属性

通过 index 属性指定是否索引。

默认为 index=true,即要进行索引,只有进行索引才可以从索引库搜索到。

但是也有一些内容不需要索引,比如:商品图片地址只被用来展示图片,不进行搜索图片,此时可以将 index设置为 false。删除索引,重新创建映射,将 picindex 设置为 false,尝试根据 pic 去搜索,结果搜索不到数据

"pic": {
    "type": "text",
    "index":false
}

store 属性

是否在 source 之外存储,每个文档索引后会在 ES 中保存一份原始文档,存放在 _source 中,一般情况下不需要设置 storetrue,因为在 source 中已经有一份原始文档了。

测试

删除 xc_course/doc 下的映射 ,发送 DELETE 请求到 http://10.1.1.168:9200/xc_course

创建,发送PUT请求 http://localhost:9200/xc_course 索引库名称

{
  "settings":{
  "index":{
      "number_of_shards":1,
      "number_of_replicas":0
   }    
  }
}

创建新映射:POST http://localhost:9200/xc_course/doc/_mapping

{
    "properties": {
        "name": {
        "type": "text",
        "analyzer":"ik_max_word",
        "search_analyzer":"ik_smart"
    },
    "description": {
        "type": "text",
        "analyzer":"ik_max_word",
        "search_analyzer":"ik_smart"
    },
    "pic":{
        "type":"text",
        "index":false
    },
    "studymodel":{
        "type":"text"
        }
    }
}

插入文档:

POST http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000

{
    "name":"Bootstrap开发框架",
    "description":"Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
    "pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
    "studymodel":"201002"
}

查询测试:

GET请求: http://localhost:9200/xc_course/_search?q=name:开发

查询结果:获取到 name 中包含 “开发” 的文档

GET请求: http://localhost:9200/xc_course/_search?q=description:开发

查询结果:获取到 description 中包含 “开发” 的文档

GET请求: http://localhost:9200/xc_course/_search?q=pic:group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg

查询结果:由于前面 pic 字段 设置了 index 属性为 false

GET请求: http://localhost:9200/xc_course/_search?q=studymodel:201002

查询结果: 由于没有为 studymodel 字段使用的是默认的分词器,默认分词器会将我们前面插入的 “201002” 索引为一个词,所以需要全部匹配才能搜索到。

通过测试发现:namedescription 都支持全文检索,pic 不可作为查询条件。

2、keyword 关键词字段

上边介绍的 text 文本字段在映射时要设置分词器,keyword 字段为关键字字段,通常搜索 keyword 是按照整体搜索,所以创建 keyword 字段的索引时是不进行分词的,比如:邮政编码、手机号码、身份证等。keyword 字段通常用于过虑、排序、聚合等。

测试

创建一个新的索引库进行测试,或者删除原来的,这里我们创建一个新的

1、创建索引库 xc_course2

PUT http://10.1.1.168:9200/xc_course2

{
  "settings":{
  "index":{
      "number_of_shards":1,
      "number_of_replicas":0
   }    
  }
}

2、创建映射

这里我们将 namestudymodel 这两个字段的 type 设置为 keyword

POST http://10.1.1.168:9200/xc_course2/doc/_mapping

{
    "properties": {
        "name": {
            "type": "keyword"
        },
        "description": {
            "type": "text"
        },
        "studymodel": {
            "type": "keyword"
        }
    }
}

3、创建文档

创建映射后,我们创建文档数据用于测试

POST http://10.1.1.168:9200/xc_course2/doc/4028e58161bcf7f40161bcf8b77c0000

{
    "name": "java编程基础",
    "description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
    "studymodel": "201001"
}

4、测试查询

GET http://localhost:9200/xc_course2/_search?q=name:java

查询结果如下

{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 0,
        "max_score": null,
        "hits": []
    }
}

namekeyword 类型,所以查询方式是精确查询,所以查询不到任何数据。

下面我们尝试一下精确的查询

GET http://10.1.1.168:9200/xc_course2/_search?q=name:java编程基础

查询结果如下

{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.2876821,
        "hits": [
            {
                "_index": "xc_course2",
                "_type": "doc",
                "_id": "4028e58161bcf7f40161bcf8b77c0000",
                "_score": 0.2876821,
                "_source": {
                    "name": "java编程基础",
                    "description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
                    "studymodel": "201001"
                }
            }
        ]
    }
}

查询成功。

3、date 日期类型

日期类型不用设置分词器,通常日期类型的字段用于排序。

format 属性

通过 format 设置日期格式

例子:

下边的设置允许 date字段存储年月日时分秒、年月日及毫秒三种格式。

POST: http://10.1.1.168:9200/xc_course/doc/_mapping

{
    "properties": {
        "timestamp": {
	        "type": "date",
	        "format": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd"
        }
    }
}

插入文档:

POST: http://localhost:9200/xc_course/doc/00002

{ 
    "name": "spring开发基础",
    "description": "spring 在java领域非常流行,java程序员都在用。",
    "studymodel": "201001",
    "pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
    "timestamp":"2018‐07‐04 18:28:58"
}

查询测试

GET http://10.1.1.168:9200/xc_course/_search?q=name:开发

4、数值类型

下边是ES支持的数值类型

1、尽量选择范围小的类型,提高搜索效率

2、对于浮点数尽量用 比例因子,比如一个价格字段,单位为元,我们将比例因子设置为 100这在 ES 中会按 分 存储,映射如下

"price": {
    "type": "scaled_float",
    "scaling_factor": 100
},

由于比例因子为100,如果我们输入的价格是 23.45ES 中会将 23.45 乘以 100 存储在ES中。

如果输入的价格是 23.456,ES 会将 23.456 乘以 100 再取一个接近原始值的数,得出 2346

使用比例因子的好处是 整型比浮点型更易压缩,节省磁盘空间。

如果比例因子不适合,则从下表选择范围小的去用:

更新已有映射,并插入文档:

POST: http://localhost:9200/xc_course/doc/3

{ 
	"name": "spring开发基础",
    "description": "spring 在java领域非常流行,java程序员都在用。",
    "studymodel": "201001",
    "pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
    "timestamp":"2018‐07‐04 18:28:58",
    "price":38.6
}

5、综合例子

1、发送 DELETE 请求到 http://10.1.1.168:9200/xc_course

2、创建,发送PUT请求 http://localhost:9200/xc_course 索引库名称

{
  "settings":{
  "index":{
      "number_of_shards":1,
      "number_of_replicas":0
   }    
  }
}

3、创建如下映射

post:http://localhost:9200/xc_course/doc/_mapping

{
    "properties": {
        "name": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
        },
        "description": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
        },
        "pic": {
            "type": "text",
            "index": false
        },
        "studymodel": {
            "type": "text"
        }
    }
}

4、插入文档

POST: http://localhost:9200/xc_course/doc/1

{
    "name": "Bootstrap开发",
    "description": "Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
    "studymodel": "201002",
    "price": 38.6,
    "timestamp": "2018-04-25 19:11:35",
    "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAA Jx5ZjNDEM428.jpg"
}

5、搜索测试

GET: http://10.1.1.168:9200/xc_course/doc/_search?q=name:开发

六、索引管理(Java API)

0x01 搭建工程

1、ES客户端简介

ES提供多种不同的客户端:

TransportClient

ES提供的传统客户端,官方计划8.0版本删除此客户端。

RestClient

RestClient 是官方推荐使用的,它包括两种:Java Low Level REST ClientJava High Level REST Client

ES在 6.0 之后提供 Java High Level REST Client, 两种客户端官方更推荐使用 Java High Level REST Client,不过当前它还处于完善中,有些功能还没有。

文章中准备采用 Java High Level REST Client,如果它有不支持的功能,则使用 Java Low Level REST Client

添加依赖:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch‐rest‐high‐level‐client</artifactId>
        <version>6.8.8</version>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
    <version>6.8.8</version>
</dependency>

2、创建搜索工程

创建搜索工程(maven工程):xc-service-search,添加 RestHighLevelClient 依赖及junit 依赖。

1)完整依赖文件如下

pom.xml

<?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">
    <parent>
        <artifactId>xc-framework-parent</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../xc-framework-parent/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>xc-service-search</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-model</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>6.8.8</version>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>6.8.8</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>
</project>

2)配置文件

application.yml

server:
  port: ${port:40100}
spring:
  application:
    name: xc-search-service
xuecheng:
  elasticsearch:
    hostlist: ${eshostlist:127.0.0.1:9200} #多个结点中间用逗号分隔

3)配置类

创建 com.xuecheng.search.config 包在其下创建配置类

package com.xuecheng.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class ElasticsearchConfig {

    @Value("${xuecheng.elasticsearch.hostlist}")
    private String hostlist;

    /**
     * restHighLevelClient
     * @return
     */
    @Bean
    public RestHighLevelClient restHighLevelClient(){
        //解析hostlist配置信息
        String[] split = hostlist.split(",");
        //创建HttpHost数组,其中存放es主机和端口的配置信息
        HttpHost[] httpHostArray = new HttpHost[split.length];
        for(int i=0;i<split.length;i++){
            String item = split[i];
            httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");
        }
        //创建RestHighLevelClient客户端
        return new RestHighLevelClient(RestClient.builder(httpHostArray));
    }

    /**
     * 项目主要使用RestHighLevelClient,对于低级的客户端暂时不用
     * @return
     */
    @Bean
    public RestClient restClient(){
        //解析hostlist配置信息
        String[] split = hostlist.split(",");
        //创建HttpHost数组,其中存放es主机和端口的配置信息
        HttpHost[] httpHostArray = new HttpHost[split.length];
        for(int i=0;i<split.length;i++){
            String item = split[i];
            httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");
        }
        return RestClient.builder(httpHostArray).build();
    }

}

4、启动类

package com.xuecheng.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;

/**
 * @author Administrator
 * @version 1.0
 **/
@SpringBootApplication
@EntityScan("com.xuecheng.framework.domain.search")//扫描实体类
@ComponentScan(basePackages={"com.xuecheng.api"})//扫描接口
@ComponentScan(basePackages={"com.xuecheng.search"})//扫描本项目下的所有类
@ComponentScan(basePackages={"com.xuecheng.framework"})//扫描common下的所有类
public class SearchApplication {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(SearchApplication.class, args);
    }
}

0x02 创建索引库

1、API

创建索引:

PUT: http://localhost:9200/索引名称

{
    "settings":{
        "index":{
            "number_of_shards":1, #分片的数量
            "number_of_replicas":0 #副本数量
        },
    },
}

创建映射:

http://localhost:9200/索引库名称/类型名称/mapping

创建类型为 xc_course 的映射,共包括三个字段:name、description、studymodel

PUT: http://localhost:9200/xc_course/doc/_mapping

{
    "properties": {
        "name": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
        },
        "description": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart"
        },
        "studymodel": {
            "type": "keyword"
        },
        "price": {
            "type": "float"
        },
        "timestamp": {
            "type": "date",
            "format": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis"
        }
    }
}

2、Java Client

package com.xuecheng.search;

import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.client.IndicesClient;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentType;
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.junit4.SpringRunner;

import java.io.IOException;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestIndex {
    @Autowired
    RestHighLevelClient restHighLevelClient;

    @Autowired
    RestClient restClient;

    //创建索引库
    @Test
    public void testCreateIndex() throws IOException {
        //创建索引请求对象
        CreateIndexRequest createIndexRequest = new CreateIndexRequest("xc_course");

        //设置索引参数
        createIndexRequest.settings(Settings.builder()
                .put("number_of_shards",1)
                .put("number_of_replicas",0)
        );

        //设置索引库的映射
        //这里的映射数据直接前面测试API的JSON字符串
        createIndexRequest.mapping("doc","{\n" +
                "    \"properties\": {\n" +
                "        \"name\": {\n" +
                "            \"type\": \"text\",\n" +
                "            \"analyzer\": \"ik_max_word\",\n" +
                "            \"search_analyzer\": \"ik_smart\"\n" +
                "        },\n" +
                "        \"description\": {\n" +
                "            \"type\": \"text\",\n" +
                "            \"analyzer\": \"ik_max_word\",\n" +
                "            \"search_analyzer\": \"ik_smart\"\n" +
                "        },\n" +
                "        \"studymodel\": {\n" +
                "            \"type\": \"keyword\"\n" +
                "        },\n" +
                "        \"price\": {\n" +
                "            \"type\": \"float\"\n" +
                "        },\n" +
                "        \"timestamp\": {\n" +
                "            \"type\": \"date\",\n" +
                "            \"format\": \"yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis\"\n" +
                "        }\n" +
                "    }\n" +
                "}",XContentType.JSON);
        //操作客户端
        IndicesClient indices = restHighLevelClient.indices();
        //创建响应对象
        CreateIndexResponse createIndexResponse = indices.create(createIndexRequest);
        //得到响应结果
        boolean shardsAcknowledged = createIndexResponse.isShardsAcknowledged();
        System.out.println(shardsAcknowledged);
    }
}

运行后成功创建映射

0x03 添加文档

1、API

格式如下: PUT /{index}/{type}/{id} { "field": "value", ... }

如果不指定 idES 会自动生成。

一个例子:
put http://localhost:9200/xc_course/doc/3

{ 
    "name":"spring cloud实战",
    "description":"本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring
    Boot 4.注册中心eureka。",
    "studymodel":"201001"
    "price":5.6
}

2、Java Client

/**
 * 添加文档
 */
@Test
public void testAddDoc() throws IOException {
    //准备json数据
    Map<String, Object> jsonMap = new HashMap<>();
    jsonMap.put("name", "spring cloud实战");
    jsonMap.put("description", "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。");
    jsonMap.put("studymodel", "201001");
    SimpleDateFormat dateFormat =new SimpleDateFormat("yyyy‐MM‐dd HH:mm:ss");
    jsonMap.put("timestamp", dateFormat.format(new Date()));
    jsonMap.put("price", 5.6f);
    //索引请求对象
    IndexRequest indexRequest = new IndexRequest("xc_course","doc");
    //指定索引文档内容
    indexRequest.source(jsonMap);
    //索引响应对象
    IndexResponse indexResponse = restHighLevelClient.index(indexRequest);
    //获取响应结果
    DocWriteResponse.Result result = indexResponse.getResult();
    System.out.println(result);
}

0x04 查询文档

1、API

格式如下: GET /{index}/{type}/{id}

2、Java Client

/**
 * 查询文档
 */
@Test
public void getDoc() throws IOException {
    GetRequest getRequest = new GetRequest(
            "xc_course",
            "doc",
            "eWb_kXEB39nW0xAzs9Pd"
    );
    GetResponse getResponse = restHighLevelClient.get(getRequest);
    boolean exists = getResponse.isExists();
    if(!exists){
        System.out.println("文档不存在");
    }else{
        Map<String, Object> sourceAsMap = getResponse.getSourceAsMap();
        System.out.println(sourceAsMap);
    }
}

0x05 更新文档

1、API

ES 更新文档的顺序是:先检索到文档、将原来的文档标记为删除、创建新文档、删除旧文档,创建新文档就会重建索引。

通过请求 Url 有两种方法:

1)完全替换

POST:http://localhost:9200/xc_test/doc/3

{ 
	"name":"spring cloud实战",
    "description":"本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战SpringBoot 4.注册中心eureka。",
    "studymodel":"201001",
    "price":5.6
}

2)局部更新

下边的例子是只更新 price 字段。

post: http://localhost:9200/xc_test/doc/3/_update

{
	"doc":{"price":66.6}
}

2、Java Client

使用 Client Api 更新文档的方法同上边第二种局部更新方法。
可以指定文档的部分字段也可以指定完整的文档内容。

/**
 * 更新文档
 */
@Test
public void updateDoc() throws IOException {
    UpdateRequest updateRequest = new UpdateRequest("xc_course", "doc","eWb_kXEB39nW0xAzs9Pd");
    Map<String, String> map = new HashMap<>();
    map.put("name", "spring cloud alibaba");
    updateRequest.doc(map);
    UpdateResponse update = restHighLevelClient.update(updateRequest);
    RestStatus status = update.status();
    System.out.println(status);
}

0x06 删除文档

1、API

根据 id 删除,格式如下:

DELETE: /{index}/{type}/{id}

搜索匹配删除,将搜索出来的记录删除,格式如下:

POST: /{index}/{type}/_delete_by_query

下边是搜索条件例子:

{
    "query":{
        "term":{
        "studymodel":"201001"
        }
    }
}

上边例子的搜索匹配删除会将 studymodel201001 的记录全部删除。

2、Java Client

//根据id删除文档
@Test
public void testDelDoc() throws IOException {
    //删除文档id
    String id = "eqP_amQBKsGOdwJ4fHiC";
    //删除索引请求对象
    DeleteRequest deleteRequest = new DeleteRequest("xc_course","doc",id);
    //响应对象
    DeleteResponse deleteResponse = client.delete(deleteRequest);
    //获取响应结果
    DocWriteResponse.Result result = deleteResponse.getResult();
    System.out.println(result);
}

搜索匹配删除还没有具体的 api,可以采用先搜索出文档 id,根据文档 id 删除。

😁 认识作者

作者:👦 LCyee ,一个向往体面生活的代码🐕

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

CSDN 博客:https://blog.csdn.net/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://codeyee.com/archives/xuecheng-day10-elasticsearch.html