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>
This commit is contained in:
2026-05-20 01:27:14 +08:00
parent 28633cf570
commit c89cad6643
19 changed files with 1211 additions and 668 deletions
@@ -689,12 +689,12 @@ func (s *MonitoringService) CollectNow(
limitApplied = true
}
if len(configuredQuestions) == 0 {
message := "当前品牌下暂无品牌库问题,未创建采集任务"
message := "当前品牌下暂无关键词库(搜索词),未创建采集任务"
if keywordID != nil {
message = "当前问题集下暂无品牌库问题,未创建采集任务"
message = "当前关键词库(搜索词)下暂无搜索词,未创建采集任务"
}
if questionID != nil {
message = "当前问题不可用,未创建采集任务"
message = "当前搜索词不可用,未创建采集任务"
}
return &MonitoringCollectNowResponse{
CollectionMode: quota.CollectionMode,
@@ -23,10 +23,48 @@ func TestBuildQuestionCombinationFillFallback(t *testing.T) {
if !reflect.DeepEqual(got.Region, []string{"上海", "华东"}) {
t.Fatalf("region = %#v", got.Region)
}
if len(got.Core) < 3 || got.Core[0] != "GEO" {
t.Fatalf("core fallback = %#v", got.Core)
wantPrefix := []string{"性价比高的", "口碑好的", "专业的", "靠谱的"}
if !reflect.DeepEqual(got.Prefix, wantPrefix) {
t.Fatalf("prefix fallback = %#v, want %#v", got.Prefix, wantPrefix)
}
wantCore := []string{"品牌词", "AI搜索", "获客", "中小企业"}
if !reflect.DeepEqual(got.Core, wantCore) {
t.Fatalf("core fallback = %#v, want %#v", got.Core, wantCore)
}
wantIndustry := []string{"GEO优化", "AI搜索优化", "品牌推广", "搜索营销"}
if !reflect.DeepEqual(got.Industry, wantIndustry) {
t.Fatalf("industry fallback = %#v, want %#v", got.Industry, wantIndustry)
}
if len(got.Prefix) != 4 || len(got.Industry) != 4 || len(got.Suffix) != 4 {
t.Fatalf("expected four hot terms for non-region columns, got %#v", got)
}
wantSuffix := []string{"哪家好", "怎么选", "推荐", "避坑"}
if !reflect.DeepEqual(got.Suffix, wantSuffix) {
t.Fatalf("suffix fallback = %#v, want %#v", got.Suffix, wantSuffix)
}
}
func TestNormalizeQuestionTextRemovesPunctuation(t *testing.T) {
got := normalizeQuestionText(" 合肥全屋定制哪家好? ")
if got != "合肥全屋定制哪家好" {
t.Fatalf("normalizeQuestionText() = %q, want %q", got, "合肥全屋定制哪家好")
}
}
func TestCompactQuestionCombinationTermsRemovesPunctuation(t *testing.T) {
got := compactQuestionCombinationTerms([]string{"官网?", "怎么选?", "推荐!", "对比"}, 4)
want := []string{"官网", "怎么选", "推荐", "对比"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("compactQuestionCombinationTerms() = %#v, want %#v", got, want)
}
}
func TestQuestionContainsExcludedEntityMatchesCompanyShortName(t *testing.T) {
brandName := "安徽海翔家居用品销售有限公司"
if !questionContainsExcludedEntity("安徽海翔家居全屋定制口碑怎么样", brandName, nil) {
t.Fatal("expected candidate containing company short name to be excluded")
}
if questionContainsExcludedEntity("合肥全屋定制哪家好", brandName, nil) {
t.Fatal("expected generic user search term to be allowed")
}
}
@@ -191,15 +191,10 @@ func (s *QuestionExpansionService) GenerateByCombination(ctx context.Context, br
for _, co := range core {
for _, in := range industry {
for _, sf := range suffix {
text := strings.TrimSpace(strings.Join(nonEmptyParts(rg, pf, co, in, sf), ""))
text := normalizeQuestionText(strings.Join(nonEmptyParts(rg, pf, co, in, sf), ""))
if text == "" {
continue
}
if sf != "" && !strings.HasSuffix(text, "?") && !strings.HasSuffix(text, "") {
if !strings.Contains(sf, "?") && !strings.Contains(sf, "") {
text += ""
}
}
key := normalizeQuestionKey(text)
if _, ok := seen[key]; ok {
continue
@@ -312,6 +307,9 @@ func (s *QuestionExpansionService) GenerateByAIDistill(ctx context.Context, bran
if text == "" {
continue
}
if questionContainsExcludedEntity(text, brandCtx.BrandName, brandCtx.CompetitorNames) {
continue
}
if _, ok := seen[key]; ok {
continue
}
@@ -754,11 +752,89 @@ func buildCandidate(text, source string, missingSuffix bool, existing map[string
}
}
func questionContainsExcludedEntity(text, brandName string, competitorNames []string) bool {
normalized := normalizeQuestionKey(text)
for _, entity := range append([]string{brandName}, competitorNames...) {
for _, entityKey := range questionEntityExclusionKeys(entity) {
if strings.Contains(normalized, entityKey) {
return true
}
}
}
return false
}
func questionEntityExclusionKeys(entity string) []string {
base := normalizeQuestionKey(entity)
if utf8.RuneCountInString(base) < 2 {
return nil
}
keys := make([]string, 0, 4)
addKey := func(value string) {
value = strings.TrimSpace(value)
if utf8.RuneCountInString(value) < 2 {
return
}
for _, key := range keys {
if key == value {
return
}
}
keys = append(keys, value)
}
addKey(base)
companyName := base
for _, suffix := range []string{
"有限责任公司",
"股份有限公司",
"集团有限公司",
"控股有限公司",
"科技有限公司",
"销售有限公司",
"用品有限公司",
"有限公司",
"股份公司",
"集团公司",
"公司",
} {
if strings.HasSuffix(companyName, suffix) {
companyName = strings.TrimSuffix(companyName, suffix)
addKey(companyName)
break
}
}
descriptorName := companyName
for _, suffix := range []string{
"用品销售",
"产品销售",
"销售",
"用品",
"产品",
"服务",
"科技",
"商贸",
"贸易",
"网络",
"信息",
"文化",
"传媒",
} {
if strings.HasSuffix(descriptorName, suffix) {
descriptorName = strings.TrimSuffix(descriptorName, suffix)
addKey(descriptorName)
break
}
}
return keys
}
func normalizeQuestionParts(values []string) []string {
result := make([]string, 0, len(values))
seen := map[string]struct{}{}
for _, value := range values {
trimmed := strings.TrimSpace(value)
trimmed := normalizeQuestionText(value)
if trimmed == "" {
continue
}
@@ -790,27 +866,27 @@ func buildQuestionCombinationFillFallback(brandCtx *questionBrandContext, region
region = compactQuestionCombinationTerms(req.Region, questionCombinationFillMax)
}
if len(region) == 0 {
region = []string{"全国", "本地", "华东"}
region = []string{"全国", "合肥", "华东"}
}
prefix := compactQuestionCombinationTerms(req.Prefix, questionCombinationFillMax)
if len(prefix) == 0 {
prefix = []string{"好的", "专业的", "靠谱的", "热门的"}
prefix = []string{"性价比高的", "口碑好的", "专业的", "靠谱的"}
}
core := compactQuestionCombinationTerms(req.Core, questionCombinationFillMax)
if len(core) == 0 {
core = questionCombinationCoreFallback(brandName, description)
core = questionCombinationPrecisionFallback(brandName, description)
}
industry := compactQuestionCombinationTerms(req.Industry, questionCombinationFillMax)
if len(industry) == 0 {
industry = []string{category, "平台", "工具", "服务商"}
industry = questionCombinationIndustryFallback(category)
}
suffix := compactQuestionCombinationTerms(req.Suffix, questionCombinationFillMax)
if len(suffix) == 0 {
suffix = []string{"哪家好", "怎么选", "推荐", "多少钱"}
suffix = []string{"哪家好", "怎么选", "推荐", "避坑"}
}
return QuestionCombinationFillResult{
@@ -1017,21 +1093,40 @@ func chineseMacroRegion(province string) string {
}
}
func questionCombinationCoreFallback(brandName, description string) []string {
func questionCombinationPrecisionFallback(brandName, description string) []string {
text := strings.ToLower(strings.TrimSpace(brandName + " " + description))
if strings.Contains(text, "geo") || strings.Contains(text, "ai") || strings.Contains(text, "搜索") || strings.Contains(text, "排名") {
return []string{"GEO", "AI搜索优化", "AI搜索排名", "品牌可见度"}
return []string{"品牌词", "AI搜索", "获客", "中小企业"}
}
if strings.Contains(text, "教育") || strings.Contains(text, "培训") || strings.Contains(text, "课程") {
return []string{"课程", "培训", "学习", "机构"}
return []string{"成人", "少儿", "线上", "入门"}
}
if strings.Contains(text, "医疗") || strings.Contains(text, "健康") || strings.Contains(text, "诊所") {
return []string{"医疗服务", "健康管理", "医生", "诊疗"}
return []string{"本地", "复诊", "儿童", "上班族"}
}
if strings.Contains(text, "家居") || strings.Contains(text, "装修") || strings.Contains(text, "建材") {
return []string{"装", "家居", "建材", "设计"}
return []string{"装", "100平", "小户型", "旧房改造"}
}
return []string{"企业", "本地", "新手", "中小企业"}
}
func questionCombinationIndustryFallback(category string) []string {
switch category {
case "营销":
return []string{"GEO优化", "AI搜索优化", "品牌推广", "搜索营销"}
case "教育":
return []string{"培训机构", "在线课程", "教育机构", "学习平台"}
case "健康":
return []string{"健康管理", "医疗服务", "诊疗机构", "医生服务"}
case "家居":
return []string{"全屋定制", "家具定制", "家居定制", "定制家装"}
case "餐饮":
return []string{"餐饮店", "美食店", "连锁餐饮", "餐饮服务"}
case "旅游":
return []string{"旅游服务", "酒店预订", "旅行社", "旅游攻略"}
default:
return []string{"服务商", "解决方案", "平台", "公司"}
}
return []string{strings.TrimSpace(brandName), "解决方案", "服务", "平台"}
}
func inferQuestionCombinationCategory(brandName, description string) string {
@@ -29,11 +29,20 @@ type ClassifiedQuestion struct {
}
func normalizeQuestionText(text string) string {
return strings.TrimSpace(text)
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(strings.TrimSpace(text))
return strings.ToLower(normalizeQuestionText(text))
}
func isValidQuestionLayer(value string) bool {
@@ -72,10 +72,10 @@ func TestNormalizeAnalyzeResultKeepsTenQueryKeywords(t *testing.T) {
"全屋定制哪家好",
"靠谱的全屋定制品牌有哪些",
"口碑好的全屋定制公司推荐",
"性价比高的全屋定制怎么选",
"专业的全屋定制设计公司哪家好",
"全屋定制和木工打柜子哪个更划算",
"全屋定制一般多少钱一平米",
"全屋定制服务商推荐",
"全屋定制官网怎么找",
"全屋定制案例怎么看",
"全屋定制常用的板材哪种环保",
"全屋定制十大品牌排名最新",
"全屋定制需要注意哪些坑",
@@ -20,7 +20,7 @@ func TestBuildPromptContextUsesChineseLabels(t *testing.T) {
"- 语言: zh-CN",
"- 标题: 测试标题",
"- 品牌名: Rankly",
"- 品牌主问题: GEO 排名",
"- 优化关键词: GEO 排名",
"- 已选段落: 引言 > 结论",
} {
if !strings.Contains(context, expected) {