diff --git a/backend/handlers/api.go b/backend/handlers/api.go index 884d467..1bc89d8 100644 --- a/backend/handlers/api.go +++ b/backend/handlers/api.go @@ -263,6 +263,10 @@ func (a *App) searchMedia(c *gin.Context) { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } + if len(results) == 0 { + c.JSON(http.StatusOK, gin.H{"results": []services.AIRecommendation{}, "warning": "Vertex AI Search returned no renderable results. Check your website indexing fields and thumbnails."}) + return + } recommended, err := a.GeminiService.Recommend(req.Query, results) if err != nil { diff --git a/backend/services/cse.go b/backend/services/cse.go index cff9b96..144c3cd 100644 --- a/backend/services/cse.go +++ b/backend/services/cse.go @@ -6,6 +6,7 @@ import ( "io" "net/http" neturl "net/url" + "regexp" "strings" "time" ) @@ -121,15 +122,33 @@ func (s *SearchService) searchLite(query string, imageSearch bool) ([]SearchResu results := make([]SearchResult, 0, len(payload.Results)) for _, item := range payload.Results { - link := firstString(item.Document.StructData, "link", "url", "uri") - title := firstString(item.Document.StructData, "title", "name") - displayLink := firstString(item.Document.StructData, "site_name", "displayLink") - snippet := firstString(item.Document.DerivedStructData, "snippets", "snippet") - thumb := firstString(item.Document.DerivedStructData, "link", "thumbnail", "image", "image_url") + link := firstNonEmpty( + firstString(item.Document.DerivedStructData, "link", "url", "uri"), + firstString(item.Document.StructData, "link", "url", "uri"), + ) + title := firstNonEmpty( + firstString(item.Document.DerivedStructData, "title", "name"), + firstString(item.Document.StructData, "title", "name"), + ) + displayLink := firstNonEmpty( + firstString(item.Document.DerivedStructData, "displayLink", "site_name"), + firstString(item.Document.StructData, "displayLink", "site_name"), + ) + snippet := firstNonEmpty( + firstString(item.Document.DerivedStructData, "snippets", "snippet", "extractive_answers"), + firstString(item.Document.StructData, "snippets", "snippet", "description"), + ) + thumb := firstNonEmpty( + firstString(item.Document.DerivedStructData, "thumbnail", "image", "image_url", "link"), + firstString(item.Document.StructData, "thumbnail", "image", "image_url"), + ) if thumb == "" { - thumb = firstString(item.Document.StructData, "thumbnail", "image", "image_url") + thumb = deriveThumbnail(link) } - if thumb == "" || link == "" { + if title == "" { + title = displayLink + } + if link == "" { continue } results = append(results, SearchResult{ @@ -144,6 +163,15 @@ func (s *SearchService) searchLite(query string, imageSearch bool) ([]SearchResu return results, nil } +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" +} + func firstString(values map[string]any, keys ...string) string { for _, key := range keys { value, ok := values[key] @@ -161,13 +189,13 @@ func firstString(values map[string]any, keys ...string) string { return text } if mapped, ok := item.(map[string]any); ok { - if text := firstString(mapped, "snippet", "htmlSnippet", "url"); text != "" { + if text := firstString(mapped, "snippet", "htmlSnippet", "url", "link", "value", "content"); text != "" { return text } } } case map[string]any: - if text := firstString(typed, "snippet", "htmlSnippet", "url"); text != "" { + if text := firstString(typed, "snippet", "htmlSnippet", "url", "link", "value", "content"); text != "" { return text } } @@ -175,6 +203,30 @@ func firstString(values map[string]any, keys ...string) string { return "" } +func deriveThumbnail(link string) string { + if link == "" { + return "" + } + if videoID := extractYouTubeID(link); videoID != "" { + return "https://i.ytimg.com/vi/" + videoID + "/hqdefault.jpg" + } + return "" +} + +func extractYouTubeID(link string) string { + patterns := []*regexp.Regexp{ + regexp.MustCompile(`(?:v=|\/shorts\/|\/embed\/)([A-Za-z0-9_-]{11})`), + regexp.MustCompile(`youtu\.be\/([A-Za-z0-9_-]{11})`), + } + for _, pattern := range patterns { + matches := pattern.FindStringSubmatch(link) + if len(matches) == 2 { + return matches[1] + } + } + return "" +} + func inferSource(displayLink string) string { switch { case strings.Contains(displayLink, "youtube"):