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

PDF与OFD电子发票解析技术实战:从格式转换到精准识别 本文深入探讨了PDF与OFD电子发票解析技术的实战应用,从格式转换到精准识别,提供了全面的解决方案。通过OFD转PDF技术、动态定位算法和多重校验机制,显著提升电子发票识别效率与准确率,助力企业财务数字化转型。 阅读详情

😎 知识点概览

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

本章节为【学成在线】项目的 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、配置课程发布页面参数

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

向 mongodb 的 cms_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、启动 cms 、course 服务
3、启动 cms_client,注意配置 routingKey 和队列名称,routingKey 为所使用的站点id

4、这里我们分别在 course 、cms、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

访问页面看下效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W09QhrNt-1595564922590)(https://qnoss.codeyee.com/20200704_10/image11)]

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

  })
},

测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gose1lTQ-1595564922592)(https://qnoss.codeyee.com/20200704_10/image12)]

二、初识 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 ,那么 es 和 solr 选择哪个?

如果你公司现在用的 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 集群从索引库检索数据。

0x02 安装 Elastic Search

1、安装

安装配置:

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

2、支持tar、zip、rpm等多种安装方式。在 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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AkFK1mze-1595564922618)(https://qnoss.codeyee.com/20200704_10/image19)]

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

config:配置文件目录

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

logs:日志目录

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

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

2、配置文件

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

1、使用 zip、tar 安装,配置文件的地址在安装目录的 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.data 和 path.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 的过程,下边是document 和 field 与关系数据库的概念的类比:

文档(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 的映射,共包括三个字段:name、description、studymondel

由于 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_word 和 ik_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 分词模式,所以 ES 的 IK 插件会进行更 细粒度 的识别。

五、映射

上边章节安装了 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 文本字段

字符串包括 text 和 keyword 两种类型

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。删除索引,重新创建映射,将 pic 的 index 设置为 false,尝试根据 pic 去搜索,结果搜索不到数据

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

store 属性

是否在 source 之外存储,每个文档索引后会在 ES 中保存一份原始文档,存放在 _source 中,一般情况下不需要设置 store 为 true,因为在 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” 索引为一个词,所以需要全部匹配才能搜索到。

通过测试发现:name 和 description 都支持全文检索,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、创建映射

这里我们将 name 和 studymodel 这两个字段的 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": []
    }
}

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

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

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.45 则 ES 中会将 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 Client 和 Java 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", ... }

如果不指定 id,ES 会自动生成。

一个例子:
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"
        }
    }
}

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

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

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

优快云 博客:https://blog.youkuaiyun.com/codeyee

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

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

线性代数——(期末突击)行列式(上)-行列式计算、行列式的性质 简单解释一下:4213原本的顺序应为1234,对于‘4’而言,‘2’、‘1’、‘3’都应该排在它的前面,所以此处记逆序数为3;对于‘2’而言,‘1’应该排在它的前面,而‘3’排在它之后 是合理的,所以此处只有一个逆序数;最后看‘1’,其后面的‘3’排在后面显然也是合理的,故而4213的逆序数为4.一般我们使用这个性质来计算四阶行列式,也就是将其变换成三角行列式,再计算对角线的值;以主对角线为分界线的就为正三角,反之则为反三角行列式。若行列式某一行(列)为0,则该行列式=0.将和的那一行分开,其余行。 阅读详情

相关推荐

聚类模型-层次聚类

聚类模型 1、层次聚类 2、原型聚类-K-means 3、模型聚类-GMM 4、EM算法-LDA主题模型 5、密度聚类-DBSCAN 6、图聚类-谱聚类 一、层次聚类 一、聚类理论 ​ 一般来说,聚类是在训练样本的标签信息不知的情况下,学习样本内在的性质和规律,将有限的集合划分成ccc类。根据“方以类聚,物以群分”的思想,类内对象尽可能的相似,...

myazi 6504

1-学成在线项目开发环境配置

学成在线项目开发环境配置

AN_NI_112的博客 1758

linux 4.19在龙芯平台上编译出错解决方法

OS: Loongnix server 1.7 工具链为:系统自带的gcc-4.8.5 和 gcc-7.3 平台:3A3000/3A4000 源码:kernel: 4.19 编译步骤 ··· make loongson3_defconfig make -j4 ··· 出错信息: HOSTCC arch/mips/boot/compressed/calc_vmlinuz_load_addr arch/mips/boot/compressed/head.S: Assembler messages: arch/m

sense的专栏 1124

《学成在线》微服务实战项目实操笔记系列(P1~P83)【上】

《学成在线》项目实操笔记系列【下】,跟视频的每一P对应,全系列12万字,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。

RuanFun的博客 2万+

【软件逆向】带壳带反调试找flag教程(VMProtect3.0+X64dbg+ScyllaHide)

【软件逆向】带壳带反调试找flag教程(VMProtect 3.0+X64dbg+ScyllaHide)

zhangjiuding的博客 7510

从网格划分到结果解读:一次完整的子弹侵彻LS-DYNA仿真避坑指南

