From 19425c9503f1a0a3029477ebf3deabbc16fd6acc Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Mon, 16 Mar 2026 17:07:53 +0900 Subject: [PATCH] Expand search breadth and modal action metadata --- TODO.md | 13 +++++++++++ backend/handlers/api.go | 9 +++++--- backend/services/cse.go | 33 +++++++++++++++++---------- backend/services/cse_test.go | 8 +++---- backend/services/gemini.go | 3 +++ backend/services/gemini_test.go | 12 ++++++---- backend/services/ranker.go | 4 ++-- backend/services/search_collectors.go | 6 ++--- 8 files changed, 60 insertions(+), 28 deletions(-) diff --git a/TODO.md b/TODO.md index f20f6ec..4643a2d 100644 --- a/TODO.md +++ b/TODO.md @@ -255,6 +255,19 @@ - backend debug broadcasts ## Recent Change Log +- Date: `2026-03-16` +- What changed: + - Expanded search breadth moderately by increasing base query count, collector query budgets, per-source caps, enrichment scope, and final visible result target while keeping Gemini review cap at `16`. + - Reworked recommendation action metadata so Google Video now advertises `Direct Download` as the primary CTA, while Envato and Artgrid advertise `Open Source`. + - Changed default modal media priority so Artgrid now prefers preview video ahead of thumbnail when both are available, and Google Video now defaults to a webpage-like thumbnail mode instead of embed-first. + - Added visible-count style debug summary fields to support checking whether the widened search budget actually increases user-facing choice. +- Why it changed: + - The UI was much healthier, but the remaining request from the user was to widen the pool of selectable results without undoing the recent quality gains, and to align modal CTA semantics with what each source can actually do. +- How it was verified: + - `go test ./...` +- What is still risky or incomplete: + - The widened search budget may increase latency on worse SearXNG days, so the frontend/UI half of this batch still needs to land before the full user-facing behavior is validated. + - Date: `2026-03-16` - What changed: - Rewired the result modal to consume backend media metadata instead of hard-coded source branches. diff --git a/backend/handlers/api.go b/backend/handlers/api.go index 2691d8a..810887b 100644 --- a/backend/handlers/api.go +++ b/backend/handlers/api.go @@ -82,6 +82,7 @@ type PreviewResponse struct { type searchDebugSummary struct { Total int `json:"total"` + VisibleCount int `json:"visibleCount,omitempty"` BySource map[string]int `json:"bySource"` WithPreview int `json:"withPreview"` WithThumbnail int `json:"withThumbnail"` @@ -477,16 +478,16 @@ func (a *App) searchMedia(c *gin.Context) { return } - merged := services.MergeRecommendations(recommended, scored, 20) + merged := services.MergeRecommendations(recommended, scored, 16) if geminiErr != nil { merged = services.BackfillRecommendations( merged, scored, - 12, + 16, "Gemini 배치 일부가 실패해 미리보기 가능한 상위 후보를 보강했습니다.", ) } - merged = services.RandomizeTopRecommendations(merged, 8) + merged = services.RandomizeTopRecommendations(merged, 6) for idx := range merged { merged[idx] = services.DecorateRecommendationMedia(merged[idx]) } @@ -665,6 +666,7 @@ func summarizeSearchResults(results []services.SearchResult, duration time.Durat } return searchDebugSummary{ Total: len(results), + VisibleCount: len(results), BySource: bySource, WithPreview: withPreview, WithThumbnail: withThumbnail, @@ -717,6 +719,7 @@ func summarizeRecommendationResults(results []services.AIRecommendation, duratio } return searchDebugSummary{ Total: len(results), + VisibleCount: len(results), BySource: bySource, WithPreview: withPreview, WithThumbnail: withThumbnail, diff --git a/backend/services/cse.go b/backend/services/cse.go index 33863cd..e7f282a 100644 --- a/backend/services/cse.go +++ b/backend/services/cse.go @@ -88,7 +88,7 @@ func (s *SearchService) SearchMediaWithDeadline(queries []string, enabledPlatfor results := make([]SearchResult, 0, 90) var lastErr error - baseQueries := limitQueries(queries, 6) + baseQueries := limitQueries(queries, 8) shuffleStrings(baseQueries) primaryQueries := baseQueries[:minInt(len(baseQueries), 3)] runSearchPass := func(bases []string, onlyMissing bool) { @@ -190,7 +190,7 @@ func (s *SearchService) EnrichResults(results []SearchResult) []SearchResult { } func (s *SearchService) EnrichResultsWithDeadline(results []SearchResult, deadline time.Time) []SearchResult { - limit := minInt(len(results), 14) + limit := minInt(len(results), 18) if limit == 0 { return results } @@ -722,13 +722,10 @@ func defaultMediaMode(source, link, previewURL, thumbnailURL string) (string, st embedURL := buildEmbedURL(source, link) switch source { case "Google Video": - if embedURL != "" { - return "embed", embedURL, "" - } if hasUsableThumbnail(thumbnailURL) { - return "thumbnail", "", "missing_google_embed" + return "thumbnail", embedURL, "webpage_like_preview_preferred" } - return "none", "", "missing_google_embed" + return "none", embedURL, "webpage_like_preview_preferred" case "Envato": if strings.TrimSpace(previewURL) != "" { return "preview_video", embedURL, "provider_embed_blocked" @@ -741,12 +738,12 @@ func defaultMediaMode(source, link, previewURL, thumbnailURL string) (string, st } return "none", "", "provider_embed_blocked" case "Artgrid": - if hasUsableThumbnail(thumbnailURL) { - return "thumbnail", embedURL, "provider_preview_unavailable" - } if strings.TrimSpace(previewURL) != "" { return "preview_video", embedURL, "provider_preview_unavailable" } + if hasUsableThumbnail(thumbnailURL) { + return "thumbnail", embedURL, "provider_preview_unavailable" + } if embedURL != "" { return "embed", embedURL, "" } @@ -774,6 +771,18 @@ func DecorateRecommendationMedia(item AIRecommendation) AIRecommendation { if item.MediaMode == "thumbnail" && !hasUsableThumbnail(item.ThumbnailURL) && strings.TrimSpace(item.PreviewVideoURL) != "" { item.MediaMode = "preview_video" } + switch item.Source { + case "Google Video": + item.ActionType = "download" + item.ActionLabel = "Direct Download" + item.SecondaryActionLabel = "Open Source" + case "Envato", "Artgrid": + item.ActionType = "open_source" + item.ActionLabel = "Open Source" + default: + item.ActionType = "open_source" + item.ActionLabel = "Open Source" + } return item } @@ -1380,9 +1389,9 @@ func limitCollectorQueries(collector string, queries []string, onlyMissing bool) limit := 2 switch collector { case "Envato", "Artgrid": - limit = 3 + limit = 4 case "Google Video": - limit = 2 + limit = 3 } if onlyMissing { limit-- diff --git a/backend/services/cse_test.go b/backend/services/cse_test.go index 99b96d8..6087f05 100644 --- a/backend/services/cse_test.go +++ b/backend/services/cse_test.go @@ -144,13 +144,13 @@ func TestLimitCollectorQueriesUsesSmallerBudgetForMissingPass(t *testing.T) { queries := []string{"a", "b", "c", "d"} got := limitCollectorQueries("Artgrid", queries, true) - if len(got) != 2 { - t.Fatalf("expected 2 queries for missing-pass Artgrid collector, got %d", len(got)) + if len(got) != 3 { + t.Fatalf("expected 3 queries for missing-pass Artgrid collector, got %d", len(got)) } got = limitCollectorQueries("Google Video", queries, false) - if len(got) != 2 { - t.Fatalf("expected 2 queries for Google Video collector, got %d", len(got)) + if len(got) != 3 { + t.Fatalf("expected 3 queries for Google Video collector, got %d", len(got)) } } diff --git a/backend/services/gemini.go b/backend/services/gemini.go index 770fbc6..609de87 100644 --- a/backend/services/gemini.go +++ b/backend/services/gemini.go @@ -58,6 +58,9 @@ type AIRecommendation struct { MediaMode string `json:"mediaMode,omitempty"` EmbedURL string `json:"embedUrl,omitempty"` PreviewBlockedReason string `json:"previewBlockedReason,omitempty"` + ActionLabel string `json:"actionLabel,omitempty"` + ActionType string `json:"actionType,omitempty"` + SecondaryActionLabel string `json:"secondaryActionLabel,omitempty"` } type QueryExpansion struct { diff --git a/backend/services/gemini_test.go b/backend/services/gemini_test.go index 17d91ef..f6fed67 100644 --- a/backend/services/gemini_test.go +++ b/backend/services/gemini_test.go @@ -118,15 +118,19 @@ func TestGeminiExpansionCacheRoundTrip(t *testing.T) { func TestDecorateRecommendationMediaUsesEmbedForGoogleVideo(t *testing.T) { item := DecorateRecommendationMedia(AIRecommendation{ - Source: "Google Video", - Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Source: "Google Video", + Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + ThumbnailURL: "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", }) - if item.MediaMode != "embed" { - t.Fatalf("expected embed media mode, got %q", item.MediaMode) + if item.MediaMode != "thumbnail" { + t.Fatalf("expected thumbnail media mode, got %q", item.MediaMode) } if item.EmbedURL == "" || !strings.Contains(item.EmbedURL, "youtube-nocookie.com/embed/") { t.Fatalf("unexpected embed url: %q", item.EmbedURL) } + if item.ActionType != "download" || item.ActionLabel != "Direct Download" { + t.Fatalf("unexpected Google Video actions: %#v", item) + } } func TestRankSearchResultsPrefersUsableVisuals(t *testing.T) { diff --git a/backend/services/ranker.go b/backend/services/ranker.go index 5cb52b4..71c3da1 100644 --- a/backend/services/ranker.go +++ b/backend/services/ranker.go @@ -427,9 +427,9 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, merged = append(merged, DecorateRecommendationMedia(item)) } - if len(merged) < min(12, limit) { + if len(merged) < min(16, limit) { for _, item := range ranked { - if len(merged) >= min(12, limit) || item.Link == "" || seen[item.Link] { + if len(merged) >= min(16, limit) || item.Link == "" || seen[item.Link] { continue } if fillerCount >= maxFiller { diff --git a/backend/services/search_collectors.go b/backend/services/search_collectors.go index da17e22..dc6ae78 100644 --- a/backend/services/search_collectors.go +++ b/backend/services/search_collectors.go @@ -15,7 +15,7 @@ type searchCollector interface { type envatoCollector struct{} func (envatoCollector) Name() string { return "Envato" } -func (envatoCollector) MaxResults() int { return 10 } +func (envatoCollector) MaxResults() int { return 12 } func (envatoCollector) Enabled(enabledPlatforms map[string]bool) bool { return len(enabledPlatforms) == 0 || enabledPlatforms["envato"] } @@ -31,7 +31,7 @@ func (envatoCollector) Enrich(searcher *SearchService, result SearchResult) Sear type artgridCollector struct{} func (artgridCollector) Name() string { return "Artgrid" } -func (artgridCollector) MaxResults() int { return 10 } +func (artgridCollector) MaxResults() int { return 12 } func (artgridCollector) Enabled(enabledPlatforms map[string]bool) bool { return len(enabledPlatforms) == 0 || enabledPlatforms["artgrid"] } @@ -47,7 +47,7 @@ func (artgridCollector) Enrich(searcher *SearchService, result SearchResult) Sea type googleVideoCollector struct{} func (googleVideoCollector) Name() string { return "Google Video" } -func (googleVideoCollector) MaxResults() int { return 6 } +func (googleVideoCollector) MaxResults() int { return 8 } func (googleVideoCollector) Enabled(enabledPlatforms map[string]bool) bool { return len(enabledPlatforms) == 0 || enabledPlatforms["google video"] }