从架构设计到性能优化的企业级搜索解决方案
一、搜索技术选型对比
主流PHP搜索方案性能测试对比(百万数据量):
方案 | 查询速度 | 相关性 | 扩展性 |
---|---|---|---|
MySQL LIKE | 1200ms+ | 差 | 低 |
MySQL全文索引 | 300-500ms | 中 | 中 |
Sphinx | 100-200ms | 良 | 高 |
Elasticsearch | 50-100ms | 优 | 极高 |
二、系统架构设计
1. 整体架构
数据层 → 索引服务层 → 搜索服务层 → API接口层 → 前端展示 ↑ ↑ ↑ ↑ ↑ MySQL 数据同步机制 复杂查询处理 RESTful接口 Vue.js
2. 数据同步方案
数据库变更 → 消息队列 → 索引处理器 → Elasticsearch
↑ ↑ ↑
binlog监听 RabbitMQ 批量写入优化
三、核心模块实现
1. Elasticsearch服务封装
<?php
namespace appcommonservice;
use ElasticElasticsearchClientBuilder;
class ElasticsearchService
{
private $client;
public function __construct()
{
$this->client = ClientBuilder::create()
->setHosts(config('es.hosts'))
->setRetries(3)
->build();
}
/**
* 创建商品索引
*/
public function createProductIndex()
{
$params = [
'index' => 'products',
'body' => [
'settings' => [
'number_of_shards' => 3,
'number_of_replicas' => 1,
'analysis' => [
'analyzer' => [
'ik_smart_pinyin' => [
'type' => 'custom',
'tokenizer' => 'ik_smart',
'filter' => ['pinyin_filter']
]
],
'filter' => [
'pinyin_filter' => [
'type' => 'pinyin',
'keep_first_letter' => true,
'keep_separate_first_letter' => false
]
]
]
],
'mappings' => [
'properties' => [
'id' => ['type' => 'integer'],
'title' => [
'type' => 'text',
'analyzer' => 'ik_smart_pinyin',
'fields' => [
'keyword' => ['type' => 'keyword']
]
],
'category_id' => ['type' => 'integer'],
'price' => ['type' => 'scaled_float', 'scaling_factor' => 100],
'sales' => ['type' => 'integer'],
'create_time' => ['type' => 'date']
]
]
]
];
return $this->client->indices()->create($params);
}
/**
* 批量索引商品数据
*/
public function bulkIndexProducts($products)
{
$params = ['body' => []];
foreach ($products as $product) {
$params['body'][] = [
'index' => [
'_index' => 'products',
'_id' => $product['id']
]
];
$params['body'][] = [
'id' => $product['id'],
'title' => $product['title'],
'category_id' => $product['category_id'],
'price' => $product['price'],
'sales' => $product['sales'],
'create_time' => $product['create_time']
];
}
return $this->client->bulk($params);
}
}
2. 数据同步服务
<?php
namespace appcommand;
use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;
use appcommonserviceElasticsearchService;
class SyncProducts extends Command
{
protected function configure()
{
$this->setName('sync:products')
->setDescription('同步商品数据到Elasticsearch');
}
protected function execute(Input $input, Output $output)
{
$lastId = cache('last_sync_product_id') ?: 0;
$products = appmodelProduct::where('id', '>', $lastId)
->limit(1000)
->select()
->toArray();
if (empty($products)) {
$output->writeln('没有需要同步的商品数据');
return;
}
$esService = new ElasticsearchService();
$result = $esService->bulkIndexProducts($products);
if ($result['errors']) {
$output->writeln('同步失败: ' . json_encode($result['items']));
} else {
cache('last_sync_product_id', end($products)['id']);
$output->writeln(sprintf(
'成功同步 %d 条商品数据,最后ID: %d',
count($products),
end($products)['id']
));
}
}
}
四、高级搜索功能
1. 多条件复合搜索
<?php
class ProductSearchService
{
public function search($params)
{
$must = [];
$filter = [];
$should = [];
// 关键词搜索
if (!empty($params['keyword'])) {
$must[] = [
'multi_match' => [
'query' => $params['keyword'],
'fields' => ['title^3', 'category_name'],
'type' => 'best_fields'
]
];
}
// 分类筛选
if (!empty($params['category_id'])) {
$filter[] = ['term' => ['category_id' => $params['category_id']]];
}
// 价格区间
if (!empty($params['min_price']) || !empty($params['max_price'])) {
$priceRange = [];
if ($params['min_price']) $priceRange['gte'] = $params['min_price'];
if ($params['max_price']) $priceRange['lte'] = $params['max_price'];
$filter[] = ['range' => ['price' => $priceRange]];
}
// 构建查询DSL
$query = [
'bool' => [
'must' => $must,
'filter' => $filter,
'should' => $should,
'minimum_should_match' => 0
]
];
// 排序
$sort = [];
switch ($params['sort'] ?? 'default') {
case 'price_asc':
$sort[] = ['price' => 'asc'];
break;
case 'price_desc':
$sort[] = ['price' => 'desc'];
break;
case 'sales':
$sort[] = ['sales' => 'desc'];
break;
default:
$sort[] = ['_score' => 'desc'];
}
$searchParams = [
'index' => 'products',
'body' => [
'query' => $query,
'sort' => $sort,
'from' => ($params['page'] - 1) * $params['size'],
'size' => $params['size'],
'highlight' => [
'fields' => [
'title' => new stdClass()
]
]
]
];
return $this->client->search($searchParams);
}
}
2. 搜索建议与纠错
<?php
class SearchSuggestionService
{
public function getSuggestions($keyword)
{
$params = [
'index' => 'products',
'body' => [
'suggest' => [
'text' => $keyword,
'simple_phrase' => [
'phrase' => [
'field' => 'title',
'size' => 1,
'gram_size' => 2,
'direct_generator' => [[
'field' => 'title',
'suggest_mode' => 'always'
]],
'highlight' => [
'pre_tag' => '<em>',
'post_tag' => '</em>'
]
]
],
'word_suggest' => [
'text' => $keyword,
'term' => [
'field' => 'title'
]
]
]
]
];
$result = $this->client->search($params);
$suggestions = [];
if (!empty($result['suggest']['simple_phrase'][0]['options'])) {
foreach ($result['suggest']['simple_phrase'][0]['options'] as $option) {
$suggestions[] = $option['highlighted'];
}
}
return array_unique($suggestions);
}
}
五、性能优化策略
1. 索引分片优化
# 商品索引分片策略
PUT /products
{
"settings": {
"number_of_shards": 5, # 根据数据量调整,建议每分片不超过30GB
"number_of_replicas": 1,
"refresh_interval": "30s", # 降低刷新频率提高写入性能
"index": {
"max_result_window": 100000 # 允许深度分页
}
}
}
# 热数据分离
PUT /products/_settings
{
"index.routing.allocation.require.box_type": "hot"
}
2. 缓存与预加载
<?php
class CachedSearchService
{
protected $cachePrefix = 'search_result:';
protected $cacheTtl = 300; // 5分钟
public function search($params)
{
$cacheKey = $this->buildCacheKey($params);
if ($result = cache($cacheKey)) {
return $result;
}
$result = $this->elasticsearch->search($params);
cache($cacheKey, $result, $this->cacheTtl);
// 预加载下一页
if ($params['page'] == 1) {
$nextParams = $params;
$nextParams['page'] = 2;
$nextKey = $this->buildCacheKey($nextParams);
$this->preloadNextPage($nextParams, $nextKey);
}
return $result;
}
protected function preloadNextPage($params, $key)
{
// 异步预加载
thinkfacadeQueue::push(new appjobPreloadSearchJob([
'params' => $params,
'cache_key' => $key,
'ttl' => $this->cacheTtl
]));
}
}
六、实战案例:电商搜索系统
1. 搜索API接口
<?php
namespace appcontroller;
use thinkRequest;
use appcommonserviceProductSearchService;
class Search extends BaseController
{
public function index(Request $request)
{
$params = $request->only([
'keyword', 'category_id',
'min_price', 'max_price',
'sort', 'page', 'size'
], 'get');
$params['page'] = $params['page'] ?? 1;
$params['size'] = $params['size'] ?? 10;
try {
$service = new ProductSearchService();
$result = $service->search($params);
return json([
'code' => 200,
'data' => [
'list' => $this->formatHits($result['hits']['hits']),
'total' => $result['hits']['total']['value'],
'suggestions' => $this->getSuggestions($params['keyword'] ?? '')
]
]);
} catch (Exception $e) {
return json([
'code' => 500,
'message' => '搜索服务异常'
]);
}
}
}
2. 数据看板实现
<?php
class SearchAnalyticsService
{
public function getHotKeywords($days = 7, $size = 10)
{
$params = [
'index' => 'search_logs',
'body' => [
'size' => 0,
'query' => [
'range' => [
'create_time' => [
'gte' => 'now-' . $days . 'd/d'
]
]
],
'aggs' => [
'hot_keywords' => [
'terms' => [
'field' => 'keyword',
'size' => $size,
'order' => ['_count' => 'desc']
]
]
]
]
];
$result = $this->client->search($params);
return array_column($result['aggregations']['hot_keywords']['buckets'], 'key');
}
public function logSearch($keyword, $userId = null)
{
$log = [
'keyword' => $keyword,
'user_id' => $userId,
'create_time' => date('Y-m-d H:i:s')
];
$this->client->index([
'index' => 'search_logs',
'body' => $log
]);
}
}