102 lines
2.6 KiB
Go
102 lines
2.6 KiB
Go
|
|
package placement
|
||
|
|
|
||
|
|
import (
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
libkg "haixun-backend/internal/library/knowledge"
|
||
|
|
)
|
||
|
|
|
||
|
|
const defaultPatrolProductFit = 78
|
||
|
|
|
||
|
|
// CollectPatrolTagQueries builds dual-track crawl jobs from user-edited patrol keywords only.
|
||
|
|
func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node) []TagQuery {
|
||
|
|
keywords = libkg.NormalizePatrolKeywordList(keywords)
|
||
|
|
if len(keywords) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
out := make([]TagQuery, 0, len(keywords)*3)
|
||
|
|
for _, tag := range keywords {
|
||
|
|
fit := productFitForPatrolTag(tag, nodes)
|
||
|
|
if q := BuildRelevanceQuery(tag); q != "" {
|
||
|
|
out = append(out, TagQuery{
|
||
|
|
Tag: tag,
|
||
|
|
Query: q,
|
||
|
|
Dimension: QueryRelevance,
|
||
|
|
ProductFitScore: fit,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
if q7 := BuildRecencyQuery(tag, IdealMaxPostAgeDays); q7 != "" {
|
||
|
|
out = append(out, TagQuery{
|
||
|
|
Tag: tag,
|
||
|
|
Query: q7,
|
||
|
|
Dimension: QueryRecency,
|
||
|
|
ProductFitScore: fit,
|
||
|
|
RecencyDays: IdealMaxPostAgeDays,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
if q30 := BuildRecencyQuery(tag, MaxPostAgeDays); q30 != "" {
|
||
|
|
if q7 := BuildRecencyQuery(tag, IdealMaxPostAgeDays); q30 != q7 {
|
||
|
|
out = append(out, TagQuery{
|
||
|
|
Tag: tag,
|
||
|
|
Query: q30,
|
||
|
|
Dimension: QueryRecency,
|
||
|
|
ProductFitScore: fit,
|
||
|
|
RecencyDays: MaxPostAgeDays,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func productFitForPatrolTag(tag string, nodes []libkg.Node) int {
|
||
|
|
tagKey := patrolTagMatchKey(tag)
|
||
|
|
best := 0
|
||
|
|
bestNodeID := ""
|
||
|
|
for _, node := range nodes {
|
||
|
|
score := node.ProductFitScore
|
||
|
|
if score <= 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
matched := false
|
||
|
|
for _, candidate := range append(append([]string{}, node.DerivedTags.Relevance...), node.DerivedTags.Recency...) {
|
||
|
|
if patrolTagMatchKey(candidate) == tagKey {
|
||
|
|
matched = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !matched && patrolTagMatchKey(node.Label) != tagKey {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if score > best {
|
||
|
|
best = score
|
||
|
|
bestNodeID = node.ID
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if best > 0 {
|
||
|
|
return best
|
||
|
|
}
|
||
|
|
_ = bestNodeID
|
||
|
|
return defaultPatrolProductFit
|
||
|
|
}
|
||
|
|
|
||
|
|
func patrolTagMatchKey(tag string) string {
|
||
|
|
tag = strings.TrimSpace(tag)
|
||
|
|
for _, suffix := range []string{" 推薦", " 請問", " 怎麼辦", " 好用嗎", " 有人用過嗎", " 有推薦嗎", " 請益"} {
|
||
|
|
if strings.HasSuffix(tag, suffix) {
|
||
|
|
tag = strings.TrimSuffix(tag, suffix)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return strings.TrimSpace(tag)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ResolveTagQueries prefers explicit patrol keywords over graph node selection.
|
||
|
|
func ResolveTagQueries(nodes []libkg.Node, patrolKeywords []string) []TagQuery {
|
||
|
|
if len(patrolKeywords) > 0 {
|
||
|
|
return CollectPatrolTagQueries(patrolKeywords, nodes)
|
||
|
|
}
|
||
|
|
return CollectTagQueries(nodes)
|
||
|
|
}
|