feat(knowledge): Phase D 统一搜索 + 种子知识冷启动
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- search/recommend API 返回 UnifiedSearchResult (文档+结构化双通道)
- POST /api/v1/knowledge/seed 种子知识冷启动 (幂等, admin权限)
- seed_knowledge service: 按标题+行业查重, source=distillation
- SearchRequest 扩展: search_structured/search_documents/industry_id
This commit is contained in:
iven
2026-04-12 20:46:43 +08:00
parent f8c5a76ce6
commit 640df9937f
4 changed files with 167 additions and 16 deletions

View File

@@ -582,6 +582,113 @@ pub async fn search(
}).filter(|r| r.score >= min_score).collect())
}
// === 统一搜索(双通道合并) ===
/// 统一搜索:同时检索文档通道和结构化通道
pub async fn unified_search(
pool: &PgPool,
request: &SearchRequest,
viewer_account_id: Option<&str>,
) -> SaasResult<UnifiedSearchResult> {
let limit = request.limit.unwrap_or(5).min(10);
let search_docs = request.search_documents.unwrap_or(true);
let search_struct = request.search_structured.unwrap_or(true);
// 文档通道
let documents = if search_docs {
search(
pool,
&request.query,
request.category_id.as_deref(),
limit,
request.min_score.unwrap_or(0.5),
).await?
} else {
Vec::new()
};
// 结构化通道
let structured = if search_struct {
query_structured(
pool,
&StructuredQueryRequest {
query: request.query.clone(),
source_id: None,
industry_id: request.industry_id.clone(),
limit: Some(limit),
},
viewer_account_id,
).await?
} else {
Vec::new()
};
Ok(UnifiedSearchResult {
documents,
structured,
})
}
// === 种子知识冷启动 ===
/// 为指定行业插入种子知识(幂等)
pub async fn seed_knowledge(
pool: &PgPool,
industry_id: &str,
category_id: &str,
items: &[(String, String, Vec<String>)], // (title, content, keywords)
system_account_id: &str,
) -> SaasResult<usize> {
let mut created = 0;
for (title, content, keywords) in items {
if content.trim().is_empty() {
continue;
}
// 幂等:按标题 + source='distillation' + tags 含行业ID 查重
let exists: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM knowledge_items \
WHERE title = $1 AND source = 'distillation' \
AND $2 = ANY(tags)"
)
.bind(title)
.bind(format!("industry:{}", industry_id))
.fetch_one(pool)
.await?;
if exists.0 > 0 {
continue;
}
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let kw_json = serde_json::to_value(keywords).unwrap_or(serde_json::json!([]));
let tags = vec![
format!("industry:{}", industry_id),
"source:distillation".to_string(),
];
sqlx::query(
"INSERT INTO knowledge_items \
(id, category_id, title, content, keywords, status, priority, visibility, account_id, source, tags, version, created_by, created_at, updated_at) \
VALUES ($1, $8, $2, $3, $4, 'active', 5, 'public', NULL, \
'distillation', $5, 1, $6, $7, $7)"
)
.bind(&id)
.bind(title)
.bind(content)
.bind(&kw_json)
.bind(&tags)
.bind(system_account_id)
.bind(&now)
.bind(category_id)
.execute(pool)
.await?;
created += 1;
}
Ok(created)
}
// === 分析 ===
/// 分析总览