Files
geo/server/internal/tenant/app/question_metadata_classify.go
T
root 37b0b32327 feat(admin-brand): add question expansion service and related functionality
- Implemented QuestionExpansionService for generating and materializing questions based on combinations and AI distillation.
- Added question metadata classification logic to infer intent and layer of questions.
- Created API handlers for question expansion operations including combination preview, AI distillation, metadata classification, and materialization.
- Introduced database migrations to support new question-related fields and constraints in brand_questions and brand_keywords tables.
- Added caching mechanism for AI distillation results to improve performance.
- Defined JSON schemas for question distillation responses to ensure data integrity.
2026-05-12 21:53:36 +08:00

143 lines
4.0 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 {
return strings.TrimSpace(text)
}
func normalizeQuestionKey(text string) string {
return strings.ToLower(strings.TrimSpace(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)
}