feat: 新增对话ID参数支持,优化搜索功能以支持对话过滤

This commit is contained in:
kuaifan 2026-01-03 03:59:51 +00:00
parent a52dc14369
commit 908171a977
3 changed files with 132 additions and 80 deletions

View File

@ -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', []);
}
}
}

View File

@ -1519,9 +1519,10 @@ class ManticoreBase
* @param int $userid 用户ID权限过滤
* @param int $limit 返回数量
* @param int $offset 偏移量
* @param int $dialogId 对话ID0表示不限制
* @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 对话ID0表示不限制
* @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 对话ID0表示不限制
* @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 = [];

View File

@ -77,9 +77,10 @@ class ManticoreMsg
* @param string $searchType 搜索类型: text/vector/hybrid
* @param int $from 起始位置
* @param int $size 返回数量
* @param int $dialogId 对话ID0表示不限制
* @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) {