Refactor search fallback and preview flows
build-push / docker (push) Failing after 20m32s

This commit is contained in:
AI Assistant
2026-03-16 11:12:43 +09:00
parent b43886e950
commit 8101f17f5b
5 changed files with 150 additions and 88 deletions
+12 -21
View File
@@ -321,27 +321,11 @@ func (a *App) searchMedia(c *gin.Context) {
scored := services.RankSearchResults(rankQuery, results)
a.debug("search ranked summary", summarizeSearchResults(scored, time.Since(started), services.GeminiCandidateLimit(len(scored)), ""))
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
recommended, geminiStats := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
recommended, geminiStats, geminiErr := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
a.debug("search gemini evaluation", geminiStats)
err = nil
if len(recommended) == 0 {
err = fmt.Errorf("gemini vision returned no recommended items across all candidate batches")
}
if err != nil {
fallback := make([]services.AIRecommendation, 0, min(20, len(scored)))
for _, result := range scored[:min(20, len(scored))] {
fallback = append(fallback, services.AIRecommendation{
Title: result.Title,
Link: result.Link,
Snippet: result.Snippet,
ThumbnailURL: result.ThumbnailURL,
PreviewVideoURL: result.PreviewVideoURL,
Source: result.Source,
Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.",
Recommended: false,
})
}
warning := err.Error()
if geminiErr != nil && len(recommended) == 0 {
warning := geminiErr.Error()
fallback := services.BuildFallbackRecommendations(scored, 20, "")
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision fallback to ranked results", "progress": 90, "message": warning})
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": warning, "queries": queryVariants})
@@ -349,8 +333,15 @@ func (a *App) searchMedia(c *gin.Context) {
}
merged := services.MergeRecommendations(recommended, scored, 20)
a.debug("search complete summary", summarizeRecommendationResults(merged, time.Since(started), ""))
warning := ""
if geminiErr != nil {
warning = geminiErr.Error()
}
a.debug("search complete summary", summarizeRecommendationResults(merged, time.Since(started), warning))
response := gin.H{"results": merged, "queries": queryVariants}
if warning != "" {
response["warning"] = warning
}
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "search complete", "progress": 100})
c.JSON(http.StatusOK, response)
}
+6 -12
View File
@@ -164,16 +164,21 @@ User query: ` + query,
}
maxImages := min(len(candidates), 10)
visualCount := 0
for idx := 0; idx < maxImages; idx++ {
img, mimeType, err := fetchCandidateVisualInlineData(g.Client, candidates[idx])
if err != nil {
continue
}
visualCount++
parts = append(parts,
geminiPart{"text": fmt.Sprintf("Candidate %d: title=%s source=%s link=%s", idx, candidates[idx].Title, candidates[idx].Source, candidates[idx].Link)},
geminiPart{"inlineData": map[string]string{"mimeType": mimeType, "data": img}},
)
}
if visualCount == 0 {
return nil, fmt.Errorf("no candidate thumbnails or preview frames could be fetched for gemini vision")
}
body := map[string]any{
"contents": []map[string]any{
@@ -248,18 +253,7 @@ User query: ` + query,
}
if len(recommendations) == 0 {
for _, candidate := range candidates[:min(8, len(candidates))] {
recommendations = append(recommendations, AIRecommendation{
Title: candidate.Title,
Link: candidate.Link,
Snippet: candidate.Snippet,
ThumbnailURL: candidate.ThumbnailURL,
PreviewVideoURL: candidate.PreviewVideoURL,
Source: candidate.Source,
Reason: "Gemini Vision 평가를 받지 못해 키워드 기준으로 보강된 결과입니다.",
Recommended: false,
})
}
recommendations = BuildFallbackRecommendations(candidates, 8, "Gemini Vision 평가를 받지 못해 키워드 기준으로 보강된 결과입니다.")
}
return recommendations, nil
+47 -3
View File
@@ -1,11 +1,14 @@
package services
import (
"fmt"
"sort"
"strings"
"sync"
)
const GeminiFallbackReason = "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다."
type GeminiBatchStats struct {
CandidateCap int `json:"candidateCap"`
Requested int `json:"requested"`
@@ -84,9 +87,13 @@ func GeminiCandidateLimit(total int) int {
return total
}
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats) {
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats, error) {
const chunkSize = 8
const maxConcurrentBatches = 2
if service == nil {
return nil, GeminiBatchStats{}, fmt.Errorf("gemini service is not configured")
}
limit := GeminiCandidateLimit(len(ranked))
stats := GeminiBatchStats{
CandidateCap: limit,
@@ -106,6 +113,9 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
batches = append(batches, ranked[start:end])
}
stats.Batches = len(batches)
if len(batches) == 0 {
return []AIRecommendation{}, stats, nil
}
results := make([]batchResult, len(batches))
var wg sync.WaitGroup
@@ -146,7 +156,41 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
}
}
stats.RecommendedCount = len(merged)
return merged, stats
switch {
case len(merged) > 0 && stats.Failed == 0:
return merged, stats, nil
case len(merged) > 0 && stats.Failed > 0:
return merged, stats, fmt.Errorf("gemini vision partially failed on %d of %d batches", stats.Failed, stats.Batches)
case stats.Failed == stats.Batches:
if len(stats.Errors) > 0 {
return nil, stats, fmt.Errorf("gemini vision failed for all batches: %s", strings.Join(stats.Errors, "; "))
}
return nil, stats, fmt.Errorf("gemini vision failed for all batches")
default:
return nil, stats, fmt.Errorf("gemini vision returned no candidate evaluations")
}
}
func BuildFallbackRecommendations(ranked []SearchResult, limit int, reason string) []AIRecommendation {
if strings.TrimSpace(reason) == "" {
reason = GeminiFallbackReason
}
fallback := make([]AIRecommendation, 0, min(limit, len(ranked)))
for _, item := range ranked[:min(limit, len(ranked))] {
fallback = append(fallback, AIRecommendation{
Title: item.Title,
Link: item.Link,
Snippet: item.Snippet,
ThumbnailURL: item.ThumbnailURL,
PreviewVideoURL: item.PreviewVideoURL,
Source: item.Source,
Reason: reason,
Recommended: false,
})
}
return fallback
}
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
@@ -184,7 +228,7 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
ThumbnailURL: item.ThumbnailURL,
PreviewVideoURL: item.PreviewVideoURL,
Source: item.Source,
Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.",
Reason: GeminiFallbackReason,
Recommended: false,
})
}