package search import ( "errors" "fmt" "net/http" "sap-cds-search/cmd/model" "sap-cds-search/cmd/ui" "slices" "strconv" "strings" "sync" "time" "github.com/lithammer/fuzzysearch/fuzzy" ) type fuzzyResult struct { CDSViewTechnicalName string hits int averageDistance float32 score float32 } type resultBuffer struct { createdAt time.Time hits int mu sync.Mutex results []fuzzyResult } const pageStep = 12 var ( ErrNoBuffer = errors.New("no buffer") ErrNoResults = errors.New("no results") ) var searchTargets map[string][]string = make(map[string][]string) var resultsBuffer map[string]*resultBuffer = make(map[string]*resultBuffer) func Load() error { keywords, err := model.GetKeywordsModel() if err != nil { return err } for _, keyword := range *keywords { table := searchTargets[keyword.CDSViewTechnicalName] table = append(table, keyword.Keyword) searchTargets[keyword.CDSViewTechnicalName] = table } ticker := time.NewTicker(5 * time.Minute) go func() { for { select { case <-ticker.C: ClearOldBuffers() } } }() return nil } func GetSearchTerm(r *http.Request) string { return strings.ToLower(r.URL.Query().Get("q")) } func GetPage(r *http.Request) int { page, _ := strconv.Atoi(r.URL.Query().Get("p")) if page < 0 { page = 0 } return page } func GetBuffer(searchTerm string) (*resultBuffer, error) { buffer, exists := resultsBuffer[searchTerm] if !exists { return nil, ErrNoBuffer } return buffer, nil } func BuildBuffer(searchTerm string) error { if searchTerm == "" { return ErrNoResults } splitSearchTerms := strings.Split(searchTerm, " ") splitSearchTerms = append(splitSearchTerms, searchTerm) splitSearchTerms = append(splitSearchTerms, strings.ReplaceAll(searchTerm, " ", searchTerm)) var fuzzyResults []fuzzyResult for viewName, targets := range searchTargets { totalHits := 0 var totalDistance float32 for i, splitSearchTerm := range splitSearchTerms { ranks := fuzzy.RankFindFold(splitSearchTerm, targets) hits := len(ranks) if hits == 0 { if i < len(splitSearchTerm)-3 { totalDistance += 500 } continue } totalHits += hits var averageDistance float32 = 0 for _, rank := range ranks { averageDistance += float32(rank.Distance) } totalDistance += averageDistance / float32(hits) } if totalHits > 0 { fuzzyResults = append(fuzzyResults, fuzzyResult{ CDSViewTechnicalName: viewName, hits: totalHits, averageDistance: totalDistance, score: totalDistance / float32(totalHits), }) } } hits := len(fuzzyResults) if hits == 0 { return ErrNoResults } slices.SortFunc(fuzzyResults, func(a, b fuzzyResult) int { if a.score > b.score { return 1 } else if a.score < b.score { return -1 } return 0 }) resultsBuffer[searchTerm] = &resultBuffer{ createdAt: time.Now(), hits: hits, results: fuzzyResults, } return nil } func HandleSearch(w http.ResponseWriter, r *http.Request) { searchTerm := GetSearchTerm(r) buffer, err := GetBuffer(searchTerm) if err != nil { err := BuildBuffer(searchTerm) if err != nil { err := ui.Template.ExecuteTemplate(w, "search-placeholder-no-result", nil) if err != nil { fmt.Println(err) } return } buffer, _ = GetBuffer(searchTerm) } buffer.mu.Lock() defer buffer.mu.Unlock() page := GetPage(r) maxPage := (buffer.hits / pageStep) - 1 if page+1 > maxPage { page = maxPage } resultsModel := model.NewResultsModel() for i := page * pageStep; i < page*pageStep+pageStep; i++ { result := buffer.results[i] cdsView, err := model.GetCDSViewModel(result.CDSViewTechnicalName) if err != nil { fmt.Println(err) continue } resultsModel.AppendResultsModelViews(cdsView) } resultsModel.SearchTerm = searchTerm resultsModel.CurrentPage = page resultsModel.MaxPage = maxPage err = ui.Template.ExecuteTemplate(w, "results", resultsModel) if err != nil { fmt.Println(err) return } } func ClearOldBuffers() { for key, buffer := range resultsBuffer { if time.Since(buffer.createdAt) >= time.Minute*10 { buffer.mu.Lock() delete(resultsBuffer, key) buffer.mu.Unlock() } } }