improved search-suggestions response time
This commit is contained in:
parent
d107d41d72
commit
6fa6a33d2d
1 changed files with 86 additions and 19 deletions
105
suggestions.go
105
suggestions.go
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue