作为后端开发,你一定也遇到过这种需求:给项目加个搜索功能。比如电商网站搜商品、博客平台搜文章、管理系统搜用户等。
来看一个常见的业务场景:你的电商平台需要实现商品搜索功能。如果用传统的 SQL LIKE 查询:
SELECT * FROM products WHERE name LIKE '%苹果%' OR name LIKE '%手机%';
这种方案存在明显的痛点:
- 搜索速度慢(特别是数据量大时)
- 搜索结果不智能("苹果水果"和"苹果手机"排在一起)
- 不支持拼音搜索(用户输入"pingguo"搜不到)
- 无法按相关性排序
面对这些问题,很多团队的第一反应是上 Elasticsearch。但 ES 对于中小型项目来说,真的合适吗?
ES 确实强大,但它也有明显的缺点:
ES 的痛点:
- 学习曲线陡峭
- 资源消耗大(内存、CPU)
- 部署维护复杂
- 对于百万级数据有点杀鸡用牛刀
难道没有更合适的选择吗?
今天给大家分享一个轻量级替代品:Manticore Search。
什么是 Manticore Search?
Manticore Search 是一个开源的轻量级搜索引擎,前身是 Sphinx Search。它最大的亮点是:完全兼容 MySQL 协议,意味着你可以用熟悉的 SQL 语句进行搜索!
与 Elasticsearch 对比
| 特性 | Manticore Search | Elasticsearch |
|---|---|---|
| 学习成本 | 低(熟悉SQL即可) | 高 |
| 内存占用 | 50MB 左右 | 1GB+ |
| 启动速度 | 3-5秒 | 30秒+ |
| 部署复杂度 | 简单 | 复杂 |
| 百万数据搜索 | 毫秒级 | 毫秒级 |
| 分布式 | 支持 | 支持 |
3分钟搞定全文搜索
环境准备
使用 Docker 快速启动(确保已安装 Docker):
docker run -d -p 9306:9306 --name manticore manticoresearch/manticore
验证启动:
docker ps
# 应该看到 manticore 容器运行中
没错,就这么简单! 一行命令,3秒启动,内存占用不到50MB。
连接测试
# 使用命令行连接
mysql -h 127.0.0.1 -P 9306
# 或者使用 Navicat、DBeaver(推荐) 等图形化工具
# 主机:127.0.0.1
# 端口:9306
# 用户名/密码:留空即可
创建搜索索引
在 Manticore 中,我们创建的是索引而不是表,但语法跟 MySQL 几乎一样:
-- 1. 创建商品索引(使用正确的 Manticore 语法)
CREATE TABLE products (
id BIGINT,
title TEXT,
description TEXT,
price FLOAT,
category STRING,
status INT,
created_at TIMESTAMP
);
-- 2. 插入测试数据
INSERT INTO products VALUES
(1, 'iPhone 15 苹果手机 旗舰版', '最新款苹果手机,A16芯片,性能强劲', 5999.0, 'electronics', 1, 1609459200),
(2, '小米降噪无线耳机', '主动降噪蓝牙耳机,续航时间长', 399.0, 'electronics', 1, 1609545600),
(3, '新鲜红富士苹果水果 5斤装', '山东红富士苹果,甜脆多汁,新鲜送达', 29.9, 'food', 1, 1609632000),
(4, '华为Mate 60 手机', '麒麟芯片,卫星通话,超可靠设计', 4999.0, 'electronics', 1, 1609718400),
(5, '苹果充电器 20W快充', '原装正品快充头,安全快速', 149.0, 'electronics', 1, 1609804800);
-- 3. 测试搜索
SELECT *, WEIGHT() as score FROM products WHERE MATCH('苹果') ORDER BY score DESC;
-- 4. 查看表结构
DESC products;
-- 5. 查看所有表
SHOW TABLES;
如图:
SpringBoot 项目集成示例
MySQL 作为主存储,Manticore 作为搜索引擎
1. 依赖配置
<!-- pom.xml -->
<dependencies>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Spring Boot JDBC (用于Manticore) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
2. 配置文件
# application.yml
spring:
# MySQL 数据源 (主数据库)
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
username: root
password: 123456
# Manticore 数据源 (搜索引擎)
manticore:
url: jdbc:mysql://localhost:9306/?characterEncoding=utf8
username:
password:
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段
logic-delete-value: 1
logic-not-delete-value: 0
3. 数据源配置
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.manticore")
public DataSource manticoreDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public JdbcTemplate manticoreJdbcTemplate() {
return new JdbcTemplate(manticoreDataSource());
}
}
4. 实体类
// MySQL 实体类
@Data
@TableName("products")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String description;
private BigDecimal price;
private Integer categoryId;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
// 搜索结果的 DTO
@Data
public class ProductSearchResult {
private Long id;
private String title;
private String description;
private BigDecimal price;
private Integer categoryId;
private Integer score; // 相关性分数
}
5. MyBatis-Plus Mapper
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
// 根据ID列表查询(保持顺序)
@Select("<script>" +
"SELECT * FROM products WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
" ORDER BY FIELD(id, " +
"<foreach collection='ids' item='id' separator=','>#{id}</foreach>" +
")" +
"</script>")
List<Product> selectByIdsInOrder(@Param("ids") List<Long> ids);
}
6. Manticore 搜索 Repository
@Repository
public class ProductSearchRepository {
private final JdbcTemplate manticoreJdbcTemplate;
public ProductSearchRepository(JdbcTemplate manticoreJdbcTemplate) {
this.manticoreJdbcTemplate = manticoreJdbcTemplate;
}
/**
* 搜索商品
*/
public List<ProductSearchResult> search(String keyword, Integer categoryId) {
String sql = "SELECT id, title, description, price, category_id, WEIGHT() as score " +
"FROM products_idx WHERE MATCH(?)";
List<Object> params = new ArrayList<>();
params.add(keyword);
if (categoryId != null) {
sql += " AND category_id = ?";
params.add(categoryId);
}
sql += " ORDER BY score DESC LIMIT 100";
return manticoreJdbcTemplate.query(sql, (rs, rowNum) -> {
ProductSearchResult result = new ProductSearchResult();
result.setId(rs.getLong("id"));
result.setTitle(rs.getString("title"));
result.setDescription(rs.getString("description"));
result.setPrice(rs.getBigDecimal("price"));
result.setCategoryId(rs.getInt("category_id"));
result.setScore(rs.getInt("score"));
return result;
}, params.toArray());
}
/**
* 同步商品到搜索索引
*/
public void syncToIndex(Product product) {
String sql = "REPLACE INTO products_idx (id, title, description, price, category_id, status) " +
"VALUES (?, ?, ?, ?, ?, ?)";
manticoreJdbcTemplate.update(sql,
product.getId(),
product.getTitle(),
product.getDescription(),
product.getPrice(),
product.getCategoryId(),
product.getStatus()
);
}
/**
* 从索引删除
*/
public void deleteFromIndex(Long id) {
String sql = "DELETE FROM products_idx WHERE id = ?";
manticoreJdbcTemplate.update(sql, id);
}
}
7. Service 层
@Service
public class ProductService {
private final ProductMapper productMapper;
private final ProductSearchRepository searchRepository;
public ProductService(ProductMapper productMapper,
ProductSearchRepository searchRepository) {
this.productMapper = productMapper;
this.searchRepository = searchRepository;
}
/**
* 添加商品(双写)
*/
public boolean addProduct(Product product) {
// 插入 MySQL
int result = productMapper.insert(product);
if (result > 0) {
// 同步到搜索索引
searchRepository.syncToIndex(product);
return true;
}
return false;
}
/**
* 更新商品(双写)
*/
public boolean updateProduct(Product product) {
// 更新 MySQL
int result = productMapper.updateById(product);
if (result > 0) {
// 同步到搜索索引
searchRepository.syncToIndex(product);
return true;
}
return false;
}
/**
* 删除商品(双删)
*/
public boolean deleteProduct(Long id) {
// 从 MySQL 删除(逻辑删除)
int result = productMapper.deleteById(id);
if (result > 0) {
// 从搜索索引删除
searchRepository.deleteFromIndex(id);
return true;
}
return false;
}
/**
* 搜索商品
*/
public List<Product> searchProducts(String keyword, Integer categoryId) {
// 1. 从 Manticore 搜索获取ID和分数
List<ProductSearchResult> searchResults = searchRepository.search(keyword, categoryId);
if (searchResults.isEmpty()) {
return Collections.emptyList();
}
// 2. 从 MySQL 获取完整商品信息
List<Long> productIds = searchResults.stream()
.map(ProductSearchResult::getId)
.collect(Collectors.toList());
return productMapper.selectByIdsInOrder(productIds);
}
/**
* 获取商品详情(从MySQL)
*/
public Product getProductDetail(Long id) {
return productMapper.selectById(id);
}
}
8. Controller 层
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
public Result addProduct(@RequestBody Product product) {
boolean success = productService.addProduct(product);
return success ? Result.success("添加成功") : Result.error("添加失败");
}
@PutMapping("/{id}")
public Result updateProduct(@PathVariable Long id, @RequestBody Product product) {
product.setId(id);
boolean success = productService.updateProduct(product);
return success ? Result.success("更新成功") : Result.error("更新失败");
}
@DeleteMapping("/{id}")
public Result deleteProduct(@PathVariable Long id) {
boolean success = productService.deleteProduct(id);
return success ? Result.success("删除成功") : Result.error("删除失败");
}
@GetMapping("/search")
public Result searchProducts(@RequestParam String q,
@RequestParam(required = false) Integer categoryId) {
List<Product> products = productService.searchProducts(q, categoryId);
return Result.success(products);
}
@GetMapping("/{id}")
public Result getProduct(@PathVariable Long id) {
Product product = productService.getProductDetail(id);
return Result.success(product);
}
}
// 统一返回结果
@Data
class Result {
private boolean success;
private String message;
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.setSuccess(true);
result.setData(data);
return result;
}
public static Result success(String message) {
Result result = new Result();
result.setSuccess(true);
result.setMessage(message);
return result;
}
public static Result error(String message) {
Result result = new Result();
result.setSuccess(false);
result.setMessage(message);
return result;
}
}
这个示例展示了:
- MySQL 负责数据存储和事务
- Manticore 负责全文搜索
- 数据双写保持同步
- 搜索时先查 Manticore 获取ID,再查 MySQL 获取完整数据
- 使用 MyBatis-Plus 进行 MySQL 操作
可以根据实际需求调整和扩展这个基础框架。
为什么 Manticore 比 LIKE 强这么多?
1. 智能分词
LIKE 的问题:
-- 搜不到!因为 LIKE 是精确匹配
SELECT * FROM products WHERE name LIKE '%苹果手机%';
Manticore 的优势:
-- 这些都能搜到相同的结果!
SELECT * FROM products WHERE MATCH('苹果手机');
SELECT * FROM products WHERE MATCH('手机 苹果');
SELECT * FROM products WHERE MATCH('苹果');
2. 相关性排序
LIKE 查询的结果是随机的,而 Manticore 会计算匹配度:
SELECT *, WEIGHT() as score
FROM products
WHERE MATCH('苹果')
ORDER BY score DESC;
结果会按相关性从高到低排列:
- “iPhone 15 苹果手机”(得分:100)
- “新鲜红富士苹果水果”(得分:85)
- “苹果充电器”(得分:60)
3. 性能对比
在 100万 条商品数据中测试:
| 搜索方式 | 响应时间 | CPU占用 |
|---|---|---|
| SQL LIKE | 2-5秒 | 高 |
| Manticore | 10-50毫秒 | 低 |
4. 高级功能
拼音搜索
-- 搜拼音也能找到!
SELECT * FROM products WHERE MATCH('pingguo');
布尔搜索
-- 搜索包含苹果但不包含水果的商品
SELECT * FROM products WHERE MATCH('苹果 -水果');
字段限定搜索
-- 只在标题中搜索
SELECT * FROM products WHERE MATCH('@title 苹果');
性能优化配置
创建更优化的索引:
CREATE TABLE products (
id BIGINT,
title TEXT,
description TEXT,
price FLOAT,
category VARCHAR(50),
tags JSON,
-- 创建字段索引加速过滤
INDEX price_index(price),
INDEX category_index(category)
)
-- 配置中文分词
charset_table = 'cjk, non_cjk, english, russian'
html_strip = '1'
min_infix_len = '2'
什么时候该用 Manticore,什么时候该用 ES?
选择 Manticore 的情况:
- 数据量在百万到千万级别
- 团队熟悉 SQL,学习成本要低
- 服务器资源有限
- 需要快速上线验证想法
- 主要需求是全文搜索,不需要复杂的聚合分析
选择 Elasticsearch 的情况:
- 数据量达到亿级以上
- 需要复杂的聚合分析和数据可视化
- 需要完整的 ELK 技术栈(日志分析)
- 有专业的运维团队
- 需要强大的集群和高可用特性
总结
Manticore Search 的优势在于:
- 简单:会 SQL 就能用
- 快速:部署3秒,搜索毫秒级
- 轻量:内存占用小
- 强大:该有的搜索功能都有
如果你的项目正被搜索性能问题困扰,或者你担心 ES 的复杂度,可以试试 Manticore Search。
相信你会被它的简单和高效所惊艳!
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《async/await 到底要不要加 try-catch?异步错误处理最佳实践》
《都在用 Java8 和 Java17,那 Java9 到 16 呢?他们真的没用吗?》

被折叠的 条评论
为什么被折叠?



