کالبدشکافی معماری الگوریتم توصیهگر X: از بازیابی تا رتبهبندی و پیادهسازی در داتنت
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۷ ۰۸:۵۰
آدرس: www.dntips.ir
چکیده: با متنباز شدن بخشهای کلیدی مخزن الگوریتم پلتفرم X (توییتر سابق)، امکان تحلیل دقیق زیرساخت پیشنهاد محتوا (Recommendation System) در مقیاس بسیار بزرگ فراهم شده است. این سیستم بر خلاف مدلهای سنتی که بر محبوبیت سراسری متمرکز بودند، بر شخصیسازی بلادرنگ (Real-time Personalization) و پیشبینی چندهدفه رفتار کاربر (Multi-Task Learning / User Action Prediction) تکیه دارد. در این مقاله، معماری چندمرحلهای خط لوله توزیع محتوا (Candidate Generation، Scoring & Ranking، و Visibility Filtering) بررسی میشود. همچنین فناوریها و مفاهیمی چون Two-Tower Neural Networks، SimClusters، گراف اعتبار user-cred-v2 و اثر کاهشی جریمههای منفی به همراه تنوعبخشی به فید بازخوانی خواهند شد. در پایان، یک پیادهسازی ماژولار، استاندارد و شیءگرا با زبان #C و فریمورک داتنت برای شبیهسازی این خط لوله ارائه میشود.
+-------------------------------------------------------------------------+
| Candidate Generation |
| In-Network (Thunder) + Out-of-Network / OON (Phoenix Two-Tower, |
| SimClusters) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Pre-Filtering |
| AgeFilter (e.g. <= 48h), Blocklist, Hard Content Filters |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Scoring & Heavy Ranking |
| Probability Prediction: P(Action_i) * Weight_i |
| User Credential Graph (user-cred-v2 PageRank) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Diversity & Visibility Rules (Hydration) |
| Author Diversity Decay, Safety Labels (ALLOW / INTERSTITIAL / DROP) |
+-------------------------------------------------------------------------+
|
v
Rendered For YouAgeFilter معمولاً پستهای قدیمیتر از یک بازه زمانی معین (مانند ۴۸ ساعت در مسیرهای پرسرعت) را از چرخه کاندیداتوری داغ خارج میکند تا فید تازگی زمانی (Freshness) خود را حفظ کند.Copy Link، DM Share، Reply و Quote وزن بسیار بالاتری نسبت به یک Like ساده دارند، زیرا نشاندهنده ارزش واقعی محتوا برای نگهداری یا ارسال به دیگران هستند.Report، Mute و Not Interested بسیار سنگین است؛ بنابراین محتواهایی با استراتژی خشمآفرینی (Rage Bait) در صورت دریافت گزارش یا میوت بالا، بلافاصله افت رتبه شدیدی پیدا میکنند.user-cred-v2)ALLOW: نمایش عادی.INTERSTITIAL: نمایش محتوا همراه با لایه هشدار (مانند محتوای حساس).DROP: حذف کامل از فید توصیهای.using System;
using System.Collections.Generic;
using System.Linq;
namespace RecommendationEngine.Core
{
public sealed record PostCandidate(
string PostId,
string AuthorId,
DateTime CreatedAtUtc,
bool IsInNetwork,
double SimClusterAffinity
);
public sealed record ActionProbabilities(
double PLike,
double PReply,
double PShareOrDm,
double PCopyLink,
double PFollowAuthor,
double PNegativeSignal // تجمیع احتمال Report، Block یا Mute
);
public enum VisibilityStatus
{
Allow,
Interstitial,
Drop
}
public sealed record ScoredPost(
PostCandidate Candidate,
double RawScore,
double FinalScore,
VisibilityStatus Visibility
);
public interface ICandidateRanker
{
IEnumerable<ScoredPost> RankAndFilter(
string userId,
IEnumerable<PostCandidate> candidates,
Func<PostCandidate, ActionProbabilities> inferenceModel,
Func<PostCandidate, VisibilityStatus> safetyCheck,
Func<string, double> getAuthorReputationScore
);
}
public class ForYouFeedRanker : ICandidateRanker
{
// ضرایب تجربی متناسب با معماری سیستم
private const double WeightLike = 0.5;
private const double WeightReply = 5.0;
private const double WeightShareOrDm = 5.0;
private const double WeightCopyLink = 20.0;
private const double WeightFollow = 4.0;
private const double WeightNegative = 200.0;
private const double AuthorDiversityDecay = 0.65;
private readonly TimeSpan _maxAge = TimeSpan.FromHours(48);
public IEnumerable<ScoredPost> RankAndFilter(
string userId,
IEnumerable<PostCandidate> candidates,
Func<PostCandidate, ActionProbabilities> inferenceModel,
Func<PostCandidate, VisibilityStatus> safetyCheck,
Func<string, double> getAuthorReputationScore)
{
var now = DateTime.UtcNow;
var scoredList = new List<ScoredPost>();
foreach (var post in candidates)
{
// ۱. فیلتر سن پست (Age Filter)
if (now - post.CreatedAtUtc > _maxAge)
continue;
// ۲. ارزیابی اولیه ایمنی و دسترسی (Visibility Check)
var visibility = safetyCheck(post);
if (visibility == VisibilityStatus.Drop)
continue;
// ۳. استنتاج احتمالات رفتار کاربر (Multi-Action Scoring)
var probs = inferenceModel(post);
double rawScore =
(probs.PLike * WeightLike) +
(probs.PReply * WeightReply) +
(probs.PShareOrDm * WeightShareOrDm) +
(probs.PCopyLink * WeightCopyLink) +
(probs.PFollowAuthor * WeightFollow) -
(probs.PNegativeSignal * WeightNegative);
// ۴. ترکیب با وزن اعتبار کاربر و همبستگی خوشهای
double authorReputation = getAuthorReputationScore(post.AuthorId);
double clusterMultiplier = post.IsInNetwork ? 1.0 : (1.0 + post.SimClusterAffinity);
double calculatedScore = rawScore * authorReputation * clusterMultiplier;
scoredList.Add(new ScoredPost(post, rawScore, calculatedScore, visibility));
}
// ۵. اعمال تنوع نویسندگان (Author Diversity Decay) و مرتبسازی نهایی
return ApplyDiversityAndSort(scoredList);
}
private static IEnumerable<ScoredPost> ApplyDiversityAndSort(List<ScoredPost> posts)
{
var authorSeenCount = new Dictionary<string, int>();
var finalList = new List<ScoredPost>(posts.Count);
foreach (var item in posts.OrderByDescending(p => p.FinalScore))
{
var authorId = item.Candidate.AuthorId;
authorSeenCount.TryGetValue(authorId, out int seenCount);
// کاهش هندسی امتیاز برای پستهای مکرر از یک نویسنده
double decayFactor = Math.Pow(AuthorDiversityDecay, seenCount);
double adjustedScore = item.FinalScore * decayFactor;
authorSeenCount[authorId] = seenCount + 1;
finalList.Add(item with { FinalScore = adjustedScore });
}
return finalList.OrderByDescending(p => p.FinalScore);
}
}
public static class Program
{
public static void Main()
{
var ranker = new ForYouFeedRanker();
var sampleCandidates = new List<PostCandidate>
{
new("P101", "DevMaster", DateTime.UtcNow.AddHours(-2), IsInNetwork: false, SimClusterAffinity: 0.8),
new("P102", "DevMaster", DateTime.UtcNow.AddHours(-3), IsInNetwork: false, SimClusterAffinity: 0.8),
new("P103", "ClickBaiter", DateTime.UtcNow.AddHours(-1), IsInNetwork: true, SimClusterAffinity: 0.2),
new("P104", "SafeAuthor", DateTime.UtcNow.AddDays(-3), IsInNetwork: true, SimClusterAffinity: 0.5) // حذف بر اساس سن
};
var feed = ranker.RankAndFilter(
userId: "User_99",
candidates: sampleCandidates,
inferenceModel: post => post.PostId switch
{
"P101" => new ActionProbabilities(0.4, 0.2, 0.1, 0.08, 0.05, 0.001),
"P102" => new ActionProbabilities(0.3, 0.1, 0.05, 0.02, 0.01, 0.001),
"P103" => new ActionProbabilities(0.8, 0.5, 0.01, 0.00, 0.00, 0.04), // سیگنال منفی بالا
_ => new ActionProbabilities(0.1, 0.01, 0.0, 0.0, 0.0, 0.0)
},
safetyCheck: _ => VisibilityStatus.Allow,
getAuthorReputationScore: author => author == "DevMaster" ? 1.4 : 1.0
);
Console.WriteLine($"{"PostId",-8} | {"Author",-12} | {"Raw Score",-10:F2} | {"Final Score",-12:F2} | Status");
Console.WriteLine(new string('-', 55));
foreach (var post in feed)
{
Console.WriteLine($"{post.Candidate.PostId,-8} | {post.Candidate.AuthorId,-12} | {post.RawScore,-10:F2} | {post.FinalScore,-12:F2} | {post.Visibility}");
}
}
}
}SimClusters) و مدلهای Two-Tower، یک محتوای باکیفیت حتی بدون داشتن دنبالکنندگان پرشمار، شانس دیدهشدن گسترده در سطح شبکه را داراست.