This commit is contained in:
@@ -323,6 +323,18 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
||||
recommended, geminiStats, geminiErr := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||
a.debug("search gemini evaluation", geminiStats)
|
||||
if services.NeedsSupplementalExploration(recommended) {
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini 평가가 약해 추가 후보를 탐색하는 중", "progress": 82})
|
||||
explorationQueries := buildSupplementalQueries(req.Query, queryVariants)
|
||||
extraResults, extraErr := a.SearchService.SearchMedia(explorationQueries, enabledPlatforms)
|
||||
if extraErr == nil && len(extraResults) > 0 {
|
||||
results = mergeSearchResults(results, extraResults)
|
||||
scored = services.RankSearchResults(strings.Join(explorationQueries[:min(len(explorationQueries), 3)], " "), results)
|
||||
recommended, geminiStats, geminiErr = services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||
a.debug("search supplemental query variants", gin.H{"variants": explorationQueries, "variantCount": len(explorationQueries)})
|
||||
a.debug("search gemini evaluation after supplemental search", geminiStats)
|
||||
}
|
||||
}
|
||||
if geminiErr != nil && len(recommended) == 0 {
|
||||
warning := geminiErr.Error()
|
||||
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
||||
@@ -419,6 +431,45 @@ func selectedPlatformLabel(platforms map[string]bool) string {
|
||||
return strings.Join(labels, ", ")
|
||||
}
|
||||
|
||||
func buildSupplementalQueries(query string, existing []string) []string {
|
||||
candidates := append([]string{}, existing...)
|
||||
candidates = append(candidates,
|
||||
query+" cinematic stock footage",
|
||||
query+" editorial b-roll",
|
||||
query+" establishing shot",
|
||||
query+" drone footage",
|
||||
)
|
||||
|
||||
seen := map[string]bool{}
|
||||
result := make([]string, 0, len(candidates))
|
||||
for _, item := range candidates {
|
||||
trimmed := strings.Join(strings.Fields(strings.TrimSpace(item)), " ")
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(trimmed)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeSearchResults(base, extra []services.SearchResult) []services.SearchResult {
|
||||
merged := make([]services.SearchResult, 0, len(base)+len(extra))
|
||||
seen := map[string]bool{}
|
||||
for _, item := range append(base, extra...) {
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, item)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func summarizeSearchResults(results []services.SearchResult, duration time.Duration, geminiCap int, warning string) searchDebugSummary {
|
||||
bySource := map[string]int{}
|
||||
withPreview := 0
|
||||
|
||||
@@ -12,13 +12,14 @@ import (
|
||||
const GeminiFallbackReason = "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다."
|
||||
|
||||
type GeminiBatchStats struct {
|
||||
CandidateCap int `json:"candidateCap"`
|
||||
Requested int `json:"requested"`
|
||||
Batches int `json:"batches"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
RecommendedCount int `json:"recommendedCount"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
CandidateCap int `json:"candidateCap"`
|
||||
Requested int `json:"requested"`
|
||||
Batches int `json:"batches"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
SequentialRetried int `json:"sequentialRetried"`
|
||||
RecommendedCount int `json:"recommendedCount"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func RankSearchResults(query string, results []SearchResult) []SearchResult {
|
||||
@@ -142,6 +143,27 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
||||
seen := map[string]bool{}
|
||||
for _, batch := range results {
|
||||
if batch.err != nil {
|
||||
recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize)
|
||||
if len(recovered) > 0 {
|
||||
stats.SequentialRetried++
|
||||
stats.Succeeded++
|
||||
for _, item := range recovered {
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, item)
|
||||
}
|
||||
if len(recoveredErrs) > 0 {
|
||||
stats.Failed++
|
||||
for _, recoveredErr := range recoveredErrs {
|
||||
if len(stats.Errors) < 5 {
|
||||
stats.Errors = append(stats.Errors, recoveredErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
stats.Failed++
|
||||
if len(stats.Errors) < 5 {
|
||||
stats.Errors = append(stats.Errors, batch.err.Error())
|
||||
@@ -210,6 +232,62 @@ func RandomizeTopRecommendations(items []AIRecommendation, window int) []AIRecom
|
||||
return shuffled
|
||||
}
|
||||
|
||||
func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex int) ([]AIRecommendation, []string) {
|
||||
recovered := make([]AIRecommendation, 0, 8)
|
||||
errs := make([]string, 0, 4)
|
||||
endIndex := min(startIndex+8, len(ranked))
|
||||
for idx := startIndex; idx < endIndex; idx++ {
|
||||
recs, err := service.Recommend(query, []SearchResult{ranked[idx]})
|
||||
if err != nil {
|
||||
if len(errs) < 4 {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
recovered = append(recovered, recs...)
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
}
|
||||
return recovered, errs
|
||||
}
|
||||
|
||||
func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
||||
if len(items) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
recommendedCount := 0
|
||||
negativeCount := 0
|
||||
for _, item := range items {
|
||||
if item.Recommended {
|
||||
recommendedCount++
|
||||
}
|
||||
if looksNegativeReason(item.Reason) {
|
||||
negativeCount++
|
||||
}
|
||||
}
|
||||
if recommendedCount >= 3 {
|
||||
return false
|
||||
}
|
||||
return negativeCount >= max(2, len(items)/2)
|
||||
}
|
||||
|
||||
func looksNegativeReason(reason string) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(reason))
|
||||
if lower == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range []string{
|
||||
"부적합", "관련이 없", "맞지 않", "의도와 맞지", "무관", "연관성 낮", "적절하지 않", "불일치",
|
||||
"not relevant", "irrelevant", "mismatch", "does not match", "unsuitable",
|
||||
} {
|
||||
if strings.Contains(lower, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
|
||||
merged := make([]AIRecommendation, 0, min(limit, len(ranked)))
|
||||
seen := map[string]bool{}
|
||||
@@ -251,3 +329,10 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user