Initial Commit
This commit is contained in:
215
cmd/search/search.go
Normal file
215
cmd/search/search.go
Normal file
@@ -0,0 +1,215 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user