本文详细解析了子弹侵彻LS-DYNA仿真中的关键技术与避坑指南,涵盖网格优化、对称边界条件设置、沙漏控制参数调整及结果可信度验证。通过HyperMesh网格划分和LS-DYNA高级参数配置,帮助工程师提升仿真精度与效率,确保计算结果物理可信。

weixin_29816689的博客 406

ElasticSearch启动错误锦集

1.Exception in thread "main" java.lang.UnsupportedClassVersionError: org/elasticsearch/common/transport/TransportAddress : Unsupported major.minor version 52.0 /Library/Java/JavaVirtualMachines/jdk1...

king的专栏 3万+

一文帮你彻底搞懂ARM Debug Interface之SWD(转)

ARM的文档写的已经很好了,但是关于上电以后的第一时间应该怎么操作,依然写的不够清晰,导致我第一次用的时候还是费了一些周折。今天做一个详细的梳理,希望能够帮助到更多的朋友。BTW, ARM_Cortex-M0/3 DesignStart系列会持续更新,只是中间可能会穿插其他知识点。SWD是Serial Wire Debug的简称,翻译成中文是”串行线调试”。

2302_79847220的博客 6861

学成在线--课程发布模块

课程信息编辑完毕即可发布课程,发布课程相当于一个确认操作,课程发布后,学习者通过学成在线平台的网站可以搜索到课程,然后查看课程的详细信息,选课进行学习。下面是课程编辑与发布的整体流程为了保证课程内容没有违规信息、课程内容安排合理,在课程发布之前,平台运营方会进行课程审核,审核通过后,课程方可发布作为课程制作方即教学机构,在课程发布前通过课程预览功能可以看到课程发布后的效果,若课程信息存在问题,方便查看,及时修改。

qq_33888850的博客 1983

【S32K3 MCAL配置】-4.1-CAN Driver:如何解决CAN帧发送丢帧问题

【S32K3 MCAL配置】-4.1-CAN Driver:如何解决CAN帧发送丢帧问题

「汽车电子助手」的博客 1442

8.Python基础语法(五)续【光说不练假把式,上题】

Python基础语法(二) 有关于Python循环语句的几道实操,让大家理解知识点更深更透彻~

孤寒者的博客 2万+

【黑马学成在线2023版】解决P7创建父工程时Maven的pom文件依赖爆红问题(亲测有效)

笔者此处使用的 Maven 版本是 3.8.6 ,直接粘贴黑马老师的 pom 文件中的依赖会出现依赖的 和 爆红,显示无法找到依赖的错误。但是基础工程的 `pom` 文件却正常。本文笔者提供两种解决方案。

Sihang_Xie的博客 2678

Spring AI Alibaba实战训练营-16 NL2SQL (自然语言转 SQL)应用开发指南

本文介绍了一个基于AI的智能BI应用开发项目,主要实现自然语言转SQL(NL2SQL)功能,包含对话模块、MCP模块、向量数据管理模块。项目采用Spring AI Alibaba框架,集成通义千问大模型,支持内存向量库和AnalyticDB向量数据库两种存储方案。重点展示了chat模块的三个控制器实现:AnalyticNl2SqlController用于生产环境,SimpleChatController适用于开发测试,Nl2sqlForGraphController支持基于图的复杂对话流程。系统通过将业务知

program哲学 1587

微服务[学成在线] day11:基于 ElasticSearch 构建搜索服务

???? 知识点概览 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。 本章节为【学成在线】项目的 day11 的内容 基于 Java 客户端实现 DSL 搜索 搭建 ElasticSearch 集群环境 使用 Logstash 自动创建 ElasticSearch 的索引、数据文档 基于 ElasticSearch 开发搜索服务接口 目录 内容会比较多,小伙伴们可以根据目录进行

通往体面生活的路上 778

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

微服务[学成在线] day15:媒资管理系统集成

???? 知识点概览 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。 本章节为【学成在线】项目的 day15 的内容 根据 课程ID 搜索该课程已发布的课程信息,并返回该课程的所有课程计划信息。 将指定课程 发布时 所的课程计划的媒资信息保存到 teachplan_media_publish 表中, 根据 课程计划id 搜索该课程计划所对应的媒资信息,需要用到的是该课程计划对应的

通往体面生活的路上 639

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

微服务[学成在线] day02:CMS前端开发

???? 写在前面 我为什么要花时间去写这个笔记? 由于在学习该项目的过程中发现了原讲义存在的一些问题,所以该笔记基于「学成在线」微服务项目 PDF 讲义进行编写,并且投入了时间去优化了笔记的格式、代码的高亮、重点的标记等。 以及一些原讲义中所描述的一些知识点使我无法理解的内容,我会对这些内容的表达方式进行修改或者提出一些问题,并且用我自己所理解的一些想法去重新的解释这个问题。 总结一下原 PDF 讲义中已知的一些问题: 从 PDF 中复制出来的代码,部分特殊符号的编码有问题,并且不易被发现,例如横杠

