mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-11 16:28:11 +00:00
430 lines
12 KiB
PHP
430 lines
12 KiB
PHP
<?php
|
||
|
||
namespace App\Module\Manticore;
|
||
|
||
use App\Models\Project;
|
||
use App\Models\ProjectUser;
|
||
use App\Module\Apps;
|
||
use App\Module\Base;
|
||
use App\Module\AI;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
/**
|
||
* Manticore Search 项目搜索类
|
||
*
|
||
* 使用方法:
|
||
*
|
||
* 1. 搜索方法
|
||
* - 搜索项目: search($userid, $keyword, $searchType, $limit);
|
||
*
|
||
* 2. 同步方法
|
||
* - 单个同步: sync(Project $project);
|
||
* - 批量同步: batchSync($projects);
|
||
* - 删除索引: delete($projectId);
|
||
*
|
||
* 3. 成员关系方法
|
||
* - 添加成员: addProjectUser($projectId, $userid);
|
||
* - 删除成员: removeProjectUser($projectId, $userid);
|
||
* - 同步所有成员: syncProjectUsers($projectId);
|
||
*
|
||
* 4. 工具方法
|
||
* - 清空索引: clear();
|
||
*/
|
||
class ManticoreProject
|
||
{
|
||
/**
|
||
* 搜索项目(支持全文、向量、混合搜索)
|
||
*
|
||
* @param int $userid 用户ID(权限过滤)
|
||
* @param string $keyword 搜索关键词
|
||
* @param string $searchType 搜索类型: text/vector/hybrid
|
||
* @param int $limit 返回数量
|
||
* @return array 搜索结果
|
||
*/
|
||
public static function search(int $userid, string $keyword, string $searchType = 'hybrid', int $limit = 20): array
|
||
{
|
||
if (empty($keyword)) {
|
||
return [];
|
||
}
|
||
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
switch ($searchType) {
|
||
case 'text':
|
||
return self::formatSearchResults(
|
||
ManticoreBase::projectFullTextSearch($keyword, $userid, $limit, 0)
|
||
);
|
||
|
||
case 'vector':
|
||
$embedding = self::getEmbedding($keyword);
|
||
if (empty($embedding)) {
|
||
return self::formatSearchResults(
|
||
ManticoreBase::projectFullTextSearch($keyword, $userid, $limit, 0)
|
||
);
|
||
}
|
||
return self::formatSearchResults(
|
||
ManticoreBase::projectVectorSearch($embedding, $userid, $limit)
|
||
);
|
||
|
||
case 'hybrid':
|
||
default:
|
||
$embedding = self::getEmbedding($keyword);
|
||
return self::formatSearchResults(
|
||
ManticoreBase::projectHybridSearch($keyword, $embedding, $userid, $limit)
|
||
);
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('Manticore project search error: ' . $e->getMessage());
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取文本的 Embedding 向量
|
||
*
|
||
* @param string $text 文本
|
||
* @return array 向量数组(空数组表示失败)
|
||
*/
|
||
private static function getEmbedding(string $text): array
|
||
{
|
||
if (empty($text)) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
$result = AI::getEmbedding($text);
|
||
if (Base::isSuccess($result)) {
|
||
return $result['data'] ?? [];
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::warning('Get embedding error: ' . $e->getMessage());
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 格式化搜索结果
|
||
*
|
||
* @param array $results Manticore 返回的结果
|
||
* @return array 格式化后的结果
|
||
*/
|
||
private static function formatSearchResults(array $results): array
|
||
{
|
||
$formatted = [];
|
||
foreach ($results as $item) {
|
||
$formatted[] = [
|
||
'project_id' => $item['project_id'],
|
||
'id' => $item['project_id'],
|
||
'userid' => $item['userid'],
|
||
'personal' => $item['personal'],
|
||
'name' => $item['project_name'],
|
||
'desc_preview' => $item['project_desc_preview'] ?? null,
|
||
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
|
||
];
|
||
}
|
||
return $formatted;
|
||
}
|
||
|
||
// ==============================
|
||
// 同步方法
|
||
// ==============================
|
||
|
||
/**
|
||
* 同步单个项目到 Manticore
|
||
*
|
||
* @param Project $project 项目模型
|
||
* @return bool 是否成功
|
||
*/
|
||
public static function sync(Project $project): bool
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return false;
|
||
}
|
||
|
||
// 已归档的项目不索引
|
||
if ($project->archived_at) {
|
||
return self::delete($project->id);
|
||
}
|
||
|
||
try {
|
||
// 构建用于搜索的文本内容
|
||
$searchableContent = self::buildSearchableContent($project);
|
||
|
||
// 获取 embedding(如果 AI 可用)
|
||
$embedding = null;
|
||
if (!empty($searchableContent) && Apps::isInstalled('ai')) {
|
||
$embeddingResult = self::getEmbedding($searchableContent);
|
||
if (!empty($embeddingResult)) {
|
||
$embedding = '[' . implode(',', $embeddingResult) . ']';
|
||
}
|
||
}
|
||
|
||
// 写入 Manticore
|
||
$result = ManticoreBase::upsertProjectVector([
|
||
'project_id' => $project->id,
|
||
'userid' => $project->userid ?? 0,
|
||
'personal' => $project->personal ?? 0,
|
||
'project_name' => $project->name ?? '',
|
||
'project_desc' => $project->desc ?? '',
|
||
'content_vector' => $embedding,
|
||
]);
|
||
|
||
return $result;
|
||
} catch (\Exception $e) {
|
||
Log::error('Manticore project sync error: ' . $e->getMessage(), [
|
||
'project_id' => $project->id,
|
||
'project_name' => $project->name,
|
||
]);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建可搜索的文本内容
|
||
*
|
||
* @param Project $project 项目模型
|
||
* @return string 可搜索的文本
|
||
*/
|
||
private static function buildSearchableContent(Project $project): string
|
||
{
|
||
$parts = [];
|
||
|
||
if (!empty($project->name)) {
|
||
$parts[] = $project->name;
|
||
}
|
||
if (!empty($project->desc)) {
|
||
$parts[] = $project->desc;
|
||
}
|
||
|
||
return implode(' ', $parts);
|
||
}
|
||
|
||
/**
|
||
* 批量同步项目
|
||
*
|
||
* @param iterable $projects 项目列表
|
||
* @return int 成功同步的数量
|
||
*/
|
||
public static function batchSync(iterable $projects): int
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return 0;
|
||
}
|
||
|
||
$count = 0;
|
||
foreach ($projects as $project) {
|
||
if (self::sync($project)) {
|
||
$count++;
|
||
}
|
||
}
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 删除项目索引
|
||
*
|
||
* @param int $projectId 项目ID
|
||
* @return bool 是否成功
|
||
*/
|
||
public static function delete(int $projectId): bool
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return false;
|
||
}
|
||
|
||
// 删除项目索引
|
||
ManticoreBase::deleteProjectVector($projectId);
|
||
// 删除项目成员关系
|
||
ManticoreBase::deleteAllProjectUsers($projectId);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 清空所有索引
|
||
*
|
||
* @return bool 是否成功
|
||
*/
|
||
public static function clear(): bool
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return false;
|
||
}
|
||
|
||
ManticoreBase::clearAllProjectVectors();
|
||
ManticoreBase::clearAllProjectUsers();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取已索引项目数量
|
||
*
|
||
* @return int 数量
|
||
*/
|
||
public static function getIndexedCount(): int
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return 0;
|
||
}
|
||
|
||
return ManticoreBase::getIndexedProjectCount();
|
||
}
|
||
|
||
// ==============================
|
||
// 成员关系方法
|
||
// ==============================
|
||
|
||
/**
|
||
* 添加项目成员到 Manticore
|
||
*
|
||
* @param int $projectId 项目ID
|
||
* @param int $userid 用户ID
|
||
* @return bool 是否成功
|
||
*/
|
||
public static function addProjectUser(int $projectId, int $userid): bool
|
||
{
|
||
if (!Apps::isInstalled("manticore") || $projectId <= 0 || $userid <= 0) {
|
||
return false;
|
||
}
|
||
|
||
return ManticoreBase::upsertProjectUser($projectId, $userid);
|
||
}
|
||
|
||
/**
|
||
* 删除项目成员
|
||
*
|
||
* @param int $projectId 项目ID
|
||
* @param int $userid 用户ID
|
||
* @return bool 是否成功
|
||
*/
|
||
public static function removeProjectUser(int $projectId, int $userid): bool
|
||
{
|
||
if (!Apps::isInstalled("manticore") || $projectId <= 0 || $userid <= 0) {
|
||
return false;
|
||
}
|
||
|
||
return ManticoreBase::deleteProjectUser($projectId, $userid);
|
||
}
|
||
|
||
/**
|
||
* 同步项目的所有成员到 Manticore
|
||
*
|
||
* @param int $projectId 项目ID
|
||
* @return bool 是否成功
|
||
*/
|
||
public static function syncProjectUsers(int $projectId): bool
|
||
{
|
||
if (!Apps::isInstalled("manticore") || $projectId <= 0) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
// 从 MySQL 获取项目成员
|
||
$userids = ProjectUser::where('project_id', $projectId)
|
||
->pluck('userid')
|
||
->toArray();
|
||
|
||
// 同步到 Manticore
|
||
return ManticoreBase::syncProjectUsers($projectId, $userids);
|
||
} catch (\Exception $e) {
|
||
Log::error('Manticore syncProjectUsers error: ' . $e->getMessage(), ['project_id' => $projectId]);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量同步所有项目成员关系(全量同步)
|
||
*
|
||
* @param callable|null $progressCallback 进度回调
|
||
* @return int 同步数量
|
||
*/
|
||
public static function syncAllProjectUsers(?callable $progressCallback = null): int
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return 0;
|
||
}
|
||
|
||
$count = 0;
|
||
$lastId = 0;
|
||
$batchSize = 1000;
|
||
|
||
// 先清空 Manticore 中的 project_users 表
|
||
ManticoreBase::clearAllProjectUsers();
|
||
|
||
// 分批同步
|
||
while (true) {
|
||
$records = ProjectUser::where('id', '>', $lastId)
|
||
->orderBy('id')
|
||
->limit($batchSize)
|
||
->get();
|
||
|
||
if ($records->isEmpty()) {
|
||
break;
|
||
}
|
||
|
||
foreach ($records as $record) {
|
||
ManticoreBase::upsertProjectUser($record->project_id, $record->userid);
|
||
$count++;
|
||
$lastId = $record->id;
|
||
}
|
||
|
||
if ($progressCallback) {
|
||
$progressCallback($count);
|
||
}
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 增量同步项目成员关系(只同步新增的)
|
||
*
|
||
* @param callable|null $progressCallback 进度回调
|
||
* @return int 同步数量
|
||
*/
|
||
public static function syncProjectUsersIncremental(?callable $progressCallback = null): int
|
||
{
|
||
if (!Apps::isInstalled("manticore")) {
|
||
return 0;
|
||
}
|
||
|
||
$count = 0;
|
||
$batchSize = 1000;
|
||
$lastKey = "sync:manticoreProjectUserLastId";
|
||
$lastId = intval(ManticoreKeyValue::get($lastKey, 0));
|
||
|
||
// 分批同步新增的记录
|
||
while (true) {
|
||
$records = ProjectUser::where('id', '>', $lastId)
|
||
->orderBy('id')
|
||
->limit($batchSize)
|
||
->get();
|
||
|
||
if ($records->isEmpty()) {
|
||
break;
|
||
}
|
||
|
||
foreach ($records as $record) {
|
||
ManticoreBase::upsertProjectUser($record->project_id, $record->userid);
|
||
$count++;
|
||
$lastId = $record->id;
|
||
}
|
||
|
||
// 保存进度
|
||
ManticoreKeyValue::set($lastKey, $lastId);
|
||
|
||
if ($progressCallback) {
|
||
$progressCallback($count);
|
||
}
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
}
|
||
|