连接搜索引擎服务 Manticore + Egg.js

2026-02-11 57 浏览 0 评论

在上一篇文章中,我们已经成功将 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.js

Egg.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, rows

MATCH() 是 Sphinx / Manticore 的核心全文检索函数:

  • 支持分词
  • 支持权重
  • 支持高级查询语法

3️⃣ 使用 SHOW META 获取搜索统计信息

SHOW META

可以获取:

  • total_found :匹配到的总记录数(用于分页)
  • time :搜索耗时
  • keyword[n] :实际命中的关键词

这一步非常适合用来构建搜索结果页的「统计信息」。


4️⃣ 搜索关键词过滤的重要性

Manticore 对搜索语法要求较严格,某些特殊字符会直接导致查询报错。

因此在 filterWord 中:

  • 主动过滤危险字符
  • 统一转义
  • 对繁体输入做简体转换

这一点在 真实用户输入场景 中非常重要,可以有效避免服务异常。


小结

到这里,我们已经完成了:

✅ Egg.js 与 Manticore Search 的连接 ✅ 基于 MySQL 协议的全文搜索 ✅ 搜索分页与统计信息获取 ✅ 搜索关键词安全过滤

下一篇文章中,我们将继续向上层推进,把搜索 Service 接入 Controller,对外提供真正可用的搜索 API。


发布评论

发布评论前请先 登录
取消
0 评论
点赞
收藏

评论列表 0

暂无评论