本文目录导读:

- 使用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方案作为过渡。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播