152 lines
5.1 KiB
PHP
152 lines
5.1 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Document;
|
||
use App\Models\KnowledgeBase;
|
||
use Illuminate\Database\Eloquent\Collection;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class DocumentSearchService
|
||
{
|
||
/**
|
||
* 搜索文档
|
||
*
|
||
* @param string $query 搜索关键词
|
||
* @param array $accessibleStationIds 权限边界的线站 IDs(空=不限制)
|
||
* @param array $filters 用户主动筛选:
|
||
* - station_ids: int[] 线站 ID
|
||
* - knowledge_base_ids: int[] 知识库 ID
|
||
* - uploaded_by: int 上传者 ID
|
||
* @return Collection
|
||
*/
|
||
public function search(string $query, array $accessibleStationIds = [], array $filters = []): Collection
|
||
{
|
||
try {
|
||
$kbIds = $this->resolveKnowledgeBaseIds($accessibleStationIds, $filters);
|
||
|
||
$searchBuilder = Document::search($query);
|
||
|
||
if (!empty($filters['uploaded_by'])) {
|
||
$searchBuilder->where('uploaded_by', $filters['uploaded_by']);
|
||
}
|
||
|
||
if (!empty($kbIds)) {
|
||
$searchBuilder->query(fn ($q) => $q->whereIn('knowledge_base_id', $kbIds));
|
||
}
|
||
|
||
return $searchBuilder->get();
|
||
} catch (\Exception $e) {
|
||
Log::error('文档搜索失败', [
|
||
'query' => $query,
|
||
'filters' => $filters,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
|
||
return new Collection();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析最终可搜索的知识库 ID 列表
|
||
*
|
||
* 1. 从 accessibleStationIds 得到可访问 KB(全局 KB + 关联 station 的 KB)
|
||
* 2. 从 filters 得到用户指定的 KB(station_ids + knowledge_base_ids 并集)
|
||
* 3. 取交集(若有 filter 指定),否则用可访问范围
|
||
*/
|
||
private function resolveKnowledgeBaseIds(array $accessibleStationIds, array $filters): array
|
||
{
|
||
// 权限边界
|
||
$accessibleKbIds = null;
|
||
if (!empty($accessibleStationIds)) {
|
||
$accessibleKbIds = KnowledgeBase::where(function ($q) use ($accessibleStationIds) {
|
||
$q->whereDoesntHave('stations')
|
||
->orWhereHas('stations', fn ($sq) => $sq->whereIn('stations.id', $accessibleStationIds));
|
||
})->pluck('id')->toArray();
|
||
}
|
||
|
||
// 用户主动筛选
|
||
$kbIdsFromStations = [];
|
||
if (!empty($filters['station_ids'])) {
|
||
$kbIdsFromStations = KnowledgeBase::where(function ($q) use ($filters) {
|
||
$q->whereDoesntHave('stations')
|
||
->orWhereHas('stations', fn ($sq) => $sq->whereIn('stations.id', $filters['station_ids']));
|
||
})->pluck('id')->toArray();
|
||
}
|
||
|
||
$kbIdsFromFilter = $filters['knowledge_base_ids'] ?? [];
|
||
|
||
if (!empty($kbIdsFromStations) && !empty($kbIdsFromFilter)) {
|
||
$filterKbIds = array_values(array_intersect($kbIdsFromStations, $kbIdsFromFilter));
|
||
} elseif (!empty($kbIdsFromStations)) {
|
||
$filterKbIds = $kbIdsFromStations;
|
||
} elseif (!empty($kbIdsFromFilter)) {
|
||
$filterKbIds = $kbIdsFromFilter;
|
||
} else {
|
||
$filterKbIds = [];
|
||
}
|
||
|
||
// 合并
|
||
if ($accessibleKbIds === null) {
|
||
return $filterKbIds; // 无权限限制
|
||
}
|
||
|
||
if (empty($filterKbIds)) {
|
||
return $accessibleKbIds; // 无主动筛选,用权限范围
|
||
}
|
||
|
||
return array_values(array_intersect($filterKbIds, $accessibleKbIds)); // 取交集
|
||
}
|
||
|
||
/**
|
||
* 准备文档的可搜索数据
|
||
*/
|
||
public function prepareSearchableData(Document $document): array
|
||
{
|
||
return [
|
||
'id' => $document->id,
|
||
'title' => $document->title,
|
||
'description' => $document->description,
|
||
'markdown_content' => $document->getMarkdownContent(),
|
||
'knowledge_base_id' => $document->knowledge_base_id,
|
||
'uploaded_by' => $document->uploaded_by,
|
||
'created_at' => $document->created_at?->timestamp,
|
||
'updated_at' => $document->updated_at?->timestamp,
|
||
];
|
||
}
|
||
|
||
public function indexDocument(Document $document): void
|
||
{
|
||
try {
|
||
if (!$document->shouldBeSearchable()) {
|
||
return;
|
||
}
|
||
$document->searchable();
|
||
} catch (\Exception $e) {
|
||
Log::error('文档索引失败', ['document_id' => $document->id, 'error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
public function updateDocumentIndex(Document $document): void
|
||
{
|
||
try {
|
||
if ($document->shouldBeSearchable()) {
|
||
$document->searchable();
|
||
} else {
|
||
$this->removeDocumentFromIndex($document);
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('文档索引更新失败', ['document_id' => $document->id, 'error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
public function removeDocumentFromIndex(Document $document): void
|
||
{
|
||
try {
|
||
$document->unsearchable();
|
||
} catch (\Exception $e) {
|
||
Log::error('文档索引移除失败', ['document_id' => $document->id, 'error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
}
|