216 lines
4.2 KiB
Go
216 lines
4.2 KiB
Go
|
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()
|
||
|
}
|
||
|
}
|
||
|
}
|