自定义数据索引:基于MyBatis-Plus实现全文搜索和模糊查询
引言:为什么需要自定义数据索引?
在日常开发中,我们经常面临这样的场景:用户需要在海量数据中快速找到相关信息,传统的精确匹配查询已经无法满足需求。无论是电商平台的商品搜索、内容管理系统的文章检索,还是企业应用的数据查询,高效的全文搜索和模糊查询能力都至关重要。
MyBatis-Plus作为MyBatis的增强工具,提供了强大的条件构造器和灵活的查询能力。本文将深入探讨如何基于MyBatis-Plus构建自定义数据索引,实现高效的全文搜索和模糊查询功能。
一、MyBatis-Plus模糊查询基础
1.1 SqlLike枚举详解
MyBatis-Plus提供了SqlLike枚举来支持不同类型的模糊查询:
public enum SqlLike {
/**
* %值 - 左模糊匹配
*/
LEFT,
/**
* 值% - 右模糊匹配
*/
RIGHT,
/**
* %值% - 全模糊匹配
*/
DEFAULT
}
1.2 基础模糊查询示例
// 创建查询包装器
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 全模糊匹配 - 搜索包含"张"的用户名
queryWrapper.like("username", "张");
// 左模糊匹配 - 搜索以"com"结尾的邮箱
queryWrapper.likeLeft("email", "com");
// 右模糊匹配 - 搜索以"138"开头的手机号
queryWrapper.likeRight("phone", "138");
// 使用Lambda表达式
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.like(User::getUsername, "张");
1.3 多字段联合搜索
// 多字段模糊查询
QueryWrapper<Article> wrapper = new QueryWrapper<>();
wrapper.like("title", keyword)
.or()
.like("content", keyword)
.or()
.like("tags", keyword);
// 对应的SQL:
// WHERE title LIKE '%keyword%' OR content LIKE '%keyword%' OR tags LIKE '%keyword%'
二、构建自定义全文搜索索引
2.1 搜索索引表设计
为了实现高效的全文搜索,我们需要设计专门的搜索索引表:
CREATE TABLE search_index (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
entity_type VARCHAR(50) NOT NULL COMMENT '实体类型',
entity_id BIGINT NOT NULL COMMENT '实体ID',
search_content TEXT NOT NULL COMMENT '搜索内容',
weight INT DEFAULT 1 COMMENT '权重',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_entity_type (entity_type),
INDEX idx_search_content (search_content(255)),
INDEX idx_entity (entity_type, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2 索引构建策略
@Service
public class SearchIndexService {
@Autowired
private SearchIndexMapper searchIndexMapper;
/**
* 构建文章搜索索引
*/
public void buildArticleIndex(Article article) {
SearchIndex index = new SearchIndex();
index.setEntityType("article");
index.setEntityId(article.getId());
// 组合搜索内容:标题+内容+标签
String searchContent = article.getTitle() + " " +
article.getContent() + " " +
String.join(" ", article.getTags());
index.setSearchContent(searchContent);
index.setWeight(calculateWeight(article));
searchIndexMapper.insertOrUpdate(index);
}
/**
* 计算搜索权重
*/
private int calculateWeight(Article article) {
int weight = 1;
// 根据发布时间、浏览量等计算权重
if (article.getPublishTime().after(DateUtils.addDays(new Date(), -7))) {
weight += 2; // 近期文章权重更高
}
if (article.getViewCount() > 1000) {
weight += 1; // 热门文章权重更高
}
return weight;
}
}
2.3 索引更新机制
@Component
public class SearchIndexListener {
@EventListener
public void handleArticleSave(ArticleSavedEvent event) {
searchIndexService.buildArticleIndex(event.getArticle());
}
@EventListener
public void handleArticleDelete(ArticleDeletedEvent event) {
searchIndexService.deleteIndex("article", event.getArticleId());
}
}
三、高级搜索功能实现
3.1 分词搜索实现
public class WordSegmenter {
private static final Pattern WORD_PATTERN = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS);
/**
* 简单中文分词
*/
public static List<String> segmentChinese(String text) {
List<String> words = new ArrayList<>();
if (StringUtils.isBlank(text)) {
return words;
}
// 简单按字符分割,实际项目中应使用专业分词库
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (isChineseCharacter(c)) {
words.add(String.valueOf(c));
}
}
return words;
}
/**
* 英文分词
*/
public static List<String> segmentEnglish(String text) {
List<String> words = new ArrayList<>();
if (StringUtils.isBlank(text)) {
return words;
}
Matcher matcher = WORD_PATTERN.matcher(text);
while (matcher.find()) {
words.add(matcher.group().toLowerCase());
}
return words;
}
private static boolean isChineseCharacter(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A;
}
}
3.2 权重排序搜索
@Service
public class AdvancedSearchService {
@Autowired
private SearchIndexMapper searchIndexMapper;
/**
* 高级搜索:支持分词、权重排序
*/
public Page<SearchResult> advancedSearch(String keyword, Pageable pageable) {
// 分词处理
List<String> keywords = WordSegmenter.segmentChinese(keyword);
keywords.addAll(WordSegmenter.segmentEnglish(keyword));
if (keywords.isEmpty()) {
return new PageImpl<>(Collections.emptyList());
}
// 构建查询条件
QueryWrapper<SearchIndex> wrapper = new QueryWrapper<>();
for (String word : keywords) {
wrapper.like("search_content", word);
}
// 按权重降序排序
wrapper.orderByDesc("weight");
// 执行查询
Page<SearchIndex> indexPage = searchIndexMapper.selectPage(
new Page<>(pageable.getPageNumber(), pageable.getPageSize()),
wrapper
);
// 转换为搜索结果
return convertToSearchResult(indexPage);
}
}
3.3 搜索建议功能
@RestController
@RequestMapping("/api/search")
public class SearchController {
@Autowired
private SearchService searchService;
/**
* 搜索建议
*/
@GetMapping("/suggestions")
public List<String> getSuggestions(@RequestParam String query) {
if (StringUtils.isBlank(query) || query.length() < 2) {
return Collections.emptyList();
}
return searchService.getSuggestions(query.trim(), 10);
}
/**
* 热门搜索词
*/
@GetMapping("/hot-keywords")
public List<String> getHotKeywords() {
return searchService.getHotKeywords(20);
}
}
四、性能优化策略
4.1 数据库索引优化
-- 创建全文索引(MySQL 5.6+)
ALTER TABLE search_index
ADD FULLTEXT INDEX idx_fulltext_search (search_content)
WITH PARSER ngram;
-- 查询使用全文索引
SELECT * FROM search_index
WHERE MATCH(search_content) AGAINST('关键词' IN NATURAL LANGUAGE MODE);
4.2 查询性能优化
public class SearchOptimizer {
/**
* 查询条件优化
*/
public static QueryWrapper<SearchIndex> optimizeQuery(String keyword) {
QueryWrapper<SearchIndex> wrapper = new QueryWrapper<>();
// 关键词长度过滤
if (keyword.length() < 2) {
return wrapper; // 返回空条件
}
// 根据关键词长度选择不同的搜索策略
if (keyword.length() <= 5) {
// 短关键词使用精确匹配
wrapper.like("search_content", keyword);
} else {
// 长关键词使用分词搜索
List<String> words = WordSegmenter.segmentChinese(keyword);
for (String word : words) {
wrapper.like("search_content", word);
}
}
return wrapper;
}
/**
* 分页优化
*/
public static Page<SearchIndex> optimizePagination(Pageable pageable) {
// 限制最大分页大小
int pageSize = Math.min(pageable.getPageSize(), 100);
return new Page<>(pageable.getPageNumber(), pageSize);
}
}
4.3 缓存策略
@Service
@CacheConfig(cacheNames = "searchCache")
public class CachedSearchService {
@Autowired
private SearchService searchService;
/**
* 带缓存的搜索
*/
@Cacheable(key = "#keyword + ':' + #pageable.pageNumber + ':' + #pageable.pageSize")
public Page<SearchResult> cachedSearch(String keyword, Pageable pageable) {
return searchService.search(keyword, pageable);
}
/**
* 清除搜索缓存
*/
@CacheEvict(allEntries = true)
public void clearCache() {
// 清除所有搜索缓存
}
}
五、实战案例:电商商品搜索
5.1 商品搜索索引构建
@Component
public class ProductIndexBuilder {
@Autowired
private SearchIndexMapper searchIndexMapper;
@Transactional
public void buildProductIndex(Product product) {
SearchIndex index = new SearchIndex();
index.setEntityType("product");
index.setEntityId(product.getId());
// 构建搜索内容:名称+品牌+分类+属性
StringBuilder content = new StringBuilder();
content.append(product.getName()).append(" ");
content.append(product.getBrand()).append(" ");
content.append(product.getCategory()).append(" ");
// 添加属性信息
product.getAttributes().forEach((key, value) -> {
content.append(key).append(" ").append(value).append(" ");
});
index.setSearchContent(content.toString());
index.setWeight(calculateProductWeight(product));
searchIndexMapper.insertOrUpdate(index);
}
private int calculateProductWeight(Product product) {
int weight = 1;
if (product.getSales() > 1000) weight += 3;
if (product.getRating() >= 4.5) weight += 2;
if (product.isNewArrival()) weight += 1;
return weight;
}
}
5.2 商品搜索服务
@Service
public class ProductSearchService {
@Autowired
private ProductMapper productMapper;
@Autowired
private SearchIndexMapper searchIndexMapper;
public Page<Product> searchProducts(String keyword, ProductSearchRequest request) {
// 搜索索引表
QueryWrapper<SearchIndex> indexWrapper = new QueryWrapper<>();
indexWrapper.eq("entity_type", "product")
.like("search_content", keyword);
// 按权重排序
indexWrapper.orderByDesc("weight");
Page<SearchIndex> indexPage = searchIndexMapper.selectPage(
new Page<>(request.getPage(), request.getSize()),
indexWrapper
);
// 获取商品ID列表
List<Long> productIds = indexPage.getRecords().stream()
.map(SearchIndex::getEntityId)
.collect(Collectors.toList());
if (productIds.isEmpty()) {
return new PageImpl<>(Collections.emptyList());
}
// 查询商品详细信息
QueryWrapper<Product> productWrapper = new QueryWrapper<>();
productWrapper.in("id", productIds);
// 添加其他筛选条件
if (StringUtils.isNotBlank(request.getCategory())) {
productWrapper.eq("category", request.getCategory());
}
if (request.getMinPrice() != null) {
productWrapper.ge("price", request.getMinPrice());
}
if (request.getMaxPrice() != null) {
productWrapper.le("price", request.getMaxPrice());
}
List<Product> products = productMapper.selectList(productWrapper);
// 保持搜索结果的排序
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));
List<Product> sortedProducts = productIds.stream()
.filter(productMap::containsKey)
.map(productMap::get)
.collect(Collectors.toList());
return new PageImpl<>(sortedProducts, PageRequest.of(request.getPage(), request.getSize()), indexPage.getTotal());
}
}
六、监控与维护
6.1 搜索性能监控
@Aspect
@Component
@Slf4j
public class SearchPerformanceMonitor {
@Around("execution(* com.example.service..*.search*(..))")
public Object monitorSearchPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
// 记录搜索性能日志
logSearchPerformance(methodName, args, duration, true);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
logSearchPerformance(methodName, args, duration, false);
throw e;
}
}
private void logSearchPerformance(String methodName, Object[] args, long duration, boolean success) {
log.info("SearchPerformance - method: {}, duration: {}ms, success: {}, args: {}",
methodName, duration, success, Arrays.toString(args));
// 可以发送到监控系统
Metrics.counter("search.performance")
.tag("method", methodName)
.tag("success", String.valueOf(success))
.record(duration);
}
}
6.2 索引维护任务
@Component
@Slf4j
public class IndexMaintenanceTask {
@Autowired
private SearchIndexMapper searchIndexMapper;
/**
* 每天凌晨清理过期索引
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupExpiredIndexes() {
log.info("开始清理过期搜索索引...");
// 删除30天前的索引记录
LocalDateTime expireTime = LocalDateTime.now().minusDays(30);
QueryWrapper<SearchIndex> wrapper = new QueryWrapper<>();
wrapper.lt("updated_time", expireTime);
int deletedCount = searchIndexMapper.delete(wrapper);
log.info("清理完成,共删除{}条过期索引记录", deletedCount);
}
/**
* 每周重建权重
*/
@Scheduled(cron = "0 0 3 * * 1") // 每周一凌晨3点
public void rebuildWeights() {
log.info("开始重建搜索索引权重...");
// 根据业务逻辑重新计算权重
// 这里可以根据销量、评分、时间等因素重新计算权重
log.info("权重重建完成");
}
}
七、总结与最佳实践
通过本文的探讨,我们了解了如何基于MyBatis-Plus构建强大的全文搜索和模糊查询系统。以下是关键的最佳实践:
7.1 设计原则
- 分离关注点:将搜索索引与业务数据分离,避免影响主业务表性能
- 适度冗余:在搜索索引中存储必要的冗余信息以提高查询性能
- 异步处理:索引构建和更新采用异步方式,避免阻塞主业务流程
7.2 性能优化
- 合理使用索引:为搜索字段创建合适的数据库索引
- 查询优化:避免全表扫描,使用分页限制结果集大小
- 缓存策略:对热门搜索结果进行缓存,减少数据库压力
7.3 可维护性
- 监控告警:建立完善的监控体系,及时发现性能问题
- 定期维护:设置定时任务进行索引清理和优化
- 版本控制:对搜索算法和索引结构进行版本管理
通过遵循这些最佳实践,您可以构建出高效、稳定、可扩展的全文搜索系统,为用户提供卓越的搜索体验。
进一步优化建议:
- 考虑引入专业的搜索引擎如Elasticsearch处理海量数据搜索
- 实现搜索词推荐和自动补全功能
- 添加搜索分析功能,了解用户搜索行为模式
- 支持多语言搜索和国际化需求
MyBatis-Plus的强大功能结合合理的架构设计,能够帮助您构建出满足各种复杂搜索需求的系统。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



