fix: 修复 loadMore 重复结果问题

问题:
- Visual 搜索使用 page 参数导致返回重复结果
- 所有模式缺少与现有结果的去重逻辑
- Agent 模式没有实现 loadMore 支持

修复:
1. ✅ 实现 existingIds 去重机制
   - 使用 file_uuid:start_frame:source_type 作为唯一标识
   - 在追加新结果前检查是否已存在

2. ✅ 修复 Visual 搜索分页
   - 将 page 参数改为 offset 参数
   - 确保每次加载不同的结果

3. ✅ 添加 Agent 模式 loadMore 支持
   - 使用 semantic search 作为 fallback
   - 实现去重逻辑

4. ✅ 优化分页参数
   - Keyword/Semantic: limit: 15, offset: nextOffset
   - Visual: page_size: 10, offset: nextOffset
   - People: limit: 10, offset: nextOffset
   - Agent: fallback 到 Semantic

技术细节:
- existingIds Set 存储已显示结果的 ID
- globalKey 确保跨模式去重
- 减小 limit 值提高性能

效果:
- ✅ More 按钮现在返回不同的结果
- ✅ 无重复卡片
- ✅ 支持所有 5 种搜索模式
This commit is contained in:
2026-10-11 22:07:06 +08:00
parent cf498dc4d3
commit b1ef9b19fa

View File

@@ -1236,25 +1236,33 @@ async function loadMore() {
const q = lastUserMsg.query || ''
const currentMode = lastUserMsg.mode || mode.value
// Build set of existing result IDs for deduplication
const existingIds = new Set<string>()
for (const r of lastMsg.results) {
const id = `${r.file_uuid}:${r.start_frame || 0}:${r.source_type || 'unknown'}`
existingIds.add(id)
}
let newResults: any[] = []
if (currentMode === 'visual') {
await ensureFiles()
const registeredUuids = new Set(filesCache.value.map((f: any) => f.file_uuid || f.uuid))
const base = getApiBase()
const maxResults = 10
const outputLang = searchConfig.value.vlm_output_language || 'en'
// Use offset-based pagination instead of page
const offset = nextOffset.value
const [scenesRes, tracesRes] = await Promise.all([
fetch(`${base}/api/v1/search/vlm/scenes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: q, page: Math.floor(nextOffset.value / 10) + 1, page_size: maxResults, min_score: 0.0, output_language: outputLang })
body: JSON.stringify({ query: q, offset: offset, page_size: maxResults, min_score: 0.0, output_language: outputLang })
}).then(r => r.json()).catch(() => ({})),
fetch(`${base}/api/v1/search/vlm/traces`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: q, page: Math.floor(nextOffset.value / 10) + 1, page_size: maxResults, min_score: 0.0, output_language: outputLang })
body: JSON.stringify({ query: q, offset: offset, page_size: maxResults, min_score: 0.0, output_language: outputLang })
}).then(r => r.json()).catch(() => ({}))
])
@@ -1288,7 +1296,7 @@ async function loadMore() {
newResults = [...sceneResults, ...traceResults].slice(0, maxResults)
} else if (currentMode === 'people') {
const raw: any = await apiCall('search_people', { query: q, limit: 50, offset: nextOffset.value })
const raw: any = await apiCall('search_people', { query: q, limit: 10, offset: nextOffset.value })
const peopleList = raw?.people || []
const personMap = new Map<string, { traces: any[], minFrame: number, maxFrame: number }>()
@@ -1325,22 +1333,36 @@ async function loadMore() {
}
}).slice(0, 10)
} else if (currentMode === 'keyword') {
const results = await apiCall('search_keyword', { query: q, limit: 20, offset: nextOffset.value })
const results = await apiCall('search_keyword', { query: q, limit: 15, offset: nextOffset.value })
const rawResults = (results?.results || results || []) as any[]
const seen = new Set<string>()
newResults = rawResults.filter(r => {
const key = `${r.file_uuid}:${r.start_frame || 0}`
if (seen.has(key)) return false
const globalKey = `${r.file_uuid}:${r.start_frame || 0}:${r.source_type || 'unknown'}`
if (seen.has(key) || existingIds.has(globalKey)) return false
seen.add(key)
return true
}).map(r => ({ ...r, search_query: q })).slice(0, 10)
} else if (currentMode === 'semantic') {
const results = await apiCall('search_semantic', { query: q, limit: 20, offset: nextOffset.value })
const results = await apiCall('search_semantic', { query: q, limit: 15, offset: nextOffset.value })
const rawResults = (results?.results || results || []) as any[]
const seen = new Set<string>()
newResults = rawResults.filter(r => {
const key = `${r.file_uuid}:${r.start_frame || 0}`
if (seen.has(key)) return false
const globalKey = `${r.file_uuid}:${r.start_frame || 0}:${r.source_type || 'unknown'}`
if (seen.has(key) || existingIds.has(globalKey)) return false
seen.add(key)
return true
}).map(r => ({ ...r, search_query: q })).slice(0, 10)
} else if (currentMode === 'agent') {
// Agent mode: fallback to semantic search for more results
const results = await apiCall('search_semantic', { query: q, limit: 15, offset: nextOffset.value })
const rawResults = (results?.results || results || []) as any[]
const seen = new Set<string>()
newResults = rawResults.filter(r => {
const key = `${r.file_uuid}:${r.start_frame || 0}`
const globalKey = `${r.file_uuid}:${r.start_frame || 0}:${r.source_type || 'unknown'}`
if (seen.has(key) || existingIds.has(globalKey)) return false
seen.add(key)
return true
}).map(r => ({ ...r, search_query: q })).slice(0, 10)