Expand search breadth and modal action metadata
build-push / docker (push) Has been cancelled

This commit is contained in:
AI Assistant
2026-03-16 17:07:53 +09:00
parent 794aec1496
commit 19425c9503
8 changed files with 60 additions and 28 deletions
+13
View File
@@ -255,6 +255,19 @@
- backend debug broadcasts - backend debug broadcasts
## Recent Change Log ## 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` - Date: `2026-03-16`
- What changed: - What changed:
- Rewired the result modal to consume backend media metadata instead of hard-coded source branches. - Rewired the result modal to consume backend media metadata instead of hard-coded source branches.
+6 -3
View File
@@ -82,6 +82,7 @@ type PreviewResponse struct {
type searchDebugSummary struct { type searchDebugSummary struct {
Total int `json:"total"` Total int `json:"total"`
VisibleCount int `json:"visibleCount,omitempty"`
BySource map[string]int `json:"bySource"` BySource map[string]int `json:"bySource"`
WithPreview int `json:"withPreview"` WithPreview int `json:"withPreview"`
WithThumbnail int `json:"withThumbnail"` WithThumbnail int `json:"withThumbnail"`
@@ -477,16 +478,16 @@ func (a *App) searchMedia(c *gin.Context) {
return return
} }
merged := services.MergeRecommendations(recommended, scored, 20) merged := services.MergeRecommendations(recommended, scored, 16)
if geminiErr != nil { if geminiErr != nil {
merged = services.BackfillRecommendations( merged = services.BackfillRecommendations(
merged, merged,
scored, scored,
12, 16,
"Gemini 배치 일부가 실패해 미리보기 가능한 상위 후보를 보강했습니다.", "Gemini 배치 일부가 실패해 미리보기 가능한 상위 후보를 보강했습니다.",
) )
} }
merged = services.RandomizeTopRecommendations(merged, 8) merged = services.RandomizeTopRecommendations(merged, 6)
for idx := range merged { for idx := range merged {
merged[idx] = services.DecorateRecommendationMedia(merged[idx]) merged[idx] = services.DecorateRecommendationMedia(merged[idx])
} }
@@ -665,6 +666,7 @@ func summarizeSearchResults(results []services.SearchResult, duration time.Durat
} }
return searchDebugSummary{ return searchDebugSummary{
Total: len(results), Total: len(results),
VisibleCount: len(results),
BySource: bySource, BySource: bySource,
WithPreview: withPreview, WithPreview: withPreview,
WithThumbnail: withThumbnail, WithThumbnail: withThumbnail,
@@ -717,6 +719,7 @@ func summarizeRecommendationResults(results []services.AIRecommendation, duratio
} }
return searchDebugSummary{ return searchDebugSummary{
Total: len(results), Total: len(results),
VisibleCount: len(results),
BySource: bySource, BySource: bySource,
WithPreview: withPreview, WithPreview: withPreview,
WithThumbnail: withThumbnail, WithThumbnail: withThumbnail,
+21 -12
View File
@@ -88,7 +88,7 @@ func (s *SearchService) SearchMediaWithDeadline(queries []string, enabledPlatfor
results := make([]SearchResult, 0, 90) results := make([]SearchResult, 0, 90)
var lastErr error var lastErr error
baseQueries := limitQueries(queries, 6) baseQueries := limitQueries(queries, 8)
shuffleStrings(baseQueries) shuffleStrings(baseQueries)
primaryQueries := baseQueries[:minInt(len(baseQueries), 3)] primaryQueries := baseQueries[:minInt(len(baseQueries), 3)]
runSearchPass := func(bases []string, onlyMissing bool) { 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 { func (s *SearchService) EnrichResultsWithDeadline(results []SearchResult, deadline time.Time) []SearchResult {
limit := minInt(len(results), 14) limit := minInt(len(results), 18)
if limit == 0 { if limit == 0 {
return results return results
} }
@@ -722,13 +722,10 @@ func defaultMediaMode(source, link, previewURL, thumbnailURL string) (string, st
embedURL := buildEmbedURL(source, link) embedURL := buildEmbedURL(source, link)
switch source { switch source {
case "Google Video": case "Google Video":
if embedURL != "" {
return "embed", embedURL, ""
}
if hasUsableThumbnail(thumbnailURL) { 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": case "Envato":
if strings.TrimSpace(previewURL) != "" { if strings.TrimSpace(previewURL) != "" {
return "preview_video", embedURL, "provider_embed_blocked" 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" return "none", "", "provider_embed_blocked"
case "Artgrid": case "Artgrid":
if hasUsableThumbnail(thumbnailURL) {
return "thumbnail", embedURL, "provider_preview_unavailable"
}
if strings.TrimSpace(previewURL) != "" { if strings.TrimSpace(previewURL) != "" {
return "preview_video", embedURL, "provider_preview_unavailable" return "preview_video", embedURL, "provider_preview_unavailable"
} }
if hasUsableThumbnail(thumbnailURL) {
return "thumbnail", embedURL, "provider_preview_unavailable"
}
if embedURL != "" { if embedURL != "" {
return "embed", embedURL, "" return "embed", embedURL, ""
} }
@@ -774,6 +771,18 @@ func DecorateRecommendationMedia(item AIRecommendation) AIRecommendation {
if item.MediaMode == "thumbnail" && !hasUsableThumbnail(item.ThumbnailURL) && strings.TrimSpace(item.PreviewVideoURL) != "" { if item.MediaMode == "thumbnail" && !hasUsableThumbnail(item.ThumbnailURL) && strings.TrimSpace(item.PreviewVideoURL) != "" {
item.MediaMode = "preview_video" 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 return item
} }
@@ -1380,9 +1389,9 @@ func limitCollectorQueries(collector string, queries []string, onlyMissing bool)
limit := 2 limit := 2
switch collector { switch collector {
case "Envato", "Artgrid": case "Envato", "Artgrid":
limit = 3 limit = 4
case "Google Video": case "Google Video":
limit = 2 limit = 3
} }
if onlyMissing { if onlyMissing {
limit-- limit--
+4 -4
View File
@@ -144,13 +144,13 @@ func TestLimitCollectorQueriesUsesSmallerBudgetForMissingPass(t *testing.T) {
queries := []string{"a", "b", "c", "d"} queries := []string{"a", "b", "c", "d"}
got := limitCollectorQueries("Artgrid", queries, true) got := limitCollectorQueries("Artgrid", queries, true)
if len(got) != 2 { if len(got) != 3 {
t.Fatalf("expected 2 queries for missing-pass Artgrid collector, got %d", len(got)) t.Fatalf("expected 3 queries for missing-pass Artgrid collector, got %d", len(got))
} }
got = limitCollectorQueries("Google Video", queries, false) got = limitCollectorQueries("Google Video", queries, false)
if len(got) != 2 { if len(got) != 3 {
t.Fatalf("expected 2 queries for Google Video collector, got %d", len(got)) t.Fatalf("expected 3 queries for Google Video collector, got %d", len(got))
} }
} }
+3
View File
@@ -58,6 +58,9 @@ type AIRecommendation struct {
MediaMode string `json:"mediaMode,omitempty"` MediaMode string `json:"mediaMode,omitempty"`
EmbedURL string `json:"embedUrl,omitempty"` EmbedURL string `json:"embedUrl,omitempty"`
PreviewBlockedReason string `json:"previewBlockedReason,omitempty"` PreviewBlockedReason string `json:"previewBlockedReason,omitempty"`
ActionLabel string `json:"actionLabel,omitempty"`
ActionType string `json:"actionType,omitempty"`
SecondaryActionLabel string `json:"secondaryActionLabel,omitempty"`
} }
type QueryExpansion struct { type QueryExpansion struct {
+8 -4
View File
@@ -118,15 +118,19 @@ func TestGeminiExpansionCacheRoundTrip(t *testing.T) {
func TestDecorateRecommendationMediaUsesEmbedForGoogleVideo(t *testing.T) { func TestDecorateRecommendationMediaUsesEmbedForGoogleVideo(t *testing.T) {
item := DecorateRecommendationMedia(AIRecommendation{ item := DecorateRecommendationMedia(AIRecommendation{
Source: "Google Video", Source: "Google Video",
Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
ThumbnailURL: "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
}) })
if item.MediaMode != "embed" { if item.MediaMode != "thumbnail" {
t.Fatalf("expected embed media mode, got %q", item.MediaMode) t.Fatalf("expected thumbnail media mode, got %q", item.MediaMode)
} }
if item.EmbedURL == "" || !strings.Contains(item.EmbedURL, "youtube-nocookie.com/embed/") { if item.EmbedURL == "" || !strings.Contains(item.EmbedURL, "youtube-nocookie.com/embed/") {
t.Fatalf("unexpected embed url: %q", item.EmbedURL) 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) { func TestRankSearchResultsPrefersUsableVisuals(t *testing.T) {
+2 -2
View File
@@ -427,9 +427,9 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
merged = append(merged, DecorateRecommendationMedia(item)) merged = append(merged, DecorateRecommendationMedia(item))
} }
if len(merged) < min(12, limit) { if len(merged) < min(16, limit) {
for _, item := range ranked { 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 continue
} }
if fillerCount >= maxFiller { if fillerCount >= maxFiller {
+3 -3
View File
@@ -15,7 +15,7 @@ type searchCollector interface {
type envatoCollector struct{} type envatoCollector struct{}
func (envatoCollector) Name() string { return "Envato" } 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 { func (envatoCollector) Enabled(enabledPlatforms map[string]bool) bool {
return len(enabledPlatforms) == 0 || enabledPlatforms["envato"] return len(enabledPlatforms) == 0 || enabledPlatforms["envato"]
} }
@@ -31,7 +31,7 @@ func (envatoCollector) Enrich(searcher *SearchService, result SearchResult) Sear
type artgridCollector struct{} type artgridCollector struct{}
func (artgridCollector) Name() string { return "Artgrid" } 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 { func (artgridCollector) Enabled(enabledPlatforms map[string]bool) bool {
return len(enabledPlatforms) == 0 || enabledPlatforms["artgrid"] return len(enabledPlatforms) == 0 || enabledPlatforms["artgrid"]
} }
@@ -47,7 +47,7 @@ func (artgridCollector) Enrich(searcher *SearchService, result SearchResult) Sea
type googleVideoCollector struct{} type googleVideoCollector struct{}
func (googleVideoCollector) Name() string { return "Google Video" } 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 { func (googleVideoCollector) Enabled(enabledPlatforms map[string]bool) bool {
return len(enabledPlatforms) == 0 || enabledPlatforms["google video"] return len(enabledPlatforms) == 0 || enabledPlatforms["google video"]
} }