Files
akmon/uni_modules/ak-ai-news/services/AIRecommendationService.uts
2026-01-20 08:04:15 +08:00

1028 lines
32 KiB
Plaintext
Raw Permalink 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.
// 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
}
}
}