本文目录导读:

- 使用Elasticsearch(推荐)
- 使用MySQL联邦表(Federated Engine)
- 使用Sphinx搜索引擎
- 使用Tantivy(Rust编写的全文搜索库)
- 基于Redis的有序集合(简单场景)
- 架构设计建议
- 技术选型对比
- 最佳实践建议
在PHP项目中解决分片检索难题,核心思路是构建全局索引,将分散在多个数据库/表中的数据统一到一个检索层,以下是几种成熟的解决方案,按推荐程度排序:
使用Elasticsearch(推荐)
这是最主流的解决方案,特别适合全文搜索和复杂聚合查询。
实现步骤:
// 1. 安装客户端
composer require elasticsearch/elasticsearch
// 2. 建立索引映射
$params = [
'index' => 'global_index',
'body' => [
'settings' => [
'number_of_shards' => 3,
'number_of_replicas' => 1
],
'mappings' => [
'properties' => [
'id' => ['type' => 'keyword'],
'title' => ['type' => 'text', 'analyzer' => 'ik_max_word'],
'content' => ['type' => 'text', 'analyzer' => 'ik_max_word'],
'shard_key' => ['type' => 'keyword'], // 记录原始分片标识
'created_at' => ['type' => 'date']
]
]
]
];
$client->indices()->create($params);
// 3. 数据同步(监听数据库变更)
class DataSyncService {
public function syncToES($data, $shardKey) {
$params = [
'index' => 'global_index',
'id' => $data['id'] . '_' . $shardKey, // 避免ID冲突
'body' => array_merge($data, ['shard_key' => $shardKey])
];
return $this->client->index($params);
}
}
// 4. 全局检索
class GlobalSearchService {
public function search($keyword, $page = 1, $perPage = 20) {
$params = [
'index' => 'global_index',
'body' => [
'query' => [
'multi_match' => [
'query' => $keyword,
'fields' => ['title^3', 'content'] // title权重更高
]
],
'from' => ($page - 1) * $perPage,
'size' => $perPage,
'sort' => ['_score' => 'desc']
]
];
return $this->client->search($params);
}
}
使用MySQL联邦表(Federated Engine)
适合数据量较小、MySQL内部的跨库查询。
-- 在每个分片库创建联邦表
CREATE TABLE `global_index` (
`id` int(11) NOT NULL AUTO_INCREMENT, varchar(255) DEFAULT NULL,
`content` text,
`shard_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
FULLTEXT KEY `ft_search` (`title`, `content`)
) ENGINE=FEDERATED
CONNECTION='mysql://user:pass@master_host:3306/central_db/global_index';
-- 在中心库创建汇总表
CREATE TABLE `central_db`.`global_index` LIKE `shard1`.`global_index`;
-- 使用触发器或定时任务同步
DELIMITER $$
CREATE TRIGGER `after_insert_shard1`
AFTER INSERT ON `shard1`.`source_table`
FOR EACH ROW
BEGIN
INSERT INTO `central_db`.`global_index`
VALUES (NEW.id, NEW.title, NEW.content, 1, NEW.created_at);
END$$
使用Sphinx搜索引擎
比ES更轻量,适合中小规模项目。
// 1. 配置Sphinx索引
// sphinx.conf
source source1 {
type = mysql
sql_host = localhost
sql_user = root
sql_pass = password
sql_db = shard1
sql_query = \
SELECT id, title, content, 1 AS shard_id, created_at \
FROM documents
sql_attr_uint = shard_id
sql_attr_timestamp = created_at
}
index global_index {
source = source1
path = /var/data/sphinx/global_index
min_word_len = 2
charset_type = utf-8
}
// 2. PHP查询
$sphinx = new SphinxClient();
$sphinx->SetServer('localhost', 9312);
$sphinx->SetMatchMode(SPH_MATCH_EXTENDED);
$sphinx->SetLimits(0, 20);
$result = $sphinx->Query('@(title,content) 搜索关键词', 'global_index');
使用Tantivy(Rust编写的全文搜索库)
通过PHP扩展调用,性能极高。
// 通过FFI调用Rust库
$index = TantivyIndex::open('/path/to/index');
$searcher = $index->searcher();
$query = $index->parse_query('title:关键词 OR content:关键词');
$top_docs = $searcher->search($query, 20);
foreach ($top_docs as $doc_address) {
$retrieved_doc = $searcher->doc($doc_address);
echo $retrieved_doc->get_first('title');
}
基于Redis的有序集合(简单场景)
适合分类标签检索,不适合全文搜索。
class RedisGlobalIndex {
private $redis;
public function addDocument($docId, $tags, $score = 0) {
foreach ($tags as $tag) {
$this->redis->zAdd("index:{$tag}", $score, $docId);
}
// 保存文档详情
$this->redis->hSet("doc:{$docId}", 'tags', json_encode($tags));
}
public function searchByTags(array $tags, $limit = 20) {
if (empty($tags)) return [];
// 使用zinterstore求交集
$keys = array_map(function($tag) {
return "index:{$tag}";
}, $tags);
$tempKey = 'temp:' . uniqid();
$this->redis->zInter($tempKey, $keys, [1], 'AGGREGATE SUM');
$results = $this->redis->zRevRange($tempKey, 0, $limit - 1, true);
$this->redis->del($tempKey);
return $results;
}
}
架构设计建议
数据同步模式:
业务数据库 → 消息队列(RabbitMQ/Kafka) → 索引同步服务 → 全局搜索引擎
PHP实现示例:
// 自定义RPC方案
class DistributedSearchService {
private $shards = ['shard1:3306', 'shard2:3306', 'shard3:3306'];
public function search($query, $page, $size) {
// 1. 并行查询所有分片
$promises = [];
foreach ($this->shards as $shard) {
$promises[] = $this->queryShardAsync($shard, $query, $page, $size);
}
// 2. 等待所有结果
$results = \GuzzleHttp\Promise\settle($promises)->wait();
// 3. 合并排序(如果各分片已排序)
$merged = [];
foreach ($results as $result) {
$merged = array_merge($merged, $result['value']);
}
// 4. 全局排序(需要跨分片比较)
usort($merged, function($a, $b) {
return $b['score'] <=> $a['score'];
});
// 5. 返回分页结果
return array_slice($merged, ($page-1)*$size, $size);
}
}
技术选型对比
| 方案 | 适合场景 | 实时性 | 复杂度 | 性能 |
|---|---|---|---|---|
| Elasticsearch | 通用全文搜索 | 近实时 | 中 | 极高 |
| Sphinx | 中小规模搜索 | 秒级 | 低 | 高 |
| MySQL联邦表 | 简单跨库查询 | 实时 | 低 | 低 |
| Redis有序集合 | 标签/分类检索 | 实时 | 低 | 高 |
| 自定义RPC | 极简单场景 | 实时 | 高 | 中 |
最佳实践建议
- 数据一致性:使用消息队列保证同步的最终一致性
- 索引生命周期:定期重建索引,清理无效数据
- 混合使用:核心业务用ES,辅助检索用Redis
- 监控告警:索引同步延迟监控、搜索性能监控
对于新项目,强烈推荐Elasticsearch方案;如果已有MySQL分片,可以考虑Sphinx方案作为过渡。