package catalog import ( "fmt" "rubric/modules/devopsmodules" "rubric/modules/gomodules" "rubric/modules/infosecmodules" "rubric/modules/linuxmodules" "rubric/utils" "sort" "strings" ) // SearchResult — одно совпадение поиска type SearchResult struct { BookKey string BookTitle string ModKey string ModTitle string ModDesc string RunFunc func() Score int // количество совпадений } // searchEntry — унифицированная запись для поиска type searchEntry struct { bookKey string bookTitle string modKey string modTitle string modDesc string keywords []string run func() } // Все записи для поиска — строим один раз var allEntries = buildSearchIndex() func buildSearchIndex() []searchEntry { entries := []searchEntry{} // Go for _, m := range gomodules.All { entries = append(entries, searchEntry{ bookKey: "go", bookTitle: "Go", modKey: m.Key, modTitle: m.Title, modDesc: m.Description, keywords: gomodules.Keywords(m.Key), run: m.Run, }) } // Linux for _, m := range linuxmodules.All { entries = append(entries, searchEntry{ bookKey: "linux", bookTitle: "Linux", modKey: m.Key, modTitle: m.Title, modDesc: m.Description, keywords: linuxmodules.Keywords(m.Key), run: m.Run, }) } // DevOps for _, m := range devopsmodules.All { entries = append(entries, searchEntry{ bookKey: "devops", bookTitle: "DevOps", modKey: m.Key, modTitle: m.Title, modDesc: m.Description, keywords: devopsmodules.Keywords(m.Key), run: m.Run, }) } // InfoSec for _, m := range infosecmodules.All { entries = append(entries, searchEntry{ bookKey: "infosec", bookTitle: "InfoSec", modKey: m.Key, modTitle: m.Title, modDesc: m.Description, keywords: infosecmodules.Keywords(m.Key), run: m.Run, }) } return entries } // Search выполняет поиск по запросу, возвращает отсортированные результаты func Search(query string) []SearchResult { query = strings.ToLower(strings.TrimSpace(query)) if query == "" { return nil } // Разбить запрос на слова words := strings.Fields(query) var results []SearchResult for _, e := range allEntries { score := 0 // Текст для поиска: ключевые слова + название + описание searchText := strings.ToLower( e.modKey + " " + e.modTitle + " " + e.modDesc + " " + strings.Join(e.keywords, " "), ) for _, word := range words { if len(word) < 2 { continue } // Точное совпадение в ключевых словах — больший вес for _, kw := range e.keywords { if strings.EqualFold(kw, word) { score += 10 } else if strings.Contains(strings.ToLower(kw), word) { score += 5 } } // Совпадение в ключе модуля if strings.Contains(e.modKey, word) { score += 8 } // Совпадение в названии if strings.Contains(strings.ToLower(e.modTitle), word) { score += 6 } // Совпадение в описании if strings.Contains(strings.ToLower(e.modDesc), word) { score += 3 } // Совпадение в общем тексте if strings.Contains(searchText, word) && score == 0 { score += 1 } } if score > 0 { results = append(results, SearchResult{ BookKey: e.bookKey, BookTitle: e.bookTitle, ModKey: e.modKey, ModTitle: e.modTitle, ModDesc: e.modDesc, RunFunc: e.run, Score: score, }) } } // Сортировать по релевантности sort.Slice(results, func(i, j int) bool { return results[i].Score > results[j].Score }) return results } // ShowSearchResults выводит результаты поиска func ShowSearchResults(query string, results []SearchResult) { fmt.Printf("\n%s%s🔍 Поиск: \"%s\"%s\n\n", utils.ColorBold, utils.ColorCyan, query, utils.ColorReset) if len(results) == 0 { fmt.Printf(" %sНичего не найдено по запросу \"%s\"%s\n\n", utils.ColorDim, query, utils.ColorReset) fmt.Printf(" Попробуйте другие слова. Примеры: ansible, nmap, docker, jwt, selinux\n\n") return } fmt.Printf(" %s%sНайдено разделов: %d%s\n\n", utils.ColorBold, utils.ColorGreen, len(results), utils.ColorReset) for i, r := range results { bookColor := bookColorFor(r.BookKey) fmt.Printf(" %s%s%2d.%s %s[%s]%s %s%-12s%s — %s%s%s\n", utils.ColorBold, utils.ColorCyan, i+1, utils.ColorReset, utils.ColorBold+bookColor, r.BookTitle, utils.ColorReset, utils.ColorYellow+utils.ColorBold, r.ModKey, utils.ColorReset, utils.ColorWhite, r.ModTitle, utils.ColorReset, ) fmt.Printf(" %s%s%s\n", utils.ColorDim, r.ModDesc, utils.ColorReset) fmt.Printf(" %sОткрыть: ./rubric -book %s -topic %s%s\n\n", utils.ColorDim, r.BookKey, r.ModKey, utils.ColorReset) } } func bookColorFor(key string) string { switch key { case "go": return utils.ColorCyan case "linux": return utils.ColorGreen case "devops": return utils.ColorMagenta case "infosec": return utils.ColorRed } return utils.ColorWhite } // InteractiveSearch — интерактивный поиск с возможностью открыть раздел func InteractiveSearch(query string, scanner interface { Scan() bool Text() string }) { results := Search(query) ShowSearchResults(query, results) if len(results) == 0 { return } fmt.Printf(" %s%sВведите номер раздела чтобы открыть (Enter — пропустить):%s ", utils.ColorBold, utils.ColorGreen, utils.ColorReset) if !scanner.Scan() { return } input := strings.TrimSpace(scanner.Text()) if input == "" { return } n := 0 for _, ch := range input { if ch >= '0' && ch <= '9' { n = n*10 + int(ch-'0') } else { return } } if n >= 1 && n <= len(results) { fmt.Println() results[n-1].RunFunc() } }