youyichannel

志于道,据于德,依于仁,游于艺!

0%

ES&MongoDB实现搜索实战

  • 搜索
  • 搜索记录

一、搭建环境

  • Elasticsearch 7.17.9
  • Mongo lastest
  • SpringBoot 2.7.12
  • MySQL 8.0.31

建表语句:

create database `prac-search-demo`;
use `prac-search-demo`;

create table tb_article
(
id bigint not null auto_increment primary key,
title varchar(64) not null comment '标题',
content text not null comment '内容'
) comment '文章表';


insert into tb_article (title, content)
VALUES ('健康饮食指南',
'饮食对我们的健康至关重要。本文将分享一些健康饮食的指南,包括均衡饮食、适量摄入各类营养素、选择新鲜食材、限制加工食品等方面的建议。'),
('如何学习编程',
'编程是一项有趣且富有挑战性的技能。本文将介绍学习编程的基本步骤和方法,包括选择编程语言、掌握编程概念、参与实践项目、寻找资源和社区支持等。'),
('旅行的必备物品清单',
'计划旅行时,准备充分的必备物品清单可以确保您在旅途中拥有愉快的体验。本文列举了旅行必备物品,例如护照、行李箱、充电器、药品等,以帮助您做好出行准备。'),
('如何建立良好的沟通技巧',
'良好的沟通技巧对于个人和职业发展都至关重要。本文将介绍一些改进沟通技巧的方法,包括倾听他人、表达清晰、掌握非语言沟通等方面的建议。'),
('如何管理个人财务',
'有效管理个人财务可以帮助您实现财务目标并确保经济安全。本文将分享一些管理个人财务的方法,如制定预算、储蓄、投资规划、消费控制等。'),
('养成良好的阅读习惯',
'阅读是一种重要的学习和娱乐方式。本文将提供一些建立良好阅读习惯的建议,例如每天设定阅读时间、选择适合自己的读物、记录读书笔记等。'),
('如何处理压力',
'在现代生活中,压力是常态化的。本文将介绍一些处理压力的方法,包括放松技巧、锻炼身体、寻求支持系统、积极思维等,帮助您更好地应对压力。'),
('职场礼仪指南',
'在职场上展现良好的职业素养和礼仪是成功的关键之一。本文将介绍一些职场礼仪的指南,包括着装得体、尊重他人、有效沟通等方面的建议。'),
('如何培养创造力',
'创造力是在各个领域取得成功的重要因素。本文将分享一些培养创造力的方法,如开放思维、接触新事物、创造性思维训练等,帮助您发展自己的创造潜力。')

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">
<modelVersion>4.0.0</modelVersion>

<groupId>com.juzi</groupId>
<artifactId>search-demo</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/>
</parent>

<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring.boot.version>2.7.12</spring.boot.version>
<mysql.version>8.0.31</mysql.version>
<mybatis.starter.version>2.2.2</mybatis.starter.version>
<mybatis.plus.stater.version>3.5.3.1</mybatis.plus.stater.version>
</properties>

<dependencies>
<!-- spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>

<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.starter.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.stater.version}</version>
</dependency>

<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!--es-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!--mongo-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>

</project>

application.yml

spring:
profiles:
active: dev
application:
name: search-demo

application-dev.yml

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/prac-search-demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 12345678
data:
mongodb:
database: prac-search-demo-history
host: ${your host}
port: 27017

server:
port: 9999
mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.juzi.search.pojo

二、项目初始化

创建ES索引库

参考文章:https://www.jianshu.com/p/56e755415e63

使用CURL

