Search/suggestions.go

283 lines
8.1 KiB
Go
Raw Normal View History

2024-08-21 20:36:45 +00:00
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"sync"
"time"
2024-08-21 20:36:45 +00:00
)
// SuggestionSource represents a search suggestion source along with its latency.
type SuggestionSource struct {
Name string
FetchFunc func(string) []string
Latency time.Duration
mu sync.Mutex
}
// Initialize suggestion sources with default latency values.
var suggestionSources = []SuggestionSource{
{
Name: "DuckDuckGo",
FetchFunc: fetchDuckDuckGoSuggestions,
Latency: 50 * time.Millisecond,
},
{
Name: "Edge",
FetchFunc: fetchEdgeSuggestions,
Latency: 50 * time.Millisecond,
},
{
Name: "Brave",
FetchFunc: fetchBraveSuggestions,
Latency: 50 * time.Millisecond,
},
{
Name: "Ecosia",
FetchFunc: fetchEcosiaSuggestions,
Latency: 50 * time.Millisecond,
},
2024-09-25 16:21:06 +00:00
// { // Not working with fetchSuggestionsFromURL func
// Name: "Qwant",
// FetchFunc: fetchQwantSuggestions,
// Latency: 50 * time.Millisecond,
// },
{
2024-09-25 16:21:06 +00:00
Name: "Startpage",
FetchFunc: fetchStartpageSuggestions,
Latency: 50 * time.Millisecond,
},
{
2024-09-25 16:21:06 +00:00
Name: "Yahoo",
FetchFunc: fetchYahooSuggestions,
Latency: 50 * time.Millisecond,
},
// I advise against it, but you can use it if you want to
// {
// Name: "Google",
// FetchFunc: fetchGoogleSuggestions,
// Latency: 500 * time.Millisecond,
// },
}
// Mutex to protect the suggestionSources during sorting.
var suggestionsMU sync.Mutex
2024-08-21 20:36:45 +00:00
func handleSuggestions(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `["",[]]`)
return
}
// Sort the suggestion sources based on their latency.
suggestionsMU.Lock()
sort.Slice(suggestionSources, func(i, j int) bool {
return suggestionSources[i].Latency < suggestionSources[j].Latency
})
suggestionsMU.Unlock()
2024-08-21 20:36:45 +00:00
var suggestions []string
for i := range suggestionSources {
source := &suggestionSources[i]
start := time.Now()
suggestions = source.FetchFunc(query)
elapsed := time.Since(start)
updateLatency(source, elapsed)
2024-08-21 20:36:45 +00:00
if len(suggestions) > 0 {
printDebug("Suggestions found using %s", source.Name)
2024-08-21 20:36:45 +00:00
break
} else {
printWarn("%s did not return any suggestions or failed.", source.Name)
2024-08-21 20:36:45 +00:00
}
}
if len(suggestions) == 0 {
2024-08-21 21:23:08 +00:00
printErr("All suggestion services failed. Returning empty response.")
2024-08-21 20:36:45 +00:00
}
// Return the final suggestions as JSON.
2024-08-21 20:36:45 +00:00
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `["",%s]`, toJSONStringArray(suggestions))
}
// updateLatency updates the latency of a suggestion source using an exponential moving average.
func updateLatency(source *SuggestionSource, newLatency time.Duration) {
source.mu.Lock()
defer source.mu.Unlock()
const alpha = 0.5 // Smoothing factor.
source.Latency = time.Duration(float64(source.Latency)*(1-alpha) + float64(newLatency)*alpha)
}
2024-08-21 20:36:45 +00:00
func fetchGoogleSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from Google: %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
func fetchDuckDuckGoSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://duckduckgo.com/ac/?q=%s&type=list", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from DuckDuckGo: %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
func fetchEdgeSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://api.bing.com/osjson.aspx?query=%s", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from Edge (Bing): %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
func fetchBraveSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://search.brave.com/api/suggest?q=%s", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from Brave: %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
func fetchEcosiaSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://ac.ecosia.org/?q=%s&type=list", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from Ecosia: %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
// Is this working?
2024-08-21 20:36:45 +00:00
func fetchQwantSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from Qwant: %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
func fetchStartpageSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://startpage.com/suggestions?q=%s", encodedQuery)
2024-08-21 21:23:08 +00:00
printDebug("Fetching suggestions from Startpage: %s", url)
2024-08-21 20:36:45 +00:00
return fetchSuggestionsFromURL(url)
}
2024-09-25 16:21:06 +00:00
func fetchYahooSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://search.yahoo.com/sugg/gossip/gossip-us-ura/?output=fxjson&command=%s", encodedQuery)
printDebug("Fetching suggestions from Yahoo: %s", url)
return fetchSuggestionsFromURL(url)
}
// func fetchBaiduSuggestions(query string) []string {
// encodedQuery := url.QueryEscape(query)
// url := fmt.Sprintf("https://suggestion.baidu.com/su?wd=%s", encodedQuery)
// printDebug("Fetching suggestions from Baidu: %s", url)
// return fetchSuggestionsFromURL(url)
// }
// func fetchSogouSuggestions(query string) []string {
// encodedQuery := url.QueryEscape(query)
// url := fmt.Sprintf("https://w.sugg.sogou.com/sugg/ajaj_json.jsp?key=%s", encodedQuery)
// printDebug("Fetching suggestions from Sogou: %s", url)
// return fetchSuggestionsFromURL(url)
// }
2024-08-21 20:36:45 +00:00
func fetchSuggestionsFromURL(url string) []string {
resp, err := http.Get(url)
if err != nil {
2024-08-21 21:23:08 +00:00
printWarn("Error fetching suggestions from %s: %v", url, err)
2024-08-21 20:36:45 +00:00
return []string{}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
2024-08-21 21:23:08 +00:00
printWarn("Error reading response body from %s: %v", url, err)
2024-08-21 20:36:45 +00:00
return []string{}
}
2024-09-25 16:21:06 +00:00
// Print the raw HTTP response for debugging
fmt.Printf("Raw response from %s:\n%s\n", url, string(body))
// Log the Content-Type for debugging.
2024-08-21 20:36:45 +00:00
contentType := resp.Header.Get("Content-Type")
2024-08-21 21:23:08 +00:00
printDebug("Response Content-Type from %s: %s", url, contentType)
2024-08-21 20:36:45 +00:00
// Check if the body is non-empty.
2024-08-21 20:36:45 +00:00
if len(body) == 0 {
2024-08-21 21:23:08 +00:00
printWarn("Received empty response body from %s", url)
2024-08-21 20:36:45 +00:00
return []string{}
}
// Attempt to parse the response as JSON regardless of Content-Type.
2024-08-21 20:36:45 +00:00
var parsedResponse []interface{}
if err := json.Unmarshal(body, &parsedResponse); err != nil {
2024-08-21 21:23:08 +00:00
printErr("Error parsing JSON from %s: %v", url, err)
printDebug("Response body: %s", string(body))
2024-08-21 20:36:45 +00:00
return []string{}
}
// Ensure the response structure is as expected.
2024-08-21 20:36:45 +00:00
if len(parsedResponse) < 2 {
2024-08-21 21:23:08 +00:00
printWarn("Unexpected response format from %v: %v", url, string(body))
2024-08-21 20:36:45 +00:00
return []string{}
}
suggestions := []string{}
if items, ok := parsedResponse[1].([]interface{}); ok {
for _, item := range items {
if suggestion, ok := item.(string); ok {
suggestions = append(suggestions, suggestion)
}
}
} else {
2024-08-21 21:23:08 +00:00
printErr("Unexpected suggestions format in response from: %v", url)
2024-08-21 20:36:45 +00:00
}
return suggestions
}
// toJSONStringArray converts a slice of strings to a JSON array string.
2024-08-21 20:36:45 +00:00
func toJSONStringArray(strings []string) string {
result := ""
for i, str := range strings {
result += fmt.Sprintf(`"%s"`, str)
if i < len(strings)-1 {
result += ","
}
}
return "[" + result + "]"
}
2024-09-25 16:21:06 +00:00
// func testSuggestionSources(query string) {
// for _, source := range suggestionSources {
// fmt.Printf("Testing %s...\n", source.Name)
// // Fetch suggestions
// suggestions := source.FetchFunc(query)
// // If we get results, print them
// if len(suggestions) > 0 {
// fmt.Printf("Suggestions from %s:\n", source.Name)
// for i, suggestion := range suggestions {
// fmt.Printf("%d: %s\n", i+1, suggestion)
// }
// } else {
// fmt.Printf("No suggestions from %s.\n", source.Name)
// }
// // Small separator for clarity
// fmt.Println("--------------------------")
// }
// }
// func main() {
// query := "test query"
// testSuggestionSources(query)
// }