搜索流程完整技术解析
🌟 流程概览
搜索是一个涉及前端交互、路由分发、控制器处理、服务层查询、索引检索、结果聚合和视图渲染的完整技术链路。
🔥 详细流程剖析
第一阶段:用户界面交互
#### 1. 搜索入口 用户可以通过多个入口进入搜索功能:
<!-- 导航栏搜索入口 -->
<li class="nav-item">
<a class="nav-link" href="/search">
<i class="bi bi-search me-1"></i>搜索
</a>
</li>
#### 2. 搜索表单设计 搜索界面采用Bootstrap 5响应式设计:
<form method="get" action="/search" class="row g-3">
<!-- 关键词输入框 -->
<div class="col-md-6">
<input type="text"
name="q"
class="form-control"
value="<?= $data['q'] ?>"
placeholder="搜索 主题 / 回复 / 用户"
required>
</div>
<!-- 搜索范围选择 -->
<div class="col-md-3">
<select name="scope" class="form-select">
<option value="all">全部</option>
<option value="topic">主题</option>
<option value="reply">回复</option>
<option value="user">用户</option>
</select>
</div>
<!-- 搜索按钮 -->
<div class="col-md-3">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search"></i> 搜索
</button>
</div>
</form>
第二阶段:路由分发与控制器初始化
#### 1. 路由注册
在index.php中的路由配置:
// 全站搜索路由
$router->get('/search', function(RequestContext $context) use ($searchController) {
$searchController->handleSearch($context);
});
#### 2. 控制器依赖注入
SearchController通过DI容器实例化:
public function __construct()
{
$this->searchService = new SearchService();
$this->topicService = new TopicService();
$this->replyService = new ReplyService();
$this->userService = new UserService();
$this->sessionManager = SessionManager::getInstance();
}
第三阶段:请求参数处理与权限验证
#### 1. 参数解析与验证
public function handleSearch(RequestContext $context): void
{
// 提取并验证搜索参数
$q = $_GET['q'] ?? ''; // 搜索关键词
$scope = $_GET['scope'] ?? 'all'; // 搜索范围
$page = max(1, (int)($_GET['page'] ?? 1)); // 页码(最小为1)
$pageSize = 200; // 每页结果数(符合项目配置)
}
#### 2. 用户身份与权限验证
// 获取当前用户
$isLoggedIn = $this->sessionManager->isLoggedIn();
$currentUserId = $this->sessionManager->getUserId();
$currentUser = null;
if ($isLoggedIn) {
$currentUser = $this->userService->getUserByID($currentUserId);
}
// 检查管理员权限(决定是否显示隐藏内容)
$includeHidden = $currentUser && $currentUser->isAdminOrSuperAdmin();
第四阶段:核心搜索服务处理
#### 1. 缓存策略检查
SearchService首先检查结果缓存:
// 生成缓存键
$cacheKey = 'search:cache:' . md5($clean . $scope . $page . $pageSize . ($includeHidden ? '1' : '0'));
// 尝试从缓存获取结果
$cachedResult = $this->redis->get($cacheKey);
if ($cachedResult !== false && $cachedResult !== null) {
$decoded = json_decode($cachedResult, true);
if ($decoded !== null) {
return $decoded; // 缓存命中,直接返回
}
}
#### 2. RediSearch索引查询
// 确保索引存在
$this->ensureIndexes();
// 构建搜索模式
$escapedTerm = $this->escapeSearchTerm($clean);
$pattern = "*{$escapedTerm}*"; // 支持部分匹配
// 根据权限过滤隐藏内容
if (!$includeHidden) {
$topicPattern = "({$pattern}) @is_hidden:{0}";
}
// 执行多索引搜索
$results = [];
if ($scope === 'topic' || $scope === 'all') {
$results['topics'] = $this->execSearch('idx:topic', $topicPattern, $offset, $pageSize);
}
if ($scope === 'reply' || $scope === 'all') {
$results['replies'] = $this->execSearch('idx:reply', $replyPattern, $offset, $pageSize);
}
if ($scope === 'user' || $scope === 'all') {
$results['users'] = $this->execSearch('idx:user', $pattern, $offset, $pageSize);
}
#### 3. 智能排序算法
private function execSearch(string $index, string $pattern, int $offset, int $limit): array
{
// 执行RediSearch查询并获取相关性得分
$raw = $this->redis->rawCommand(
'FT.SEARCH',
$index,
$pattern,
'WITHSCORES', // 返回相关性得分
'LIMIT', 0, 1000 // 先获取更多结果用于排序
);
// 智能排序:相关性得分 + 时间排序
usort($allDocs, function($a, $b) {
// 1. 按相关性得分降序(命中次数越多得分越高)
$scoreDiff = $b['_score'] <=> $a['_score'];
if ($scoreDiff !== 0) {
return $scoreDiff;
}
// 2. 得分相同时按创建时间降序(最新的在前)
$timeA = (int)($a['created_at'] ?? 0);
$timeB = (int)($b['created_at'] ?? 0);
return $timeB <=> $timeA;
});
// 应用分页
$docs = array_slice($allDocs, $offset, $limit);
return ['total' => $total, 'docs' => $docs];
}
第五阶段:数据重构与业务对象获取
#### 1. ID提取与验证 由于RediSearch返回的是索引键,需要提取实际的业务ID:
// 处理Topic结果
if (isset($rawResults['topics'])) {
$topics = [];
foreach ($rawResults['topics']['docs'] as $doc) {
// 从键中提取ID(格式:zhichai:search:topic:ID)
$key = $doc['_key'];
if (preg_match('/:(\d+)$/', $key, $matches)) {
$topicId = (int)$matches[1];
// 从业务缓存获取完整Topic对象
$topic = $this->topicService->getTopicByID($topicId);
if ($topic) {
$topics[] = $topic;
}
}
}
$searchResults['topics'] = [
'total' => $rawResults['topics']['total'],
'items' => $topics
];
}
#### 2. 多类型结果聚合 系统对Topic、Reply、User三种类型的搜索结果进行统一处理:
// Reply结果处理
if (isset($rawResults['replies'])) {
$replies = [];
foreach ($rawResults['replies']['docs'] as $doc) {
$key = $doc['_key'];
if (preg_match('/:(\d+)$/', $key, $matches)) {
$replyId = (int)$matches[1];
$reply = $this->replyService->getReplyByID($replyId);
if ($reply) {
$replies[] = $reply;
}
}
}
$searchResults['replies'] = [
'total' => $rawResults['replies']['total'],
'items' => $replies
];
}
// User结果处理
if (isset($rawResults['users'])) {
$users = [];
foreach ($rawResults['users']['docs'] as $doc) {
$key = $doc['_key'];
if (preg_match('/:(\d+)$/', $key, $matches)) {
$userId = (int)$matches[1];
$user = $this->userService->getUserByID($userId);
if ($user) {
$users[] = $user;
}
}
}
$searchResults['users'] = [
'total' => $rawResults['users']['total'],
'items' => $users
];
}
第六阶段:视图数据准备与渲染
#### 1. 模板数据组装
private function renderSearchPage(RequestContext $context, string $q, string $scope, int $page, int $pageSize, array $results, int $totalResults): void
{
// 准备完整的模板数据
$data = [
'Title' => '搜索',
'Page' => 'search',
'IsLoggedIn' => $isLoggedIn,
'Username' => $username,
'CurrentUserID' => $currentUserId,
'IsAdmin' => $currentUser ? $currentUser->isAdminOrSuperAdmin() : false,
'q' => htmlspecialchars($q), // XSS防护
'scope' => $scope,
'page' => $page,
'pageSize' => $pageSize,
'results' => $results,
'totalResults' => $totalResults,
'currentUser' => $currentUser,
];
}
#### 2. 分页计算逻辑
// 计算总页数
$totalPages = 0;
if ($scope === 'all') {
$totalPages = ceil($totalResults / $pageSize);
} elseif ($scope === 'topic' && isset($results['topics'])) {
$totalPages = ceil($results['topics']['total'] / $pageSize);
} elseif ($scope === 'reply' && isset($results['replies'])) {
$totalPages = ceil($results['replies']['total'] / $pageSize);
} elseif ($scope === 'user' && isset($results['users'])) {
$totalPages = ceil($results['users']['total'] / $pageSize);
}
$data['totalPages'] = $totalPages;
第七阶段:前端展示与用户交互
#### 1. 结果分类展示 搜索结果按类型分别展示,每种类型使用不同的颜色主题:
<!-- 主题结果 -->
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-chat-square-text"></i>
主题 (<?= $data['results']['topics']['total'] ?>)
</h5>
</div>
<!-- 回复结果 -->
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-chat-dots"></i>
回复 (<?= $data['results']['replies']['total'] ?>)
</h5>
</div>
<!-- 用户结果 -->
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="bi bi-person"></i>
用户 (<?= $data['results']['users']['total'] ?>)
</h5>
</div>
#### 2. 详细信息卡片 每个搜索结果都展示丰富的上下文信息:
<!-- Topic结果卡片 -->
<a href="/topic/<?= $topic->id ?>" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">
<?= htmlspecialchars($topic->title) ?>
<!-- 状态标签 -->
<?php if ($topic->is_closed): ?>
<span class="badge bg-warning">已关闭</span>
<?php endif; ?>
<?php if ($topic->is_hidden): ?>
<span class="badge bg-danger">已隐藏</span>
<?php endif; ?>
</h6>
<small class="text-muted"><?= htmlspecialchars($topic->created_at) ?></small>
</div>
<p class="mb-1 text-muted">
<?= htmlspecialchars(mb_substr(strip_tags($topic->content), 0, 150)) ?>...
</p>
<small class="text-muted">
作者: <?= htmlspecialchars($topic->author_nickname ?: $topic->author_username) ?> |
回复: <?= $topic->reply_count ?> |
浏览: <?= $topic->view_count ?>
</small>
</a>
#### 3. 智能分页导航
<nav aria-label="搜索结果分页">
<ul class="pagination justify-content-center">
<?php if ($data['page'] > 1): ?>
<li class="page-item">
<a class="page-link" href="/search?q=<?= urlencode($_GET['q']) ?>&scope=<?= $data['scope'] ?>&page=<?= $data['page'] - 1 ?>">
上一页
</a>
</li>
<?php endif; ?>
<?php for ($i = max(1, $data['page'] - 2); $i <= min($data['totalPages'], $data['page'] + 2); $i++): ?>
<li class="page-item <?= $i === $data['page'] ? 'active' : '' ?>">
<a class="page-link" href="/search?q=<?= urlencode($_GET['q']) ?>&scope=<?= $data['scope'] ?>&page=<?= $i ?>">
<?= $i ?>
</a>
</li>
<?php endfor; ?>
</ul>
</nav>
🎯 特殊场景处理
1. 空搜索状态
<div class="text-center py-5">
<i class="bi bi-search" style="font-size: 4rem; color: #ccc;"></i>
<h4 class="mt-3">开始搜索</h4>
<p class="text-muted">输入关键词搜索主题、回复或用户</p>
</div>
2. 无结果状态
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
没有找到与 "<strong><?= $data['q'] ?></strong>" 相关的结果
</div>
3. 管理员特殊权限
管理员用户可以搜索到隐藏的内容,普通用户则会被自动过滤:// 根据权限过滤隐藏内容
if (!$includeHidden) {
$topicPattern = "({$pattern}) @is_hidden:{0}";
$replyPattern = "({$pattern}) @is_hidden:{0}";
}
🚀 性能优化亮点
1. 分层缓存策略
- L1缓存: RediSearch查询结果缓存(TTL 60秒)
- L2缓存: 业务对象缓存(TopicService、ReplyService、UserService)
- 智能缓存键: 包含所有查询参数,确保缓存准确性
2. 查询优化
- 大批量获取: 先从RediSearch获取1000条结果
- 应用层排序: 结合相关性得分和时间戳的智能排序
- 精确分页: 在排序后应用分页,确保结果准确性
3. 安全防护
- XSS防护: 所有输出使用
htmlspecialchars() - 参数验证: 页码强制为正整数
- 权限过滤: 基于用户角色的内容访问控制
📊 总结
整个用户搜索流程体现了以下设计精髓:
1. 用户体验优先: 响应式设计、智能提示、丰富的结果展示 2. 性能为王: 多层缓存、索引优化、智能排序 3. 安全可靠: 权限控制、XSS防护、参数验证 4. 架构清晰: 分层设计、职责分离、易于维护 5. 功能完备: 支持多类型搜索、分页、状态过滤
这套搜索系统不仅满足了当前的功能需求,还为未来的扩展(如高级搜索语法、搜索建议、搜索历史等)预留了充分的架构空间。