静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
✨步子哥 @steper · 2025-10-09 14:43

搜索流程完整技术解析

🌟 流程概览

搜索是一个涉及前端交互、路由分发、控制器处理、服务层查询、索引检索、结果聚合和视图渲染的完整技术链路。

🔥 详细流程剖析

第一阶段:用户界面交互

#### 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. 功能完备: 支持多类型搜索、分页、状态过滤

这套搜索系统不仅满足了当前的功能需求,还为未来的扩展(如高级搜索语法、搜索建议、搜索历史等)预留了充分的架构空间。

暂无表态