This commit is contained in:
+12
-21
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user