rubric/catalog/search.go
2026-07-21 14:52:55 +03:00

239 lines
6 KiB
Go
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}