|
@@ -1,7 +1,7 @@
|
|
|
import { config } from '../config.ts';
|
|
import { config } from '../config.ts';
|
|
|
import { searchCreators, type JustOneCreator } from './justone.service.ts';
|
|
import { searchCreators, type JustOneCreator } from './justone.service.ts';
|
|
|
import type { ContentSample } from './tikhub.service.ts';
|
|
import type { ContentSample } from './tikhub.service.ts';
|
|
|
-import { searchLocalCreators } from './local-creator-db.service.ts';
|
|
|
|
|
|
|
+import { recordRetrievalEvent, searchLocalCreators } from './local-creator-db.service.ts';
|
|
|
|
|
|
|
|
export interface NormalizedCandidate {
|
|
export interface NormalizedCandidate {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -43,6 +43,7 @@ interface SearchCriteria {
|
|
|
gender?: string;
|
|
gender?: string;
|
|
|
contentTags?: string[];
|
|
contentTags?: string[];
|
|
|
excludeTags?: string[];
|
|
excludeTags?: string[];
|
|
|
|
|
+ targetCount?: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ScoringWeights {
|
|
interface ScoringWeights {
|
|
@@ -63,6 +64,7 @@ const DEFAULT_WEIGHTS: ScoringWeights = {
|
|
|
|
|
|
|
|
const MAX_KEYWORDS_PER_PLATFORM = 1;
|
|
const MAX_KEYWORDS_PER_PLATFORM = 1;
|
|
|
const PAGES_PER_PLATFORM = 3;
|
|
const PAGES_PER_PLATFORM = 3;
|
|
|
|
|
+const DEFAULT_MIN_CANDIDATE_POOL = 30;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 主推荐流程:根据搜索条件从多个来源召回候选人并评分排序
|
|
* 主推荐流程:根据搜索条件从多个来源召回候选人并评分排序
|
|
@@ -74,21 +76,27 @@ export async function generateRecommendations(
|
|
|
criteria = normalizeSearchCriteria(criteria);
|
|
criteria = normalizeSearchCriteria(criteria);
|
|
|
console.log('[Recommend] ========== 开始推荐流程 ==========');
|
|
console.log('[Recommend] ========== 开始推荐流程 ==========');
|
|
|
console.log('[Recommend] 搜索条件:', JSON.stringify(criteria, null, 2));
|
|
console.log('[Recommend] 搜索条件:', JSON.stringify(criteria, null, 2));
|
|
|
- onProgress?.('search', '正在从第三方平台召回候选达人...');
|
|
|
|
|
|
|
+ const targetCandidatePool = resolveTargetCandidatePool(criteria);
|
|
|
|
|
+ onProgress?.('search', `正在检索候选达人,目标候选池 ${targetCandidatePool} 位...`);
|
|
|
|
|
|
|
|
// Step 1: 优先从本地/自有达人库召回候选
|
|
// Step 1: 优先从本地/自有达人库召回候选
|
|
|
const allCandidates: JustOneCreator[] = [];
|
|
const allCandidates: JustOneCreator[] = [];
|
|
|
- const localCandidates = await searchLocalCreators(criteria, config.recommendation.maxCandidates);
|
|
|
|
|
|
|
+ const localCandidates = await searchLocalCreators(criteria, Math.min(config.recommendation.maxCandidates, targetCandidatePool));
|
|
|
if (localCandidates.length > 0) {
|
|
if (localCandidates.length > 0) {
|
|
|
console.log(`[Recommend] 本地达人库命中: ${localCandidates.length} 位`);
|
|
console.log(`[Recommend] 本地达人库命中: ${localCandidates.length} 位`);
|
|
|
- onProgress?.('search', `本地达人库命中 ${localCandidates.length} 位候选达人`);
|
|
|
|
|
|
|
+ onProgress?.('search', `本地达人库命中 ${localCandidates.length}/${targetCandidatePool} 位候选达人`);
|
|
|
allCandidates.push(...localCandidates);
|
|
allCandidates.push(...localCandidates);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// JustOne API 仅支持小红书和抖音
|
|
// JustOne API 仅支持小红书和抖音
|
|
|
const supportedPlatforms = ['xiaohongshu', '小红书', 'douyin', '抖音'];
|
|
const supportedPlatforms = ['xiaohongshu', '小红书', 'douyin', '抖音'];
|
|
|
|
|
|
|
|
- if (allCandidates.length === 0) {
|
|
|
|
|
|
|
+ let providerCandidateCount = 0;
|
|
|
|
|
+ if (allCandidates.length < targetCandidatePool) {
|
|
|
|
|
+ const shortage = targetCandidatePool - allCandidates.length;
|
|
|
|
|
+ console.log(`[Recommend] 本地候选不足,继续调用 JustOne 补足: 缺口 ${shortage} 位`);
|
|
|
|
|
+ onProgress?.('search', `本地候选不足,继续调用 JustOne 补足 ${shortage} 位...`);
|
|
|
|
|
+
|
|
|
const searchKeywords = buildCompactSearchKeywords(criteria);
|
|
const searchKeywords = buildCompactSearchKeywords(criteria);
|
|
|
for (const platform of uniquePlatforms(criteria.platforms)) {
|
|
for (const platform of uniquePlatforms(criteria.platforms)) {
|
|
|
const platformName = mapPlatformName(platform);
|
|
const platformName = mapPlatformName(platform);
|
|
@@ -116,10 +124,16 @@ export async function generateRecommendations(
|
|
|
|
|
|
|
|
console.log(`[Recommend] ${platformName}/${keyword}/page-${page} 返回 ${results.length} 条结果`);
|
|
console.log(`[Recommend] ${platformName}/${keyword}/page-${page} 返回 ${results.length} 条结果`);
|
|
|
allCandidates.push(...results);
|
|
allCandidates.push(...results);
|
|
|
|
|
+ providerCandidateCount += results.length;
|
|
|
|
|
+ if (allCandidates.length >= targetCandidatePool) break;
|
|
|
if (results.length === 0) break;
|
|
if (results.length === 0) break;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (allCandidates.length >= targetCandidatePool) break;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (allCandidates.length >= targetCandidatePool) break;
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(`[Recommend] 本地候选已满足目标候选池 ${targetCandidatePool} 位,跳过 JustOne`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
console.log(`[Recommend] 总召回: ${allCandidates.length} 位候选达人`);
|
|
console.log(`[Recommend] 总召回: ${allCandidates.length} 位候选达人`);
|
|
@@ -133,6 +147,13 @@ export async function generateRecommendations(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
const uniqueCandidates = Array.from(uniqueMap.values());
|
|
const uniqueCandidates = Array.from(uniqueMap.values());
|
|
|
|
|
+ await recordRetrievalEvent({
|
|
|
|
|
+ queryText: buildCompactSearchKeywords(criteria).join(' '),
|
|
|
|
|
+ criteria,
|
|
|
|
|
+ localHitCount: localCandidates.length,
|
|
|
|
|
+ providerHitCount: providerCandidateCount,
|
|
|
|
|
+ finalCount: uniqueCandidates.length,
|
|
|
|
|
+ }).catch((error) => console.error('[Recommend] 记录检索事件失败:', error));
|
|
|
|
|
|
|
|
console.log(`[Recommend] 去重后: ${uniqueCandidates.length} 位候选`);
|
|
console.log(`[Recommend] 去重后: ${uniqueCandidates.length} 位候选`);
|
|
|
onProgress?.('processing', `去重后 ${uniqueCandidates.length} 位候选,开始评分筛选...`);
|
|
onProgress?.('processing', `去重后 ${uniqueCandidates.length} 位候选,开始评分筛选...`);
|
|
@@ -351,9 +372,23 @@ function normalizeSearchCriteria(criteria: SearchCriteria): SearchCriteria {
|
|
|
budgetRange: normalizeBudgetRange(criteria.budgetRange),
|
|
budgetRange: normalizeBudgetRange(criteria.budgetRange),
|
|
|
platforms: criteria.platforms?.length ? criteria.platforms : ['xiaohongshu'],
|
|
platforms: criteria.platforms?.length ? criteria.platforms : ['xiaohongshu'],
|
|
|
keywords: criteria.keywords?.length ? criteria.keywords : ['生活方式'],
|
|
keywords: criteria.keywords?.length ? criteria.keywords : ['生活方式'],
|
|
|
|
|
+ targetCount: normalizeTargetCount(criteria.targetCount),
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function resolveTargetCandidatePool(criteria: SearchCriteria): number {
|
|
|
|
|
+ const targetCount = normalizeTargetCount(criteria.targetCount);
|
|
|
|
|
+ if (targetCount > 0) {
|
|
|
|
|
+ return Math.min(config.recommendation.maxCandidates, Math.max(DEFAULT_MIN_CANDIDATE_POOL, targetCount * config.recommendation.multiplier));
|
|
|
|
|
+ }
|
|
|
|
|
+ return Math.min(config.recommendation.maxCandidates, DEFAULT_MIN_CANDIDATE_POOL);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function normalizeTargetCount(value?: number): number | undefined {
|
|
|
|
|
+ const count = Number(value || 0);
|
|
|
|
|
+ return count > 0 ? Math.ceil(count) : undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function normalizeFanRange(range: { min: number; max: number }): { min: number; max: number } {
|
|
function normalizeFanRange(range: { min: number; max: number }): { min: number; max: number } {
|
|
|
const min = Number(range?.min || 0);
|
|
const min = Number(range?.min || 0);
|
|
|
const max = Number(range?.max || 0);
|
|
const max = Number(range?.max || 0);
|