连接搜索引擎服务 Manticore + Egg.js
在上一篇文章中,我们已经成功将 Web 服务跑了起来,整体的 Egg.js 项目结构也已经搭建完成。接下来,就进入一个非常关键的环节: 连接搜索引擎服务,实现真正可用的搜索能力 。
本文将以 Manticore Search 作为全文检索引擎,通过 MySQL 协议 与 Egg.js 服务进行通信,完成搜索查询、分页以及统计信息的获取。
为什么选择 Manticore 的 MySQL 接口?
Manticore Search 在 Sphinx 的基础上进行了大量增强,它提供了多种访问方式:
- HTTP JSON API
- 原生 TCP 协议
- MySQL 协议(9306 端口)
在实际工程中,MySQL 协议有几个明显优势:
- 查询语法接近 SQL,学习成本低
- 可以直接复用现有的 MySQL 客户端库
- 性能稳定,适合高并发搜索场景
因此,这里我们选择 通过 MySQL 协议连接 Manticore 。
为什么不使用 egg-mysql?
一开始我尝试使用 egg-mysql 插件来连接 Manticore,但在实践中发现:
egg-mysql本质上是为 标准 MySQL Server 设计的- 在连接 Manticore(SphinxQL)时,存在兼容性问题
- 无法稳定执行
MATCH()、SHOW META等搜索相关语句
多次尝试未果后,最终放弃该方案。
选择 mysql2/promise 的原因
经过多方尝试,最终发现:
👉 mysql2/promise 可以稳定连接 Manticore 的 MySQL 接口
不过它也有一个需要注意的点:
- 长连接在一段时间后会自动断开
- 需要自行处理重连逻辑
在搜索场景中, 每次请求新建一个连接 是完全可以接受的:
- 连接耗时非常短
- 搜索请求通常是短生命周期
- 避免了复杂的连接池与心跳维护
因此本文采用的策略是: 每次搜索新建连接,用完即关 。
创建 Search Service
在 Egg.js 中,所有与业务相关的逻辑都推荐放在 app/service 目录下。
我们在项目中新增文件:
app/service/search.jsEgg.js 的整体目录结构可以参考官方文档: 👉 https://v3.eggjs.org/zh-CN/basics/structure
Search Service 完整代码
下面是完整的 search.js 实现代码(保持原样):
/* eslint-disable eqeqeq */
/* eslint-disable array-bracket-spacing */
'use strict';
const Service = require('egg').Service;
const mysql = require('mysql2/promise');
const { fantiConvert } = require('../../plugins/fanti');
module.exports = class extends Service {
async get(params) {
const connection = await mysql.createConnection({
host: '127.0.0.1',
port: 9306, // Manticore 默认端口
user: 'root', // 如果 Manticore 没有设置用户,可写空字符串 ''
password: '', // 没有密码也可以留空
database: 'p2pspider', // Sphinx 索引名,对应的“数据库名”
charset: 'utf8mb4',
});
// 默认参数
const defaultParams = {
word: '',
page: 1,
rows: 10,
};
// 合并参数
params = Object.assign(defaultParams, params);
params.page = params.page || 1;
params.rows = params.rows || 20;
let sql = 'select id, title from p2pspider';
sql += ` where MATCH('${params.word}')`;
if (!params.count) {
sql += ` limit ${(params.page - 1) * params.rows}, ${params.rows}`;
}
// 获取搜索结果
const val = await connection.query(sql);
// 获取统计数据
const meta = await connection.query('SHOW META');
const ret = { matches: val[0], words: [] };
for (let i = 0; i < meta[0].length; i++) {
if (/keyword/.test(meta[0][i].Variable_name)) {
ret.words.push(meta[0][i].Value);
}
if (meta[0][i].Variable_name == 'total_found') {
ret.total = meta[0][i].Value;
}
if (meta[0][i].Variable_name == 'time') {
ret.time = meta[0][i].Value;
}
}
// 关闭连接
await connection.end();
return ret;
}
// 过滤搜索关键词,避免某些非法字符导致 Manticore 报错
filterWord(word) {
if (this.ctx.helper.isEmpty(word)) return '';
word = word.replace(/\//g, ' ');
word = word.replace(/\\/g, ' ');
word = word.replace(/--/g, ' ');
word = word.replace(/@/g, ' ');
word = word.replace(/\(\)/g, ' ');
word = word.replace(/\!/g, ' ');
word = word.replace(/\^/g, ' ');
word = this.ctx.helper.escape(word);
// 由于最开始保存种子信息时已将繁体转换为简体
// 这里对用户输入的繁体同样进行转换
return fantiConvert(0, word);
}
};代码核心逻辑讲解
1️⃣ 连接 Manticore
mysql.createConnection({
host: '127.0.0.1',
port: 9306,
});9306是 Manticore 默认的 MySQL 协议端口database实际上对应的是 索引名称 ,并不是真正的 MySQL 数据库
2️⃣ 使用 MATCH 进行全文检索
SELECT id, title
FROM p2pspider
WHERE MATCH('关键词')
LIMIT offset, rowsMATCH() 是 Sphinx / Manticore 的核心全文检索函数:
- 支持分词
- 支持权重
- 支持高级查询语法
3️⃣ 使用 SHOW META 获取搜索统计信息
SHOW META可以获取:
total_found:匹配到的总记录数(用于分页)time:搜索耗时keyword[n]:实际命中的关键词
这一步非常适合用来构建搜索结果页的「统计信息」。
4️⃣ 搜索关键词过滤的重要性
Manticore 对搜索语法要求较严格,某些特殊字符会直接导致查询报错。
因此在 filterWord 中:
- 主动过滤危险字符
- 统一转义
- 对繁体输入做简体转换
这一点在 真实用户输入场景 中非常重要,可以有效避免服务异常。
小结
到这里,我们已经完成了:
✅ Egg.js 与 Manticore Search 的连接 ✅ 基于 MySQL 协议的全文搜索 ✅ 搜索分页与统计信息获取 ✅ 搜索关键词安全过滤
下一篇文章中,我们将继续向上层推进,把搜索 Service 接入 Controller,对外提供真正可用的搜索 API。




