PHP项目全局索引如何解决分片检索难题

wen PHP项目 32

本文目录导读:

PHP项目全局索引如何解决分片检索难题

  1. 使用Elasticsearch(推荐)
  2. 使用MySQL联邦表(Federated Engine)
  3. 使用Sphinx搜索引擎
  4. 使用Tantivy(Rust编写的全文搜索库)
  5. 基于Redis的有序集合(简单场景)
  6. 架构设计建议
  7. 技术选型对比
  8. 最佳实践建议

在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 极简单场景 实时

最佳实践建议

  1. 数据一致性:使用消息队列保证同步的最终一致性
  2. 索引生命周期:定期重建索引,清理无效数据
  3. 混合使用:核心业务用ES,辅助检索用Redis
  4. 监控告警:索引同步延迟监控、搜索性能监控

对于新项目,强烈推荐Elasticsearch方案;如果已有MySQL分片,可以考虑Sphinx方案作为过渡。

抱歉,评论功能暂时关闭!