1028 lines
32 KiB
Plaintext
1028 lines
32 KiB
Plaintext
// AI Recommendation Service - Personalized content recommendation system
|
||
|
||
import {
|
||
RecommendationResult,
|
||
ContentInfo,
|
||
AIProvider,
|
||
AIResponse,
|
||
AIServiceConfig,
|
||
AIServiceError,
|
||
BatchProcessingOptions
|
||
} from '../types/ai-types.uts'
|
||
|
||
// 用户行为数据
|
||
type UserBehavior = {
|
||
userId: string
|
||
contentId: string
|
||
actionType: 'view' | 'like' | 'share' | 'comment' | 'save' | 'skip'
|
||
timestamp: number
|
||
duration?: number // 阅读时长(秒)
|
||
deviceType?: string
|
||
location?: string
|
||
context?: UTSJSONObject
|
||
}
|
||
|
||
// 用户画像
|
||
type UserProfile = {
|
||
userId: string
|
||
demographics: {
|
||
age?: number
|
||
gender?: string
|
||
location?: string
|
||
education?: string
|
||
occupation?: string
|
||
}
|
||
interests: {
|
||
categories: Record<string, number> // 分类偏好权重
|
||
keywords: Record<string, number> // 关键词偏好权重
|
||
sources: Record<string, number> // 来源偏好权重
|
||
languages: Record<string, number> // 语言偏好权重
|
||
}
|
||
behavior: {
|
||
readingTime: number // 平均阅读时长
|
||
activeHours: number[] // 活跃时段
|
||
preferredContentLength: 'short' | 'medium' | 'long'
|
||
engagementRate: number // 互动率
|
||
}
|
||
preferences: {
|
||
newsStyle: 'breaking' | 'analysis' | 'opinion' | 'feature'
|
||
updateFrequency: 'realtime' | 'hourly' | 'daily' | 'weekly'
|
||
contentFreshness: number // 内容新鲜度偏好 (0-1)
|
||
diversityLevel: number // 多样性偏好 (0-1)
|
||
}
|
||
lastUpdated: number
|
||
}
|
||
|
||
// 推荐算法类型
|
||
type RecommendationAlgorithm =
|
||
| 'collaborative_filtering'
|
||
| 'content_based'
|
||
| 'hybrid'
|
||
| 'trending'
|
||
| 'similarity'
|
||
| 'ml_based'
|
||
|
||
// 推荐配置
|
||
type RecommendationConfig = {
|
||
algorithm: RecommendationAlgorithm
|
||
maxResults: number
|
||
diversityWeight: number // 多样性权重
|
||
freshnessWeight: number // 新鲜度权重
|
||
personalizedWeight: number // 个性化权重
|
||
qualityThreshold: number // 质量阈值
|
||
excludeViewed: boolean // 排除已浏览内容
|
||
includeCategories?: string[]
|
||
excludeCategories?: string[]
|
||
timeRange?: number // 时间范围(小时)
|
||
}
|
||
|
||
// 推荐上下文
|
||
type RecommendationContext = {
|
||
currentTime: number
|
||
userLocation?: string
|
||
deviceType?: string
|
||
sessionDuration?: number
|
||
recentViews: string[] // 最近浏览的内容ID
|
||
currentCategory?: string
|
||
searchQuery?: string
|
||
}
|
||
|
||
// 相似度计算结果
|
||
type SimilarityResult = {
|
||
contentId: string
|
||
similarity: number
|
||
reasons: string[]
|
||
}
|
||
|
||
// 推荐统计
|
||
type RecommendationStats = {
|
||
totalRecommendations: number
|
||
clickThroughRate: number
|
||
averageEngagementTime: number
|
||
algorithmPerformance: Record<RecommendationAlgorithm, {
|
||
usage: number
|
||
successRate: number
|
||
avgScore: number
|
||
}>
|
||
categoryDistribution: Record<string, number>
|
||
userSatisfactionScore: number
|
||
}
|
||
|
||
/**
|
||
* AI推荐服务类
|
||
* 提供个性化内容推荐,支持多种推荐算法和实时优化
|
||
*/
|
||
export class AIRecommendationService {
|
||
private config: AIServiceConfig
|
||
private userProfiles: Map<string, UserProfile> = new Map()
|
||
private userBehaviors: Map<string, UserBehavior[]> = new Map()
|
||
private contentSimilarityCache: Map<string, SimilarityResult[]> = new Map()
|
||
private stats: RecommendationStats = {
|
||
totalRecommendations: 0,
|
||
clickThroughRate: 0,
|
||
averageEngagementTime: 0,
|
||
algorithmPerformance: {} as Record<RecommendationAlgorithm, any>,
|
||
categoryDistribution: {},
|
||
userSatisfactionScore: 0
|
||
}
|
||
|
||
constructor(config: AIServiceConfig) {
|
||
this.config = config
|
||
this.initializeStats()
|
||
}
|
||
|
||
/**
|
||
* 获取个性化推荐
|
||
* @param userId 用户ID
|
||
* @param availableContent 可用内容列表
|
||
* @param config 推荐配置
|
||
* @param context 推荐上下文
|
||
*/
|
||
async getPersonalizedRecommendations(
|
||
userId: string,
|
||
availableContent: ContentInfo[],
|
||
config: RecommendationConfig = {
|
||
algorithm: 'hybrid',
|
||
maxResults: 10,
|
||
diversityWeight: 0.3,
|
||
freshnessWeight: 0.2,
|
||
personalizedWeight: 0.5,
|
||
qualityThreshold: 0.7,
|
||
excludeViewed: true
|
||
},
|
||
context: RecommendationContext = { currentTime: Date.now(), recentViews: [] }
|
||
): Promise<AIResponse<RecommendationResult[]>> {
|
||
try {
|
||
this.stats.totalRecommendations++
|
||
|
||
// 获取或创建用户画像
|
||
const userProfile = await this.getUserProfile(userId)
|
||
|
||
// 过滤内容
|
||
let filteredContent = this.filterContent(availableContent, config, context, userProfile)
|
||
|
||
// 根据算法生成推荐
|
||
let recommendations: RecommendationResult[] = []
|
||
|
||
switch (config.algorithm) {
|
||
case 'collaborative_filtering':
|
||
recommendations = await this.collaborativeFiltering(userId, filteredContent, config, userProfile)
|
||
break
|
||
case 'content_based':
|
||
recommendations = await this.contentBasedFiltering(userId, filteredContent, config, userProfile)
|
||
break
|
||
case 'hybrid':
|
||
recommendations = await this.hybridRecommendation(userId, filteredContent, config, userProfile, context)
|
||
break
|
||
case 'trending':
|
||
recommendations = await this.trendingRecommendation(filteredContent, config, context)
|
||
break
|
||
case 'similarity':
|
||
recommendations = await this.similarityBasedRecommendation(userId, filteredContent, config, context)
|
||
break
|
||
case 'ml_based':
|
||
recommendations = await this.mlBasedRecommendation(userId, filteredContent, config, userProfile)
|
||
break
|
||
default:
|
||
recommendations = await this.hybridRecommendation(userId, filteredContent, config, userProfile, context)
|
||
}
|
||
|
||
// 应用多样性和质量过滤
|
||
recommendations = this.applyDiversityAndQuality(recommendations, config)
|
||
|
||
// 排序和截取
|
||
recommendations = recommendations
|
||
.sort((a, b) => b.score - a.score)
|
||
.slice(0, config.maxResults)
|
||
.map((rec, index) => ({ ...rec, position: index + 1 }))
|
||
|
||
// 更新统计
|
||
this.updateRecommendationStats(config.algorithm, recommendations)
|
||
|
||
return {
|
||
success: true,
|
||
data: recommendations,
|
||
provider: 'ai_recommendation'
|
||
}
|
||
|
||
} catch (error) {
|
||
const aiError: AIServiceError = {
|
||
code: 'RECOMMENDATION_ERROR',
|
||
message: error.message || 'Failed to generate recommendations',
|
||
retryable: false
|
||
}
|
||
|
||
return {
|
||
success: false,
|
||
error: aiError.message,
|
||
errorCode: aiError.code
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录用户行为
|
||
* @param behavior 用户行为数据
|
||
*/
|
||
async recordUserBehavior(behavior: UserBehavior): Promise<void> {
|
||
const userId = behavior.userId
|
||
|
||
if (!this.userBehaviors.has(userId)) {
|
||
this.userBehaviors.set(userId, [])
|
||
}
|
||
|
||
const behaviors = this.userBehaviors.get(userId)!
|
||
behaviors.push(behavior)
|
||
|
||
// 保持最近1000条行为记录
|
||
if (behaviors.length > 1000) {
|
||
behaviors.splice(0, behaviors.length - 1000)
|
||
}
|
||
|
||
// 异步更新用户画像
|
||
this.updateUserProfile(userId, behavior)
|
||
}
|
||
|
||
/**
|
||
* 获取相似内容推荐
|
||
* @param contentId 基准内容ID
|
||
* @param availableContent 可用内容列表
|
||
* @param maxResults 最大结果数
|
||
*/
|
||
async getSimilarContent(
|
||
contentId: string,
|
||
availableContent: ContentInfo[],
|
||
maxResults: number = 5
|
||
): Promise<AIResponse<RecommendationResult[]>> {
|
||
try {
|
||
const baseContent = availableContent.find(c => c.id === contentId)
|
||
if (!baseContent) {
|
||
throw new Error('Base content not found')
|
||
}
|
||
|
||
// 检查缓存
|
||
const cached = this.contentSimilarityCache.get(contentId)
|
||
if (cached) {
|
||
const recommendations = cached.slice(0, maxResults).map((sim, index) => ({
|
||
contentId: sim.contentId,
|
||
userId: '',
|
||
score: sim.similarity,
|
||
reason: sim.reasons.join(', '),
|
||
algorithm: 'similarity',
|
||
contextFactors: { similarityReasons: sim.reasons },
|
||
recommendationType: 'similar' as const,
|
||
position: index + 1,
|
||
createdAt: Date.now()
|
||
}))
|
||
|
||
return { success: true, data: recommendations }
|
||
}
|
||
|
||
// 计算相似度
|
||
const similarities = await this.calculateContentSimilarities(baseContent, availableContent)
|
||
|
||
// 缓存结果
|
||
this.contentSimilarityCache.set(contentId, similarities)
|
||
|
||
// 生成推荐结果
|
||
const recommendations = similarities.slice(0, maxResults).map((sim, index) => ({
|
||
contentId: sim.contentId,
|
||
userId: '',
|
||
score: sim.similarity,
|
||
reason: sim.reasons.join(', '),
|
||
algorithm: 'similarity',
|
||
contextFactors: { similarityReasons: sim.reasons },
|
||
recommendationType: 'similar' as const,
|
||
position: index + 1,
|
||
createdAt: Date.now()
|
||
}))
|
||
|
||
return { success: true, data: recommendations }
|
||
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error.message || 'Failed to get similar content'
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取热门推荐
|
||
* @param availableContent 可用内容列表
|
||
* @param timeRange 时间范围(小时)
|
||
* @param maxResults 最大结果数
|
||
*/
|
||
async getTrendingRecommendations(
|
||
availableContent: ContentInfo[],
|
||
timeRange: number = 24,
|
||
maxResults: number = 10
|
||
): Promise<AIResponse<RecommendationResult[]>> {
|
||
try {
|
||
const cutoffTime = Date.now() - (timeRange * 60 * 60 * 1000)
|
||
|
||
// 计算热门度分数
|
||
const trendingScores = availableContent.map(content => {
|
||
// 时间衰减因子
|
||
const ageHours = (Date.now() - content.publishedAt) / (1000 * 60 * 60)
|
||
const timeFactor = Math.exp(-ageHours / 24) // 24小时半衰期
|
||
|
||
// 互动分数
|
||
const engagementScore = (content.viewCount * 1 + content.likeCount * 3 + content.shareCount * 5) / 100
|
||
|
||
// 质量分数
|
||
const qualityScore = content.quality || 0.5
|
||
|
||
// 综合分数
|
||
const score = (engagementScore * 0.5 + qualityScore * 0.3 + timeFactor * 0.2)
|
||
|
||
return {
|
||
contentId: content.id,
|
||
score,
|
||
engagementScore,
|
||
timeFactor,
|
||
qualityScore
|
||
}
|
||
})
|
||
|
||
// 排序并生成推荐
|
||
const recommendations = trendingScores
|
||
.sort((a, b) => b.score - a.score)
|
||
.slice(0, maxResults)
|
||
.map((item, index) => ({
|
||
contentId: item.contentId,
|
||
userId: '',
|
||
score: item.score,
|
||
reason: `热门内容 - 互动度: ${item.engagementScore.toFixed(2)}, 新鲜度: ${item.timeFactor.toFixed(2)}`,
|
||
algorithm: 'trending',
|
||
contextFactors: {
|
||
engagementScore: item.engagementScore,
|
||
timeFactor: item.timeFactor,
|
||
qualityScore: item.qualityScore
|
||
},
|
||
recommendationType: 'trending' as const,
|
||
position: index + 1,
|
||
createdAt: Date.now()
|
||
}))
|
||
|
||
return { success: true, data: recommendations }
|
||
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error.message || 'Failed to get trending recommendations'
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量生成推荐
|
||
* @param userIds 用户ID列表
|
||
* @param availableContent 可用内容列表
|
||
* @param config 推荐配置
|
||
* @param batchOptions 批处理选项
|
||
*/
|
||
async generateBatchRecommendations(
|
||
userIds: string[],
|
||
availableContent: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
batchOptions: BatchProcessingOptions = {
|
||
batchSize: 10,
|
||
concurrency: 3,
|
||
retryCount: 2,
|
||
delayMs: 100
|
||
}
|
||
): Promise<AIResponse<Record<string, RecommendationResult[]>>> {
|
||
try {
|
||
const results: Record<string, RecommendationResult[]> = {}
|
||
const batches = this.createBatches(userIds, batchOptions.batchSize)
|
||
|
||
for (let i = 0; i < batches.length; i++) {
|
||
const batch = batches[i]
|
||
const batchPromises = batch.map(async (userId) => {
|
||
try {
|
||
const response = await this.getPersonalizedRecommendations(userId, availableContent, config)
|
||
if (response.success && response.data) {
|
||
return { userId, recommendations: response.data }
|
||
}
|
||
throw new Error(response.error || 'Recommendation failed')
|
||
} catch (error) {
|
||
if (batchOptions.onError) {
|
||
batchOptions.onError(error, userId)
|
||
}
|
||
return { userId, recommendations: [] }
|
||
}
|
||
})
|
||
|
||
const batchResults = await Promise.allSettled(batchPromises)
|
||
|
||
for (const result of batchResults) {
|
||
if (result.status === 'fulfilled') {
|
||
results[result.value.userId] = result.value.recommendations
|
||
}
|
||
}
|
||
|
||
// 进度回调
|
||
if (batchOptions.onProgress) {
|
||
batchOptions.onProgress(Object.keys(results).length, userIds.length)
|
||
}
|
||
|
||
// 批次间延迟
|
||
if (i < batches.length - 1 && batchOptions.delayMs > 0) {
|
||
await this.delay(batchOptions.delayMs)
|
||
}
|
||
}
|
||
|
||
return { success: true, data: results }
|
||
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error.message || 'Batch recommendation generation failed'
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取推荐统计
|
||
*/
|
||
getRecommendationStatistics(): RecommendationStats {
|
||
return { ...this.stats }
|
||
}
|
||
|
||
/**
|
||
* 获取用户画像
|
||
* @param userId 用户ID
|
||
*/
|
||
async getUserProfile(userId: string): Promise<UserProfile> {
|
||
let profile = this.userProfiles.get(userId)
|
||
|
||
if (!profile) {
|
||
profile = this.createDefaultUserProfile(userId)
|
||
this.userProfiles.set(userId, profile)
|
||
}
|
||
|
||
return profile
|
||
}
|
||
|
||
/**
|
||
* 更新用户画像
|
||
* @param userId 用户ID
|
||
* @param behavior 用户行为
|
||
*/
|
||
private async updateUserProfile(userId: string, behavior: UserBehavior): Promise<void> {
|
||
const profile = await this.getUserProfile(userId)
|
||
|
||
// 更新行为统计
|
||
const behaviors = this.userBehaviors.get(userId) || []
|
||
|
||
// 计算平均阅读时长
|
||
const readingTimes = behaviors
|
||
.filter(b => b.duration && b.duration > 0)
|
||
.map(b => b.duration!)
|
||
|
||
if (readingTimes.length > 0) {
|
||
profile.behavior.readingTime = readingTimes.reduce((sum, time) => sum + time, 0) / readingTimes.length
|
||
}
|
||
|
||
// 更新活跃时段
|
||
const hour = new Date(behavior.timestamp).getHours()
|
||
if (!profile.behavior.activeHours.includes(hour)) {
|
||
profile.behavior.activeHours.push(hour)
|
||
}
|
||
|
||
// 更新互动率
|
||
const engagementActions = behaviors.filter(b =>
|
||
['like', 'share', 'comment', 'save'].includes(b.actionType)
|
||
).length
|
||
const totalActions = behaviors.length
|
||
profile.behavior.engagementRate = totalActions > 0 ? engagementActions / totalActions : 0
|
||
|
||
// 根据行为类型更新偏好权重
|
||
if (behavior.actionType === 'like' || behavior.actionType === 'share') {
|
||
// 增加对应内容的权重(这里需要内容信息,暂时跳过)
|
||
}
|
||
|
||
profile.lastUpdated = Date.now()
|
||
}
|
||
|
||
// Private methods
|
||
|
||
private async collaborativeFiltering(
|
||
userId: string,
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
userProfile: UserProfile
|
||
): Promise<RecommendationResult[]> {
|
||
// 简化的协同过滤实现
|
||
const recommendations: RecommendationResult[] = []
|
||
|
||
// 找到相似用户(基于兴趣分类相似度)
|
||
const similarUsers = await this.findSimilarUsers(userId, userProfile)
|
||
|
||
// 获取相似用户喜欢的内容
|
||
for (const content_item of content) {
|
||
let score = 0
|
||
let voterCount = 0
|
||
|
||
for (const similarUser of similarUsers) {
|
||
const behaviors = this.userBehaviors.get(similarUser.userId) || []
|
||
const interaction = behaviors.find(b => b.contentId === content_item.id)
|
||
|
||
if (interaction) {
|
||
const weight = similarUser.similarity
|
||
if (interaction.actionType === 'like') score += 0.8 * weight
|
||
else if (interaction.actionType === 'share') score += 1.0 * weight
|
||
else if (interaction.actionType === 'view') score += 0.3 * weight
|
||
voterCount++
|
||
}
|
||
}
|
||
|
||
if (voterCount > 0) {
|
||
score /= voterCount
|
||
recommendations.push({
|
||
contentId: content_item.id,
|
||
userId,
|
||
score,
|
||
reason: `基于${voterCount}个相似用户的偏好`,
|
||
algorithm: 'collaborative_filtering',
|
||
contextFactors: { voterCount, similarUserCount: similarUsers.length },
|
||
recommendationType: 'personalized',
|
||
position: 0,
|
||
createdAt: Date.now()
|
||
})
|
||
}
|
||
}
|
||
|
||
return recommendations
|
||
}
|
||
|
||
private async contentBasedFiltering(
|
||
userId: string,
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
userProfile: UserProfile
|
||
): Promise<RecommendationResult[]> {
|
||
const recommendations: RecommendationResult[] = []
|
||
|
||
for (const item of content) {
|
||
let score = 0
|
||
const reasons: string[] = []
|
||
|
||
// 分类匹配
|
||
if (item.categoryId && userProfile.interests.categories[item.categoryId]) {
|
||
const categoryScore = userProfile.interests.categories[item.categoryId]
|
||
score += categoryScore * 0.4
|
||
reasons.push(`分类偏好: ${categoryScore.toFixed(2)}`)
|
||
}
|
||
|
||
// 关键词匹配
|
||
let keywordScore = 0
|
||
let keywordCount = 0
|
||
for (const keyword of item.keywords) {
|
||
if (userProfile.interests.keywords[keyword]) {
|
||
keywordScore += userProfile.interests.keywords[keyword]
|
||
keywordCount++
|
||
}
|
||
}
|
||
if (keywordCount > 0) {
|
||
keywordScore /= keywordCount
|
||
score += keywordScore * 0.3
|
||
reasons.push(`关键词匹配: ${keywordCount}个`)
|
||
}
|
||
|
||
// 质量分数
|
||
score += (item.quality || 0.5) * 0.2
|
||
|
||
// 时效性
|
||
const ageHours = (Date.now() - item.publishedAt) / (1000 * 60 * 60)
|
||
const freshness = Math.exp(-ageHours / 24)
|
||
score += freshness * 0.1
|
||
reasons.push(`新鲜度: ${freshness.toFixed(2)}`)
|
||
|
||
recommendations.push({
|
||
contentId: item.id,
|
||
userId,
|
||
score,
|
||
reason: reasons.join(', '),
|
||
algorithm: 'content_based',
|
||
contextFactors: {
|
||
categoryScore: userProfile.interests.categories[item.categoryId || ''] || 0,
|
||
keywordScore,
|
||
qualityScore: item.quality || 0.5,
|
||
freshness
|
||
},
|
||
recommendationType: 'personalized',
|
||
position: 0,
|
||
createdAt: Date.now()
|
||
})
|
||
}
|
||
|
||
return recommendations
|
||
}
|
||
|
||
private async hybridRecommendation(
|
||
userId: string,
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
userProfile: UserProfile,
|
||
context: RecommendationContext
|
||
): Promise<RecommendationResult[]> {
|
||
// 结合多种算法
|
||
const [
|
||
collaborativeRecs,
|
||
contentBasedRecs,
|
||
trendingRecs
|
||
] = await Promise.all([
|
||
this.collaborativeFiltering(userId, content, config, userProfile),
|
||
this.contentBasedFiltering(userId, content, config, userProfile),
|
||
this.trendingRecommendation(content, config, context)
|
||
])
|
||
|
||
// 合并和重新评分
|
||
const hybridRecs: RecommendationResult[] = []
|
||
const contentScores: Record<string, {
|
||
collaborative: number,
|
||
contentBased: number,
|
||
trending: number
|
||
}> = {}
|
||
|
||
// 收集各算法的分数
|
||
collaborativeRecs.forEach(rec => {
|
||
if (!contentScores[rec.contentId]) contentScores[rec.contentId] = { collaborative: 0, contentBased: 0, trending: 0 }
|
||
contentScores[rec.contentId].collaborative = rec.score
|
||
})
|
||
|
||
contentBasedRecs.forEach(rec => {
|
||
if (!contentScores[rec.contentId]) contentScores[rec.contentId] = { collaborative: 0, contentBased: 0, trending: 0 }
|
||
contentScores[rec.contentId].contentBased = rec.score
|
||
})
|
||
|
||
trendingRecs.forEach(rec => {
|
||
if (!contentScores[rec.contentId]) contentScores[rec.contentId] = { collaborative: 0, contentBased: 0, trending: 0 }
|
||
contentScores[rec.contentId].trending = rec.score
|
||
})
|
||
|
||
// 计算混合分数
|
||
for (const [contentId, scores] of Object.entries(contentScores)) {
|
||
const hybridScore = (
|
||
scores.collaborative * 0.4 +
|
||
scores.contentBased * 0.4 +
|
||
scores.trending * 0.2
|
||
)
|
||
|
||
hybridRecs.push({
|
||
contentId,
|
||
userId,
|
||
score: hybridScore,
|
||
reason: `混合算法 - 协同: ${scores.collaborative.toFixed(2)}, 内容: ${scores.contentBased.toFixed(2)}, 热门: ${scores.trending.toFixed(2)}`,
|
||
algorithm: 'hybrid',
|
||
contextFactors: scores,
|
||
recommendationType: 'personalized',
|
||
position: 0,
|
||
createdAt: Date.now()
|
||
})
|
||
}
|
||
|
||
return hybridRecs
|
||
}
|
||
|
||
private async trendingRecommendation(
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
context: RecommendationContext
|
||
): Promise<RecommendationResult[]> {
|
||
const response = await this.getTrendingRecommendations(content, config.timeRange || 24, content.length)
|
||
return response.data || []
|
||
}
|
||
|
||
private async similarityBasedRecommendation(
|
||
userId: string,
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
context: RecommendationContext
|
||
): Promise<RecommendationResult[]> {
|
||
if (!context.recentViews || context.recentViews.length === 0) {
|
||
return []
|
||
}
|
||
|
||
const recommendations: RecommendationResult[] = []
|
||
|
||
// 基于最近浏览的内容推荐相似内容
|
||
for (const viewedContentId of context.recentViews.slice(-3)) {
|
||
const response = await this.getSimilarContent(viewedContentId, content, 3)
|
||
if (response.success && response.data) {
|
||
recommendations.push(...response.data.map(rec => ({
|
||
...rec,
|
||
userId,
|
||
algorithm: 'similarity' as const,
|
||
recommendationType: 'similar' as const
|
||
})))
|
||
}
|
||
}
|
||
|
||
return recommendations
|
||
}
|
||
|
||
private async mlBasedRecommendation(
|
||
userId: string,
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
userProfile: UserProfile
|
||
): Promise<RecommendationResult[]> {
|
||
// 简化的ML基础推荐,实际实现需要训练模型
|
||
const recommendations: RecommendationResult[] = []
|
||
|
||
for (const item of content) {
|
||
// 特征提取
|
||
const features = this.extractFeatures(item, userProfile)
|
||
|
||
// 简单的线性评分模型
|
||
const score = this.calculateMLScore(features)
|
||
|
||
recommendations.push({
|
||
contentId: item.id,
|
||
userId,
|
||
score,
|
||
reason: `ML模型预测分数: ${score.toFixed(3)}`,
|
||
algorithm: 'ml_based',
|
||
contextFactors: { features },
|
||
recommendationType: 'personalized',
|
||
position: 0,
|
||
createdAt: Date.now()
|
||
})
|
||
}
|
||
|
||
return recommendations
|
||
}
|
||
|
||
private extractFeatures(content: ContentInfo, userProfile: UserProfile): number[] {
|
||
const features: number[] = []
|
||
|
||
// 内容特征
|
||
features.push(content.quality || 0.5) // 质量分数
|
||
features.push(content.viewCount / 10000) // 标准化浏览数
|
||
features.push(content.likeCount / 1000) // 标准化点赞数
|
||
features.push(content.shareCount / 100) // 标准化分享数
|
||
|
||
// 时间特征
|
||
const ageHours = (Date.now() - content.publishedAt) / (1000 * 60 * 60)
|
||
features.push(Math.exp(-ageHours / 24)) // 时效性
|
||
|
||
// 用户偏好匹配
|
||
const categoryPreference = userProfile.interests.categories[content.categoryId || ''] || 0
|
||
features.push(categoryPreference)
|
||
|
||
// 关键词匹配度
|
||
let keywordMatch = 0
|
||
for (const keyword of content.keywords) {
|
||
keywordMatch += userProfile.interests.keywords[keyword] || 0
|
||
}
|
||
features.push(keywordMatch / Math.max(content.keywords.length, 1))
|
||
|
||
return features
|
||
}
|
||
|
||
private calculateMLScore(features: number[]): number {
|
||
// 简单的线性模型权重
|
||
const weights = [0.2, 0.1, 0.15, 0.1, 0.15, 0.2, 0.1]
|
||
|
||
let score = 0
|
||
for (let i = 0; i < Math.min(features.length, weights.length); i++) {
|
||
score += features[i] * weights[i]
|
||
}
|
||
|
||
return Math.max(0, Math.min(1, score))
|
||
}
|
||
|
||
private filterContent(
|
||
content: ContentInfo[],
|
||
config: RecommendationConfig,
|
||
context: RecommendationContext,
|
||
userProfile: UserProfile
|
||
): ContentInfo[] {
|
||
return content.filter(item => {
|
||
// 质量过滤
|
||
if ((item.quality || 0) < config.qualityThreshold) return false
|
||
|
||
// 排除已浏览
|
||
if (config.excludeViewed && context.recentViews.includes(item.id)) return false
|
||
|
||
// 分类过滤
|
||
if (config.includeCategories && config.includeCategories.length > 0) {
|
||
if (!item.categoryId || !config.includeCategories.includes(item.categoryId)) return false
|
||
}
|
||
|
||
if (config.excludeCategories && config.excludeCategories.length > 0) {
|
||
if (item.categoryId && config.excludeCategories.includes(item.categoryId)) return false
|
||
}
|
||
|
||
// 时间范围过滤
|
||
if (config.timeRange) {
|
||
const cutoffTime = Date.now() - (config.timeRange * 60 * 60 * 1000)
|
||
if (item.publishedAt < cutoffTime) return false
|
||
}
|
||
|
||
return true
|
||
})
|
||
}
|
||
|
||
private applyDiversityAndQuality(
|
||
recommendations: RecommendationResult[],
|
||
config: RecommendationConfig
|
||
): RecommendationResult[] {
|
||
if (config.diversityWeight <= 0) return recommendations
|
||
|
||
const diversified: RecommendationResult[] = []
|
||
const selectedCategories: Set<string> = new Set()
|
||
|
||
// 按分数排序
|
||
const sorted = [...recommendations].sort((a, b) => b.score - a.score)
|
||
|
||
for (const rec of sorted) {
|
||
// 简单的多样性检查(基于内容ID的哈希)
|
||
const categoryHash = rec.contentId.substring(0, 3) // 简化的分类标识
|
||
|
||
if (!selectedCategories.has(categoryHash) || diversified.length < config.maxResults / 2) {
|
||
diversified.push(rec)
|
||
selectedCategories.add(categoryHash)
|
||
|
||
if (diversified.length >= config.maxResults) break
|
||
}
|
||
}
|
||
|
||
return diversified
|
||
}
|
||
|
||
private async calculateContentSimilarities(
|
||
baseContent: ContentInfo,
|
||
availableContent: ContentInfo[]
|
||
): Promise<SimilarityResult[]> {
|
||
const similarities: SimilarityResult[] = []
|
||
|
||
for (const content of availableContent) {
|
||
if (content.id === baseContent.id) continue
|
||
|
||
let similarity = 0
|
||
const reasons: string[] = []
|
||
|
||
// 分类相似度
|
||
if (content.categoryId === baseContent.categoryId) {
|
||
similarity += 0.4
|
||
reasons.push('相同分类')
|
||
}
|
||
|
||
// 关键词相似度
|
||
const commonKeywords = content.keywords.filter(k => baseContent.keywords.includes(k))
|
||
if (commonKeywords.length > 0) {
|
||
const keywordSimilarity = commonKeywords.length / Math.max(content.keywords.length, baseContent.keywords.length)
|
||
similarity += keywordSimilarity * 0.3
|
||
reasons.push(`共同关键词: ${commonKeywords.length}个`)
|
||
}
|
||
|
||
// 标题相似度(简化版)
|
||
const titleSimilarity = this.calculateTextSimilarity(content.title, baseContent.title)
|
||
similarity += titleSimilarity * 0.2
|
||
if (titleSimilarity > 0.3) {
|
||
reasons.push('标题相似')
|
||
}
|
||
|
||
// 时间相似度
|
||
const timeDiff = Math.abs(content.publishedAt - baseContent.publishedAt)
|
||
const timeHours = timeDiff / (1000 * 60 * 60)
|
||
const timeSimilarity = Math.exp(-timeHours / 168) // 一周半衰期
|
||
similarity += timeSimilarity * 0.1
|
||
|
||
if (similarity > 0.1) {
|
||
similarities.push({
|
||
contentId: content.id,
|
||
similarity,
|
||
reasons
|
||
})
|
||
}
|
||
}
|
||
|
||
return similarities.sort((a, b) => b.similarity - a.similarity)
|
||
}
|
||
|
||
private calculateTextSimilarity(text1: string, text2: string): number {
|
||
// 简单的文本相似度计算
|
||
const words1 = new Set(text1.toLowerCase().split(/\s+/))
|
||
const words2 = new Set(text2.toLowerCase().split(/\s+/))
|
||
|
||
const intersection = new Set([...words1].filter(x => words2.has(x)))
|
||
const union = new Set([...words1, ...words2])
|
||
|
||
return intersection.size / union.size
|
||
}
|
||
|
||
private async findSimilarUsers(userId: string, userProfile: UserProfile): Promise<Array<{ userId: string, similarity: number }>> {
|
||
const similarUsers: Array<{ userId: string, similarity: number }> = []
|
||
|
||
// 遍历所有其他用户(实际实现中应该有更高效的方法)
|
||
for (const [otherUserId, otherProfile] of this.userProfiles.entries()) {
|
||
if (otherUserId === userId) continue
|
||
|
||
const similarity = this.calculateUserSimilarity(userProfile, otherProfile)
|
||
if (similarity > 0.5) {
|
||
similarUsers.push({ userId: otherUserId, similarity })
|
||
}
|
||
}
|
||
|
||
return similarUsers.sort((a, b) => b.similarity - a.similarity).slice(0, 10)
|
||
}
|
||
|
||
private calculateUserSimilarity(profile1: UserProfile, profile2: UserProfile): number {
|
||
let similarity = 0
|
||
|
||
// 分类偏好相似度
|
||
const categories1 = Object.keys(profile1.interests.categories)
|
||
const categories2 = Object.keys(profile2.interests.categories)
|
||
const commonCategories = categories1.filter(c => categories2.includes(c))
|
||
|
||
if (commonCategories.length > 0) {
|
||
let categoryScore = 0
|
||
for (const category of commonCategories) {
|
||
const score1 = profile1.interests.categories[category]
|
||
const score2 = profile2.interests.categories[category]
|
||
categoryScore += 1 - Math.abs(score1 - score2)
|
||
}
|
||
similarity += (categoryScore / commonCategories.length) * 0.6
|
||
}
|
||
|
||
// 行为相似度
|
||
const behavior1 = profile1.behavior
|
||
const behavior2 = profile2.behavior
|
||
|
||
const readingTimeSimilarity = 1 - Math.abs(behavior1.readingTime - behavior2.readingTime) / Math.max(behavior1.readingTime, behavior2.readingTime, 1)
|
||
const engagementSimilarity = 1 - Math.abs(behavior1.engagementRate - behavior2.engagementRate)
|
||
|
||
similarity += (readingTimeSimilarity + engagementSimilarity) * 0.2
|
||
|
||
return similarity
|
||
}
|
||
|
||
private createDefaultUserProfile(userId: string): UserProfile {
|
||
return {
|
||
userId,
|
||
demographics: {},
|
||
interests: {
|
||
categories: {},
|
||
keywords: {},
|
||
sources: {},
|
||
languages: { 'zh-CN': 1.0 }
|
||
},
|
||
behavior: {
|
||
readingTime: 120, // 默认2分钟
|
||
activeHours: [],
|
||
preferredContentLength: 'medium',
|
||
engagementRate: 0
|
||
},
|
||
preferences: {
|
||
newsStyle: 'brief',
|
||
updateFrequency: 'daily',
|
||
contentFreshness: 0.7,
|
||
diversityLevel: 0.5
|
||
},
|
||
lastUpdated: Date.now()
|
||
}
|
||
}
|
||
|
||
private createBatches<T>(items: T[], batchSize: number): T[][] {
|
||
const batches: T[][] = []
|
||
for (let i = 0; i < items.length; i += batchSize) {
|
||
batches.push(items.slice(i, i + batchSize))
|
||
}
|
||
return batches
|
||
}
|
||
|
||
private async delay(ms: number): Promise<void> {
|
||
return new Promise(resolve => setTimeout(resolve, ms))
|
||
}
|
||
|
||
private initializeStats(): void {
|
||
const algorithms: RecommendationAlgorithm[] = [
|
||
'collaborative_filtering', 'content_based', 'hybrid',
|
||
'trending', 'similarity', 'ml_based'
|
||
]
|
||
|
||
algorithms.forEach(algorithm => {
|
||
this.stats.algorithmPerformance[algorithm] = {
|
||
usage: 0,
|
||
successRate: 1.0,
|
||
avgScore: 0.5
|
||
}
|
||
})
|
||
}
|
||
|
||
private updateRecommendationStats(algorithm: RecommendationAlgorithm, recommendations: RecommendationResult[]): void {
|
||
this.stats.algorithmPerformance[algorithm].usage++
|
||
|
||
if (recommendations.length > 0) {
|
||
const avgScore = recommendations.reduce((sum, rec) => sum + rec.score, 0) / recommendations.length
|
||
const current = this.stats.algorithmPerformance[algorithm]
|
||
current.avgScore = (current.avgScore * (current.usage - 1) + avgScore) / current.usage
|
||
}
|
||
}
|
||
}
|