Vue3+SpringBoot+Elasticsearch+ik分词实现分词搜索功能

系列文章目录

🥥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( ̄▽ ̄)ブ🎉🎉🎉

实现搜索引擎一般需要以下步骤: 1. 数据库建表和数据导入:根据需要建立相应的数据库表,并导入数据。 2. Elasticsearch安装和配置:安装Elasticsearch,配置Elasticsearch集群,并将数据导入到Elasticsearch中。 3. Spring BootVue.js项目搭建:使用Spring BootVue.js框架搭建项目。 4. 搜索功能实现:使用Elasticsearch进行搜索功能实现。 具体实现步骤如下: 1. 数据库建表和数据导入 根据需求建立相应的数据库表,并将数据导入到数据库中。这里以MySQL为例,建立一个books表: CREATE TABLE `books` ( `id` int NOT NULL AUTO_INCREMENT, `title` varchar(255) DEFAULT NULL, `author` varchar(255) DEFAULT NULL, `content` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 将数据导入到books表中: INSERT INTO `books` (`id`, `title`, `author`, `content`) VALUES (1, &#39;Java编程思想&#39;, &#39;Bruce Eckel&#39;, &#39;Java编程思想是一本Java入门书籍。&#39;), (2, &#39;Spring Boot实战&#39;, &#39;Craig Walls&#39;, &#39;Spring Boot实战是一本介绍Spring Boot框架的书籍。&#39;), (3, &#39;Vue.js实战&#39;, &#39;梁灏&#39;, &#39;Vue.js实战是一本介绍Vue.js框架的书籍。&#39;); 2. Elasticsearch安装和配置 安装Elasticsearch,配置Elasticsearch集群,并将数据导入到Elasticsearch中。这里以Elasticsearch 7.2.0为例,安装步骤如下: (1)下载Elasticsearch 官网下载地址:https://www.elastic.co/downloads/elasticsearch (2)解压并启动Elasticsearch 解压后进入bin目录,执行以下命令启动Elasticsearch./elasticsearch3)安装中文分词Elasticsearch默认使用英文分词器,需要安装中文分词器,这里使用IK Analyzer中文分词器。IK Analyzer的GitHub地址为:https://github.com/medcl/elasticsearch-analysis-ik 下载IK Analyzer插件: wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.2.0/elasticsearch-analysis-ik-7.2.0.zip 将插件安装到Elasticsearch中: ./elasticsearch-plugin install file:///path/to/elasticsearch-analysis-ik-7.2.0.zip (4)将数据导入到Elasticsearch中 使用Elasticsearch的API将数据库中的数据导入到Elasticsearch中。 3. Spring BootVue.js项目搭建 使用Spring BootVue.js框架搭建项目,这里不再赘述。 4. 搜索功能实现 (1)在Spring Boot中使用Elasticsearch进行搜索 使用Spring Data Elasticsearch实现Elasticsearch的交互,具体步骤如下: 1. 在pom.xml中添加Spring Data Elasticsearch依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> 2. 在application.yml中配置Elasticsearchspring: data: elasticsearch: cluster-name: elasticsearch cluster-nodes: 127.0.0.1:9300 3. 创建一个Book实体类,并使用@Document注解标注该实体类对应的Elasticsearch索引和类型: @Document(indexName = "books", type = "book") public class Book { @Id private Long id; private String title; private String author; private String content; // getter和setter方法省略 } 4. 创建一个BookRepository接口,继承ElasticsearchRepository接口: public interface BookRepository extends ElasticsearchRepository<Book, Long> { } 5. 在BookService中实现搜索功能: @Service public class BookService { @Autowired private BookRepository bookRepository; public List<Book> search(String keyword) { QueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(keyword, "title", "author", "content"); Iterable<Book> iterable = bookRepository.search(queryBuilder); List<Book> books = new ArrayList<>(); iterable.forEach(books::add); return books; } } (2)在Vue.js中调用搜索接口 使用axios库调用Spring Boot的接口,具体步骤如下: 1. 安装axios库: npm install axios --save 2.Vue.js中调用搜索接口: <script> import axios from &#39;axios&#39;; export default { data() { return { keyword: &#39;&#39;, books: [] } }, methods: { search() { axios.get(&#39;/api/search?keyword=&#39; + this.keyword).then(response => { this.books = response.data; }).catch(error => { console.log(error); }); } } } </script> 以上就是使用Spring BootVue.js实现Elasticsearch搜索引擎的步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值