系列文章目录
🥥Vue3+SpringBoot+Elasticsearch+ik分词实现分词搜索功能
🍉Vue3+Ts+SpringBoot+Redis实现发送QQ邮箱注册功能
🍎Vue3+SpringBoot+MySql使用Dplay.js实现弹幕功能
文章目录
前言
在做期末大作业项目的过程中,因为我做的是视频网站所以是少不了搜索视频的功能的,但是简单的模糊查询感觉不够亮点,所以我使用了
Elasticsearch搜索和数据分析引擎去实现复杂的搜索功能,并且记录我的项目开发过程以及分享给大家。附上官网链接:https://www.elastic.co/cn/elasticsearch
Elasticsearch 是一个开源的分布式 RESTful 搜索和分析引擎,几乎实时搜索。

这里附上效果图😁
一、下载Elasticsearch
我使用的Elasticsearch版本是:7.17.0
1.所有版本下载地址
Elasticsearch所有版本下载地址:>所有版本下载地址<
2.版本7.17.0下载地址
Elasticsearch7.17.0下载地址:>7.17.0下载地址<
二、下载Kibana
为了方便使用Elasticsearch我们可以下载他的可视化平台kibana。
注意:版本需要和Elasticsearch下载的版本一致
我使用的Kibana版本是:7.17.0
1.所有版本下载地址
Kibana所有版本下载地址:>所有版本下载地址<
2.版本7.17.0下载地址
Kibana7.17.0下载地址:>7.17.0下载地址<
三、安装Elasticsearch和Kibana
1.解压Elasticsearch
下载好之后,分别解压到建议是D盘,如图所示:

2.运行Elasticsearch
进入到文件里,打开bin目录,双击启动Elasticsearch,如图所示:

3.访问Elasticsearch
运行完之后,浏览器输入http://localhost:9200/如果返回了以下格式的数据,说明Elasticsearch启动完成,如图所示:

4.Kibana同样操作
解压完成之后,进入bin目录打开kibana.bat启动,运行完成之后浏览器输入http://localhost:5601/app/dev_tools#/console,访问成功就可以在控制台左边写我们的DSL了,点击右边的运行按钮即可直接执行查询语句,如图所示:

四、下载ik分词插件
分词就是把一段中文或者别的划分成一个个的关键字,我们在搜索时会把自己的信息进行分词,会把数据库中或者索引库中的数据进行分词,然后进行一个匹配的操作,默认的中文分词器是将每一个字看成一个词,比如“我爱中国”会被分成“我”、“爱”、“中”、“国”,这显然是不符合要求的,所以我们需要安装中文分词器IK来解决这个问题!
IK分词器的分词算法有:
•ik__smart 最少切分
•ik_max_word 最细粒度划分
注意:版本需要和Elasticsearch下载的版本一致
我使用的ik分词插件版本是:7.17.0
1.所有版本下载地址
ik分词插件所有版本下载地址:>所有版本下载地址<
2.版本7.17.0下载地址
ik分词插件7.17.0下载地址:>7.17.0下载地址<
3.解压ik分词插件
需要把下载完成的压缩包解压到Elasticsearch目录下的plugins目录下的ik里面,解压完成之后,需要重新启动Elasticsearch,如图所示:

4.查看ik分词插件
然后我们可以在Kibana控制台里面输入GET /_cat/plugins?v查询所有插件,查看是否安装成功,如果显示了插件则安装成功,如图所示:

