init: initial commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"AI-Expert-Sidebar/internal/database"
|
||||
"AI-Expert-Sidebar/internal/models"
|
||||
)
|
||||
|
||||
const maxResults = 5
|
||||
|
||||
var ErrDBUnavailable = errors.New("database unavailable")
|
||||
|
||||
// SearchResult is the DTO returned to the frontend.
|
||||
type SearchResult struct {
|
||||
ID uint `json:"id"`
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
Category string `json:"category"`
|
||||
Score int `json:"score"` // 2=keyword, 1=question, 0=fallback
|
||||
IsFallback bool `json:"is_fallback"`
|
||||
}
|
||||
|
||||
// SearchKnowledge performs fuzzy search in the active knowledge library.
|
||||
func SearchKnowledge(query string) ([]SearchResult, error) {
|
||||
db := database.Get()
|
||||
if db == nil {
|
||||
return nil, ErrDBUnavailable
|
||||
}
|
||||
if len(query) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type res struct {
|
||||
rows []SearchResult
|
||||
err error
|
||||
}
|
||||
ch := make(chan res, 1)
|
||||
|
||||
go func() {
|
||||
var total int64
|
||||
db.Model(&models.Entry{}).Count(&total)
|
||||
if total == 0 {
|
||||
ch <- res{[]SearchResult{}, nil}
|
||||
return
|
||||
}
|
||||
|
||||
like := "%" + query + "%"
|
||||
var rows []models.Entry
|
||||
err := db.Where("keyword LIKE ? OR question LIKE ?", like, like).
|
||||
Order("updated_at DESC").Limit(maxResults).Find(&rows).Error
|
||||
if err != nil {
|
||||
ch <- res{nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
isFallback := len(rows) == 0
|
||||
if isFallback {
|
||||
log.Printf("[Search] No match for %q, returning fallback", query)
|
||||
db.Order("updated_at DESC").Limit(3).Find(&rows)
|
||||
}
|
||||
|
||||
out := make([]SearchResult, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
score := 0
|
||||
if !isFallback {
|
||||
if containsIgnoreCase(r.Keyword, query) {
|
||||
score = 2
|
||||
} else if containsIgnoreCase(r.Question, query) {
|
||||
score = 1
|
||||
}
|
||||
}
|
||||
out = append(out, SearchResult{
|
||||
ID: r.ID, Question: r.Question, Answer: r.Answer,
|
||||
Category: r.Category, Score: score, IsFallback: isFallback,
|
||||
})
|
||||
}
|
||||
ch <- res{out, nil}
|
||||
}()
|
||||
|
||||
r := <-ch
|
||||
return r.rows, r.err
|
||||
}
|
||||
|
||||
func containsIgnoreCase(s, sub string) bool {
|
||||
if len(sub) == 0 {
|
||||
return true
|
||||
}
|
||||
sl, subl := []rune(s), []rune(sub)
|
||||
if len(sl) < len(subl) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= len(sl)-len(subl); i++ {
|
||||
match := true
|
||||
for j, c := range subl {
|
||||
if toLower(sl[i+j]) != toLower(c) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func toLower(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r + 32
|
||||
}
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user