Manticore Search
在 PHP ThinkPHP 6 (简称 TP6) 框架中使用 Manticore Search,最推荐、最现代化的方式是使用官方提供的 Manticore PHP 客户端(基于 PSR-18 规范,支持 JSON Over HTTP 接口),或者利用 TP6 的数据库连接池通过 MySQL 协议直接查询。
以下为您梳理两种在 TP6 中集成和使用 Manticore Search 的主流方案。
方案一:使用官方扩展包(推荐,支持向量搜索与新特性)
官方的 manticoresearch/manticoresearch-php 扩展包功能最全,能够完美适配 Manticore 的全文检索、向量搜索(KNN)以及最新的各种复杂 JSON 查询。
1. 安装官方扩展包
在你的 TP6 项目根目录下,通过 Composer 安装: [1]
bash
composer require manticoresearch/manticoresearch-php
请谨慎使用此类代码。
2. 在 TP6 中封装单例服务(Service)
为了方便在控制器(Controller)或模型(Model)中复用,建议将其注册为 TP6 的服务或创建公共类。
创建文件 app/common/service/Manticore.php:
php
<?php
namespace app\common\service;
use ManticoreSearch\Client;
class Manticore
{
protected static $client = null;
public static function getClient()
{
if (self::$client === null) {
// 读取 TP6 config 目录下的自定义配置文件,或直接写死
$config = [
'host' => '127.0.0.1',
'port' => 9308 // Manticore 默认 HTTP 端口
];
self::$client = new Client($config);
}
return self::$client;
}
}
请谨慎使用此类代码。
3. 在控制器中进行增删改查
在控制器中,你可以直接调用官方客户端的方法:
php
<?php
namespace app\controller;
use app\BaseController;
use app\common\service\Manticore;
class Search extends BaseController
{
public function index()
{
$client = Manticore::getClient();
$index = $client->index('products'); // 假设你有一个名为 products 的索引
// 1. 添加文档
$index->addDocument([
'title' => '搭载 M4 芯片的全新 MacBook Pro',
'price' => 12999,
'tags' => ['apple', 'laptop']
], 101); // 101 为文档 ID
// 2. 全文检索
$results = $index->search('MacBook')->get();
// 3. 循环输出结果
$data = [];
foreach ($results as $hit) {
$data[] = [
'id' => $hit->getId(),
'title' => $hit->title,
'price' => $hit->price,
];
}
return json(['code' => 200, 'data' => $data]);
}
}
请谨慎使用此类代码。
方案二:通过 MySQL 协议接入(无需安装额外包,零门槛)
Manticore Search 原生支持 MySQL 协议(默认端口 9306)。你可以直接把 Manticore 当作一个普通的 MySQL 数据库,在 TP6 的配置文件中进行配置,然后使用 TP6 原生的 Db 门面(Facade)执行原生 SQL。
1. 配置数据库连接
打开 TP6 的数据库配置文件 config/database.php,在 connections 数组中追加 Manticore 的连接配置:
php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [ /* 你的主数据库配置 */ ],
// 追加 Manticore 配置
'manticore' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => '', // Manticore 采用扁平索引,留空即可
'username' => 'root', // 默认不需要账号密码,若设置了请填写
'password' => '',
'hostport' => '9306', // 注意:这里是 Manticore 的 MySQL 协议端口
'charset' => 'utf8mb4',
'prefix' => '',
],
],
];
请谨慎使用此类代码。
2. 在控制器中通过原生 SQL 查询
由于 Manticore 使用的是 SphinxQL(与标准 SQL 高度相似),你可以直接在 TP6 中使用 Db::connect('manticore') 执行查询:
php
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
class Search extends BaseController
{
public function searchBySql()
{
$keyword = input('keyword', 'MacBook');
// 使用 MATCH 进行高性能全文检索
// 注意:Manticore 的全文检索必须使用 MATCH('...') 语法
$sql = "SELECT id, title, price FROM products WHERE MATCH(:keyword) ORDER BY price DESC LIMIT 0, 20";
$results = Db::connect('manticore')->query($sql, [
'keyword' => $keyword
]);
return json(['code' => 200, 'data' => $results]);
}
public function insertBySql()
{
// 插入数据示例
$sql = "INSERT INTO products(id, title, price) VALUES(102, 'iPhone 17 Pro Max', 9999)";
Db::connect('manticore')->execute($sql);
return json(['code' => 200, 'msg' => '插入成功']);
}
}
请谨慎使用此类代码。
💡 核心开发建议
写操作路由:如果是海量日志或高频写入,推荐使用方案一。因为官方 PHP SDK 的 HTTP 协议在高并发批量写入(Bulk Insert)时,比频繁创建和断开 MySQL 链接更稳定且效率更高。
读操作与高亮显示:如果你需要对搜索关键词进行标签高亮(Highlighting),Manticore 支持在 SQL 中使用
HIGHLIGHT()函数。例如:sql
SELECT id, HIGHLIGHT() FROM products WHERE MATCH('MacBook')请谨慎使用此类代码。
你可以很方便地在方案二的原生查询中直接使用。
中文分词:请确保你的 Manticore 实例在创建
products索引(Table)时配置了中文字符集和分词器(如内置的jieba分词),否则中文匹配精度会大幅下降。
您目前的项目里,搜索场景主要是海量商品/文章的检索,还是需要做诸如 Log 日志分析或 AI 向量检索?可以告诉我具体的业务需求,我为您提供更针对性的代码设计建议。
Log 日志分析
针对 Log 日志分析 场景,Manticore Search 是一个极佳的轻量级、高性能方案,特别适合替代笨重的 ELK(Elasticsearch)架构。在 ThinkPHP 6(TP6)中实现日志分析,核心逻辑是:“异步/批量写入 + 列式存储聚合查询”。
为了确保在高并发日志写入下 TP6 不会卡死,同时能在前端实现毫秒级的日志检索与聚合分析,下面为您提供一套完整的架构设计与核心代码实现。
🏗️ 1. Manticore 表结构设计(推荐使用列式存储)
在 Manticore 中,日志分析场景强烈推荐使用 列式存储(Columnar Storage)。它可以让类似 GROUP BY、AVG、COUNT 的聚合统计速度提升数十倍,且极大地节省内存。
请先在 Manticore 中创建日志表(可以通过 MySQL 客户端执行):
sql
CREATE TABLE app_logs (
id bigint,
timestamp timestamp,
level string attribute,
env string attribute,
module string attribute,
message text,
ip string attribute,
execution_time float attribute
) engine='columnar'; -- 声明使用列式存储引擎
请谨慎使用此类代码。
注:message 作为 text 类型用于全文检索(查关键词、堆栈信息),其他字段作为 attribute(属性)用于过滤和聚合统计。
📥 2. 日志写入:TP6 自定义日志驱动(异步/批量)
高并发下,如果每产生一条日志就同步写入 Manticore,会严重拖慢 PHP 的响应速度。建议使用 TP6 的 自定义日志驱动,在请求结束时批量(Bulk)写入 Manticore。
步骤一:创建自定义日志驱动类
新建 app/common/log/driver/Manticore.php:
php
<?php
namespace app\common\log\driver;
use ManticoreSearch\Client;
use think\contract\LogHandlerInterface;
class Manticore implements LogHandlerInterface
{
protected $client;
protected $index = 'app_logs';
public function __construct()
{
// 初始化 Manticore 官方客户端
$this->client = new Client([
'host' => '127.0.0.1',
'port' => 9308 // HTTP 端口,批量写入性能更好
]);
}
public function save(array $log): bool
{
$docs = [];
// 雪花算法生成唯一ID(Manticore 的 ID 必须是正整数)
$snowflake = new \think\Id();
foreach ($log as $type => $messages) {
foreach ($messages as $msg) {
// 格式化或解析你的日志内容,这里假设日志是规范的字符串或数组
// 实际生产中可以解析出当前请求的 IP、执行时间等
$docs[] = [
'insert' => [
'index' => $this->index,
'id' => (int)$snowflake->id(),
'doc' => [
'timestamp' => time(),
'level' => $type,
'env' => app()->env('app_env', 'prod'),
'module' => app('http')->getName(), // 获取当前多应用/模块名
'message' => is_string($msg) ? $msg : json_encode($msg, JSON_UNESCAPED_UNICODE),
'ip' => request()->ip(),
'execution_time' => (float)number_format(microtime(true) - app()->getStartTime(), 4)
]
]
];
}
}
if (!empty($docs)) {
try {
// 使用 Manticore 的 Bulk 接口进行批量高效写入
$this->client->bulk(['body' => $docs]);
} catch (\Exception $e) {
// 降级处理:若 Manticore 异常,写入本地文件防止日志丢失
error_log($e->getMessage(), 3, runtime_path() . 'manticore_error.log');
return false;
}
}
return true;
}
}
请谨慎使用此类代码。
步骤二:修改 TP6 日志配置文件
打开 config/log.php,将通道切换或追加为 manticore:
php
return [
'default' => 'manticore',
'channels' => [
'file' => [ /* 原有的文件日志配置 */ ],
// 追加 Manticore 通道
'manticore' => [
'type' => \app\common\log\driver\Manticore::class,
],
],
];
请谨慎使用此类代码。
配置完成后,你在 TP6 中使用的 Log::error('用户登录失败') 或 trace('SQL执行超时', 'sql') 都会自动批量流入 Manticore。
📊 3. 日志分析与检索:控制器实现
日志收集进来后,我们需要在后台进行两类操作:关键词全文检索(排查 Bug) 和 多维聚合统计(监控大盘)。由于查询不涉及高频并发,直接使用 SQL 协议(方案二) 编写代码最直观、最易读。
新建日志分析控制器 app/controller/LogAnalyzer.php:
php
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
class LogAnalyzer extends BaseController
{
/**
* 1. 日志高级检索(支持关键词全文检索、级别过滤、时间范围)
*/
public function search()
{
$keyword = input('keyword', ''); // 搜索关键词,如 "NullPointerException"
$level = input('level', ''); // 日志级别,如 "error"
$startTime = input('start_time', time() - 86400); // 默认查24小时内
$endTime = input('end_time', time());
$page = input('page/d', 1);
$limit = input('limit/d', 20);
$offset = ($page - 1) * $limit;
// 基础 SQL
$where = ["timestamp BETWEEN :start AND :end"];
$params = ['start' => $startTime, 'end' => $endTime];
// 全文检索必须使用 MATCH
if (!empty($keyword)) {
$where[] = "MATCH(:keyword)";
$params['keyword'] = $keyword;
}
// 属性过滤直接用 =
if (!empty($level)) {
$where[] = "level = :level";
$params['level'] = $level;
}
$whereSql = implode(' AND ', $where);
// 执行查询(带上高亮函数 HIGHLIGHT(),方便前端查看关键词)
$sql = "SELECT id, timestamp, level, module, ip, execution_time, HIGHLIGHT() as message
FROM app_logs
WHERE {$whereSql}
ORDER BY timestamp DESC
LIMIT {$offset}, {$limit}";
$list = Db::connect('manticore')->query($sql, $params);
// 获取总数(Manticore 推荐使用 SHOW META 查看上一次查询的命中总数,比 COUNT(*) 快得多)
$meta = Db::connect('manticore')->query("SHOW META");
$total = 0;
foreach ($meta as $row) {
if ($row['Variable_name'] === 'total_found') {
$total = (int)$row['Value'];
break;
}
}
return json([
'code' => 200,
'data' => $list,
'total' => $total,
'page' => $page
]);
}
/**
* 2. 聚合统计:获取各错误级别的日志数量统计(类似 Kibana 饼图数据)
*/
public function getLevelStats()
{
// 得益于 engine='columnar',以下聚合操作在千万级数据下也是毫秒级响应
$sql = "SELECT level, COUNT(*) as count FROM app_logs GROUP BY level ORDER BY count DESC";
$stats = Db::connect('manticore')->query($sql);
return json(['code' => 200, 'data' => $stats]);
}
/**
* 3. 性能监控:统计各模块的平均接口响应时间(类似 APM 线图数据)
*/
public function getModulePerformance()
{
$sql = "SELECT module, AVG(execution_time) as avg_time, MAX(execution_time) as max_time
FROM app_logs
GROUP BY module
ORDER BY avg_time DESC";
$performance = Db::connect('manticore')->query($sql);
return json(['code' => 200, 'data' => $performance]);
}
}
请谨慎使用此类代码。
💡 生产环境避坑与优化指南
ID 唯一性约束:Manticore 的文档
id必须是唯一且大于 0 的整数(32位或64位)。TP6 默认的自增 ID 如果跨服务器会冲突,因此代码中推荐引入think-id扩展包使用雪花算法(Snowflake)或者通过 Redis 生成全局唯一递增 ID。最大结果集限制(max_matches):Manticore 为了保护内存,默认单次查询最多只能翻页到第 1000 条(
max_matches = 1000)。如果你的日志运营后台需要翻到很深的页码,需要在 Manticore 的配置文件manticore.conf中调大该参数,或者在 SQL 中动态设置:sql
SELECT * FROM app_logs WHERE ... OPTION max_matches=50000;请谨慎使用此类代码。
日志定期清理(TTL):日志数据具有时效性。你可以写一个 TP6 的定时任务(Command),每天凌晨执行原生 SQL 清理 30 天前的旧日志,保持 Manticore 轻量运行:
php
$timeLimit = time() - (86400 * 30); Db::connect('manticore')->execute("DELETE FROM app_logs WHERE timestamp < {$timeLimit}");请谨慎使用此类代码。
您目前每天的日志产出量大概在什么级别(例如 十万级、千万级、还是亿级)?如果数据量极大,我们可以进一步探讨 Manticore 的实时分布式分片(Distributed Table)配置。