curl -XPUT "http://127.0.0.1:9200/search_article" -H 'Content-Type: application/json' -d '
{
"mappings": {
"properties": {
"id": {
"type": "long"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}'

使用POSTMAN等软件皆可

  • GET请求查询映射:http://127.0.0.1:9200/search_article
  • DELETE请求,删除索引及映射:http://127.0.0.1:9200/search_article
  • GET请求,查询所有文档:http://127.0.0.1:9200/search_article/_search

数据初始化到索引库

Article

package com.juzi.search.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
* 文章表
*/
@Document(indexName = "search_article")
@TableName(value = "tb_article")
@Data
public class Article implements Serializable {

@Id
@TableId(type = IdType.AUTO)
private Long id;

/**
* 标题
*/
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String title;

/**
* 内容
*/
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String content;

@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

ArticleRepository

package com.juzi.search.esdao;

import com.juzi.search.pojo.Article;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Service;

/**
* @author codejuzi
*/
@Service
public interface ArticleRepository extends ElasticsearchRepository<Article, Long> {
}

Init

package com.juzi.search;

import com.juzi.search.esdao.ArticleRepository;
import com.juzi.search.mapper.ArticleMapper;
import com.juzi.search.pojo.Article;
import com.juzi.search.pojo.ArticleAssociateWords;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import java.util.List;


/**
* @author codejuzi
*/
@SpringBootTest
class SearchApplicationTest {

@Resource
private ArticleMapper articleMapper;

@Resource
private ArticleRepository articleRepository;

@Test
void init() {
List<Article> articleList = articleMapper.selectList(null);
articleRepository.saveAll(articleList);
}
}

测试:GET请求查询映射:http://127.0.0.1:9200/search_article

实现搜索功能

ArticleSearchDTO

package com.juzi.search.dto;

import lombok.Data;

import java.io.Serializable;

/**
* @author codejuzi
*/
@Data
public class ArticleSearchDTO implements Serializable {

private static final long serialVersionUID = 4819278807867077563L;

private String searchText;
/**
* 当前页
*/
private int pageNum = 1;
/**
* 分页条数
*/
private int pageSize = 5;
}

具体实现搜索:

@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;

public List<Article> searchArticle(ArticleSearchDTO articleSearchDTO) {
String searchText = articleSearchDTO.getSearchText();
if (searchText.isEmpty()) {
return null;
}

BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));

int pageNum = articleSearchDTO.getPageNum();
int pageSize = articleSearchDTO.getPageSize();

PageRequest pageRequest = PageRequest.of(pageNum - 1, pageSize);

HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
highlightBuilder.postTags("</font>");

// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).withHighlightBuilder(highlightBuilder).build();

SearchHits<Article> searchHits = elasticsearchRestTemplate.search(searchQuery, Article.class);

// 解析结果
List<Article> res = new ArrayList<>();
if (!searchHits.hasSearchHits()) {
return res;
}
List<SearchHit<Article>> hitList = searchHits.getSearchHits();

for (SearchHit<Article> hit : hitList) {
Article article = hit.getContent();
// 做一些处理
res.add(article);
}
return res;
}

实现搜索历史记录功能

搜索记录,需要给每一个用户都保存一份,数据量较大,要求加载速度快,通常这样的数据存储到mongodb更合适,不建议直接存储到关系型数据库中

  • 展示用户的搜索记录10条,按照搜索关键词的时间倒序
  • 可以删除搜索记录
  • 保存历史记录,保存10条,多余的则删除最久的历史记录

创建映射

ArticleAssociateWords

package com.juzi.search.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;

import java.io.Serializable;
import java.util.Date;

/**
* @author codejuzi
*/
@Data
@Document("article_associate_words")
@NoArgsConstructor
public class ArticleAssociateWords implements Serializable {
private static final long serialVersionUID = 1L;

private String id;

/**
* 联想词
*/
private String associateWords;

/**
* 创建时间
*/
private Date createdTime;

public ArticleAssociateWords(String associateWords, Date createdTime) {
this.associateWords = associateWords;
this.createdTime = createdTime;
}
}

测试:

package com.juzi.search;

import com.juzi.search.pojo.Article;
import com.juzi.search.pojo.ArticleAssociateWords;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;

import javax.annotation.Resource;

import java.util.Date;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
* @author codejuzi
*/
@SpringBootTest
class SearchApplicationTest {

@Resource
private MongoTemplate mongoTemplate;

@Test
void testMongoAdd() {
for (int i = 0; i < 10; i++) {
ArticleAssociateWords articleAssociateWords = new ArticleAssociateWords();
articleAssociateWords.setAssociateWords("如何 demo" + i);
articleAssociateWords.setCreatedTime(new Date());
mongoTemplate.save(articleAssociateWords);
}
}


@Test
void testMongoFind() {
ArticleAssociateWords articleAssociateWords
= mongoTemplate.findById("64ab76892a4d8c3201388dad", ArticleAssociateWords.class);
System.out.println("articleAssociateWords = " + articleAssociateWords);
}

@Test
void testMongoQuery() {
Query query = Query.query(Criteria.where("associateWords").is("如何"))
.with(Sort.by(Sort.Direction.DESC, "createdTime"));
List<ArticleAssociateWords> associateWordsList = mongoTemplate.find(query, ArticleAssociateWords.class);
System.out.println(associateWordsList);
}

@Test
void testMongoDelete() {
mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("如何 demo9")),
ArticleAssociateWords.class);
}
}

具体实现:

1)插入历史记录

public void insertHistory(String associateWords) {
// 1. 搜索关键词
Query query = Query.query(Criteria.where("associateWords").is(associateWords));
ArticleAssociateWords articleAssociateWords = mongoTemplate.findOne(query, ArticleAssociateWords.class);
// 2. 存在,更新时间
if (!Objects.isNull(articleAssociateWords)) {
articleAssociateWords.setCreatedTime(new Date());
mongoTemplate.save(articleAssociateWords);
return;
}
// 3. 不存在,只保留最新的10条存储
ArticleAssociateWords newArticleAssociateWords = new ArticleAssociateWords(associateWords, new Date());
query = new Query().with(Sort.by(Sort.Direction.DESC, "createdTime"));
List<ArticleAssociateWords> articleAssociateWordsList = mongoTemplate.find(query, ArticleAssociateWords.class);

// 记录小于10
if (articleAssociateWordsList.size() < 10) {
mongoTemplate.save(newArticleAssociateWords);
return;
}
// 记录大于10,获取最后一条记录,替换
ArticleAssociateWords oldWord = articleAssociateWordsList.get(articleAssociateWordsList.size() - 1);
mongoTemplate.findAndReplace(
Query.query(Criteria.where("id").is(oldWord.getId())),
newArticleAssociateWords
);
}

2)加载历史记录

public List<ArticleAssociateWords> loadHistory() {
return mongoTemplate.find(new Query().with(Sort.by(Sort.Direction.DESC, "createdTime")),
ArticleAssociateWords.class);
}

3)删除历史记录

public Boolean deleteHistory(String id) {
if (id.isEmpty()) {
return Boolean.FALSE;
}

mongoTemplate.remove(Query.query(Criteria.where("id").is(id)), ArticleAssociateWords.class);

return Boolean.TRUE;
}

在用户进行搜索的时候,异步记录搜索关键词

public List<Article> searchArticle(ArticleSearchDTO articleSearchDTO) {
String searchText = articleSearchDTO.getSearchText();
if (searchText.isEmpty()) {
return null;
}

// 异步保存历史记录
CompletableFuture.runAsync(() -> insertHistory(searchText));
// ....
}