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 }