Files
ai-media-hub/backend/services/ranker.go
T
AI Assistant e4262613c3
build-push / docker (push) Successful in 4m16s
Fix Artgrid collector matching and split ranker
2026-03-13 19:31:57 +09:00

160 lines
4.1 KiB
Go

package services
import (
"sort"
"strings"
)
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"`
}
func RankSearchResults(query string, results []SearchResult) []SearchResult {
queryTerms := strings.Fields(strings.ToLower(query))
positiveTerms := []string{
"b-roll", "b roll", "stock", "stock footage", "footage", "cinematic", "editorial",
"establishing", "4k", "hd", "drone", "ambient", "scene", "urban", "cityscape",
}
negativeTerms := []string{
"shocking", "amazing", "crazy", "must watch", "reaction", "gossip", "celebrity",
"thumbnail", "meme", "prank", "drama", "breaking", "viral", "tutorial",
"how to", "review", "walkthrough", "course", "lesson", "podcast", "interview",
"premiere pro", "after effects", "explained", "breakdown", "vlog",
}
type scoredResult struct {
item SearchResult
score int
}
scored := make([]scoredResult, 0, len(results))
for _, result := range results {
score := 0
text := strings.ToLower(result.Title + " " + result.Snippet + " " + result.Source)
for _, term := range queryTerms {
if strings.Contains(text, term) {
score += 3
}
}
for _, term := range positiveTerms {
if strings.Contains(text, term) {
score += 2
}
}
for _, term := range negativeTerms {
if strings.Contains(text, term) {
score -= 4
}
}
if result.ThumbnailURL != "" {
score += 2
}
if result.PreviewVideoURL != "" {
score += 3
}
switch result.Source {
case "Google Video":
score -= 1
case "Envato":
score += 7
case "Artgrid":
score += 7
}
scored = append(scored, scoredResult{item: result, score: score})
}
sort.SliceStable(scored, func(i, j int) bool {
return scored[i].score > scored[j].score
})
ranked := make([]SearchResult, 0, len(scored))
for _, item := range scored {
ranked = append(ranked, item.item)
}
return ranked
}
func GeminiCandidateLimit(total int) int {
switch {
case total <= 8:
return total
case total <= 16:
return 12
default:
return 16
}
}
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats) {
const chunkSize = 8
limit := GeminiCandidateLimit(len(ranked))
stats := GeminiBatchStats{
CandidateCap: limit,
Requested: min(limit, len(ranked)),
}
merged := make([]AIRecommendation, 0, len(ranked))
seen := map[string]bool{}
for start := 0; start < limit; start += chunkSize {
end := start + chunkSize
if end > limit {
end = limit
}
batch := ranked[start:end]
stats.Batches++
recommended, err := service.Recommend(query, batch)
if err != nil {
stats.Failed++
if len(stats.Errors) < 5 {
stats.Errors = append(stats.Errors, err.Error())
}
continue
}
stats.Succeeded++
for _, item := range recommended {
if item.Link == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
merged = append(merged, item)
}
}
stats.RecommendedCount = len(merged)
return merged, stats
}
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
merged := make([]AIRecommendation, 0, min(limit, len(ranked)))
seen := map[string]bool{}
for _, item := range recommended {
if item.Link == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
merged = append(merged, item)
}
for _, item := range ranked {
if len(merged) >= limit || item.Link == "" || seen[item.Link] {
continue
}
seen[item.Link] = true
merged = append(merged, AIRecommendation{
Title: item.Title,
Link: item.Link,
Snippet: item.Snippet,
ThumbnailURL: item.ThumbnailURL,
PreviewVideoURL: item.PreviewVideoURL,
Source: item.Source,
Reason: "Keyword-ranked result added without extra Gemini vision tokens.",
Recommended: true,
})
}
return merged
}