五、功能实现
1.SpringBoot导入pom依赖
1.先引入spring boot
pom.xml依赖
注意:
Elasticsearch 7.x:推荐使用 Spring Data Elasticsearch 4.4.x 或 4.3.x,这些版本都与 Spring Boot 2.6.x 兼容。
Elasticsearch 8.x:推荐使用 Spring Data Elasticsearch 5.x, 与Spring Boot 2.7.x 或更高版本 兼容。
我的spring boot版本为:2.6.6
<!--elasticsearch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.6.6</version> <!-- 根据你的 Spring Boot 版本来选择 -->
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>4.4.6</version> <!-- 与 Spring Boot 2.6.x 兼容的版本 -->
</dependency>
2.配置application.yml文件
# ElasticSearch 设置
elasticsearch:
# 设置 Elasticsearch 集群的名称
cluster-name: my-elasticsearch-cluster
# 设置 Elasticsearch 节点的地址,通常是主节点的 IP 地址和端口
cluster-nodes: 127.0.0.1:9200
# 启用检查机制,确保 Elasticsearch 服务可用
check-enabled: true
# Elasticsearch 认证用户名,默认用户名为 elastic
username: elastic
# Elasticsearch 认证密码
password: rqMnjy6CT+x2CUqSrXnl
3.前端示例代码
• 搜索页面示例代码
关键字内容高亮是通过正则表达式匹配实现的
<template>
<!-- 这里只是一个视频卡片的代码 -->
<div class="list">
<div class="video-list" v-loading="loadingVideoList">
<div class="item" v-for="item in searchResults">
<div class="video-card">
<div class="video-card-wrap">
<router-link :to="'/Video?id=' + item.id">
<div class="image">
<div class="image-warp">
<div class="cover">
<img class="img" :src="item.coverUrl" alt="">
</div>
</div>
<div class="mask">
<div class="stats">
<div class="wrap">
<div class="left">
<span class="item">
<img class="icon" src="/src/assets/searchMain/play.svg" alt="">
<span class="text">{{ item.views
}}</span>
</span>
<span class="item">
<img class="icon" src="/src/assets/searchMain/danmu.svg" alt="">
<span class="text">{{ item.danmus
}}</span>
</span>
</div>
<span class="duration">
07:18:03
</span>
</div>
</div>
</div>
</div>
</router-link>
<div class="info">
<div class="wrap">
<router-link :to="'/Video?id=' + item.id">
<h3 :title="item.title" class="title">
<!-- 使用高亮后的描述 -->
<span v-html="highlightedDescription(item.title)"></span>
</h3>
</router-link>
<p class="bottom">
<router-link class="owner" to="/">
<img class="icon" src="/src/assets/searchMain/owner.svg" alt="">
<span class="author">{{ item.nikename }}</span>
<span class="date"> · {{ item.createTime }}</span>
</router-link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { $searchVideo } from '../api/video.ts'
// 定义一个响应式的字符串,用来存储用户输入的搜索关键字
const searchValue = ref<string>('');
// 定义一个响应式的数组,用来存储搜索结果
const searchResults = ref<any[]>([]);
// 定义一个响应式的布尔值,表示加载状态
const loadingVideoList = ref(false);
// 搜索按钮点击事件的处理函数
const handleSearch = () => {
// 判断搜索框是否为空,如果为空则提示用户输入关键字
if (searchValue.value === '') {
// 使用 Element UI 的消息提示组件,显示警告信息
ElMessage.warning('请输入关键字');
return; // 如果没有输入关键字,直接返回,不进行搜索
}
// 设置加载状态为 true,表示开始加载数据
loadingVideoList.value = true;
// 调用搜索接口,传入用户输入的搜索关键字(searchValue.value),并设置为空的描述(第二个参数为空字符串)
$searchVideo(searchValue.value, '').then(res => {
// 如果请求成功,且返回的数据是有效的(res.success 为 true)
if (res.success) {
// 将返回的搜索结果(res.result)存储到 searchResults 中
searchResults.value = res.result;
}
});
// 模拟加载的延时,500毫秒后将加载状态设为 false
setTimeout(() => {
// 设置加载状态为 false,表示加载完成
loadingVideoList.value = false;
}, 500); // 模拟一个加载过程,实际中可以根据真实的接口响应时间来设置
};
// 高亮显示
const highlightedDescription = (description: string) => {
const keyword = videoSearchStore.keyword;
if (!keyword) return description;
// 将关键字按字符拆分(例如:"原神" -> ["原", "神"])
const keywords = keyword.split('');
// 构造正则表达式来匹配每个部分
const regex = new RegExp(keywords.join('|'), 'gi'); // 使用 | 将每个部分连接起来,形成匹配任意一个的正则
// 使用正则替换,将匹配到的部分包裹在 <em> 标签中
const result = description.replace(regex, (match) => {
return `<em class="keyword">${match}</em>`;
});
return result;
};
</script>
这里是axios的设置以及api的二次封装
// 创建一个 axios 实例,配置了基础 URL、请求超时时间和自定义请求头
const instance = axios.create({
baseURL: 'http://localhost:8080/api/', // 请求的基础URL,所有的请求都会自动加上这个前缀
timeout: 30000, // 设置请求的超时时间,单位为毫秒,这里设置为30秒
headers: { 'X-Custom-Header': 'foobar' } // 自定义请求头,所有请求都会带上这个头部信息
});
// 添加请求拦截器,用于在请求发送前对请求进行处理
instance.interceptors.request.use(
function (config) {
// 请求被发送前可以在此处对请求进行修改,如添加 token 等
return config; // 必须返回 config,否则请求不会继续发送
},
function (error) {
// 请求错误时做一些处理,如网络问题
return Promise.reject(error); // 如果请求失败,返回错误信息
}
);
// 添加响应拦截器,用于在响应返回后对数据进行处理
instance.interceptors.response.use(
function (response) {
// 当响应状态码在 2xx 范围内时,会触发此函数
// 在这里可以对响应数据进行处理,比如统一处理返回的数据格式
return response; // 必须返回响应数据,否则后续的代码无法获取到响应内容
},
function (error) {
// 当响应状态码超出 2xx 范围时,触发此函数
if (error.response) {
// 服务器已响应,但状态码非 2xx,通常可以在这里处理错误信息
// 如:显示用户友好的错误提示信息等
} else {
// 没有响应(如网络错误、请求超时等情况)
// 在这里可以处理网络错误,提示用户检查网络
}
return Promise.reject(error); // 返回错误信息,保证请求的链式调用可以继续
}
);
// GET 请求方法,接收 URL 和参数,返回数据
export const $get = async (url: string, params: object = {}) => {
// 使用 axios 实例发送 GET 请求,将参数附加到 URL 中
let { data } = await instance.get(url, { params });
// 解构获取到的 response 中的 data 并返回
return data;
}
// POST 请求方法,接收 URL 和请求参数,返回数据
export const $post = async (url: string, params: object = {}) => {
// 使用 axios 实例发送 POST 请求,请求体中包含传递的参数
let { data } = await instance.post(url, params);
// 解构获取到的 response 中的 data 并返回
return data;
}
import { $get, $post } from '../utils/request.ts'
// 搜索视频
export const $searchVideo = async (title: string, description: string) => {
try {
// 发起 GET 请求
const res = await $get('/pc/video/search', {
title: title,
description: description
});
return res; // 返回接口响应数据
} catch (error) {
// 错误处理
console.error('获取视频信息失败', error);
throw error; // 抛出错误,方便调用者处理
}
};
4.后端示例代码
• 我视频表的实体类
/**
* @Description: 视频表
* @Author: L
* @Date: 2024-11-27
* @Version: V1.0
*/
@Data
@TableName("kd_video")
public class KdVideo implements Serializable {
private static final long serialVersionUID = 1L;
/**主键*/
@TableId(type = IdType.ASSIGN_ID)
private String id;
/**创建人*/
private String createBy;
/**创建日期*/
private Date createTime;
/**更新人*/
private String updateBy;
/**更新日期*/
private Date updateTime;
/**用户id*/
private String userId;
/**标题*/
private String title;
/**简介*/
private String description;
/**地址*/
private String url;
/**封面地址*/
private String coverUrl;
/**类型*/
private Integer type;
/**标签*/
private String tag;
/**分区*/
private Integer subzone;
/**播放量*/
private Integer views;
/**弹幕量*/
private Integer danmus;
/**点赞量*/
private Integer likes;
/**分享量*/
private Integer shares;
/**收藏量*/
private Integer collects;
/**视频状态*/
private Integer status;
}
• 创建Elasticsearch实体类
按照需要查询的实体类,在es实体类创建对应的字段,比如我需要查询我视频表的以下字段
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.io.Serializable;
import java.util.Date;
// 使用 Lombok
@Data
// 标识该类为 Elasticsearch 的文档类,定义索引名称、分片数和副本数
@Document(indexName = "kd_video", shards = 1, replicas = 0)
public class ElasticSearchVideo implements Serializable {
private static final long serialVersionUID = 1L;
@Id // 标识该字段为 Elasticsearch 文档的主键
private String id;
@Field(type = FieldType.Text, analyzer = "custom_ngram_analyzer") // 使用自定义分词器进行文本分析
private String title; // 视频标题,用于搜索时进行文本分析
@Field(type = FieldType.Text, analyzer = "custom_ngram_analyzer") // 使用自定义分词器进行文本分析
private String description; // 视频简介,用于搜索时进行文本分析
@Field(type = FieldType.Text) // 字段类型为文本,不进行特殊分析
private String url;
@Field(type = FieldType.Text) // 字段类型为文本,不进行特殊分析
private String coverUrl;
@Field(type = FieldType.Integer) // 字段类型为整型,表示视频的播放量
private Integer views;
@Field(type = FieldType.Integer) // 字段类型为整型,表示视频的弹幕量
private Integer danmus;
@Field(type = FieldType.Text) // 字段类型为文本,用于存储用户名
private String userName;
@Field(type = FieldType.Text) // 字段类型为文本,用于存储用户 ID
private String userId;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime; // 日期格式字段,指定日期格式
}
• 创建Elasticsearch的服务类
/**
* @Description: 搜索服务类
* @Author: L
* @Date: 2024-12-16
* @Version: V1.0
*/
public interface ElasticSearchService {
/**
* 保存或更新视频到 Elasticsearch
*
* @param elasticSearchVideo
* @return
*/
void saveOrUpdate(ElasticSearchVideo elasticSearchVideo);
/**
* 根据标题或简介进行分词查询
*
* @param title 视频标题
* @param description 视频简介
* @return 匹配的视频列表
*/
List<KdVideo> searchByTitleOrDescription(String title, String description);
}
• 创建Elasticsearch的服务实现类
@Slf4j
@Service
public class ElasticSearchServiceImpl implements ElasticSearchService{
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
private ElasticSearchVideoToKdVideoConverter converter;
@Autowired
private IKdUserService kdUserService;
/**
* 使用 ElasticsearchRestTemplate 保存或更新数据
*/
@Override
public void saveOrUpdate(ElasticSearchVideo elasticSearchVideo) {
try {
// 创建 IndexQuery 对象
IndexQuery indexQuery = new IndexQuery();
indexQuery.setId(elasticSearchVideo.getId()); // 设置文档ID
indexQuery.setObject(elasticSearchVideo); // 设置要索引的对象
// 打印日志,确保索引对象设置正确
log.info("Indexing video with ID: " + elasticSearchVideo.getId());
log.info("Video details: " + elasticSearchVideo);
// 创建 IndexCoordinates 对象,用于指定索引名称
IndexCoordinates indexCoordinates = IndexCoordinates.of("kd_video");
// 使用 ElasticsearchRestTemplate 保存数据到 Elasticsearch
String documentId = elasticsearchRestTemplate.index(indexQuery, indexCoordinates);
// 输出响应结果
log.info("Index created with ID: " + documentId);
} catch (Exception e) {
log.error("Error while indexing video: " + elasticSearchVideo, e);
throw new RuntimeException("Error while indexing video", e);
}
}
/**
* 根据标题或简介进行分词查询
*/
@Override
public List<KdVideo> searchByTitleOrDescription(String title, String description) {
// 创建 Bool 查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (title != null && !title.isEmpty()) {
boolQuery.should(QueryBuilders.matchQuery("title", title)); // 使用分词查询
}
if (description != null && !description.isEmpty()) {
boolQuery.should(QueryBuilders.matchQuery("description", description)); // 使用分词查询
}
// 使用 ElasticsearchRestTemplate 执行查询
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(boolQuery);
// 执行查询,返回 SearchHits
SearchHits<ElasticSearchVideo> searchHits = elasticsearchRestTemplate.search(queryBuilder.build(), ElasticSearchVideo.class);
// 将查询结果转换为 KdVideo
List<KdVideo> result = searchHits.getSearchHits().stream()
.map(hit -> {
// 获取 ElasticSearchVideo 对象
ElasticSearchVideo elasticSearchVideo = hit.getContent();
// 根据 userId 查询 userName
KdUser kdUser = kdUserService.getById(elasticSearchVideo.getUserId());
// 设置 userName、views 和 danmus
elasticSearchVideo.setUserName(kdUser.getNikename());
// 转换为 KdVideo 对象
KdVideo kdVideo = converter.convert(elasticSearchVideo);
// 返回转换后的 KdVideo
return kdVideo;
})
.collect(Collectors.toList());
return result;
}
}
• 创建Elasticsearch的查询接口
/**
* 根据视频标题或简介进行分词查询
*
* @param title 视频标题
* @param description 视频简介
* @return 匹配的视频列表
*/
@GetMapping("/search")
public Result<List<KdVideo>> searchByTitleOrDescription(@RequestParam(required = false) String title,
@RequestParam(required = false) String description) {
List<KdVideo> kdVideos = elasticSearchService.searchByTitleOrDescription(title, description);
return Result.OK(kdVideos);
}
• 创建实体类的转换器
我这里创建了两个类用来进行实体类的相互转换
/**
* 自定义转换器,将 ElasticSearchVideo 转换为 KdVideo
*/
@Component
public class ElasticSearchVideoToKdVideoConverter implements Converter<ElasticSearchVideo, KdVideo> {
@Override
public KdVideo convert(ElasticSearchVideo source) {
if (source == null) {
return null;
}
// 创建 KdVideo 对象并设置属性
KdVideo kdVideo = new KdVideo();
kdVideo.setId(source.getId());
kdVideo.setTitle(source.getTitle());
kdVideo.setDescription(source.getDescription());
kdVideo.setUrl(source.getUrl());
kdVideo.setCoverUrl(source.getCoverUrl());
kdVideo.setViews(source.getViews());
kdVideo.setDanmus(source.getDanmus());
kdVideo.setNikename(source.getUserName());
kdVideo.setUserId(source.getUserId());
kdVideo.setCreateTime(source.getCreateTime());
return kdVideo;
}
}
/**
* 自定义转换器,将 KdVideo 转换为 ElasticSearchVideo
*/
@Component
public class KdVideoToElasticSearchVideoConverter implements Converter<KdVideo, ElasticSearchVideo> {
@Override
public ElasticSearchVideo convert(KdVideo source) {
if (source == null) {
return null;
}
ElasticSearchVideo elasticSearchVideo = new ElasticSearchVideo();
elasticSearchVideo.setId(source.getId());
elasticSearchVideo.setTitle(source.getTitle());
elasticSearchVideo.setDescription(source.getDescription());
elasticSearchVideo.setUrl(source.getUrl());
elasticSearchVideo.setCoverUrl(source.getCoverUrl());
elasticSearchVideo.setViews(source.getViews());
elasticSearchVideo.setDanmus(source.getDanmus());
elasticSearchVideo.setUserName(source.getNikename());
elasticSearchVideo.setUserId(source.getUserId());
elasticSearchVideo.setCreateTime(source.getCreateTime());
return elasticSearchVideo;
}
}
• 关于数据同步问题
我使用了quartz定时任务,进行同步数据库视频表的数据到ElasticSearch中,也可以在Kibana控制台里手动添加数据
/**
* 同步数据定时任务
*
* @Author L
*/
@Slf4j
public class SyncDatabaseToElasticsearchJob implements Job{
@Resource
private IKdVideoService kdVideoService; // 通过 KdVideoService 查询数据库中的视频数据
@Resource
private KdVideoToElasticSearchVideoConverter converter; // 使用 KdVideo 转换器
@Resource
private ElasticSearchService elasticsearchService; // 通过 ElasticsearchService 保存数据到 Elasticsearch
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
// 查询数据库中的所有视频数据
List<KdVideo> kdVideos = kdVideoService.list();
// 将视频数据同步到 Elasticsearch
for (KdVideo kdVideo : kdVideos) {
// 使用转换器将 KdVideo 转换为 ElasticSearchVideo
ElasticSearchVideo elasticSearchVideo = converter.convert(kdVideo); // 转换
// 保存或更新数据到 Elasticsearch
elasticsearchService.saveOrUpdate(elasticSearchVideo); // 保存
log.info("已同步视频数据: " + kdVideo.getTitle());
}
log.info("同步数据库到 Elasticsearch 成功!");
} catch (Exception e) {
log.error("Error during Elasticsearch save or update", e);
log.error("Response details: " + e.getMessage());
}
}
}
5.关于DSL语言
我们先学习
DSL(Domain Specific Language) 语言,它是一种查询语言,它通过 JSON 格式向 Elasticsearch 发起请求,用于执行各种查询、聚合、索引等操作。
(1)创建索引
创建索引,可以指定索引的配置(如分片数、复制数等)以及映射(即字段类型和索引方式)。如果不指定映射,Elasticsearch 会自动为字段推断类型。
•number_of_shards:设置索引的主分片数。
•number_of_replicas:设置索引的副本数。
•mappings:指定字段的类型和索引方式,如 text、keyword、integer、date 等。
PUT /my_index
{
"settings": {
"number_of_shards": 3, # 设置分片数
"number_of_replicas": 2 # 设置副本数
},
"mappings": {
"properties": {
"title": {
"type": "text" # 设置字段类型为文本类型
},
"userId": {
"type": "keyword" # 设置字段类型为关键字类型(不分词)
},
"views": {
"type": "integer" # 设置字段类型为整数
},
"createTime": {
"type": "date" # 设置字段类型为日期
}
}
}
}
(2)查看索引
• 查看所有索引
查看所有索引 该命令会列出当前 Elasticsearch 集群中的所有索引,并显示索引的一些基本信息(如健康状态、文档数量、分片数量等)。
GET /_cat/indices?v
• 查看单个索引
查看单个索引 该命令返回 my_index 索引的详细信息,包括设置、映射等。
GET /my_index
• 查看索引的映射
查看索引的映射 该命令会返回 my_index 索引的字段定义和数据类型。
GET /my_index/_mapping
(3)修改索引
• 更新索引设置
更新索引设置 某些索引设置(如分片数、副本数)在创建时就已固定,不能更改。但可以更新其他一些设置,如动态设置副本数:
PUT /my_index/_settings
{
"settings": {
"number_of_replicas": 3
}
}
• 添加新字段
添加新字段 在 my_index 索引中添加 new_field 字段,字段类型为 text。
PUT /my_index/_mapping
{
"properties": {
"new_field": {
"type": "text"
}
}
}
(4)删除索引
• 删除某个索引
删除某个索引 删除 my_index 索引及其所有数据,操作不可恢复。
DELETE /my_index
• 删除多个索引
删除多个索引 该命令会删除 index1、index2 和 index3 三个索引。
DELETE /index1,index2,index3
(5)其他常见操作
• 关闭索引
关闭索引 关闭索引后,索引的所有数据将无法访问,且无法进行搜索、更新等操作。关闭索引时不会删除数据。
POST /my_index/_close
• 打开索引
打开索引 重新打开 my_index 索引,使其可以进行查询和更新操作。
POST /my_index/_open
• 查看索引的状态
查看索引的状态 该命令返回 my_index 索引的各种统计信息(如文档数量、存储大小等)。
GET /my_index/_stats
• 查看索引的健康状态
查看索引的健康状态 该命令会返回所有索引的健康状态(green、yellow、red)和其他信息。
GET /_cat/indices?v&h=index,health
(6)基础查询
• match 查询
基础查询 match 查询是最常见的查询类型之一,主要用于全文搜索。当搜索的字段是文本类型时,它会进行分词和全文匹配,这个查询会匹配所有包含 “Elasticsearch” 的 title 字段。
{
"query": {
"match": {
"title": "Elasticsearch"
}
}
}
• Term 查询
基础查询 term 查询用于精确匹配,没有分词。一般用于关键词、ID 或其他不需要分析的字段,这个查询会查找 userId 为 12345 的文档。
{
"query": {
"term": {
"userId": "12345"
}
}
}
• Range 查询
基础查询 range 查询用于查找字段值在一定范围内的文档,这个查询会查找 date 字段值在 2020 年内的文档。
{
"query": {
"range": {
"date": {
"gte": "2020-01-01",
"lte": "2020-12-31"
}
}
}
}
(7)布尔查询
布尔查询 允许组合多个查询条件,包括 must(必须匹配)、should(可以匹配)、must_not(不能匹配)。
•must:标题中包含 “Elasticsearch” 并且简介中包含 “search”。
•should:视图数为 100(提高匹配的相关性分数)。
•must_not:排除 userId 为 12345 的文档。
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Elasticsearch" } },
{ "match": { "description": "search" } }
],
"should": [
{ "term": { "views": 100 } }
],
"must_not": [
{ "term": { "userId": "12345" } }
]
}
}
}
(8)聚合
聚合 用于计算统计信息,比如求平均值、最大值、最小值、分组统计等。
• Terms 聚合
Terms聚合 terms 聚合用于将文档按某个字段进行分组,返回每组的文档数量,这个聚合会返回每个 userId 对应的文档数量。
{
"aggs": {
"group_by_user": {
"terms": {
"field": "userId"
}
}
}
}
• Range 聚合
Range聚合 range 聚合用于对某个字段进行范围分组,这个聚合将 price 字段按照指定的范围(小于 50,50 到 100,大于 100)进行分组。
{
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 50 },
{ "from": 50, "to": 100 },
{ "from": 100 }
]
}
}
}
}
(9)排序
排序 可以对查询结果进行排序,通常通过 sort 字段来指定排序方式,这个查询会按 views 字段进行降序排序。
{
"query": {
"match": {
"title": "Elasticsearch"
}
},
"sort": [
{ "views": { "order": "desc" } }
]
}
(10)分页
分页 使用 from 和 size 控制查询结果的起始位置和返回的最大数量,这个查询会从第 10 条记录开始,返回最多 20 条记录,适用于分页场景。
{
"query": {
"match": {
"title": "Elasticsearch"
}
},
"from": 10,
"size": 20
}
(11)多字段查询
多字段查询 有时候,我们需要同时在多个字段上进行查询。可以使用 multi_match 查询来实现,这个查询会在 title 和 description 字段中查找 “Elasticsearch” 或 “search”。
{
"query": {
"multi_match": {
"query": "Elasticsearch search",
"fields": ["title", "description"]
}
}
}
(12)模糊查询
fuzzy查询 是对文本进行模糊匹配的查询。适用于用户输入不确定或可能存在拼写错误的场景,这个查询会查找类似 “Elasticsarch” 的标题,fuzziness 设置为 AUTO,会自动计算适当的模糊度。
{
"query": {
"fuzzy": {
"title": {
"value": "Elasticsarch",
"fuzziness": "AUTO"
}
}
}
}
(13)嵌套查询
嵌套查询 对于嵌套类型的字段,可以使用嵌套查询来访问子文档中的数据,这个查询会查找 comments 数组中包含 “user” 为 “John” 的嵌套文档。
{
"query": {
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{ "match": { "comments.user": "John" } }
]
}
}
}
}
}
6.创建索引
创建好es的实体类之后,@Document注解的indexName = "kd_video"参数标识了该索引,所以我们创建kd_video索引,以下是我使用的索引模式。
PUT /kd_video
{
"settings": {
"index.max_ngram_diff": 25, // 设置 ngram 分词的最大长度差异。即 min_gram 与 max_gram 的差异最大为 25。
"analysis": { // 设置分析器和分词器的配置
"tokenizer": { // 自定义的分词器设置
"ngram_tokenizer": { // ngram 分词器
"type": "ngram", // 设置为 ngram 分词器,ngram 分词器将输入字符串分解为连续的子字符串(n-grams)
"min_gram": 1, // 设置最小 ngram 长度为 1
"max_gram": 10, // 设置最大 ngram 长度为 10
"token_chars": ["letter", "digit", "whitespace", "punctuation"] // 指定要分词的字符类型,包含字母、数字、空格和标点符号
},
"edge_ngram_tokenizer": { // edge_ngram 分词器
"type": "edge_ngram", // 设置为 edge_ngram 分词器,edge_ngram 将输入字符串从开始处分解为连续的子字符串
"min_gram": 1, // 设置最小 ngram 长度为 1
"max_gram": 10, // 设置最大 ngram 长度为 10
"token_chars": ["letter", "digit", "whitespace", "punctuation"] // 指定要分词的字符类型,包含字母、数字、空格和标点符号
}
},
"analyzer": { // 自定义分析器设置
"custom_ngram_analyzer": { // 自定义 ngram 分词分析器
"type": "custom", // 自定义分析器
"tokenizer": "ngram_tokenizer", // 使用上面定义的 ngram_tokenizer 作为分词器
"filter": ["lowercase"] // 设置过滤器为 lowercase,转换所有文本为小写
},
"edge_ngram_analyzer": { // 自定义 edge_ngram 分词分析器
"type": "custom", // 自定义分析器
"tokenizer": "edge_ngram_tokenizer", // 使用上面定义的 edge_ngram_tokenizer 作为分词器
"filter": ["lowercase"] // 设置过滤器为 lowercase,转换所有文本为小写
}
}
}
},
"mappings": {
"properties": { // 字段映射设置
"title": { // "title" 字段
"type": "text", // 设置字段类型为 text(全文检索)
"analyzer": "custom_ngram_analyzer" // 使用自定义的 ngram 分词分析器进行分析
},
"description": { // "description" 字段
"type": "text", // 设置字段类型为 text(全文检索)
"analyzer": "custom_ngram_analyzer" // 使用自定义的 ngram 分词分析器进行分析
},
"url": { // "url" 字段
"type": "text", // 设置字段类型为 text(全文检索)
"analyzer": "custom_ngram_analyzer" // 使用自定义的 ngram 分词分析器进行分析
},
"coverUrl": { // "coverUrl" 字段
"type": "text", // 设置字段类型为 text(全文检索)
"analyzer": "custom_ngram_analyzer" // 使用自定义的 ngram 分词分析器进行分析
},
"views": { // "views" 字段
"type": "integer" // 设置字段类型为整数
},
"danmus": { // "danmus" 字段
"type": "integer" // 设置字段类型为整数
},
"userName": { // "userName" 字段
"type": "text", // 设置字段类型为 text(全文检索)
"analyzer": "custom_ngram_analyzer" // 使用自定义的 ngram 分词分析器进行分析
},
"userId": { // "userId" 字段
"type": "keyword" // 设置字段类型为 keyword(精确匹配,通常用于过滤、排序)
},
"createTime": { // "createTime" 字段
"type": "date", // 设置字段类型为日期
"format": "yyyy-MM-dd HH:mm:ss" // 设置日期的格式
}
}
}
}
然后这是控制台执行
GET /kd_video/_search查询的视频数据
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 15,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861856423086452738",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861856423086452738",
"title" : "原神真的存在物理学!新角色欧洛伦和恰斯卡到底藏了多少秘密?",
"description" : "原神真的存在物理学!新角色欧洛伦和恰斯卡到底藏了多少秘密?",
"url" : "http://127.0.0.1:9000/pmvideo/temp/原神_1732736083205.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/原神_1732736121191.jpg",
"views" : 25,
"danmus" : 13,
"userId" : "1862146928382918657",
"createTime" : "2024-11-27 19:35:38"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861883566768107521",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861883566768107521",
"title" : "“你干嘛阿哎哟”纯享版原曲:新星招募令第二季鬼畜",
"description" : "“你干嘛阿哎哟”纯享版原曲:新星招募令第二季鬼畜",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-kcerprj4e017iefu_1732742592563.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/srchttp___pic.rmb.bdstatic.com_mvideo_99d7fa08e2_1732742567362.jpg",
"views" : 1,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-27 21:23:29"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861884015889985538",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861884015889985538",
"title" : "vscode写vue3项目卡顿的解决办法",
"description" : "vscode写vue3项目卡顿的解决办法",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-nik5yskpdrr14kaa_1732742701692.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/srchttp___f7.baidu.com_it_u38519346792024616696fm222app106fJPEG_w_1732742707148.jpg",
"views" : 2,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-27 21:25:16"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861884465464848386",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861884465464848386",
"title" : "我的世界:6个隐藏细节,看似没用却真实存在!",
"description" : "我的世界:6个隐藏细节,看似没用却真实存在!",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-qkhfibd6z8rib2g1_1732742808763.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/srchttp___f7.baidu.com_it_u2_1732742787343.jpg",
"views" : 1,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-27 21:27:03"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861886088073293825",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861886088073293825",
"title" : "东皇教官:爆炸!裸帽子+大书!究极核弹流东皇初体验!",
"description" : "东皇教官:爆炸!裸帽子+大书!究极核弹流东皇初体验!",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-qidhuez8pwxwdquz_1732743202726.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/05227453329660_1732743194098.jpg",
"views" : 1,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-27 21:33:30"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861938081550684161",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861938081550684161",
"title" : "美国所有总统党派盘点",
"description" : "美国所有总统党派盘点",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-qkh3dydxkuqgstgn_1732755600591.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/8658219102599_1732755589075.jpg",
"views" : 8,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-28 01:00:06"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861938918054285314",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861938918054285314",
"title" : "碎片化睡眠危害等同于熬夜,会造成代谢紊乱,专家称如同隐形杀手",
"description" : "碎片化睡眠危害等同于熬夜,会造成代谢紊乱,专家称如同隐形杀手",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-qksnvy13bbga64b5_1732755802023.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/51459255_1732755793421.jpg",
"views" : 0,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-28 01:03:26"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861939735004041218",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861939735004041218",
"title" : "听我说,谢谢你!感谢你断了我所有的路",
"description" : "听我说,谢谢你!感谢你断了我所有的路",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-ngrb4y8ixckpu54v_1732755997612.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/04102066918074_100_1732755980544.jpg",
"views" : 0,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-28 01:06:41"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1861941632683331585",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1861941632683331585",
"title" : "春游野炊没带钱靠勤劳填肚子,不曾想无心插柳却荣获顶级待遇",
"description" : "春游野炊没带钱靠勤劳填肚子,不曾想无心插柳却荣获顶级待遇",
"url" : "http://127.0.0.1:9000/pmvideo/temp/mda-pdak7f455wh7w507_1732756444533.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/temp/512320fm_1732756449330.jpg",
"views" : 0,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-11-28 01:14:13"
}
},
{
"_index" : "kd_video",
"_type" : "_doc",
"_id" : "1869704710862667778",
"_score" : 1.0,
"_source" : {
"_class" : "modules.kd.entity.ElasticSearchVideo",
"id" : "1869704710862667778",
"title" : "asasdasda",
"description" : "sdddddddddddddddddddddddddddd",
"url" : "http://127.0.0.1:9000/pmvideo/mda-qhpdjxkuss87e5b4_1734607304157.mp4",
"coverUrl" : "http://127.0.0.1:9000/pmvideo/1_1734607306926.jpg",
"views" : 0,
"danmus" : 0,
"userId" : "1862146928382918657",
"createTime" : "2024-12-19 11:21:55"
}
}
]
}
}
7.前端查询结果
请求查询接口之后,会返回数据给到前端,可以看到我们的分词分为了,原,神,原神,所以搜索的视频结果为这两个视频。

总结
总算也是实现了这个功能,期间踩了很多的坑,不过一直研究和实验查询相关的资料也是完成了,希望能够帮助到大家。到这里就完成了分词搜索功能了,感谢观看~o( ̄▽ ̄)ブ🎉🎉🎉

249

被折叠的 条评论
为什么被折叠?



