Files
geo/server/internal/tenant/app/question_metadata_classify.go
T
root 0b5d8d72f2
Deployment Config CI / Deployment Config (push) Successful in 27s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m53s
Desktop Client Build / Resolve Build Metadata (push) Waiting to run
Desktop Client Build / Build Desktop Client (push) Blocked by required conditions
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
fix(brand-questions): accept generic search terms in lexicon validation
Search terms like "全屋定制" or "GEO排名优化" were rejected as invalid because
both the Vue candidate validator and the Go question_metadata classifier
required interrogative cues (什么/如何/为什么/...). Lexicon entries are search
terms, not questions, so require only letters or digits plus the existing
4-char minimum. Rename helpers to searchTermLooksValid/questionLooksValid
to reflect the looser semantics, and relabel the navigation entry from
"公司和词库" to "品牌和词库" to match the actual domain.
2026-05-24 22:19:57 +08:00

149 lines
3.9 KiB
Go

package app
import (
"strings"
"unicode"
"unicode/utf8"
)
const (
QuestionLayerL1 = "L1"
QuestionLayerL2 = "L2"
QuestionLayerL3 = "L3"
QuestionLayerL4 = "L4"
QuestionLayerL5 = "L5"
QuestionIntentInformational = "informational"
QuestionIntentEvaluative = "evaluative"
QuestionIntentDecisional = "decisional"
QuestionSourceManual = "manual"
QuestionSourceCombination = "combination"
QuestionSourceAIDistill = "ai_distill"
)
type ClassifiedQuestion struct {
Text string `json:"text"`
Layer string `json:"layer"`
Intent string `json:"intent"`
}
func normalizeQuestionText(text string) string {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return ""
}
return strings.Map(func(r rune) rune {
if unicode.IsPunct(r) {
return -1
}
return r
}, trimmed)
}
func normalizeQuestionKey(text string) string {
return strings.ToLower(normalizeQuestionText(text))
}
func isValidQuestionLayer(value string) bool {
switch strings.TrimSpace(value) {
case QuestionLayerL1, QuestionLayerL2, QuestionLayerL3, QuestionLayerL4, QuestionLayerL5:
return true
default:
return false
}
}
func isValidQuestionIntent(value string) bool {
switch strings.TrimSpace(value) {
case QuestionIntentInformational, QuestionIntentEvaluative, QuestionIntentDecisional:
return true
default:
return false
}
}
func isValidExternalQuestionSource(value string) bool {
switch strings.TrimSpace(value) {
case QuestionSourceManual, QuestionSourceCombination, QuestionSourceAIDistill:
return true
default:
return false
}
}
func inferQuestionIntent(text string) string {
normalized := strings.ToLower(strings.TrimSpace(text))
if containsAny(normalized, []string{"哪家好", "哪个好", "怎么选", "推荐", "哪里买", "适合谁", "多少钱", "报价", "购买"}) {
return QuestionIntentDecisional
}
if containsAny(normalized, []string{"对比", "区别", "哪个更", "优劣", "测评", "排名", "值不值", "替代", "竞品"}) {
return QuestionIntentEvaluative
}
return QuestionIntentInformational
}
func inferQuestionLayer(text, brandName string, competitorNames []string) string {
normalized := strings.ToLower(strings.TrimSpace(text))
brandHits := 0
if containsFold(normalized, brandName) {
brandHits++
}
for _, competitor := range competitorNames {
if containsFold(normalized, competitor) {
brandHits++
}
}
if brandHits >= 2 || containsAny(normalized, []string{"对比", "区别", "哪个更", "优劣", "竞品", "替代"}) {
return QuestionLayerL5
}
if brandHits == 1 {
return QuestionLayerL1
}
if containsAny(normalized, []string{"北京", "上海", "广州", "深圳", "杭州", "成都", "合肥", "华东", "华南", "华北", "中小企业", "企业", "团队", "老板", "新手", "预算", "价格", "费用", "场景", "本地"}) {
return QuestionLayerL4
}
if containsAny(normalized, []string{"痛点", "问题", "踩坑", "解决", "失败", "难", "风险", "怎么办"}) {
return QuestionLayerL3
}
return QuestionLayerL2
}
func classifyQuestionText(text, brandName string, competitorNames []string) ClassifiedQuestion {
trimmed := normalizeQuestionText(text)
return ClassifiedQuestion{
Text: trimmed,
Layer: inferQuestionLayer(trimmed, brandName, competitorNames),
Intent: inferQuestionIntent(trimmed),
}
}
func questionLooksValid(text string) bool {
trimmed := normalizeQuestionText(text)
if utf8.RuneCountInString(trimmed) < 4 {
return false
}
hasSearchTermChar := false
for _, r := range trimmed {
if unicode.IsLetter(r) || unicode.IsNumber(r) {
hasSearchTermChar = true
break
}
}
return hasSearchTermChar
}
func containsAny(value string, needles []string) bool {
for _, needle := range needles {
if strings.Contains(value, strings.ToLower(strings.TrimSpace(needle))) {
return true
}
}
return false
}
func containsFold(value, needle string) bool {
needle = strings.ToLower(strings.TrimSpace(needle))
return needle != "" && strings.Contains(value, needle)
}