Files
geo/server/internal/tenant/app/question_metadata_classify.go
T
root c89cad6643 feat(keywords): rename brand-question terminology to search-keyword + add entity exclusion
- Rename UI/prompt labels: "品牌主问题" → "优化关键词", "搜索问题" → "搜索词"
- Add brand/competitor entity exclusion filter in question expansion (AI distill path)
- Refactor combination question text normalization, remove manual question-mark appending
- Update prompts to emphasize commercial-intent search queries over editorial phrasing
- Sync prompt changes across server/configs, deploy, and k3s configs
- Update i18n, frontend views (BrandQuestionCreate, TemplateWizard), and test fixtures

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 01:27:14 +08:00

152 lines
4.1 KiB
Go
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 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
}
hasLetterOrNumber := false
for _, r := range trimmed {
if unicode.IsLetter(r) || unicode.IsNumber(r) {
hasLetterOrNumber = true
break
}
}
if !hasLetterOrNumber {
return false
}
return containsAny(strings.ToLower(trimmed), []string{"?", "", "什么", "如何", "怎么", "哪", "为什么", "是否", "能不能", "适合", "区别", "对比", "推荐"})
}
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)
}