Switch search backend to SearXNG
build-push / docker (push) Has been cancelled

This commit is contained in:
AI Assistant
2026-03-13 10:10:13 +09:00
parent 6734887fc6
commit ee316de7ab
8 changed files with 466 additions and 199 deletions
+80 -2
View File
@@ -26,6 +26,10 @@ type AIRecommendation struct {
Recommended bool `json:"recommended"`
}
type QueryExpansion struct {
Querywords []string `json:"querywords"`
}
func NewGeminiService(apiKey string) *GeminiService {
return &GeminiService{
APIKey: apiKey,
@@ -33,6 +37,80 @@ func NewGeminiService(apiKey string) *GeminiService {
}
}
func (g *GeminiService) ExpandQuery(query string) ([]string, error) {
if g.APIKey == "" {
return []string{query}, nil
}
body := map[string]any{
"contents": []map[string]any{
{
"parts": []map[string]string{
{
"text": `Return JSON only in this shape: {"querywords":["..."]}.
Generate at most 4 concise search variations for media discovery across Google Video, Envato, and Artgrid.
Keep the original language when possible. User query: ` + query,
},
},
},
},
"generationConfig": map[string]any{
"responseMimeType": "application/json",
"temperature": 0.2,
"maxOutputTokens": 120,
},
}
rawBody, _ := json.Marshal(body)
endpoint := "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + g.APIKey
resp, err := g.Client.Post(endpoint, "application/json", bytes.NewReader(rawBody))
if err != nil {
return []string{query}, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return []string{query}, fmt.Errorf("gemini returned status %d for query expansion", resp.StatusCode)
}
var payload struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return []string{query}, err
}
if len(payload.Candidates) == 0 || len(payload.Candidates[0].Content.Parts) == 0 {
return []string{query}, nil
}
var parsed QueryExpansion
if err := json.Unmarshal([]byte(payload.Candidates[0].Content.Parts[0].Text), &parsed); err != nil {
return []string{query}, err
}
queries := []string{query}
seen := map[string]bool{strings.ToLower(strings.TrimSpace(query)): true}
for _, item := range parsed.Querywords {
trimmed := strings.TrimSpace(item)
if trimmed == "" {
continue
}
key := strings.ToLower(trimmed)
if seen[key] {
continue
}
seen[key] = true
queries = append(queries, trimmed)
}
return queries, nil
}
func (g *GeminiService) Recommend(query string, candidates []SearchResult) ([]AIRecommendation, error) {
if g.APIKey == "" {
return nil, fmt.Errorf("gemini api key is not configured")
@@ -46,11 +124,11 @@ func (g *GeminiService) Recommend(query string, candidates []SearchResult) ([]AI
{
"text": `Analyze the provided images for the user's search intent. Return JSON only in this shape:
{"recommendations":[{"index":0,"reason":"short reason","recommended":true}]}
Mark only the best matches as recommended=true. Keep reasons concise. User query: ` + query,
Mark only the best matches as recommended=true. Keep reasons concise. Recommend up to 8 items. User query: ` + query,
},
}
maxImages := min(len(candidates), 8)
maxImages := min(len(candidates), 10)
for idx := 0; idx < maxImages; idx++ {
img, mimeType, err := fetchImageAsInlineData(g.Client, candidates[idx].ThumbnailURL)
if err != nil {