Skip to content

Commit

Permalink
feat(suggestions): ranking
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksasiriski committed Jun 23, 2024
1 parent 7945e9b commit 5c873a9
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 58 deletions.
2 changes: 1 addition & 1 deletion src/config/defaults_ranking.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func EmptyRanking(engs []engines.Name) CategoryRanking {
}

for _, eng := range engs {
rnk.Engines[eng.ToLower()] = CategoryEngineRanking{
rnk.Engines[eng.String()] = CategoryEngineRanking{
Mul: 1,
Const: 0,
}
Expand Down
25 changes: 16 additions & 9 deletions src/router/routes/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,21 @@ type ErrorResponse struct {
Value string `json:"value"`
}

type responseBase struct {
Version string `json:"version"`
Duration int64 `json:"duration"`
}

type ResultsResponse struct {
Version string `json:"version"`
Duration int64 `json:"duration"`
Results []result.ResultOutput `json:"results"`
responseBase

Results []result.ResultOutput `json:"results"`
}

type SuggestionsResponse struct {
responseBase

Suggestions []result.Suggestion `json:"suggestions"`
}

func writeResponse(w http.ResponseWriter, status int, body string) error {
Expand All @@ -42,7 +53,7 @@ func writeResponseJSON(w http.ResponseWriter, status int, body any) error {
return err
}

func writeResponseSuggestions(w http.ResponseWriter, status int, query string, suggestions []string, api bool) error {
func writeResponseSuggestions(w http.ResponseWriter, status int, query string, suggestions []string) error {
jsonStruct := make([]any, 2)
jsonStruct[0] = query
jsonStruct[1] = suggestions
Expand All @@ -57,11 +68,7 @@ func writeResponseSuggestions(w http.ResponseWriter, status int, query string, s
return err
}

if api {
w.Header().Set("Content-Type", "application/x-suggestions+json")
} else {
w.Header().Set("Content-Type", "application/json")
}
w.Header().Set("Content-Type", "application/x-suggestions+json")
w.WriteHeader(status)
_, err = w.Write(res)
return err
Expand Down
12 changes: 8 additions & 4 deletions src/router/routes/route_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package routes
import (
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -168,7 +169,8 @@ func routeSearch(w http.ResponseWriter, r *http.Request, ver string, catsConf ma
}

// Rank the results.
rankedRes := rank.Rank(scrapedRes, catsConf[categoryName].Ranking)
var rankedRes rank.Results = slices.Clone(scrapedRes)
rankedRes.Rank(catsConf[categoryName].Ranking)

// Store the results in cache.
if err := db.SetResults(query, categoryName, opts, rankedRes, ttlConf.Time); err != nil {
Expand All @@ -184,9 +186,11 @@ func routeSearch(w http.ResponseWriter, r *http.Request, ver string, catsConf ma

// Create the response.
res := ResultsResponse{
Version: ver,
Duration: time.Since(startTime).Milliseconds(),
Results: outpusRes,
responseBase{
ver,
time.Since(startTime).Milliseconds(),
},
outpusRes,
}

// If writing response failes, return the error.
Expand Down
37 changes: 28 additions & 9 deletions src/router/routes/route_suggest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ package routes
import (
"fmt"
"net/http"
"slices"
"strings"
"time"

"github.com/hearchco/agent/src/config"
"github.com/hearchco/agent/src/search"
"github.com/hearchco/agent/src/search/engines/options"
"github.com/hearchco/agent/src/search/result"
"github.com/hearchco/agent/src/search/result/rank"
)

func routeSuggest(w http.ResponseWriter, r *http.Request, catConf config.Category) error {
func routeSuggest(w http.ResponseWriter, r *http.Request, ver string, catConf config.Category) error {
// Capture start time.
startTime := time.Now()

// Parse form data (including query params).
if err := r.ParseForm(); err != nil {
// Server error.
Expand Down Expand Up @@ -59,15 +65,28 @@ func routeSuggest(w http.ResponseWriter, r *http.Request, catConf config.Categor
return err
}

// TODO: Rank the suggestions.
// rankedSugs := rank.Rank(scrapedSugs, Ranking)

// Convert the suggestions to output format.
outputSugs := result.ConvertSuggestionsToOutput(scrapedSugs)
// Rank the suggestions.
var rankedSugs rank.Suggestions = slices.Clone(scrapedSugs)
rankedSugs.Rank(catConf.Ranking)

// Check if the response should be in API format or normal JSON format.
api := strings.Contains(r.Header.Get("Accept"), "application/x-suggestions+json")
if strings.Contains(r.Header.Get("Accept"), "application/x-suggestions+json") {
// Convert the suggestions to slice of strings.
stringSugs := result.ConvertSuggestionsToOutput(rankedSugs)

// If writing response failes, return the error.
return writeResponseSuggestions(w, http.StatusOK, query, outputSugs, api)
// If writing response failes, return the error.
return writeResponseSuggestions(w, http.StatusOK, query, stringSugs)
} else {
// Create the response.
res := SuggestionsResponse{
responseBase{
ver,
time.Since(startTime).Milliseconds(),
},
rankedSugs,
}

// If writing response failes, return the error.
return writeResponseJSON(w, http.StatusOK, res)
}
}
4 changes: 2 additions & 2 deletions src/router/routes/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func Setup(mux *chi.Mux, ver string, db cache.DB, conf config.Config) {

// /suggestions
mux.Get("/suggestions", func(w http.ResponseWriter, r *http.Request) {
err := routeSuggest(w, r, conf.Categories[category.SUGGESTIONS])
err := routeSuggest(w, r, ver, conf.Categories[category.SUGGESTIONS])
if err != nil {
log.Error().
Err(err).
Expand All @@ -70,7 +70,7 @@ func Setup(mux *chi.Mux, ver string, db cache.DB, conf config.Config) {
}
})
mux.Post("/suggestions", func(w http.ResponseWriter, r *http.Request) {
err := routeSuggest(w, r, conf.Categories[category.SUGGESTIONS])
err := routeSuggest(w, r, ver, conf.Categories[category.SUGGESTIONS])
if err != nil {
log.Error().
Err(err).
Expand Down
2 changes: 1 addition & 1 deletion src/search/engines/duckduckgo/suggest.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (se Engine) Suggest(query string, locale options.Locale, sugChan chan resul
Strs("suggestions", suggs).
Msg("Sending suggestions to channel")
for i, sug := range suggs {
sugChan <- result.NewSuggestionScraped(sug, se.Name, i)
sugChan <- result.NewSuggestionScraped(sug, se.Name, i+1)
}
if !foundResults.Load() {
foundResults.Store(true)
Expand Down
2 changes: 1 addition & 1 deletion src/search/engines/google/suggest.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (se Engine) Suggest(query string, locale options.Locale, sugChan chan resul
Strs("suggestions", suggs).
Msg("Sending suggestions to channel")
for i, sug := range suggs {
sugChan <- result.NewSuggestionScraped(sug, se.Name, i)
sugChan <- result.NewSuggestionScraped(sug, se.Name, i+1)
}
if !foundResults.Load() {
foundResults.Store(true)
Expand Down
20 changes: 20 additions & 0 deletions src/search/result/rank/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package rank

import (
"github.com/hearchco/agent/src/search/engines"
)

type scoreRanker interface {
Score() float64
}

type scoreEngineRanker[T ranker] interface {
scoreRanker

EngineRanks() []T
}

type ranker interface {
SearchEngine() engines.Name
Rank() int
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,16 @@ import (

type Results []result.Result

// Cast []result.Result to Results, call rank() and return []result.Result slice.
func Rank(results []result.Result, rconf config.CategoryRanking) []result.Result {
resType := make(Results, 0, len(results))
for _, res := range results {
resType = append(resType, res)
}

// Rank the results.
resType.rank(rconf)

rankedRes := make([]result.Result, 0, len(resType))
for _, res := range resType {
rankedRes = append(rankedRes, res)
}
return rankedRes
}

// Calculates the Score, sorts by it and then populates the Rank field of every result.
func (r Results) rank(rconf config.CategoryRanking) {
func (r Results) Rank(rconf config.CategoryRanking) {
// Fill Rank field for every EngineRank.
r.fillEngineRankRank()

// Calculate and set scores.
r.calculateScores(rconf)

// Sort slice by score.
sort.Sort(ByScore(r))
sort.Sort(ByScore[result.Result](r))

// Set correct ranks, by iterating over the sorted slice.
r.correctRanks()
Expand Down
19 changes: 13 additions & 6 deletions src/search/result/rank/score.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"math"

"github.com/hearchco/agent/src/config"
"github.com/hearchco/agent/src/search/result"
)

// Calculates and sets scores for all results.
Expand All @@ -14,16 +13,24 @@ func (r Results) calculateScores(rconf config.CategoryRanking) {
}
}

// Calculates and sets scores for all results.
func (s Suggestions) calculateScores(rconf config.CategoryRanking) {
for i := range s {
sug := &s[i]
sug.SetScore(calculateScore(sug, rconf))
}
}

// Calculates the score for one result.
func calculateScore(res result.Result, rconf config.CategoryRanking) float64 {
func calculateScore[T ranker](val scoreEngineRanker[T], rconf config.CategoryRanking) float64 {
var retRankScore float64 = 0
for _, er := range res.EngineRanks() {
eng := rconf.Engines[er.SearchEngine().ToLower()]
for _, er := range val.EngineRanks() {
eng := rconf.Engines[er.SearchEngine().String()]
retRankScore += (100.0/math.Pow(float64(er.Rank())*rconf.A+rconf.B, rconf.REXP)*rconf.C+rconf.D)*eng.Mul + eng.Const
}

retRankScore /= float64(len(res.EngineRanks()))
timesReturnedScore := math.Log(float64(len(res.EngineRanks()))*rconf.TRA+rconf.TRB)*10*rconf.TRC + rconf.TRD
retRankScore /= float64(len(val.EngineRanks()))
timesReturnedScore := math.Log(float64(len(val.EngineRanks()))*rconf.TRA+rconf.TRB)*10*rconf.TRC + rconf.TRD
score := retRankScore + timesReturnedScore

return score
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"github.com/hearchco/agent/src/search/result"
)

type ByScore []result.Result
type ByScore[T scoreRanker] []T

func (r ByScore) Len() int { return len(r) }
func (r ByScore) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByScore) Less(i, j int) bool { return r[i].Score() > r[j].Score() }
func (r ByScore[T]) Len() int { return len(r) }
func (r ByScore[T]) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByScore[T]) Less(i, j int) bool { return r[i].Score() > r[j].Score() }

type ByPageAndOnPageRank []*result.Rank

Expand Down
29 changes: 29 additions & 0 deletions src/search/result/rank/suggestions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package rank

import (
"sort"

"github.com/hearchco/agent/src/config"
"github.com/hearchco/agent/src/search/result"
)

type Suggestions []result.Suggestion

// Calculates the Score, sorts by it and then populates the Rank field of every result.
func (s Suggestions) Rank(rconf config.CategoryRanking) {
// Calculate and set scores.
s.calculateScores(rconf)

// Sort slice by score.
sort.Sort(ByScore[result.Suggestion](s))

// Set correct ranks, by iterating over the sorted slice.
s.correctRanks()
}

func (s Suggestions) correctRanks() {
for i := range s {
sug := &s[i]
sug.SetRank(i + 1)
}
}
18 changes: 18 additions & 0 deletions src/search/result/suggestion.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,31 @@ type Suggestion struct {

type suggestionJSON struct {
Value string `json:"value"`
Rank int `json:"rank"`
Score float64 `json:"score"`
EngineRanks []RankSimple `json:"engine_ranks"`
}

func (s Suggestion) Value() string {
return s.suggestionJSON.Value
}

func (s Suggestion) Rank() int {
return s.suggestionJSON.Rank
}

func (s *Suggestion) SetRank(rank int) {
s.suggestionJSON.Rank = rank
}

func (s Suggestion) Score() float64 {
return s.suggestionJSON.Score
}

func (s *Suggestion) SetScore(score float64) {
s.suggestionJSON.Score = score
}

func (s Suggestion) EngineRanks() []RankSimple {
if s.suggestionJSON.EngineRanks == nil {
log.Panic().Msg("EngineRanks is nil")
Expand Down
4 changes: 2 additions & 2 deletions src/search/result/suggestion_scraped.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ func (s SuggestionScraped) Convert(erCap int) Suggestion {
engineRanks = append(engineRanks, s.Rank().Convert())
return Suggestion{
suggestionJSON{
s.Value(),
engineRanks,
Value: s.Value(),
EngineRanks: engineRanks,
},
}
}

0 comments on commit 5c873a9

Please sign in to comment.