improved search-suggestions response time

This commit is contained in:
partisan 2024-09-25 14:07:12 +02:00
parent 6ed06b05b1
commit a5622b9099

View file

@ -6,8 +6,62 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"sort"
"sync"
"time"
) )
// 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,
},
{
Name: "Qwant",
FetchFunc: fetchQwantSuggestions,
Latency: 50 * time.Millisecond,
},
{
Name: "Startpage",
FetchFunc: fetchStartpageSuggestions,
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
func handleSuggestions(w http.ResponseWriter, r *http.Request) { func handleSuggestions(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q") query := r.URL.Query().Get("q")
if query == "" { if query == "" {
@ -16,25 +70,27 @@ func handleSuggestions(w http.ResponseWriter, r *http.Request) {
return return
} }
// Define the fallback sequence with Google lower in the hierarchy // Sort the suggestion sources based on their latency.
suggestionSources := []func(string) []string{ suggestionsMU.Lock()
fetchDuckDuckGoSuggestions, sort.Slice(suggestionSources, func(i, j int) bool {
fetchEdgeSuggestions, return suggestionSources[i].Latency < suggestionSources[j].Latency
fetchBraveSuggestions, })
fetchEcosiaSuggestions, suggestionsMU.Unlock()
fetchQwantSuggestions,
fetchStartpageSuggestions,
// fetchGoogleSuggestions, // I advise against it, but you can use it if you want to
}
var suggestions []string var suggestions []string
for _, fetchFunc := range suggestionSources { for i := range suggestionSources {
suggestions = fetchFunc(query) source := &suggestionSources[i]
start := time.Now()
suggestions = source.FetchFunc(query)
elapsed := time.Since(start)
updateLatency(source, elapsed)
if len(suggestions) > 0 { if len(suggestions) > 0 {
printDebug("Suggestions found using %T", fetchFunc) printDebug("Suggestions found using %s", source.Name)
break break
} else { } else {
printWarn("%T did not return any suggestions or failed.", fetchFunc) printWarn("%s did not return any suggestions or failed.", source.Name)
} }
} }
@ -42,11 +98,19 @@ func handleSuggestions(w http.ResponseWriter, r *http.Request) {
printErr("All suggestion services failed. Returning empty response.") printErr("All suggestion services failed. Returning empty response.")
} }
// Return the final suggestions as JSON // Return the final suggestions as JSON.
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `["",%s]`, toJSONStringArray(suggestions)) 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)
}
func fetchGoogleSuggestions(query string) []string { func fetchGoogleSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query) encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery) url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery)
@ -82,6 +146,7 @@ func fetchEcosiaSuggestions(query string) []string {
return fetchSuggestionsFromURL(url) return fetchSuggestionsFromURL(url)
} }
// Is this working?
func fetchQwantSuggestions(query string) []string { func fetchQwantSuggestions(query string) []string {
encodedQuery := url.QueryEscape(query) encodedQuery := url.QueryEscape(query)
url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery) url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery)
@ -96,6 +161,7 @@ func fetchStartpageSuggestions(query string) []string {
return fetchSuggestionsFromURL(url) return fetchSuggestionsFromURL(url)
} }
// fetchSuggestionsFromURL fetches suggestions from the given URL.
func fetchSuggestionsFromURL(url string) []string { func fetchSuggestionsFromURL(url string) []string {
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
@ -110,17 +176,17 @@ func fetchSuggestionsFromURL(url string) []string {
return []string{} return []string{}
} }
// Log the Content-Type for debugging // Log the Content-Type for debugging.
contentType := resp.Header.Get("Content-Type") contentType := resp.Header.Get("Content-Type")
printDebug("Response Content-Type from %s: %s", url, contentType) printDebug("Response Content-Type from %s: %s", url, contentType)
// Check if the body is non-empty // Check if the body is non-empty.
if len(body) == 0 { if len(body) == 0 {
printWarn("Received empty response body from %s", url) printWarn("Received empty response body from %s", url)
return []string{} return []string{}
} }
// Attempt to parse the response as JSON regardless of Content-Type // Attempt to parse the response as JSON regardless of Content-Type.
var parsedResponse []interface{} var parsedResponse []interface{}
if err := json.Unmarshal(body, &parsedResponse); err != nil { if err := json.Unmarshal(body, &parsedResponse); err != nil {
printErr("Error parsing JSON from %s: %v", url, err) printErr("Error parsing JSON from %s: %v", url, err)
@ -128,7 +194,7 @@ func fetchSuggestionsFromURL(url string) []string {
return []string{} return []string{}
} }
// Ensure the response structure is as expected // Ensure the response structure is as expected.
if len(parsedResponse) < 2 { if len(parsedResponse) < 2 {
printWarn("Unexpected response format from %v: %v", url, string(body)) printWarn("Unexpected response format from %v: %v", url, string(body))
return []string{} return []string{}
@ -148,6 +214,7 @@ func fetchSuggestionsFromURL(url string) []string {
return suggestions return suggestions
} }
// toJSONStringArray converts a slice of strings to a JSON array string.
func toJSONStringArray(strings []string) string { func toJSONStringArray(strings []string) string {
result := "" result := ""
for i, str := range strings { for i, str := range strings {