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;
|
|
}
|
|
}
|
|
|