Harden keyword cleaning market noise guards
This commit is contained in:
parent
ffdcaa146f
commit
e542e212b0
|
|
@ -218,6 +218,34 @@ const CONSUMER_AI_NOISE_PATTERNS = [
|
||||||
/нейросет[а-яёa-z0-9-]*\s+(бесплатн[а-яёa-z0-9-]*|онлайн|для\s+текст[а-яёa-z0-9-]*|для\s+картин[а-яёa-z0-9-]*)/i
|
/нейросет[а-яёa-z0-9-]*\s+(бесплатн[а-яёa-z0-9-]*|онлайн|для\s+текст[а-яёa-z0-9-]*|для\s+картин[а-яёa-z0-9-]*)/i
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const OFF_CONTEXT_RELATED_NOISE_PATTERNS = [
|
||||||
|
/(^|\s)(бизнес[-\s]?зал[а-яё]*|доступ[а-яё]*\s+в\s+бизнес[-\s]?зал[а-яё]*|карта\s+для\s+доступ[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(личн[а-яё]*\s+кабинет|доступ\s+в\s+личн[а-яё]*\s+кабинет|вход\s+в\s+личн[а-яё]*\s+кабинет)(\s|$)/i,
|
||||||
|
/(^|\s)(парковочн[а-яё]*\s+ассистент|оптическ[а-яё]*\s+парковочн[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(доступ\s+к\s+сервису|подписк[а-яё]*\s+на\s+сервис)(\s|$)/i,
|
||||||
|
/(^|\s)(клиент[а-яё]*\s+операционн[а-яё]*\s+систем[а-яё]*|операционн[а-яё]*\s+систем[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(операционн[а-яё]*\s+обслуживан[а-яё]*\s+клиент[а-яё]*|обслуживан[а-яё]*\s+клиент[а-яё]*)(\s|$)/i
|
||||||
|
];
|
||||||
|
|
||||||
|
const THIRD_PARTY_BRAND_PATTERNS = [
|
||||||
|
/(^|\s)(яндекс|yandex|сбер|sber|мегафон|megafon|тинькофф|tinkoff|google|microsoft|openai|chatgpt|giga\s?chat|gigachat)(\s|$)/i
|
||||||
|
];
|
||||||
|
const THIRD_PARTY_BRAND_ALLOWED_CONTEXT_PATTERN =
|
||||||
|
/(^|\s)(интеграц[а-яёa-z0-9-]*|api|подключ[а-яёa-z0-9-]*|коннектор[а-яёa-z0-9-]*|обмен\s+данн[а-яёa-z0-9-]*|миграц[а-яёa-z0-9-]*|импорт|экспорт)(\s|$)/i;
|
||||||
|
|
||||||
|
const BUSINESS_PLANNING_INFO_PATTERNS = [
|
||||||
|
/(^|\s)(бизнес[-\s]?план[а-яё]*|создани[ея]\s+бизнес[а-яё]*|открыти[ея]\s+бизнес[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(бизнес[-\s]?модел[а-яё]*|модел[ьи]\s+для\s+бизнес[а-яё]*|стратеги[яи]\s+для\s+бизнес[-\s]?модел[а-яё]*)(\s|$)/i
|
||||||
|
];
|
||||||
|
const COMPARISON_INFO_PATTERNS = [
|
||||||
|
/(^|\s)(лучши[а-яё]*|топ|рейтинг|обзор|сравнен[а-яё]*|како[йе]\s+лучше)(\s|$)/i
|
||||||
|
];
|
||||||
|
const TARIFF_DEMAND_PATTERN = /(^|\s)(тариф[а-яё]*|расценк[а-яё]*)(\s|$)/i;
|
||||||
|
const NON_PRODUCT_TARIFF_CONTEXT_PATTERN =
|
||||||
|
/(^|\s)(банк[а-яё]*|банковск[а-яё]*|счет[а-яё]*|счёт[а-яё]*|ип|рко|эквайринг|интернет|связ[ьи]|мобил[а-яё]*|телефон[а-яё]*|оператор[а-яё]*|альфа|втб|сбер|сбербанк|тинькофф|мегафон|билайн|мтс|tele2|360)(\s|$)/i;
|
||||||
|
const PRODUCT_TARIFF_QUALIFIER_PATTERN =
|
||||||
|
/(^|\s)(ai|ии|llm|нейросет[а-яё]*|искусственн[а-яё]*\s+интеллект|агент[а-яё]*|ассистент[а-яё]*|автоматизац[а-яё]*|платформ[а-яё]*|сервис[а-яё]*|crm|erp|bpm|workflow|интеграц[а-яё]*|внедрен[а-яё]*|разработк[а-яё]*|корпоративн[а-яё]*|enterprise)(\s|$)/i;
|
||||||
|
|
||||||
const AI_KEYWORD_PATTERN = /(^|\s)(ai|ии)(\s|$)|искусственн[а-яёa-z0-9-]*\s+интеллект|нейросет/i;
|
const AI_KEYWORD_PATTERN = /(^|\s)(ai|ии)(\s|$)|искусственн[а-яёa-z0-9-]*\s+интеллект|нейросет/i;
|
||||||
const B2B_KEYWORD_FACET_PATTERN =
|
const B2B_KEYWORD_FACET_PATTERN =
|
||||||
/бизнес|процесс|разработ|приложен|агент|ассистент|интеграц|решен|платформ|стоимост|внедрен|купить|заказать|тариф|1с|crm|erp|bpm|workflow|digital|twin|цифров|двойн|bim|бим|инженер|данн|тендер|закуп|юрид|договор|беспилот|iot|телеметр|строител|эксплуатац|проект|офис|облак|оркестр|инфраструктур|безопасн|предприят|корпоратив/i;
|
/бизнес|процесс|разработ|приложен|агент|ассистент|интеграц|решен|платформ|стоимост|внедрен|купить|заказать|тариф|1с|crm|erp|bpm|workflow|digital|twin|цифров|двойн|bim|бим|инженер|данн|тендер|закуп|юрид|договор|беспилот|iot|телеметр|строител|эксплуатац|проект|офис|облак|оркестр|инфраструктур|безопасн|предприят|корпоратив/i;
|
||||||
|
|
@ -229,7 +257,7 @@ const KEYWORD_CLEANING_MODEL_TOP_RESULT_LIMIT = 120;
|
||||||
|
|
||||||
const ARTICLE_PATTERNS = [
|
const ARTICLE_PATTERNS = [
|
||||||
/(^|\s)(как|что такое|что\s+это|почему|зачем|когда|пример|виды|этапы|способы|инструкция|гайд|определен[а-яёa-z0-9-]*|вопрос[а-яёa-z0-9-]*|ответ[а-яёa-z0-9-]*)(\s|$)/i,
|
/(^|\s)(как|что такое|что\s+это|почему|зачем|когда|пример|виды|этапы|способы|инструкция|гайд|определен[а-яёa-z0-9-]*|вопрос[а-яёa-z0-9-]*|ответ[а-яёa-z0-9-]*)(\s|$)/i,
|
||||||
/(^|\s)(выбрать|сравнение|обзор|решение проблемы)(\s|$)/i,
|
/(^|\s)(выбрать|сравнение|обзор|лучши[а-яё]*|топ|рейтинг|како[йе]\s+лучше|решение проблемы)(\s|$)/i,
|
||||||
/(^|\s).+\s+это$/i
|
/(^|\s).+\s+это$/i
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -601,7 +629,10 @@ function shouldRescueExactTrashItem(market: MarketEnrichmentContract, item: Keyw
|
||||||
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
||||||
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
||||||
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
||||||
!isBroadAiKeywordNoise(item.normalizedPhrase)
|
!isBroadAiKeywordNoise(item.normalizedPhrase) &&
|
||||||
|
!isOffContextRelatedNoisePhrase(item.normalizedPhrase) &&
|
||||||
|
!hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) &&
|
||||||
|
!hasNonProductMarketIntentMismatch(item.normalizedPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -637,7 +668,10 @@ function shouldPromoteExactDemandItem(
|
||||||
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
||||||
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
||||||
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
||||||
!isBroadAiKeywordNoise(item.normalizedPhrase)
|
!isBroadAiKeywordNoise(item.normalizedPhrase) &&
|
||||||
|
!isOffContextRelatedNoisePhrase(item.normalizedPhrase) &&
|
||||||
|
!hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) &&
|
||||||
|
!hasNonProductMarketIntentMismatch(item.normalizedPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -678,11 +712,52 @@ function promoteExactDemandItems(market: MarketEnrichmentContract, items: Keywor
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enforceKeywordCleaningNoiseGuard(items: KeywordCleaningItem[]): KeywordCleaningItem[] {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (isOffContextRelatedNoisePhrase(item.normalizedPhrase)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
blockers: unique([...(Array.isArray(item.blockers) ? item.blockers : []), "off_context_related_noise"]),
|
||||||
|
confidence: Math.max(item.confidence, 0.88),
|
||||||
|
decision: "trash",
|
||||||
|
reason:
|
||||||
|
"Backend guard отбросил фразу как омонимический Wordstat related: формулировка ушла в потребительский сервис/доступ/кабинет/парковку, а не в продуктовый B2B-спрос."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) && (item.decision === "use" || item.decision === "support")) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
blockers: unique([...(Array.isArray(item.blockers) ? item.blockers : []), "third_party_brand_not_product_offer"]),
|
||||||
|
confidence: Math.min(item.confidence, 0.62),
|
||||||
|
decision: "article",
|
||||||
|
reason:
|
||||||
|
"Backend guard понизил third-party brand спрос в article/backlog: это может быть обзор или конкурентная ветка, но не продуктовая карта Stage 3 без явной интеграционной связи."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNonProductMarketIntentMismatch(item.normalizedPhrase) && (item.decision === "use" || item.decision === "support")) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
blockers: unique([...(Array.isArray(item.blockers) ? item.blockers : []), "non_product_market_intent"]),
|
||||||
|
confidence: Math.min(item.confidence, 0.62),
|
||||||
|
decision: "article",
|
||||||
|
reason:
|
||||||
|
"Backend guard понизил фразу в article/backlog: Wordstat спрос ушёл в тарифы банков/связи, бизнес-планы, сравнения или создание бизнеса, а не в продуктовый B2B offer."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function enforceKeywordCleaningBackendGuards(
|
function enforceKeywordCleaningBackendGuards(
|
||||||
market: MarketEnrichmentContract,
|
market: MarketEnrichmentContract,
|
||||||
items: KeywordCleaningItem[]
|
items: KeywordCleaningItem[]
|
||||||
): KeywordCleaningItem[] {
|
): KeywordCleaningItem[] {
|
||||||
return promoteExactDemandItems(market, rescueExactTrashItems(market, enforceKeywordCleaningSemanticGuard(market, items)));
|
return enforceKeywordCleaningNoiseGuard(
|
||||||
|
promoteExactDemandItems(market, rescueExactTrashItems(market, enforceKeywordCleaningSemanticGuard(market, items)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeywordCleaningMergeKeys(item: KeywordCleaningItem) {
|
function getKeywordCleaningMergeKeys(item: KeywordCleaningItem) {
|
||||||
|
|
@ -701,7 +776,10 @@ function shouldBackfillMissingExactItem(market: MarketEnrichmentContract, item:
|
||||||
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
||||||
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
||||||
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
||||||
!isBroadAiKeywordNoise(item.normalizedPhrase)
|
!isBroadAiKeywordNoise(item.normalizedPhrase) &&
|
||||||
|
!isOffContextRelatedNoisePhrase(item.normalizedPhrase) &&
|
||||||
|
!hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) &&
|
||||||
|
!hasNonProductMarketIntentMismatch(item.normalizedPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1004,7 +1082,10 @@ function getKeywordCleaningModelSeedScore(seed: SeedQueueItem) {
|
||||||
(seed.normalization?.marketRole === "commercial" ? 14 : 0) +
|
(seed.normalization?.marketRole === "commercial" ? 14 : 0) +
|
||||||
(seed.normalization?.marketRole === "core" ? 10 : 0) +
|
(seed.normalization?.marketRole === "core" ? 10 : 0) +
|
||||||
(seed.priority === "high" ? 12 : seed.priority === "medium" ? 6 : 0) +
|
(seed.priority === "high" ? 12 : seed.priority === "medium" ? 6 : 0) +
|
||||||
Math.min(seed.relatedCount ?? 0, 10)
|
Math.min(seed.relatedCount ?? 0, 10) -
|
||||||
|
(hasNonProductMarketIntentMismatch(seed.phrase) ? 42 : 0) -
|
||||||
|
(isOffContextRelatedNoisePhrase(seed.phrase) ? 70 : 0) -
|
||||||
|
(hasThirdPartyBrandDemandMismatch(seed.phrase) ? 34 : 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1015,7 +1096,10 @@ function getKeywordCleaningModelTopResultScore(result: WordstatTopResult) {
|
||||||
(result.sourcePhrase ? 10 : 0) +
|
(result.sourcePhrase ? 10 : 0) +
|
||||||
(result.sourceClusterId ? 6 : 0) +
|
(result.sourceClusterId ? 6 : 0) +
|
||||||
(hasB2BKeywordFacet(result.phrase) ? 8 : 0) -
|
(hasB2BKeywordFacet(result.phrase) ? 8 : 0) -
|
||||||
(isConsumerAiNoisePhrase(result.phrase) ? 50 : 0)
|
(isConsumerAiNoisePhrase(result.phrase) ? 50 : 0) -
|
||||||
|
(isOffContextRelatedNoisePhrase(result.phrase) ? 70 : 0) -
|
||||||
|
(hasThirdPartyBrandDemandMismatch(result.phrase) ? 34 : 0) -
|
||||||
|
(hasNonProductMarketIntentMismatch(result.phrase) ? 42 : 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1096,6 +1180,51 @@ function isConsumerAiNoisePhrase(phrase: string) {
|
||||||
return AI_KEYWORD_PATTERN.test(normalizedPhrase) && hasPattern(normalizedPhrase, CONSUMER_AI_NOISE_PATTERNS);
|
return AI_KEYWORD_PATTERN.test(normalizedPhrase) && hasPattern(normalizedPhrase, CONSUMER_AI_NOISE_PATTERNS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOffContextRelatedNoisePhrase(phrase: string) {
|
||||||
|
return hasPattern(normalizePhrase(phrase), OFF_CONTEXT_RELATED_NOISE_PATTERNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasThirdPartyBrandDemandMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
|
||||||
|
return hasPattern(normalizedPhrase, THIRD_PARTY_BRAND_PATTERNS) && !THIRD_PARTY_BRAND_ALLOWED_CONTEXT_PATTERN.test(normalizedPhrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBusinessPlanningInfoMismatch(phrase: string) {
|
||||||
|
return hasPattern(normalizePhrase(phrase), BUSINESS_PLANNING_INFO_PATTERNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasComparisonInfoMismatch(phrase: string) {
|
||||||
|
return hasPattern(normalizePhrase(phrase), COMPARISON_INFO_PATTERNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTariffDemandMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
|
||||||
|
if (!TARIFF_DEMAND_PATTERN.test(normalizedPhrase)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NON_PRODUCT_TARIFF_CONTEXT_PATTERN.test(normalizedPhrase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
/(^|\s)для\s+(?:мал[а-яё]*\s+|средн[а-яё]*\s+|крупн[а-яё]*\s+)?бизнес[а-яё]*(\s|$)/i.test(normalizedPhrase) &&
|
||||||
|
!PRODUCT_TARIFF_QUALIFIER_PATTERN.test(normalizedPhrase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNonProductMarketIntentMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasBusinessPlanningInfoMismatch(normalizedPhrase) ||
|
||||||
|
hasComparisonInfoMismatch(normalizedPhrase) ||
|
||||||
|
hasTariffDemandMismatch(normalizedPhrase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasB2BKeywordFacet(phrase: string) {
|
function hasB2BKeywordFacet(phrase: string) {
|
||||||
return B2B_KEYWORD_FACET_PATTERN.test(normalizePhrase(phrase));
|
return B2B_KEYWORD_FACET_PATTERN.test(normalizePhrase(phrase));
|
||||||
}
|
}
|
||||||
|
|
@ -1125,6 +1254,7 @@ function getClassification(contract: MarketEnrichmentContract, item: KeywordClea
|
||||||
const isTrash =
|
const isTrash =
|
||||||
item.marketRole === "exclude" ||
|
item.marketRole === "exclude" ||
|
||||||
hasPattern(item.normalizedPhrase, HARD_TRASH_PATTERNS) ||
|
hasPattern(item.normalizedPhrase, HARD_TRASH_PATTERNS) ||
|
||||||
|
isOffContextRelatedNoisePhrase(item.normalizedPhrase) ||
|
||||||
isConsumerAiNoisePhrase(item.normalizedPhrase) ||
|
isConsumerAiNoisePhrase(item.normalizedPhrase) ||
|
||||||
isBroadAiKeywordNoise(item.normalizedPhrase) ||
|
isBroadAiKeywordNoise(item.normalizedPhrase) ||
|
||||||
(item.source === "wordstat_related" && isBroadAiKeywordNoise(item.sourcePhrase)) ||
|
(item.source === "wordstat_related" && isBroadAiKeywordNoise(item.sourcePhrase)) ||
|
||||||
|
|
@ -1155,6 +1285,26 @@ function getClassification(contract: MarketEnrichmentContract, item: KeywordClea
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasThirdPartyBrandDemandMismatch(item.normalizedPhrase)) {
|
||||||
|
return {
|
||||||
|
blockers: unique([...blockers, "third_party_brand_not_product_offer"]),
|
||||||
|
confidence: exactEvidence ? 0.62 : 0.48,
|
||||||
|
decision: "article" as const,
|
||||||
|
reason:
|
||||||
|
"Фраза привязана к стороннему бренду/сервису без интеграционного intent; держим как конкурентный или обзорный backlog, не как продуктовую Stage 3 карту."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNonProductMarketIntentMismatch(item.normalizedPhrase)) {
|
||||||
|
return {
|
||||||
|
blockers: unique([...blockers, "non_product_market_intent"]),
|
||||||
|
confidence: exactEvidence ? 0.66 : 0.5,
|
||||||
|
decision: "article" as const,
|
||||||
|
reason:
|
||||||
|
"Фраза описывает смежный информационный/административный спрос: тарифы банков/связи, бизнес-планы, сравнения или создание бизнеса. Это не продуктовая карта Stage 3 без явного product qualifier."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (item.evidenceStatus === "collection_failed") {
|
if (item.evidenceStatus === "collection_failed") {
|
||||||
return {
|
return {
|
||||||
blockers,
|
blockers,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue