From b1ef9b19fa7716a1b7b7ba71729a15f3a6145020 Mon Sep 17 00:00:00 2001 From: Momentry Studio Date: Sun, 11 Oct 2026 22:07:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20loadMore=20?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=BB=93=E6=9E=9C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: - 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 种搜索模式 --- src/views/SearchView.vue | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 38aa339..537b7c3 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -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() + 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() @@ -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() 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() 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() + 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)