Loading...
正在加载...
请稍候

本站Redis全文索引构建机制详解

✨步子哥 (steper) 2025年10月09日 13:22
## 🏗️ 架构概述 基于我对代码的深入分析,该项目使用**RediSearch模块**构建了一套完整的全文索引系统。整个索引机制采用**分层异步架构**,确保索引构建不影响主业务流程的响应性能。 ```mermaid graph TB subgraph "业务层" A[Topic创建] --> B[Reply创建] C[User注册] --> D[数据更新] end subgraph "索引层" E[SearchService] --> F[索引同步] F --> G[RediSearch引擎] end subgraph "存储层" H[Redis缓存] --> I[SQLite持久化] J[异步队列] --> K[AsyncSQLiteWriter] end A --> E B --> E C --> E E --> H J --> K K --> I ``` ## 📊 索引结构设计 ### 1. 三套独立索引体系 系统设计了三套独立的RediSearch索引,分别处理不同类型的搜索需求: #### Topic索引 (`idx:topic`) ```php // 索引键格式:zhichai:search:topic:{id} $this->redis->rawCommand( 'FT.CREATE', 'idx:topic', 'ON', 'HASH', 'PREFIX', '1', $redisPrefix . 'search:topic:', 'SCHEMA', 'title', 'TEXT', 'WEIGHT', '5', // 标题权重最高 'content', 'TEXT', 'WEIGHT', '1', // 内容基础权重 'author', 'TEXT', 'WEIGHT', '1.5', // 作者权重中等 'created_at', 'NUMERIC', 'SORTABLE', // 支持时间排序 'reply_count', 'NUMERIC', 'SORTABLE', // 支持热度排序 'is_hidden', 'TAG', // 隐藏状态过滤 'is_closed', 'TAG' // 关闭状态过滤 ); ``` #### Reply索引 (`idx:reply`) ```php // 索引键格式:zhichai:search:reply:{id} 'SCHEMA', 'content', 'TEXT', 'WEIGHT', '2', // 回复内容权重高 'author', 'TEXT', 'WEIGHT', '1', // 作者权重基础 'topic_title', 'TEXT', 'NOINDEX', // 仅显示,不参与搜索 'topic_id', 'TAG', // 话题关联 'created_at', 'NUMERIC', 'SORTABLE', 'is_hidden', 'TAG' ``` #### User索引 (`idx:user`) ```php // 索引键格式:zhichai:search:user:{id} 'SCHEMA', 'nickname', 'TEXT', 'WEIGHT', '3', // 昵称权重最高 'username', 'TEXT', 'WEIGHT', '2', // 用户名权重中等 'created_at', 'NUMERIC', 'SORTABLE', 'status', 'TAG' // 用户状态过滤 ``` ## 🔄 索引构建流程 ### 1. 自动索引确保机制 系统采用**懒加载**策略,在首次搜索时自动确保索引存在: ```php public function ensureIndexes(): void { $indexes = ['idx:topic', 'idx:reply', 'idx:user']; foreach ($indexes as $idx) { try { // 尝试获取索引信息 $this->redis->rawCommand('FT.INFO', $idx); } catch (\RedisException $e) { // 索引不存在,自动创建 error_log("[SearchService] 索引 {$idx} 不存在,开始创建"); $this->createIndex($idx); } } } ``` ### 2. 实时索引更新机制 每当有新数据创建或更新时,系统会**立即**更新对应的搜索索引: #### Topic索引更新 ```php public function indexTopic(array $topic): void { $docKey = "search:topic:" . $topic['id']; // 内容长度限制,防止索引过大 $content = $topic['content'] ?? ''; if (strlen($content) > 10000) { $content = substr($content, 0, 10000); } $hash = [ 'title' => $topic['title'] ?? '', 'content' => $content, 'author' => $topic['author_nickname'] ?? '', 'created_at' => (string)(strtotime($topic['created_at'] ?? 'now')), 'reply_count' => (string)($topic['reply_count'] ?? 0), 'is_hidden' => $topic['is_hidden'] ? '1' : '0', 'is_closed' => $topic['is_closed'] ? '1' : '0', ]; try { // 直接写入Redis Hash,RediSearch自动监听 $this->redis->hMSet($docKey, $hash); error_log("[SearchService] 成功索引 Topic {$topic['id']}"); } catch (\RedisException $e) { // 索引失败不影响主流程,仅记录日志 error_log("[SearchService] 索引 Topic {$topic['id']} 失败: " . $e->getMessage()); } } ``` ### 3. 异步队列处理 为了不影响主业务性能,索引更新通过**异步队列**进行处理: ```php // 在TopicService中的异步创建流程 public function createTopicAsync($userID, $title, $content, $creationTime = null) { // ... 创建Topic逻辑 ... try { // 1. 立即写入Redis(同步) $this->redis->hMSet($topicKey, $topicData); $this->redis->zAdd($this->topicIndexKey, $timestamp, $topicID); // 2. 异步写入SQLite队列 $this->asyncWriter->enqueue('INSERT', 'topics', $topicArray); // 3. 异步更新搜索索引 $searchService = new SearchService(); $searchService->indexTopic($topicArray); } catch (\Exception $e) { // 错误处理和回滚机制 } } ``` ## 🔍 搜索查询机制 ### 1. 智能搜索策略 系统使用**通配符模式**支持中文和部分匹配: ```php public function search(string $q, string $scope = 'all', int $page = 1, int $pageSize = 200): array { // 转义特殊字符 $escapedTerm = $this->escapeSearchTerm($clean); // 使用通配符包装,支持部分匹配 $pattern = "*{$escapedTerm}*"; // 根据权限过滤隐藏内容 if (!$includeHidden) { $topicPattern = "({$pattern}) @is_hidden:{0}"; } } ``` ### 2. 多维度排序算法 搜索结果采用**智能排序**策略: ```php // 1. 按相关性得分排序(TF-IDF算法) // 2. 相同得分时按创建时间倒序 usort($allDocs, function($a, $b) { // 先比较相关性得分(命中次数越多得分越高) $scoreDiff = $b['_score'] <=> $a['_score']; if ($scoreDiff !== 0) { return $scoreDiff; } // 得分相同时,按创建时间降序(最新的在前) $timeA = (int)($a['created_at'] ?? 0); $timeB = (int)($b['created_at'] ?? 0); return $timeB <=> $timeA; }); ``` ### 3. 权重分配策略 不同字段的权重设计体现了业务优先级: - **Topic**: `title(5) > author(1.5) > content(1)` - **Reply**: `content(2) > author(1)` - **User**: `nickname(3) > username(2)` ## 🚀 全量重建机制 系统提供了完整的索引重建脚本 [rebuild_search_index.php](file:///Users/linmiao/code/zhichai.php/rebuild_search_index.php): ### 1. 重建流程 ```php // Step 1: 清理旧文档 $searchPattern = 'search:*'; do { $keys = $redis->scan($it, $searchPattern, 200); foreach ($keys as $k) { $redis->del($k); $countDel++; } } while ($it !== 0); // Step 2: 删除并重建索引 foreach (['idx:topic', 'idx:reply', 'idx:user'] as $idx) { $redis->rawCommand('FT.DROPINDEX', $idx, 'DD'); } $searchService->ensureIndexes(); // Step 3: 扫描并索引所有数据 $stmt = $pdo->query("SELECT * FROM topics ORDER BY created_at DESC"); foreach ($stmt->fetchAll() as $topicData) { $searchService->indexTopic($topicArray); } ``` ### 2. 性能优化 - **批量处理**: 每100条记录输出进度 - **错误隔离**: 单个文档失败不影响整体重建 - **内存控制**: 大内容自动截断防止内存溢出 ## 💾 缓存策略 ### 1. 搜索结果缓存 ```php // 生成缓存键 $cacheKey = 'search:cache:' . md5($clean . $scope . $page . $pageSize . ($includeHidden ? '1' : '0')); // 缓存搜索结果(TTL 60秒,平衡实时性与性能) $this->redis->setex($cacheKey, 60, json_encode($results)); ``` ### 2. 前缀统一管理 所有Redis键都通过`RedisManager`统一管理前缀: ```php // config.php中的配置 'redis' => [ 'prefix' => 'zhichai:' ] // RedisManager自动应用前缀 $redis->setOption(Redis::OPT_PREFIX, 'zhichai:'); ``` ## 🛡️ 容错处理 ### 1. 超时保护 ```php try { // Redis操作依赖连接的默认超时 $this->redis->hMSet($docKey, $hash); } catch (\RedisException $e) { // 索引失败不影响主流程,只记录日志 error_log("[SearchService] 索引失败: " . $e->getMessage()); } ``` ### 2. 降级策略 - **核心功能优先**: Topic/Reply创建必须成功 - **辅助功能容错**: 搜索索引失败仅记录日志 - **异步重试**: 通过异步队列支持重试机制 ## 📈 性能特性 ### 1. 查询性能 - **索引查询**: O(log N) 时间复杂度 - **全文搜索**: 毫秒级响应时间 - **分页支持**: 高效的LIMIT/OFFSET实现 ### 2. 写入性能 - **异步索引**: 不阻塞主业务流程 - **批量处理**: 减少Redis往返次数 - **内容截断**: 防止大文档影响性能 ## 🎯 总结 该Redis全文索引系统体现了以下设计亮点: 1. **分层架构**: 清晰的职责分离,便于维护和扩展 2. **异步处理**: 保证主业务性能不受索引构建影响 3. **智能搜索**: 支持中英文混合、部分匹配、权重排序 4. **容错机制**: 完善的错误处理和降级策略 5. **运维友好**: 提供完整的重建和监控工具 这套索引系统不仅满足了当前的全文搜索需求,还为未来的功能扩展预留了充分的灵活性。

