2025-03-24 08:22:43 +00:00
|
|
|
/*
|
|
|
|
Copyright (C) 2025 snoutie
|
|
|
|
Authors: snoutie (copyright@achtarmig.org)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2025-03-24 07:50:01 +00:00
|
|
|
package search
|
|
|
|
|
|
|
|
import (
|
2025-03-24 09:01:47 +00:00
|
|
|
"api-cds-search/cmd/model"
|
|
|
|
"api-cds-search/cmd/ui"
|
2025-03-24 07:50:01 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"slices"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
|
|
|
)
|
|
|
|
|
|
|
|
type fuzzyResult struct {
|
|
|
|
CDSViewTechnicalName string
|
|
|
|
hits int
|
2025-04-11 14:48:50 +00:00
|
|
|
perfectHits int
|
2025-03-24 07:50:01 +00:00
|
|
|
averageDistance float32
|
2025-04-11 14:48:50 +00:00
|
|
|
score float64
|
2025-03-24 07:50:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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, " ")
|
|
|
|
|
2025-04-11 12:55:52 +00:00
|
|
|
if len(splitSearchTerms) > 1 {
|
|
|
|
splitSearchTerms = append(splitSearchTerms, searchTerm)
|
|
|
|
splitSearchTerms = append(splitSearchTerms, strings.ReplaceAll(searchTerm, " ", ""))
|
|
|
|
}
|
2025-03-24 07:50:01 +00:00
|
|
|
|
|
|
|
var fuzzyResults []fuzzyResult
|
|
|
|
|
|
|
|
for viewName, targets := range searchTargets {
|
2025-04-11 14:48:50 +00:00
|
|
|
var (
|
|
|
|
totalHits, perfectHits int
|
|
|
|
totalDistance float32
|
|
|
|
)
|
2025-04-11 12:55:52 +00:00
|
|
|
|
2025-04-11 14:48:50 +00:00
|
|
|
totalDistance = 1
|
2025-03-24 07:50:01 +00:00
|
|
|
for i, splitSearchTerm := range splitSearchTerms {
|
2025-04-11 12:55:52 +00:00
|
|
|
if splitSearchTerm == "" {
|
|
|
|
continue
|
|
|
|
}
|
2025-04-11 14:48:50 +00:00
|
|
|
|
2025-03-24 07:50:01 +00:00
|
|
|
ranks := fuzzy.RankFindFold(splitSearchTerm, targets)
|
|
|
|
|
|
|
|
hits := len(ranks)
|
|
|
|
totalHits += hits
|
|
|
|
|
2025-04-11 14:48:50 +00:00
|
|
|
if hits == 0 {
|
|
|
|
totalDistance += 100
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-03-24 07:50:01 +00:00
|
|
|
var averageDistance float32 = 0
|
|
|
|
for _, rank := range ranks {
|
|
|
|
averageDistance += float32(rank.Distance)
|
2025-04-11 12:55:52 +00:00
|
|
|
if i >= len(splitSearchTerms)-2 && rank.Distance == 0 {
|
|
|
|
perfectHits += 1
|
|
|
|
}
|
2025-03-24 07:50:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
totalDistance += averageDistance / float32(hits)
|
|
|
|
}
|
|
|
|
|
|
|
|
if totalHits > 0 {
|
|
|
|
fuzzyResults = append(fuzzyResults, fuzzyResult{
|
|
|
|
CDSViewTechnicalName: viewName,
|
|
|
|
hits: totalHits,
|
|
|
|
averageDistance: totalDistance,
|
2025-04-11 14:48:50 +00:00
|
|
|
perfectHits: perfectHits,
|
|
|
|
score: (float64(totalDistance) / float64(totalHits)) * (float64(len(targets)) / float64(totalHits)),
|
2025-03-24 07:50:01 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hits := len(fuzzyResults)
|
|
|
|
if hits == 0 {
|
|
|
|
return ErrNoResults
|
|
|
|
}
|
|
|
|
|
|
|
|
slices.SortFunc(fuzzyResults, func(a, b fuzzyResult) int {
|
2025-04-11 14:48:50 +00:00
|
|
|
if a.perfectHits < b.perfectHits {
|
|
|
|
return 1
|
|
|
|
} else if a.perfectHits > b.perfectHits {
|
|
|
|
return -1
|
|
|
|
}
|
2025-03-24 07:50:01 +00:00
|
|
|
if a.score > b.score {
|
|
|
|
return 1
|
|
|
|
} else if a.score < b.score {
|
|
|
|
return -1
|
|
|
|
}
|
2025-04-11 12:55:52 +00:00
|
|
|
if a.CDSViewTechnicalName > b.CDSViewTechnicalName {
|
|
|
|
return 1
|
|
|
|
} else if a.CDSViewTechnicalName < b.CDSViewTechnicalName {
|
|
|
|
return -1
|
|
|
|
}
|
2025-03-24 07:50:01 +00:00
|
|
|
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)
|
2025-04-11 12:55:52 +00:00
|
|
|
var maxPage int
|
|
|
|
if buffer.hits <= pageStep {
|
|
|
|
maxPage = 0
|
|
|
|
} else {
|
|
|
|
maxPage = (buffer.hits / pageStep)
|
|
|
|
}
|
2025-03-24 07:50:01 +00:00
|
|
|
|
|
|
|
if page+1 > maxPage {
|
|
|
|
page = maxPage
|
|
|
|
}
|
|
|
|
|
|
|
|
resultsModel := model.NewResultsModel()
|
|
|
|
for i := page * pageStep; i < page*pageStep+pageStep; i++ {
|
2025-04-11 12:55:52 +00:00
|
|
|
if i > len(buffer.results)-1 {
|
|
|
|
break
|
|
|
|
}
|
2025-03-24 07:50:01 +00:00
|
|
|
result := buffer.results[i]
|
|
|
|
cdsView, err := model.GetCDSViewModel(result.CDSViewTechnicalName)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-04-11 12:55:52 +00:00
|
|
|
resultsModel.AppendResultsModelViews(cdsView, result.score)
|
2025-03-24 07:50:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|