👉 這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然

👉這是一個或許對你有用的開源專案國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERP、CRM、AI 大模型等等功能:
Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud 影片教程:https://doc.iocoder.cn 【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本

1. 技術選型
-
Elasticsearch優化了全文搜尋: MySQL 在處理複雜的文字搜尋(如模糊匹配、全文搜尋)時效能較差。尤其是當查詢的資料量和文字內容增大時,MySQL 的效能會急劇下降。而 Elasticsearch 專門為高效的文字搜尋設計,能夠透過倒排索引和分散式架構最佳化查詢效能,適用於大規模資料集的全文搜尋,查詢速度通常比 MySQL 快得多。 -
高效的複雜查詢: Elasticsearch 對於複雜的查詢,如多條件搜尋、範圍查詢、聚合查詢等,提供了比 MySQL 更高效的執行方式。Elasticsearch 支援文件級的分詞、詞彙匹配、近似匹配等複雜查詢方式,這在 MySQL 中是非常難以高效實現的。 -
即時搜尋: Elasticsearch 提供了快速的即時資料檢索能力,尤其適用於需要快速反饋結果的場景。與之相比,MySQL 在高併發時處理複雜查詢的能力相對較弱。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
2. 建立elasticsearch公共包

基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/yudao-cloud 影片教程:https://doc.iocoder.cn/video/
3. 匯入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
4. 資料庫準備

