init: initial commit

This commit is contained in:
Blizzard
2026-04-01 14:09:33 +08:00
commit aef2e152dc
66 changed files with 6540 additions and 0 deletions
+114
View File
@@ -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
}