thread-master/backend/internal/library/knowledge/search_keyword_shape.go

123 lines
3.0 KiB
Go

package knowledge
import (
"strings"
"unicode/utf8"
)
const (
maxThreadsSearchRunes = 14
maxWebRecencyCoreRunes = 12
maxWebRelevanceTagRunes = 16
)
var searchIntentMarkers = []string{"推薦", "請問", "怎麼辦", "好用嗎", "有人", "求助", "請益", "有推薦"}
// ShortPatrolSearchCore compresses a patrol tag for Threads API / recency Search API.
// Keeps pain + product tokens; drops filler and redundant intent words (RECENT/TOP handles freshness).
func ShortPatrolSearchCore(tag string) string {
tag = strings.TrimSpace(tag)
if tag == "" {
return ""
}
tokens := selectSearchTokens(tag, 3)
if len(tokens) == 0 {
return truncateRunes(stripSearchIntent(tag), maxThreadsSearchRunes)
}
core := strings.Join(tokens, " ")
return truncateRunes(core, maxWebRecencyCoreRunes)
}
// ThreadsAPIKeyword is the plain keyword sent to official Threads keyword search.
func ThreadsAPIKeyword(tag string) string {
core := ShortPatrolSearchCore(tag)
if core != "" {
return core
}
return truncateRunes(stripSearchIntent(tag), maxThreadsSearchRunes)
}
// FullPatrolWebSearchTag keeps a slightly longer phrase for relevance Search API (exact-match quotes).
func FullPatrolWebSearchTag(tag string) string {
tag = strings.TrimSpace(tag)
if tag == "" {
return ""
}
if !hasSearchIntentMarker(tag) {
candidate := strings.TrimSpace(tag + " 推薦")
if utf8.RuneCountInString(candidate) <= maxWebRelevanceTagRunes {
tag = candidate
}
}
if utf8.RuneCountInString(tag) <= maxWebRelevanceTagRunes {
return tag
}
tokens := selectSearchTokens(tag, 4)
if len(tokens) == 0 {
return truncateRunes(tag, maxWebRelevanceTagRunes)
}
full := strings.Join(tokens, " ")
if !hasSearchIntentMarker(full) {
withIntent := strings.TrimSpace(full + " 推薦")
if utf8.RuneCountInString(withIntent) <= maxWebRelevanceTagRunes {
return withIntent
}
}
return truncateRunes(full, maxWebRelevanceTagRunes)
}
func selectSearchTokens(tag string, maxTokens int) []string {
tag = stripSearchIntent(tag)
parts := []string{}
seen := map[string]struct{}{}
add := func(token string) {
token = strings.TrimSpace(token)
if isPatrolFiller(token) || len([]rune(token)) < 2 {
return
}
if _, ok := seen[token]; ok {
return
}
seen[token] = struct{}{}
parts = append(parts, token)
}
for _, anchor := range patrolTopicAnchors {
if strings.Contains(tag, anchor) {
add(anchor)
}
}
for _, hint := range productFormHints {
if strings.Contains(tag, hint) {
add(hint)
}
}
for _, token := range intentTokenize(tag) {
add(token)
if len(parts) >= maxTokens {
break
}
}
if len(parts) > maxTokens {
parts = parts[:maxTokens]
}
return parts
}
func stripSearchIntent(tag string) string {
tag = strings.TrimSpace(tag)
for _, marker := range searchIntentMarkers {
tag = strings.ReplaceAll(tag, marker, " ")
}
return strings.Join(strings.Fields(tag), " ")
}
func hasSearchIntentMarker(tag string) bool {
for _, marker := range searchIntentMarkers {
if strings.Contains(tag, marker) {
return true
}
}
return false
}