fix(PictureService, SearchDialog): enhance vector search with error handling and tab management #9

This commit is contained in:
shiyu
2025-05-25 11:58:04 +08:00
parent 27a59e8938
commit 6866580dce
2 changed files with 101 additions and 60 deletions
+89 -53
View File
@@ -50,18 +50,31 @@ public class PictureService(
// 决定是使用向量搜索还是普通搜索 // 决定是使用向量搜索还是普通搜索
if (useVectorSearch && !string.IsNullOrWhiteSpace(searchQuery)) if (useVectorSearch && !string.IsNullOrWhiteSpace(searchQuery))
{ {
return await PerformVectorSearchAsync( try
dbContext, page, pageSize, searchQuery, tags, {
startDate, endDate, userId, onlyWithGps, similarityThreshold, return await PerformVectorSearchAsync(
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic); dbContext, page, pageSize, searchQuery, tags,
} startDate, endDate, userId, onlyWithGps, similarityThreshold,
else excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
{ }
return await PerformStandardSearchAsync( catch (Exception ex)
dbContext, page, pageSize, searchQuery, tags, {
startDate, endDate, userId, sortBy, onlyWithGps, // 如果向量搜索失败,记录错误并回退到标准搜索
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic); Console.WriteLine($"向量搜索失败,回退到标准搜索: {ex.Message}");
// 如果是明确的配置错误,则向上抛出异常
if (ex.Message.Contains("请检查嵌入模型配置"))
{
throw;
}
}
} }
// 执行标准搜索(作为默认方法或向量搜索的回退选项)
return await PerformStandardSearchAsync(
dbContext, page, pageSize, searchQuery, tags,
startDate, endDate, userId, sortBy, onlyWithGps,
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
} }
// 执行向量搜索 // 执行向量搜索
@@ -82,55 +95,78 @@ public class PictureService(
int? ownerId, int? ownerId,
bool includeAllPublic) bool includeAllPublic)
{ {
var queryEmbedding = await embeddingService.GetEmbeddingAsync(searchQuery); try
var queryVector = new Vector(queryEmbedding); {
float[]? queryEmbedding = null;
// 构建基础查询 try
var query = dbContext.Pictures
.Include(p => p.Tags)
.Include(p => p.User)
.Where(p => p.Embedding != null);
// 应用共通的查询条件
query = ApplyCommonFilters(query, tags, startDate, endDate, userId, onlyWithGps,
excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
// 执行向量搜索
var allResults = await query
.Select(p => new
{ {
Picture = p, queryEmbedding = await embeddingService.GetEmbeddingAsync(searchQuery);
Similarity = 1.0 - p.Embedding!.CosineDistance(queryVector)
}) // 检查嵌入向量是否有效
.Where(p => p.Similarity >= similarityThreshold) if (queryEmbedding == null || queryEmbedding.Length == 0)
.OrderByDescending(p => p.Similarity) {
.ToListAsync(); throw new InvalidOperationException("嵌入模型返回了空向量");
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"向量搜索失败,请检查嵌入模型配置: {ex.Message}", ex);
}
var queryVector = new Vector(queryEmbedding);
// 计算总数并分页 // 构建基础查询
var totalCount = allResults.Count; var query = dbContext.Pictures
.Include(p => p.Tags)
.Include(p => p.User)
.Where(p => p.Embedding != null);
var paginatedResults = allResults // 应用共通的查询条件
.Skip((page - 1) * pageSize) query = ApplyCommonFilters(query, tags, startDate, endDate, userId, onlyWithGps,
.Take(pageSize) excludeAlbumId, albumId, onlyFavorites, ownerId, includeAllPublic);
.Select(r => MapPictureToResponse(r.Picture))
.ToList();
// 处理收藏信息 // 执行向量搜索
await PopulateFavoriteInfo(dbContext, paginatedResults, userId); var allResults = await query
.Select(p => new
{
Picture = p,
Similarity = 1.0 - p.Embedding!.CosineDistance(queryVector)
})
.Where(p => p.Similarity >= similarityThreshold)
.OrderByDescending(p => p.Similarity)
.ToListAsync();
// 为当前用户的图片添加相册信息 // 计算总数并分页
if (userId.HasValue) var totalCount = allResults.Count;
{
await PopulateAlbumInfo(dbContext, paginatedResults, userId.Value); var paginatedResults = allResults
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(r => MapPictureToResponse(r.Picture))
.ToList();
// 处理收藏信息
await PopulateFavoriteInfo(dbContext, paginatedResults, userId);
// 为当前用户的图片添加相册信息
if (userId.HasValue)
{
await PopulateAlbumInfo(dbContext, paginatedResults, userId.Value);
}
return new PaginatedResult<PictureResponse>
{
Data = paginatedResults,
Page = page,
PageSize = pageSize,
TotalCount = totalCount
};
} }
catch (Exception ex)
return new PaginatedResult<PictureResponse>
{ {
Data = paginatedResults, Console.WriteLine($"向量搜索失败: {ex.Message}");
Page = page, throw new InvalidOperationException($"向量搜索失败: {ex.Message}", ex);
PageSize = pageSize, }
TotalCount = totalCount
};
} }
// 执行标准搜索 // 执行标准搜索
+12 -7
View File
@@ -126,6 +126,17 @@ const SearchDialog: React.FC<SearchDialogProps> = ({
} }
}; };
// 处理标签页切换,同时更新 useVectorSearch 状态
const handleTabChange = (key: string) => {
setActiveTabKey(key);
// 根据标签页自动设置向量搜索状态
if (key === 'vector') {
setUseVectorSearch(true);
} else {
setUseVectorSearch(false);
}
};
return ( return (
<Modal <Modal
title={ title={
@@ -143,7 +154,7 @@ const SearchDialog: React.FC<SearchDialogProps> = ({
> >
<Tabs <Tabs
activeKey={activeTabKey} activeKey={activeTabKey}
onChange={setActiveTabKey} onChange={handleTabChange}
className="search-tabs" className="search-tabs"
items={[ items={[
{ {
@@ -163,9 +174,6 @@ const SearchDialog: React.FC<SearchDialogProps> = ({
autoFocus={activeTabKey === 'text'} autoFocus={activeTabKey === 'text'}
/> />
</div> </div>
<Divider orientation="left" plain></Divider>
<div className="search-option-group"> <div className="search-option-group">
<Text strong className="option-label">:</Text> <Text strong className="option-label">:</Text>
<Select <Select
@@ -212,9 +220,6 @@ const SearchDialog: React.FC<SearchDialogProps> = ({
autoFocus={activeTabKey === 'vector'} autoFocus={activeTabKey === 'vector'}
/> />
</div> </div>
<Divider orientation="left" plain></Divider>
<div className="search-option-group vector-options"> <div className="search-option-group vector-options">
<div className="vector-switch-container"> <div className="vector-switch-container">
<Switch <Switch