package knowledge import ( "sort" "strings" ) // SelectBestSearchKeywords pools research map, graph, product, and optional hints, // then returns the most intent-relevant phrases for Search API (not UI display order). func SelectBestSearchKeywords(explicit, saved []string, in PatrolTagInput, nodes []Node, limit int) []string { if limit <= 0 { limit = MaxScanPatrolKeywords } profile := BuildIntentProfile(in) candidates := BuildPatrolCandidates(in, nodes) for i, tag := range NormalizePatrolKeywordList(explicit) { addPatrolCandidate(&candidates, tag, 156-i, "掃描指定", patrolIntentRelevance) } for i, tag := range NormalizePatrolKeywordList(saved) { if containsNormalizedTag(explicit, tag) { continue } addPatrolCandidate(&candidates, tag, 88-i, "研究地圖儲存", patrolIntentRelevance) } type ranked struct { tag string score int } scored := make([]ranked, 0, len(candidates)) seen := map[string]struct{}{} for _, item := range selectTopPatrolCandidates(candidates, len(candidates)+32) { tag := strings.TrimSpace(item.Tag) if tag == "" { continue } key := patrolTagDedupeKey(tag) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} if IsTangentialToIntent(tag, profile) { continue } intent := ScoreIntentSimilarity(tag, profile) if intent < 8 && item.Score < 65 { continue } final := (item.Score*2 + intent*4) / 6 if final <= 0 { continue } scored = append(scored, ranked{tag: tag, score: final}) } sort.SliceStable(scored, func(i, j int) bool { if scored[i].score == scored[j].score { return scored[i].tag < scored[j].tag } return scored[i].score > scored[j].score }) out := make([]string, 0, limit) for _, item := range scored { out = append(out, item.tag) if len(out) >= limit { break } } return out } func containsNormalizedTag(items []string, target string) bool { targetKey := patrolTagDedupeKey(target) for _, item := range items { if patrolTagDedupeKey(item) == targetKey { return true } } return false }