通往体面生活的路上 614

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

微服务[学成在线] day04:页面静态化

???? 知识点概览 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。 本章节为【学成在线】项目的 day04 的内容 页面静态化的基本概念 Freemarker 框架基础入门 使用 MongoDB 提供的 GridFS 来实现页面数据的分布式储存 使用 freemarker 框架实现页面静态化以及发布 目录 内容会比较多,小伙伴们可以根据目录进行按需查阅。 文章目录????

通往体面生活的路上 555

微服务[学成在线] day12:基于 Nuxt.js 构建搜索前端工程

???? 知识点概览 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。 本章节为【学成在线】项目的 day12 的内容 Nuxt.js 的基本使用 基于 Nuxt.js 开发搜索门户前端 目录 知识点结合实战应用会更有意义,所以这里我就不再对单个知识点进行拆分成单个笔记,内容会比较多,这里我们可以根据目录进行按需阅读。 文章目录???? 知识点概览目录一、搜索前端技术需求0x01 什

通往体面生活的路上 698

最新传智学成在线JAVAEE微服务项目实战视频教程

目录 ├─day01 搭建环境 CMS服务端开发 ├─day02 CMS前端开发 ├─day03 CMS页面管理开发 ├─day04 页面静态化 ├─day05 消息中间件RabbitMQ ├─day06 页面发布 课程管理 ├─day07 课程管理实战 ├─day08 课程图片管理 分布式文件系统 ├─day09 课程预览 Eureka Feign ├─day10 课程发布 ElasticSea...

u011576602的博客 992

微服务实战笔记-学成在线-day02

知识点: vue webpack vue中的跨域解决方案-proxyTable vue MVVM MVVM拆分解释为: Model:负责数据存储 View:负责页面展示 View Model:负责业务逻辑处理(比如Ajax请求等),对数据进行加工后交给视图展示 MVVM要解决的问题是将业务逻辑代码与视图代码进行完全分离,使各自的职责更加清晰,后期代码维护更加简单 用图解的形式分析Ajax请求回...

sssdal19995的博客 349

学成在线--day01 搭建环境 CMS服务端开发

1.1 项目背景受互联网+概念的催化,当今中国在线教育市场的发展可谓是百花齐放、如火如荼。按照市场领域细分为:学前教 育、K12教育、高等教育、留学教育、职业教育、语言教育、兴趣教育以及综合平台,其中,职业教育和语言教育 的市场优势突出。根据Analysys易观发布的数据显示,预计2019年中国互联网教育市场交易规模将达到3718亿元 人民币,未来三年互联网教育市场规模保持高速增长。

you4580的专栏 221

微服务实战笔记-学成在线-day08

分布式文件管理系统FastDFS fastdfs基础知识 ​ fastdfs 是一个分布式文件管理系统,它与传统的分布式文件管理系统不同,它不对文件进行分块存储(前面使用到的gridfs会对文件进行切割),所以在小文件的存储和读取上,它的性能十分优越。 架构 tracker(追踪/调度服务器) ​ tracker的作用是负载均衡和调度,应用程序通过请求调度器来获取哪个storage为本次请求服务。 storage ​ storage的作用是存储。值得注意的是storage并没有实现自己的文件管理系统而是直

sssdal19995的博客 325

学成在线小问题

为了保证任务不重复执行,如果使用synchronized关键字,由于此项目是分布式架构,采用分布式任务执行器来执行任务,而执行器部署在不同jvm中,而synchronized只能锁本地jvm,也就是本地锁,对其他jvm没有用。利用数据库主键唯一性的特点,或利用数据库唯一索引、行级锁的特点,比如:多个线程同时向数据库插入主键相同的同一条记录,谁插入成功谁就获取锁,多个线程同时去更新相同的记录,谁更新成功谁就抢到锁。事务控制问题:如果是开启事务的方法调用非事务方法,那么非事务方法也会被事务控制。

yangsir_javaeasy的博客 1018

微服务实战学成在线day04——(一)FreeMarker的使用

本章介绍了freemarker的基本使用。

聂泳的博客 478

微服务实战学成在线day02——(三)CMS前端开发

本小节为CMS前端开发,涉及Element-ui,vue.js,proxyTable等技术。

聂泳的博客 514

微服务实战笔记-学成在线-day07

知识点 springboot中实体字段为dType 传到前台json为dtype的原因与解决方案 vue中watch的基本用法 element-ui中MessageBox的自定义校验 mybatis中collection的用法 1. springboot中实体字段为dType 传到前台json为dtype的原因与解决方案 原因 springboot 使用fastjson 在对象的序列化和反序列化的时候对字段进行了处理 解决方案 在bean的get方法上添加@JsonProperty注解 @Data @T

sssdal19995的博客 304
上一篇: 微服务[学成在线] day09:Eureka、Feign、课程预览实现
下一篇: 微服务[学成在线] day11:基于 ElasticSearch 构建搜索服务
cory_lin
博客等级 码龄7年 22粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值