/*
Navicat Premium Data Transfer
Source Server : docker-oj
Source Server Type : MySQL
Source Server Version : 50744
Source Host : localhost:3307
Source Schema : bitoj_dev
Target Server Type : MySQL
Target Server Version : 50744
File Encoding : 65001
Date: 04/12/2024 12:12:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS =
0
;
-- ----------------------------
-- Table structure
for
tb_question
-- ----------------------------
DROP TABLE IF EXISTS `tb_question`;
CREATE TABLE `tb_question` (
`question_id` bigint(
20
) UNSIGNED NOT NULL COMMENT
'題目id'
,
`title` varchar(
50
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`difficulty` tinyint(
4
) NOT NULL COMMENT
'題目難度1:簡單 2:中等 3:困難'
,
`time_limit`
int
(
11
) NOT NULL COMMENT
'時間限制'
,
`space_limit`
int
(
11
) NOT NULL COMMENT
'空間限制'
,
`content` varchar(
1000
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT
'題目內容'
,
`question_case` varchar(
1000
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT
'題目用例'
,
`default_code` varchar(
1000
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT
'預設程式碼塊'
,
`main_func` varchar(
500
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT
'main函式'
,
`create_by` bigint(
20
) UNSIGNED NOT NULL COMMENT
'建立人'
,
`create_time` datetime NOT NULL COMMENT
'建立時間'
,
`update_by` bigint(
20
) UNSIGNED NULL DEFAULT NULL COMMENT
'更新人'
,
`update_time` datetime NULL DEFAULT NULL COMMENT
'更新時間'
,
`is_del` tinyint(
4
) NOT NULL DEFAULT
0
COMMENT
'邏輯刪除標誌位 0:未被刪除 1:被刪除'
,
PRIMARY
KEY(`question_id`)
USING BTREE
) ENGINE
= InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of tb_question
-- ----------------------------
INSERT INTO `tb_question` VALUES (
1860314392613736449
,
'兩數相加'
,
2
,
1000
,
256
,
'給定兩個非負整數,分別用連結串列表示,每個節點表示一位數字。將這兩個數字相加並以相同形式返回結果。'
,
'[{\"input\":\"[2,4,3]\\n[5,6,4]\",\"output\":\"[7,0,8]\"}, {\"input\":\"[0]\\n[0]\",\"output\":\"[0]\"}]'
,
'public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\\n // TODO: 實現你的演算法\\n}'
,
'public static void main(String[] args) {\\n ListNode l1 = new ListNode(2, new ListNode(4, new ListNode(3)));\\n ListNode l2 = new ListNode(5, new ListNode(6, new ListNode(4)));\\n ListNode result = addTwoNumbers(l1, l2);\\n System.out.println(result);\\n}'
,
1
,
'2024-11-23 21:28:09'
,
1
, NULL,
0
);
INSERT INTO `tb_question` VALUES (
1860315513155604481
,
'test'
,
2
,
12
,
12
,
'<p>113厄爾</p>'
,
'222'
,
'22'
,
'222'
,
1
,
'2024-11-23 21:32:36'
,
1
, NULL,
0
);
INSERT INTO `tb_question` VALUES (
1860317209277616130
,
'兩數相加2'
,
2
,
1000
,
256
,
'給定兩個非負整數,分別用連結串列表示,每個節點表示一位數字。將這兩個數字相加並以相同形式返回結果。'
,
'[{\"input\":\"[2,4,3]\\n[5,6,4]\",\"output\":\"[7,0,8]\"}, {\"input\":\"[0]\\n[0]\",\"output\":\"[0]\"}]'
,
'public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\\n // TODO: 實現你的演算法\\n}'
,
'public static void main(String[] args) {\\n ListNode l1 = new ListNode(2, new ListNode(4, new ListNode(3)));\\n ListNode l2 = new ListNode(5, new ListNode(6, new ListNode(4)));\\n ListNode result = addTwoNumbers(l1, l2);\\n System.out.println(result);\\n}'
,
1
,
'2024-11-23 21:39:20'
,
1
, NULL,
0
);
INSERT INTO `tb_question` VALUES (
1860319609832869890
,
'兩數相加21'
,
2
,
1000
,
256
,
'<p>給定兩個非負整數,分別用連結串列表示,每個節點表示一位數字。將這兩個數字相加並以相同形式返回結果。</p>'
,
'[{\"input\":\"[2,4,3]\\n[5,6,4]\",\"output\":\"[7,0,8]\"}, {\"input\":\"[0]\\n[0]\",\"output\":\"[0]\"}]'
,
'public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\\n // TODO: 實現你的演算法\\n}'
,
'public static void main(String[] args) {\\n ListNode l1 = new ListNode(2, new ListNode(4, new ListNode(3)));\\n ListNode l2 = new ListNode(5, new ListNode(6, new ListNode(4)));\\n ListNode result = addTwoNumbers(l1, l2);\\n System.out.println(result);\\n}'
,
1
,
'2024-11-23 21:48:53'
,
1
,
'2024-11-24 16:03:57'
,
0
);
INSERT INTO `tb_question` VALUES (
1860319646323314689
,
'兩數相加3'
,
2
,
1000
,
256
,
'給定兩個非負整數,分別用連結串列表示,每個節點表示一位數字。將這兩個數字相加並以相同形式返回結果。'
,
'[{\"input\":\"[2,4,3]\\n[5,6,4]\",\"output\":\"[7,0,8]\"}, {\"input\":\"[0]\\n[0]\",\"output\":\"[0]\"}]'
,
'public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\\n // TODO: 實現你的演算法\\n}'
,
'public static void main(String[] args) {\\n ListNode l1 = new ListNode(2, new ListNode(4, new ListNode(3)));\\n ListNode l2 = new ListNode(5, new ListNode(6, new ListNode(4)));\\n ListNode result = addTwoNumbers(l1, l2);\\n System.out.println(result);\\n}'
,
1
,
'2024-11-23 21:49:01'
,
1
, NULL,
0
);
INSERT INTO `tb_question` VALUES (
1860331174208598018
,
'兩數相加3秀愛'
,
2
,
1000
,
256
,
'<p>給定兩個非負整數,分別用連結串列表示,每個節點表示一位數字。將這兩個數字相加並以相同形式返回結果。</p>'
,
'[{\"input\":\"[2,4,3]\\n[5,6,4]\",\"output\":\"[7,0,8]\"}, {\"input\":\"[0]\\n[0]\",\"output\":\"[0]\"}]'
,
'public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\\n // TODO: 實現你的演算法\\n}'
,
'public static void main(String[] args) {\\n ListNode l1 = new ListNode(2, new ListNode(4, new ListNode(3)));\\n ListNode l2 = new ListNode(5, new ListNode(6, new ListNode(4)));\\n ListNode result = addTwoNumbers(l1, l2);\\n System.out.println(result);\\n}'
,
1
,
'2024-11-23 22:34:50'
,
1
,
'2024-11-24 15:58:17'
,
0
);
INSERT INTO `tb_question` VALUES (
1860524253771296769
,
'21'
,
1
,
2
,
2
,
'<p>2</p>'
,
'2'
,
'2'
,
'2'
,
1
,
'2024-11-24 11:22:04'
,
1
,
'2024-11-24 15:58:07'
,
0
);
SET FOREIGN_KEY_CHECKS =
1
;
import
org.springframework.data.elasticsearch.annotations.Document;
import
lombok.Getter;
import
lombok.Setter;
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.time.LocalDateTime;
@Getter
@Setter
@Document
(indexName =
"idx_question"
)
publicclassQuestionES
{
@Id
@Field
(type = FieldType.Long)
private
Long questionId;
@Field
(type = FieldType.Text, analyzer =
"ik_max_word"
, searchAnalyzer =
"ik_max_word"
)
private
String title;
@Field
(type = FieldType.Byte)
private
Integer difficulty;
@Field
(type = FieldType.Long)
private
Long timeLimit;
@Field
(type = FieldType.Long)
private
Long spaceLimit;
@Field
(type = FieldType.Text, analyzer =
"ik_max_word"
, searchAnalyzer =
"ik_max_word"
)
private
String content;
@Field
(type = FieldType.Text)
private
String questionCase;
@Field
(type = FieldType.Text)
private
String mainFunc;
@Field
(type = FieldType.Text)
private
String defaultCode;
@Field
(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private
LocalDateTime createTime;
}
import
com.baomidou.mybatisplus.annotation.IdType;
import
com.baomidou.mybatisplus.annotation.TableId;
import
com.baomidou.mybatisplus.annotation.TableName;
import
com.guan.common.core.domain.BaseEntity;
import
lombok.Getter;
import
lombok.Setter;
@TableName
(
"tb_question"
)
@Getter
@Setter
publicclassQuestionextendsBaseEntity
{
@TableId
(type = IdType.ASSIGN_ID)
private
Long questionId;
private
String title;
private
Integer difficulty;
private
Long timeLimit;
private
Long spaceLimit;
private
String content;
private
String questionCase;
private
String defaultCode;
private
String mainFunc;
}
4.1. @Document(indexName = "idx_question")
idx_question
。這意味著 Elasticsearch 會將這個類的資料儲存在名為 idx_question
的索引中。4.2. Id
4.3. @Field
Spring Data Elasticsearch
提供的一個註解,用於定義如何在 Elasticsearch 中對映資料。5. 實現Repository 介面(ES)和Mapper(MySQL)
5.1. Elasticsearch — Repository 介面
Spring Data Elasticsearch
的 Repository 介面,用於與 Elasticsearch 互動。它繼承了 ElasticsearchRepository
,這使得 Spring Data Elasticsearch
可以自動為它提供基本的 CRUD 操作。這個介面專門用於操作 QuestionES 型別的文件,並提供了一些自定義查詢方法。可以類比於用於操作資料庫的mapper介面類。import
com.guan.friend.domain.question.es.QuestionES;
import
org.springframework.data.domain.Page;
import
org.springframework.data.domain.Pageable;
import
org.springframework.data.elasticsearch.annotations.Query;
import
org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import
org.springframework.stereotype.Repository;
@Repository
publicinterfaceIQuestionRepositoryextendsElasticsearchRepository<QuestionES, Long>
{
Page<QuestionES> findQuestionByDifficulty(Integer difficulty, Pageable pageable)
;
//select * from tb_question where (title like '%aaa%' or content like '%bbb%') and difficulty = 1
@Query
(
"{\"bool\": {\"should\": [{ \"match\": { \"title\": \"?0\" } }, { \"match\": { \"content\": \"?1\" } }], \"minimum_should_match\": 1, \"must\": [{\"term\": {\"difficulty\": \"?2\"}}]}}"
)
Page<QuestionES> findByTitleOrContentAndDifficulty(String keywordTitle, String keywordContent,Integer difficulty, Pageable pageable)
;
@Query
(
"{\"bool\": {\"should\": [{ \"match\": { \"title\": \"?0\" } }, { \"match\": { \"content\": \"?1\" } }], \"minimum_should_match\": 1}}"
)
Page<QuestionES> findByTitleOrContent(String keywordTitle, String keywordContent, Pageable pageable)
;
}
findQuestionByDifficulty
Page<QuestionES>
,表示分頁查詢的結果。difficulty 引數是查詢條件,Pageable 引數是分頁資訊,Pageable 包含了頁數和每頁條數等資訊。Spring Data Elasticsearch
的查詢派發機制生成的,不需要手動編寫查詢語句。它會自動根據方法名推匯出對應的查詢操作。findByTitleOrContentAndDifficulty
Elasticsearch Query DSL
(Elasticsearch 查詢語言)。
{
"bool"
: {
"should"
: [
{
"match"
: {
"title"
:
"?0"
} },
{
"match"
: {
"content"
:
"?1"
} }
],
"minimum_should_match"
:
1
,
"must"
: [
{
"term"
: {
"difficulty"
:
"?2"
} }
]
}
}
-
should: 表示“或”條件,查詢中 title 或 content 欄位必須匹配給定的關鍵字( ?0
和?1
分別是方法引數keywordTitle
和keywordContent
)。minimum_should_match: 1
意味著至少一個 should 子句必須匹配。 -
must: 表示“且”條件,查詢中 difficulty 欄位必須匹配給定的難度(?2 是方法引數 difficulty)。該查詢會檢索標題或內容包含關鍵詞的文件,並且難度符合指定值。
findByTitleOrContent
findByTitleOrContentAndDifficulty
方法類似,但沒有新增 difficulty 欄位的篩選條件。查詢的條件是標題或內容匹配給定的關鍵詞,minimum_should_match: 1
表示至少一個 should 子句匹配。
{
"bool"
: {
"should"
: [
{
"match"
: {
"title"
:
"?0"
} },
{
"match"
: {
"content"
:
"?1"
} }
],
"minimum_should_match"
:
1
}
}
?0
和 ?1
分別是方法引數 keywordTitle
和 keywordContent
)。minimum_should_match: 1
表示至少一個 should 子句匹配。5.2. MySQL–Mapper
import
com.baomidou.mybatisplus.core.mapper.BaseMapper;
import
com.guan.friend.domain.question.Question;
publicinterfaceQuestionMapperextendsBaseMapper<Question>
{
}
6. Service程式碼
import
cn.hutool.core.bean.BeanUtil;
import
cn.hutool.core.collection.CollectionUtil;
import
cn.hutool.core.util.StrUtil;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import
com.guan.common.core.domain.TableDataInfo;
import
com.guan.friend.domain.question.Question;
import
com.guan.friend.domain.question.dto.QuestionQueryDTO;
import
com.guan.friend.domain.question.es.QuestionES;
import
com.guan.friend.domain.question.vo.QuestionVO;
import
com.guan.friend.elasticsearch.IQuestionRepository;
import
com.guan.friend.mapper.question.QuestionMapper;
import
com.guan.friend.service.question.IQuestionService;
import
jakarta.annotation.Resource;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.data.domain.Page;
import
org.springframework.data.domain.PageRequest;
import
org.springframework.data.domain.Pageable;
import
org.springframework.data.domain.Sort;
import
org.springframework.stereotype.Service;
import
java.util.List;
@Service
publicclassQuestionServiceImplimplementsIQuestionService
{
@Autowired
private
IQuestionRepository questionRepository;
@Resource
private
QuestionMapper questionMapper;
@Override
public TableDataInfo list(QuestionQueryDTO questionQueryDTO)
{
long
count = questionRepository.count();
// 如果ES沒有資料,從資料庫同步
if
(count <=
0
){
refreshQuestion();
}
// 指定排序規則是 按照建立時間 降序(新建立的題目在最上面)
Sort orders = Sort.by(Sort.Direction.DESC,
"createTime"
);
// 維護分頁
Pageable pageable = PageRequest.
of(questionQueryDTO.getPageNum() -
1
, questionQueryDTO.getPageSize(), orders);
Integer difficulty = questionQueryDTO.getDifficulty();
String keywords = questionQueryDTO.getKeywords();
Page<QuestionES> questionESPage;
if
(difficulty ==
null
&& StrUtil.isEmpty(keywords)){
// 查詢引數都為空
questionESPage = questionRepository.findAll(pageable);
}
elseif
(StrUtil.isEmpty(keywords)){
// 查詢題目或內容為空
questionESPage = questionRepository.findQuestionByDifficulty(difficulty, pageable);
}
elseif
(difficulty ==
null
){
// 查詢難度為空
questionESPage = questionRepository.findByTitleOrContent(keywords, keywords, pageable);
}
else
{
// 查詢條件都不為空
questionESPage = questionRepository.findByTitleOrContentAndDifficulty(keywords, keywords, difficulty, pageable);
}
// 獲取es中檢索到的全部資料的數量
long
total = questionESPage.getTotalElements();
if
(total <=
0
){
return
TableDataInfo.empty();
}
// 將ES的資料轉換成VO
List<QuestionES> questionESList = questionESPage.getContent();
List<QuestionVO> questionVOList = BeanUtil.copyToList(questionESList, QuestionVO
.class)
;
return
TableDataInfo.success(questionVOList, total);
}
privatevoidrefreshQuestion()
{
List<Question> questionList = questionMapper.selectList(
new
LambdaQueryWrapper<Question>());
if
(CollectionUtil.isEmpty(questionList)){
return
;
}
// 將資料庫查到的題目列表資料 重新整理到 ES 中
// 轉換列表資料型別
List<QuestionES> questionESList = BeanUtil.copyToList(questionList, QuestionES
.class)
;
questionRepository.saveAll(questionESList);
}
}









