<think>
我们使用Spring Boot 3整合Lucene和MyBatis-Flex来实现全文检索功能。Lucene是一个强大的全文搜索引擎库,我们将用它来创建索引和进行搜索。MyBatis-Flex是一个灵活的MyBatis增强框架,用于数据库操作。
步骤:
1. 创建Spring Boot项目并添加依赖。
2. 配置MyBatis-Flex和数据源。
3. 创建实体类和Mapper(使用MyBatis-Flex注解)。
4. 集成Lucene,创建索引工具类和搜索工具类。
5. 在服务层实现索引的创建和搜索。
6. 控制器层暴露API。
注意:由于Lucene本身不是分布式,这里我们使用单个索引示例。
一、添加依赖(pom.xml)
```xml
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Flex -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>1.7.5</version>
</dependency>
<!-- 数据库驱动(以MySQL为例) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lucene -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>9.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>9.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>8.11.2</version> <!-- 注意:9.x版本中analyzers-common已经合并到lucene-core,但这里我们使用8.x的common包,因为9.x之后分词器有改动,常用分词器在9.x中可能还未更新 -->
</dependency>
<!-- 其他可能需要的依赖,比如lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
```
注意:Lucene 9.x版本中,一些分词器已经迁移到不同的模块,如果你需要使用IKAnalyzer等第三方分词器,需要单独引入。这里我们使用标准分词器(StandardAnalyzer),它在lucene-core中已经包含。
二、配置数据源和MyBatis-Flex
application.yml:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-flex:
mapper-locations: classpath*:/mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 可选,打印SQL
```
三、创建实体类和Mapper
实体类(Article.java):
```java
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
@Table("article")
public class Article {
@Id
private Long id;
private String title;
private String content;
// getter和setter省略,可以使用lombok
}
```
Mapper接口(ArticleMapper.java):
```java
import com.mybatisflex.core.BaseMapper;
import com.example.demo.entity.Article;
public interface ArticleMapper extends BaseMapper<Article> {
}
```
四、Lucene工具类
创建索引工具类(LuceneIndexUtil.java)和搜索工具类(LuceneSearchUtil.java)。我们将索引存储在文件系统中,例如在项目的index目录下。
首先,配置索引存放路径(可以在application.yml中自定义,这里我们写死为"./index")。
创建索引工具类:
```java
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
@Component
public class LuceneIndexUtil {
private final String indexDir = "./index";
/**
* 创建索引
* @param articles 文章列表
*/
public void createIndex(List<Article> articles) throws IOException {
// 索引存放路径
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 标准分词器
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig config = new IndexWriterConfig(analyzer);
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE); // 每次都重新创建
IndexWriter indexWriter = new IndexWriter(directory, config);
for (Article article : articles) {
Document doc = new Document();
doc.add(new TextField("id", article.getId().toString(), Field.Store.YES));
doc.add(new TextField("title", article.getTitle(), Field.Store.YES));
doc.add(new TextField("content", article.getContent(), Field.Store.YES));
indexWriter.addDocument(doc);
}
indexWriter.close();
directory.close();
}
}
```
搜索工具类:
```java
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@Component
public class LuceneSearchUtil {
private final String indexDir = "./index";
public List<String> search(String field, String keyword) throws IOException, ParseException {
Directory directory = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
Analyzer analyzer = new StandardAnalyzer();
QueryParser parser = new QueryParser(field, analyzer);
Query query = parser.parse(keyword);
TopDocs topDocs = searcher.search(query, 10); // 取前10个
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
List<String> results = new ArrayList<>();
for (ScoreDoc scoreDoc : scoreDocs) {
Document doc = searcher.doc(scoreDoc.doc);
results.add(doc.get("title") + ": " + doc.get("content"));
}
reader.close();
directory.close();
return results;
}
}
```
五、服务层
ArticleService.java:
```java
import com.example.demo.entity.Article;
import com.example.demo.mapper.ArticleMapper;
import com.example.demo.util.LuceneIndexUtil;
import com.example.demo.util.LuceneSearchUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
@Service
public class ArticleService {
@Autowired
private ArticleMapper articleMapper;
@Autowired
private LuceneIndexUtil luceneIndexUtil;
@Autowired
private LuceneSearchUtil luceneSearchUtil;
public void createIndex() throws IOException {
List<Article> articles = articleMapper.selectAll();
luceneIndexUtil.createIndex(articles);
}
public List<String> search(String keyword) throws Exception {
// 这里我们对标题和内容都搜索,所以使用"content"字段,也可以使用多字段查询,但这里简单起见只搜索内容
return luceneSearchUtil.search("content", keyword);
}
}
```
六、控制器层
ArticleController.java:
```java
import com.example.demo.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ArticleController {
@Autowired
private ArticleService articleService;
@GetMapping("/createIndex")
public String createIndex() {
try {
articleService.createIndex();
return "索引创建成功";
} catch (Exception e) {
e.printStackTrace();
return "索引创建失败";
}
}
@GetMapping("/search")
public List<String> search(@RequestParam String keyword) {
try {
return articleService.search(keyword);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
```
七、启动类
```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
八、测试
1. 启动项目。
2. 访问`http://localhost:8080/createIndex`创建索引。
3. 访问`http://localhost:8080/search?keyword=spring`进行搜索。
注意:上述代码中,索引存储在当前运行目录下的index文件夹中。每次调用createIndex都会重新创建整个索引。
优化点:
1. 索引增量更新:可以使用IndexWriterConfig.OpenMode.CREATE_OR_APPEND,并记录最后更新时间。
2. 使用更合适的分词器(如IKAnalyzer中文分词)。
3. 多字段查询(可以使用MultiFieldQueryParser)。
4. 分页查询。
这里只是一个基础示例。
§§相关问题§§:
1. Lucene索引的增量更新如何实现?
2. 如何集成中文分词器(如IKAnalyzer)到Lucene?
3. 在Lucene中如何实现多字段同时检索?
4. 如何对搜索结果进行分页?
</think>
# Spring Boot3 + Lucene + MyBatis-Flex 构建全文检索方案
下面我将详细讲解如何使用Spring Boot 3整合Lucene和MyBatis-Flex构建全文检索系统。
## 整体方案设计
### 技术栈
- **Spring Boot 3**:基础框架
- **Lucene 9.x**:全文搜索引擎库
- **MyBatis-Flex**:高性能ORM框架
- **IK Analyzer**:中文分词器
- **MySQL**:数据存储
### 功能模块
1. 实体类映射数据库表
2. Lucene索引创建与维护
3. 全文检索服务实现
4. RESTful API接口
## 实现步骤
### 1. 添加依赖 (pom.xml)
```xml
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Flex -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>1.7.5</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lucene Core -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>9.8.0</version>
</dependency>
<!-- Lucene Query Parser -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>9.8.0</version>
</dependency>
<!-- IK Analyzer (中文分词) -->
<dependency>
<groupId>com.github.magese</groupId>
<artifactId>ik-analyzer</artifactId>
<version>8.5.0</version>
</dependency>
</dependencies>
```
### 2. 数据模型与Mapper
#### 实体类 (Article.java)
```java
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
@Table("articles")
public class Article {
@Id
private Long id;
@Column("title")
private String title;
@Column("content")
private String content;
@Column("create_time")
private LocalDateTime createTime;
// getters and setters
}
```
#### Mapper接口 (ArticleMapper.java)
```java
import com.mybatisflex.core.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
}
```
### 3. Lucene服务实现
#### Lucene配置类 (LuceneConfig.java)
```java
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.io.IOException;
import java.nio.file.Paths;
@Configuration
public class LuceneConfig {
@Bean
public Directory directory() throws IOException {
// 存储索引的目录
return FSDirectory.open(Paths.get("lucene-index"));
}
@Bean
public Analyzer analyzer() {
// 使用IK中文分词器
return new IKAnalyzer();
}
}
```
#### Lucene索引服务 (LuceneIndexService.java)
```java
import lombok.RequiredArgsConstructor;
import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
@Service
@RequiredArgsConstructor
public class LuceneIndexService {
private final Directory directory;
private final Analyzer analyzer;
// 添加文档到索引
public void addDocument(Article article) throws IOException {
try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(analyzer))) {
Document doc = new Document();
doc.add(new StringField("id", article.getId().toString(), Field.Store.YES));
doc.add(new TextField("title", article.getTitle(), Field.Store.YES));
doc.add(new TextField("content", article.getContent(), Field.Store.YES));
writer.addDocument(doc);
}
}
// 更新索引中的文档
public void updateDocument(Article article) throws IOException {
try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(analyzer))) {
Document doc = new Document();
doc.add(new StringField("id", article.getId().toString(), Field.Store.YES));
doc.add(new TextField("title", article.getTitle(), Field.Store.YES));
doc.add(new TextField("content", article.getContent(), Field.Store.YES));
writer.updateDocument(new Term("id", article.getId().toString()), doc);
}
}
// 删除索引中的文档
public void deleteDocument(Long id) throws IOException {
try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(analyzer))) {
writer.deleteDocuments(new Term("id", id.toString()));
}
}
// 批量创建索引
public void batchIndex(List<Article> articles) throws IOException {
try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(analyzer))) {
for (Article article : articles) {
Document doc = new Document();
doc.add(new StringField("id", article.getId().toString(), Field.Store.YES));
doc.add(new TextField("title", article.getTitle(), Field.Store.YES));
doc.add(new TextField("content", article.getContent(), Field.Store.YES));
writer.addDocument(doc);
}
writer.commit();
}
}
}
```
#### 全文检索服务 (SearchService.java)
```java
import lombok.RequiredArgsConstructor;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SearchService {
private final Directory directory;
private final Analyzer analyzer;
public List<Document> search(String keyword, int maxResults) throws Exception {
List<Document> results = new ArrayList<>();
try (IndexReader reader = DirectoryReader.open(directory)) {
IndexSearcher searcher = new IndexSearcher(reader);
// 在title和content字段中搜索
String[] fields = {"title", "content"};
QueryParser parser = new MultiFieldQueryParser(fields, analyzer);
Query query = parser.parse(keyword);
TopDocs topDocs = searcher.search(query, maxResults);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document doc = searcher.doc(scoreDoc.doc);
results.add(doc);
}
}
return results;
}
}
```
### 4. 业务逻辑层与控制器
#### 文章服务 (ArticleService.java)
```java
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ArticleService {
private final ArticleMapper articleMapper;
private final LuceneIndexService indexService;
@Transactional
public void addArticle(Article article) throws IOException {
articleMapper.insert(article);
indexService.addDocument(article);
}
@Transactional
public void updateArticle(Article article) throws IOException {
articleMapper.updateById(article);
indexService.updateDocument(article);
}
@Transactional
public void deleteArticle(Long id) throws IOException {
articleMapper.deleteById(id);
indexService.deleteDocument(id);
}
// 初始化所有文章的索引
public void initAllIndex() throws IOException {
List<Article> articles = articleMapper.selectAll();
indexService.batchIndex(articles);
}
}
```
#### 搜索控制器 (SearchController.java)
```java
import lombok.RequiredArgsConstructor;
import org.apache.lucene.document.Document;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
public class SearchController {
private final SearchService searchService;
@GetMapping
public List<Document> search(@RequestParam String keyword,
@RequestParam(defaultValue = "10") int size) {
try {
return searchService.search(keyword, size);
} catch (Exception e) {
throw new RuntimeException("搜索失败", e);
}
}
}
```
### 5. 索引初始化与维护
在应用启动时初始化索引:
```java
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@RequiredArgsConstructor
public class Application {
private final ArticleService articleService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@PostConstruct
public void init() {
try {
articleService.initAllIndex();
} catch (Exception e) {
System.err.println("索引初始化失败: " + e.getMessage());
}
}
}
```
## 代码解释
### 关键组件说明
1. **Lucene索引服务**:
- 负责文档的增删改操作
- 使用IKAnalyzer进行中文分词
- 索引存储在文件系统的"lucene-index"目录
2. **全文检索服务**:
- 使用MultiFieldQueryParser在多个字段中搜索
- 支持分页(通过maxResults参数)
- 返回包含匹配文档的列表
3. **MyBatis-Flex整合**:
- 简化数据库操作
- 提供高效的ORM映射
- 支持事务管理
### 索引维护策略
1. **初始化索引**:应用启动时全量同步
2. **实时同步**:
- 新增文章时自动创建索引
- 更新文章时更新索引
- 删除文章时删除索引
3. **定期间隔**:可添加定时任务增量更新索引
## 性能优化建议
1. **索引优化**:
- 定期使用`IndexWriter.forceMerge()`减少索引段数量
- 启用复合文件格式减少文件数量
2. **搜索优化**:
- 使用`BooleanQuery`组合多个查询条件
- 缓存常用查询结果
3. **并发处理**:
- 使用`NIOFSDirectory`替代`FSDirectory`提高并发性能
- 实现读写锁机制