diff --git a/app/Http/Controllers/Api/SearchController.php b/app/Http/Controllers/Api/SearchController.php index bf693d370..4083d76b1 100644 --- a/app/Http/Controllers/Api/SearchController.php +++ b/app/Http/Controllers/Api/SearchController.php @@ -47,7 +47,7 @@ class SearchController extends AbstractController $key = trim(Request::input('key')); $searchType = Request::input('search_type', 'hybrid'); - $take = min(50, max(1, intval(Request::input('take', 20)))); + $take = Base::getPaginate(50, 20, 'take'); if (empty($key)) { return Base::retSuccess('success', []); @@ -103,7 +103,7 @@ class SearchController extends AbstractController $key = trim(Request::input('key')); $searchType = Request::input('search_type', 'hybrid'); - $take = min(50, max(1, intval(Request::input('take', 20)))); + $take = Base::getPaginate(50, 20, 'take'); if (empty($key)) { return Base::retSuccess('success', []); @@ -158,7 +158,7 @@ class SearchController extends AbstractController $key = trim(Request::input('key')); $searchType = Request::input('search_type', 'hybrid'); - $take = min(50, max(1, intval(Request::input('take', 20)))); + $take = Base::getPaginate(50, 20, 'take'); if (empty($key)) { return Base::retSuccess('success', []); @@ -214,7 +214,7 @@ class SearchController extends AbstractController $key = trim(Request::input('key')); $searchType = Request::input('search_type', 'hybrid'); - $take = min(50, max(1, intval(Request::input('take', 20)))); + $take = Base::getPaginate(50, 20, 'take'); if (empty($key)) { return Base::retSuccess('success', []); @@ -256,6 +256,11 @@ class SearchController extends AbstractController * @apiParam {String} key 搜索关键词 * @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid) * @apiParam {Number} [take] 获取数量(默认:20,最大:50) + * @apiParam {String} [mode] 返回模式(message/position/dialog,默认:message) + * - message: 返回消息详细信息 + * - position: 只返回消息ID + * - dialog: 返回对话级数据 + * @apiParam {Number} [dialog_id] 对话ID(筛选指定对话内的消息) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -271,46 +276,89 @@ class SearchController extends AbstractController $key = trim(Request::input('key')); $searchType = Request::input('search_type', 'hybrid'); - $take = min(50, max(1, intval(Request::input('take', 20)))); + $take = Base::getPaginate(50, 20, 'take'); + $mode = Request::input('mode', 'message'); + $dialogId = intval(Request::input('dialog_id', 0)); + + // 验证 mode 参数 + if (!in_array($mode, ['message', 'position', 'dialog'])) { + $mode = 'message'; + } if (empty($key)) { return Base::retSuccess('success', []); } - $results = ManticoreMsg::search($user->userid, $key, $searchType, 0, $take); - - // 补充消息完整信息 - $msgIds = array_column($results, 'msg_id'); - if (!empty($msgIds)) { - $msgs = WebSocketDialogMsg::whereIn('id', $msgIds) - ->with(['user' => function ($query) { - $query->select(User::$basicField); - }]) - ->get() - ->keyBy('id'); - - $formattedResults = []; - foreach ($results as $item) { - $msgData = $msgs->get($item['msg_id']); - if ($msgData) { - $formattedResults[] = [ - 'id' => $msgData->id, - 'msg_id' => $msgData->id, - 'dialog_id' => $msgData->dialog_id, - 'userid' => $msgData->userid, - 'type' => $msgData->type, - 'msg' => $msgData->msg, - 'created_at' => $msgData->created_at, - 'user' => $msgData->user, - 'relevance' => $item['relevance'] ?? 0, - 'content_preview' => $item['content_preview'] ?? null, - ]; - } - } - return Base::retSuccess('success', $formattedResults); + // 如果指定了 dialog_id,需要验证用户有权限访问该对话 + if ($dialogId > 0) { + \App\Models\WebSocketDialog::checkDialog($dialogId); } - return Base::retSuccess('success', []); + $results = ManticoreMsg::search($user->userid, $key, $searchType, 0, $take, $dialogId); + + // 根据 mode 返回不同格式的数据 + switch ($mode) { + case 'position': + // 只返回消息ID + $data = array_column($results, 'msg_id'); + return Base::retSuccess('success', compact('data')); + + case 'dialog': + // 返回对话级数据 + $list = []; + $seenDialogs = []; + foreach ($results as $item) { + $dialogIdFromResult = $item['dialog_id']; + // 每个对话只返回一次 + if (isset($seenDialogs[$dialogIdFromResult])) { + continue; + } + $seenDialogs[$dialogIdFromResult] = true; + + if ($dialog = \App\Models\WebSocketDialog::find($dialogIdFromResult)) { + $dialogData = array_merge($dialog->toArray(), [ + 'search_msg_id' => $item['msg_id'], + ]); + $list[] = \App\Models\WebSocketDialog::synthesizeData($dialogData, $user->userid); + } + } + return Base::retSuccess('success', ['data' => $list]); + + case 'message': + default: + // 返回消息详细信息(默认行为) + $msgIds = array_column($results, 'msg_id'); + if (!empty($msgIds)) { + $msgs = WebSocketDialogMsg::whereIn('id', $msgIds) + ->with(['user' => function ($query) { + $query->select(User::$basicField); + }]) + ->get() + ->keyBy('id'); + + $formattedResults = []; + foreach ($results as $item) { + $msgData = $msgs->get($item['msg_id']); + if ($msgData) { + $formattedResults[] = [ + 'id' => $msgData->id, + 'msg_id' => $msgData->id, + 'dialog_id' => $msgData->dialog_id, + 'userid' => $msgData->userid, + 'type' => $msgData->type, + 'msg' => $msgData->msg, + 'created_at' => $msgData->created_at, + 'user' => $msgData->user, + 'relevance' => $item['relevance'] ?? 0, + 'content_preview' => $item['content_preview'] ?? null, + ]; + } + } + return Base::retSuccess('success', $formattedResults); + } + + return Base::retSuccess('success', []); + } } } diff --git a/app/Module/Manticore/ManticoreBase.php b/app/Module/Manticore/ManticoreBase.php index bbb9a39fa..ff6312bc2 100644 --- a/app/Module/Manticore/ManticoreBase.php +++ b/app/Module/Manticore/ManticoreBase.php @@ -1519,9 +1519,10 @@ class ManticoreBase * @param int $userid 用户ID(权限过滤) * @param int $limit 返回数量 * @param int $offset 偏移量 + * @param int $dialogId 对话ID(0表示不限制) * @return array 搜索结果 */ - public static function msgFullTextSearch(string $keyword, int $userid = 0, int $limit = 20, int $offset = 0): array + public static function msgFullTextSearch(string $keyword, int $userid = 0, int $limit = 20, int $offset = 0, int $dialogId = 0): array { if (empty($keyword)) { return []; @@ -1530,39 +1531,30 @@ class ManticoreBase $instance = new self(); $escapedKeyword = self::escapeMatch($keyword); + // 构建过滤条件 + $conditions = ["MATCH('@content {$escapedKeyword}')"]; if ($userid > 0) { - // 使用 MVA 权限过滤 - $sql = " - SELECT - id, - msg_id, - dialog_id, - userid, - msg_type, - content, - created_at, - WEIGHT() as relevance - FROM msg_vectors - WHERE MATCH('@content {$escapedKeyword}') - AND allowed_users = " . (int)$userid . " - ORDER BY relevance DESC - LIMIT " . (int)$limit . " OFFSET " . (int)$offset; - } else { - $sql = " - SELECT - id, - msg_id, - dialog_id, - userid, - msg_type, - content, - created_at, - WEIGHT() as relevance - FROM msg_vectors - WHERE MATCH('@content {$escapedKeyword}') - ORDER BY relevance DESC - LIMIT " . (int)$limit . " OFFSET " . (int)$offset; + $conditions[] = "allowed_users = " . (int)$userid; } + if ($dialogId > 0) { + $conditions[] = "dialog_id = " . (int)$dialogId; + } + $whereClause = implode(' AND ', $conditions); + + $sql = " + SELECT + id, + msg_id, + dialog_id, + userid, + msg_type, + content, + created_at, + WEIGHT() as relevance + FROM msg_vectors + WHERE {$whereClause} + ORDER BY relevance DESC + LIMIT " . (int)$limit . " OFFSET " . (int)$offset; return $instance->query($sql); } @@ -1573,9 +1565,10 @@ class ManticoreBase * @param array $queryVector 查询向量 * @param int $userid 用户ID(权限过滤) * @param int $limit 返回数量 + * @param int $dialogId 对话ID(0表示不限制) * @return array 搜索结果 */ - public static function msgVectorSearch(array $queryVector, int $userid = 0, int $limit = 20): array + public static function msgVectorSearch(array $queryVector, int $userid = 0, int $limit = 20, int $dialogId = 0): array { if (empty($queryVector)) { return []; @@ -1584,8 +1577,9 @@ class ManticoreBase $instance = new self(); $vectorStr = '(' . implode(',', $queryVector) . ')'; - // KNN 搜索需要先获取更多结果,再在应用层过滤权限 - $fetchLimit = $userid > 0 ? $limit * 5 : $limit; + // KNN 搜索需要先获取更多结果,再在应用层过滤权限和对话 + $needFilter = $userid > 0 || $dialogId > 0; + $fetchLimit = $needFilter ? $limit * 5 : $limit; $sql = " SELECT @@ -1622,6 +1616,14 @@ class ManticoreBase $results = array_values($results); } + // 对话过滤 + if ($dialogId > 0 && !empty($results)) { + $results = array_filter($results, function ($item) use ($dialogId) { + return $item['dialog_id'] == $dialogId; + }); + $results = array_values($results); + } + return array_slice($results, 0, $limit); } @@ -1632,12 +1634,13 @@ class ManticoreBase * @param array $queryVector 查询向量 * @param int $userid 用户ID(权限过滤) * @param int $limit 返回数量 + * @param int $dialogId 对话ID(0表示不限制) * @return array 搜索结果 */ - public static function msgHybridSearch(string $keyword, array $queryVector, int $userid = 0, int $limit = 20): array + public static function msgHybridSearch(string $keyword, array $queryVector, int $userid = 0, int $limit = 20, int $dialogId = 0): array { - $textResults = self::msgFullTextSearch($keyword, $userid, 50, 0); - $vectorResults = !empty($queryVector) ? self::msgVectorSearch($queryVector, $userid, 50) : []; + $textResults = self::msgFullTextSearch($keyword, $userid, 50, 0, $dialogId); + $vectorResults = !empty($queryVector) ? self::msgVectorSearch($queryVector, $userid, 50, $dialogId) : []; $scores = []; $items = []; diff --git a/app/Module/Manticore/ManticoreMsg.php b/app/Module/Manticore/ManticoreMsg.php index e2c9c3fb9..7f1c375bb 100644 --- a/app/Module/Manticore/ManticoreMsg.php +++ b/app/Module/Manticore/ManticoreMsg.php @@ -77,9 +77,10 @@ class ManticoreMsg * @param string $searchType 搜索类型: text/vector/hybrid * @param int $from 起始位置 * @param int $size 返回数量 + * @param int $dialogId 对话ID(0表示不限制) * @return array 搜索结果 */ - public static function search(int $userid, string $keyword, string $searchType = 'hybrid', int $from = 0, int $size = 20): array + public static function search(int $userid, string $keyword, string $searchType = 'hybrid', int $from = 0, int $size = 20, int $dialogId = 0): array { if (empty($keyword)) { return []; @@ -94,7 +95,7 @@ class ManticoreMsg case 'text': // 纯全文搜索 return self::formatSearchResults( - ManticoreBase::msgFullTextSearch($keyword, $userid, $size, $from) + ManticoreBase::msgFullTextSearch($keyword, $userid, $size, $from, $dialogId) ); case 'vector': @@ -103,11 +104,11 @@ class ManticoreMsg if (empty($embedding)) { // embedding 获取失败,降级到全文搜索 return self::formatSearchResults( - ManticoreBase::msgFullTextSearch($keyword, $userid, $size, $from) + ManticoreBase::msgFullTextSearch($keyword, $userid, $size, $from, $dialogId) ); } return self::formatSearchResults( - ManticoreBase::msgVectorSearch($embedding, $userid, $size) + ManticoreBase::msgVectorSearch($embedding, $userid, $size, $dialogId) ); case 'hybrid': @@ -115,7 +116,7 @@ class ManticoreMsg // 混合搜索 $embedding = self::getEmbedding($keyword); return self::formatSearchResults( - ManticoreBase::msgHybridSearch($keyword, $embedding, $userid, $size) + ManticoreBase::msgHybridSearch($keyword, $embedding, $userid, $size, $dialogId) ); } } catch (\Exception $e) {