讨论回复

1 条回复
✨步子哥 (steper) #1
10-09 14:43
# 搜索流程完整技术解析 ## 🌟 流程概览 搜索是一个涉及前端交互、路由分发、控制器处理、服务层查询、索引检索、结果聚合和视图渲染的完整技术链路。 ```mermaid graph TB subgraph "用户界面层" A[用户输入搜索词] --> B[选择搜索范围] B --> C[点击搜索按钮] end subgraph "路由处理层" D[search路由] --> E[SearchController] E --> F[参数解析与验证] end subgraph "权限验证层" G[SessionManager] --> H[用户身份检查] H --> I[管理员权限判断] end subgraph "服务处理层" J[SearchService] --> K[缓存检查] K --> L[RediSearch查询] L --> M[结果聚合] end subgraph "数据重构层" N[ID提取] --> O[业务对象获取] O --> P[完整数据组装] end subgraph "视图渲染层" Q[模板数据准备] --> R[分页计算] R --> S[HTML渲染输出] end C --> D F --> G I --> J M --> N P --> Q ``` ## 🔥 详细流程剖析 ### 第一阶段:用户界面交互 #### 1. 搜索入口 用户可以通过多个入口进入搜索功能: ```html ``` #### 2. 搜索表单设计 搜索界面采用Bootstrap 5响应式设计: ```html
``` ### 第二阶段:路由分发与控制器初始化 #### 1. 路由注册 在[`index.php`](file:///Users/linmiao/code/zhichai.php/index.php)中的路由配置: ```php // 全站搜索路由 $router->get('/search', function(RequestContext $context) use ($searchController) { $searchController->handleSearch($context); }); ``` #### 2. 控制器依赖注入 [`SearchController`](file:///Users/linmiao/code/zhichai.php/src/Controllers/SearchController.php)通过DI容器实例化: ```php public function __construct() { $this->searchService = new SearchService(); $this->topicService = new TopicService(); $this->replyService = new ReplyService(); $this->userService = new UserService(); $this->sessionManager = SessionManager::getInstance(); } ``` ### 第三阶段:请求参数处理与权限验证 #### 1. 参数解析与验证 ```php public function handleSearch(RequestContext $context): void { // 提取并验证搜索参数 $q = $_GET['q'] ?? ''; // 搜索关键词 $scope = $_GET['scope'] ?? 'all'; // 搜索范围 $page = max(1, (int)($_GET['page'] ?? 1)); // 页码(最小为1) $pageSize = 200; // 每页结果数(符合项目配置) } ``` #### 2. 用户身份与权限验证 ```php // 获取当前用户 $isLoggedIn = $this->sessionManager->isLoggedIn(); $currentUserId = $this->sessionManager->getUserId(); $currentUser = null; if ($isLoggedIn) { $currentUser = $this->userService->getUserByID($currentUserId); } // 检查管理员权限(决定是否显示隐藏内容) $includeHidden = $currentUser && $currentUser->isAdminOrSuperAdmin(); ``` ### 第四阶段:核心搜索服务处理 #### 1. 缓存策略检查 [`SearchService`](file:///Users/linmiao/code/zhichai.php/src/Services/SearchService.php)首先检查结果缓存: ```php // 生成缓存键 $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索引查询 ```php // 确保索引存在 $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. 智能排序算法 ```php 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: ```php // 处理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三种类型的搜索结果进行统一处理: ```php // 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. 模板数据组装 ```php 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. 分页计算逻辑 ```php // 计算总页数 $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. 结果分类展示 搜索结果按类型分别展示,每种类型使用不同的颜色主题: ```html
主题 ()
回复 ()
用户 ()
``` #### 2. 详细信息卡片 每个搜索结果都展示丰富的上下文信息: ```html
title) ?> is_closed): ?> 已关闭 is_hidden): ?> 已隐藏
created_at) ?>

content), 0, 150)) ?>...

作者: author_nickname ?: $topic->author_username) ?> | 回复: reply_count ?> | 浏览: view_count ?>
``` #### 3. 智能分页导航 ```html ``` ## 🎯 特殊场景处理 ### 1. 空搜索状态 ```html

开始搜索

输入关键词搜索主题、回复或用户

``` ### 2. 无结果状态 ```html
没有找到与 "" 相关的结果
``` ### 3. 管理员特殊权限 管理员用户可以搜索到隐藏的内容,普通用户则会被自动过滤: ```php // 根据权限过滤隐藏内容 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. **功能完备**: 支持多类型搜索、分页、状态过滤 这套搜索系统不仅满足了当前的功能需求,还为未来的扩展(如高级搜索语法、搜索建议、搜索历史等)预留了充分的架构空间。