dootask/app/Module/Manticore/ManticoreUser.php
kuaifan 746337c2f6 feat(search): 语义搜索迁移至 Manticore Auto Embeddings,向量走 ai 插件免费模型
- embedding 统一改走 ai 插件 /embeddings(免费向量模型,1024 维),删除第三方 key 分支
- 5 张向量表启用引擎 Auto Embeddings(MODEL_NAME/FROM/API_URL),写入/更新时引擎自动生成向量
- 表定义使用派生密钥 sha256(APP_KEY:embeddings),避免主密钥进入引擎元数据
- 写入改为 REPLACE(单行原子)与多行批量 REPLACE(整语句一次向量化调用,失败回退逐行)
- 结构指纹 vector:schema 标记:版本/模型/端点/APP_KEY 变化时自动清空重建并重置同步指针
- 退役 PHP 向量管道:删除 generate-vectors 命令、双指针、withVector 分支与批量向量生成方法
- 根治缺陷:全文重同步不再抹掉向量;内容编辑后向量由引擎自动重算,不再过期
2026-07-15 10:45:20 +00:00

257 lines
6.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Module\Manticore;
use App\Models\User;
use App\Models\UserTag;
use App\Module\Apps;
use Illuminate\Support\Facades\Log;
/**
* Manticore Search 用户搜索类(联系人搜索)
*
* 使用方法:
*
* 1. 搜索方法
* - 搜索用户: search($keyword, $searchType, $limit);
*
* 2. 同步方法
* - 单个同步: sync(User $user);
* - 批量同步: batchSync($users);
* - 删除索引: delete($userid);
*
* 3. 工具方法
* - 清空索引: clear();
*/
class ManticoreUser
{
/**
* 搜索用户(支持全文、向量、混合搜索)
*
* @param string $keyword 搜索关键词
* @param string $searchType 搜索类型: text/vector/hybrid
* @param int $limit 返回数量
* @return array 搜索结果
*/
public static function search(string $keyword, string $searchType = 'hybrid', int $limit = 20): array
{
if (empty($keyword)) {
return [];
}
if (!Apps::isInstalled("search")) {
return [];
}
try {
switch ($searchType) {
case 'text':
return self::formatSearchResults(
ManticoreBase::userFullTextSearch($keyword, $limit, 0)
);
case 'vector':
$embedding = ManticoreBase::getEmbedding($keyword);
if (empty($embedding)) {
return self::formatSearchResults(
ManticoreBase::userFullTextSearch($keyword, $limit, 0)
);
}
return self::formatSearchResults(
ManticoreBase::userVectorSearch($embedding, $limit)
);
case 'hybrid':
default:
$embedding = ManticoreBase::getEmbedding($keyword);
return self::formatSearchResults(
ManticoreBase::userHybridSearch($keyword, $embedding, $limit)
);
}
} catch (\Exception $e) {
Log::error('Manticore user search error: ' . $e->getMessage());
return [];
}
}
/**
* 格式化搜索结果
*
* @param array $results Manticore 返回的结果
* @return array 格式化后的结果
*/
private static function formatSearchResults(array $results): array
{
$formatted = [];
foreach ($results as $item) {
$formatted[] = [
'userid' => $item['userid'],
'nickname' => $item['nickname'],
'email' => $item['email'],
'profession' => $item['profession'],
'tags' => $item['tags'] ?? '',
'introduction_preview' => isset($item['introduction']) ? mb_substr($item['introduction'], 0, 200) : null,
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
];
}
return $formatted;
}
// ==============================
// 同步方法
// ==============================
/**
* 获取用户的标签(按认可数排序,最多10个)
*
* @param int $userid 用户ID
* @return string 标签名称,空格分隔
*/
public static function getUserTags(int $userid): string
{
$tags = UserTag::where('user_id', $userid)
->withCount('recognitions')
->orderByDesc('recognitions_count')
->limit(10)
->pluck('name')
->toArray();
return implode(' ', $tags);
}
/**
* 同步单个用户到 Manticore(向量由引擎 Auto Embeddings 按行内文本自动生成)
*
* @param User $user 用户模型
* @return bool 是否成功
*/
public static function sync(User $user): bool
{
if (!Apps::isInstalled("search")) {
return false;
}
// 不处理机器人账号
if ($user->bot) {
return true;
}
// 不处理已禁用的账号
if ($user->disable_at) {
return self::delete($user->userid);
}
try {
return ManticoreBase::upsertUserVector(self::buildRow($user));
} catch (\Exception $e) {
Log::error('Manticore user sync error: ' . $e->getMessage(), [
'userid' => $user->userid,
'nickname' => $user->nickname,
]);
return false;
}
}
/**
* 构建用户索引行数据(含标签 Top 10)
*
* @param User $user 用户模型
* @return array 行数据
*/
private static function buildRow(User $user): array
{
$tags = self::getUserTags($user->userid);
return [
'id' => $user->userid,
'userid' => $user->userid,
'nickname' => $user->nickname ?? '',
'email' => $user->email ?? '',
'profession' => $user->profession ?? '',
'tags' => $tags,
'introduction' => $user->introduction ?? '',
];
}
/**
* 批量同步用户(每块一条多行 REPLACE,向量由引擎自动生成)
*
* @param iterable $users 用户列表
* @return int 成功同步的数量
*/
public static function batchSync(iterable $users): int
{
if (!Apps::isInstalled("search")) {
return 0;
}
$count = 0;
$rows = [];
foreach ($users as $user) {
if ($user->bot) {
$count++;
continue;
}
if ($user->disable_at) {
if (self::delete($user->userid)) {
$count++;
}
continue;
}
try {
$rows[] = self::buildRow($user);
} catch (\Exception $e) {
Log::error('Manticore user batchSync build error: ' . $e->getMessage(), [
'userid' => $user->userid,
]);
}
}
return $count + ManticoreBase::batchUpsertVectors('user', $rows);
}
/**
* 删除用户索引
*
* @param int $userid 用户ID
* @return bool 是否成功
*/
public static function delete(int $userid): bool
{
if (!Apps::isInstalled("search")) {
return false;
}
return ManticoreBase::deleteUserVector($userid);
}
/**
* 清空所有索引
*
* @return bool 是否成功
*/
public static function clear(): bool
{
if (!Apps::isInstalled("search")) {
return false;
}
return ManticoreBase::clearAllUserVectors();
}
/**
* 获取已索引用户数量
*
* @return int 数量
*/
public static function getIndexedCount(): int
{
if (!Apps::isInstalled("search")) {
return 0;
}
return ManticoreBase::getIndexedUserCount();
}
}