- Architecture modulaire avec injection de dépendances - Système de scoring intelligent multi-facteurs (spécificité, fraîcheur, qualité, réutilisation) - Moteur anti-injection 4 couches (preprocessing, patterns, sémantique, pénalités) - API REST complète avec validation et rate limiting - Repository JSON avec index mémoire et backup automatique - Provider LLM modulaire pour génération de contenu - Suite de tests complète (Jest) : * Tests unitaires pour sécurité et scoring * Tests d'intégration API end-to-end * Tests de sécurité avec simulation d'attaques * Tests de performance et charge - Pipeline CI/CD avec GitHub Actions - Logging structuré et monitoring - Configuration ESLint et environnement de test 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
23 KiB
Système de Scoring Intelligent SourceFinder
Vue d'Ensemble
Le système de scoring de SourceFinder évalue intelligemment la pertinence des articles d'actualités canines selon quatre critères pondérés, conformément aux spécifications du CDC (Cahier des Charges). Chaque article reçoit un score final de 0 à 100 points selon la formule :
Score Final = (Spécificité × 0.4) + (Fraîcheur × 0.3) + (Qualité × 0.2) + (Réutilisabilité × 0.1)
Cette approche multi-critères garantit une sélection équilibrée entre pertinence thématique, actualité, fiabilité des sources et optimisation de la réutilisation du contenu.
Architecture Modulaire
Organisation des Composants
BasicScoringEngine (Orchestrateur principal)
├── SpecificityCalculator (40% du score)
├── FreshnessCalculator (30% du score)
├── QualityCalculator (20% du score)
└── ReuseCalculator (10% du score)
Chaque calculateur est indépendant et interchangeable, respectant le principe d'architecture modulaire du système.
Interface IScoringEngine
Tous les moteurs de scoring implémentent cette interface standardisée :
interface IScoringEngine {
async scoreArticle(newsItem, context): Promise<ScoringResult>
async batchScore(newsItems, context): Promise<Array<ScoredArticle>>
explainScore(scoredArticle): Object
}
1. Calculateur de Spécificité (40% du Score)
Principe
La spécificité évalue la pertinence du contenu par rapport à la race de chien recherchée. C'est le critère le plus important car il détermine directement l'utilité de l'article pour le client final.
Hiérarchie de Scoring
| Niveau | Score | Critère | Exemple |
|---|---|---|---|
| Mention Exacte | 100 pts | Nom exact de la race trouvé | "Berger Allemand", "Golden Retriever" |
| Groupe/Famille | 70 pts | Famille de race mentionnée | "Chiens de berger", "Retrievers" |
| Taille Similaire | 50 pts | Catégorie de taille | "Grands chiens", "Petite race" |
| Usage Similaire | 40 pts | Usage/fonction similaire | "Chien de garde", "Chien de famille" |
| Générique Chiens | 25 pts | Mention générale canine | "Chiens", "Compagnons" |
| Animaux Domestiques | 10 pts | Contexte animal général | "Animaux de compagnie" |
Base de Données des Races
Le système intègre une base de données des races FCI avec :
// Exemple : Berger Allemand (352-1)
{
name: 'berger allemand',
variants: ['german shepherd', 'berger d\'allemagne'],
group: 'chiens de berger',
families: ['bergers', 'chiens de troupeau'],
size: 'grands chiens',
usages: ['chien de garde', 'chien de travail', 'chien policier']
}
Algorithme de Détection
- Normalisation du contenu : Conversion en minuscules, suppression de la ponctuation
- Recherche par regex : Détection des mots-clés avec délimiteurs (
\b) - Scoring hiérarchique : Attribution du score le plus élevé trouvé
- Traçabilité : Enregistrement des termes correspondants pour audit
Cas Spéciaux
- Races composées : "Berger Allemand à poil long" → Détection du nom principal
- Synonymes multiples : "Labrador" → "Labrador Retriever"
- Variantes linguistiques : "German Shepherd" → "Berger Allemand"
2. Calculateur de Fraîcheur (30% du Score)
Principe
La fraîcheur évalue la récence de l'article. Plus un article est récent, plus il est susceptible d'être pertinent pour la génération de contenu actualisé.
Seuils d'Évaluation
| Catégorie | Âge | Score | Usage Recommandé |
|---|---|---|---|
| Excellent | < 7 jours | 100 pts | Actualités urgentes |
| Bon | 7-30 jours | 70 pts | Contenu récent |
| Correct | 30-90 jours | 40 pts | Informations générales |
| Ancien | 90-180 jours | 20 pts | Contenu de référence |
| Obsolète | > 180 jours | 5 pts | Archives uniquement |
Gestion des Dates
Formats Supportés
- ISO 8601 :
2024-01-15T10:30:00Z - Français :
15/01/2024,15-01-2024,15.01.2024 - Timestamps : Unix timestamp (secondes ou millisecondes)
- Objets Date : Instances JavaScript Date
Validation et Sécurité
// Plage de dates valides : 1990 à (année actuelle + 5)
isValidDate(date) {
const year = date.getFullYear();
const currentYear = new Date().getFullYear();
return year >= 1990 && year <= currentYear + 5;
}
Ajustements Contextuels
Bonus Contenu "Evergreen" (+20 pts max)
Articles à valeur permanente identifiés par mots-clés :
- Guides : "guide", "comment", "conseils"
- Éducation : "dressage", "formation", "méthode"
- Santé générale : "prévention", "bien-être"
Malus Actualités Périmées (-30% du score)
Articles d'actualité urgente devenus obsolètes :
- Mots-clés : "actualité", "urgent", "breaking", "annonce"
- Appliqué si score de base < 40 points
Bonus Recherche d'Archives (+15 pts max)
Si context.allowOldContent = true, améliore la valorisation du contenu ancien.
Calcul de l'Âge
calculateAgeInDays(publishDate, searchDate) {
const diffMs = searchDate.getTime() - publishDate.getTime();
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
}
Gestion des Cas d'Erreur
- Date future : Score = 0 (erreur de publication)
- Date manquante : Score = 0 (non fiable)
- Date invalide : Score = 0 (format incorrect)
3. Calculateur de Qualité (20% du Score)
Principe
La qualité évalue la fiabilité et l'autorité de la source de publication. Ce critère garantit la crédibilité du contenu généré.
Classification des Sources
Sources Premium (90-100 pts)
Organismes Officiels et Institutions
centrale-canine.fr(Société Centrale Canine) : 100 ptsfci.be(Fédération Cynologique Internationale) : 100 ptsveterinaire.fr(Ordre des Vétérinaires) : 95 pts- Sites universitaires vétérinaires : 95 pts
Critères d'identification :
- Extension
.eduou.frofficielle - Mentions légales complètes
- Références scientifiques
- Autorité reconnue dans le domaine
Sources Spécialisées (70-85 pts)
Médias Spécialisés Canins
30millionsdamis.fr: 85 ptswamiz.com: 80 ptswoopets.fr: 80 pts- Clubs de race officiels : 85 pts
Caractéristiques :
- Spécialisation exclusive dans le domaine canin
- Équipe éditoriale identifiée
- Historique de publication
- Partenariats avec organismes officiels
Sources Standard (50-70 pts)
Médias Généralistes de Qualité
lefigaro.fr/animaux: 65 ptsouest-france.fr/animaux: 60 pts- Magazines lifestyle avec section animaux : 55 pts
Évaluation :
- Réputation générale du média
- Qualité éditoriale
- Processus de vérification
- Expertise occasionnelle sur les animaux
Sources Fallback (20-50 pts)
Contenu Généraliste ou Non-Vérifié
- Blogs personnels : 30 pts
- Forums : 25 pts
- Réseaux sociaux : 20 pts
- Sources inconnues : 25 pts
Indicateurs de Qualité
Indicateurs Positifs (+5 à +15 pts)
qualityIndicators = {
hasAuthor: +10, // Auteur identifié
hasPublishDate: +10, // Date de publication
hasReferences: +15, // Références citées
hasVetReview: +15, // Validation vétérinaire
hasCitations: +10, // Citations scientifiques
isRecent: +5, // Publication récente
hasImages: +5, // Illustrations présentes
hasStructure: +5 // Contenu bien structuré
}
Indicateurs Négatifs (-5 à -20 pts)
qualityPenalties = {
hasAds: -5, // Publicités excessives
poorWriting: -10, // Qualité rédactionnelle
noContact: -10, // Pas de contact
noLegal: -15, // Pas de mentions légales
anonymousContent: -10, // Contenu anonyme
clickbait: -15, // Titre aguicheur
outdatedInfo: -20 // Informations obsolètes
}
Détection Automatique
Analyse du Contenu
// Détection de qualité par analyse textuelle
analyzeContentQuality(content) {
const wordCount = content.split(/\s+/).length;
const sentenceCount = content.split(/[.!?]+/).length;
const avgSentenceLength = wordCount / sentenceCount;
return {
isSubstantial: wordCount > 200,
isWellStructured: avgSentenceLength > 8 && avgSentenceLength < 25,
hasVariety: this.calculateLexicalDiversity(content) > 0.6
};
}
Analyse des Métadonnées
- Présence d'auteur et date
- Structure HTML appropriée
- Balises meta descriptions
- Schema.org markup
Pondération Contextuelle
Le score de qualité peut être ajusté selon le contexte :
// Bonus pour recherche spécialisée
if (context.requireHighQuality) {
// Réduction des scores sources non-premium
if (baseScore < 70) baseScore *= 0.8;
}
// Malus cumul sources faibles
if (context.lowQualityCount > 3) {
baseScore *= 0.9;
}
4. Calculateur de Réutilisabilité (10% du Score)
Principe
La réutilisabilité optimise l'usage du stock d'articles en évitant la sur-utilisation et en respectant les périodes de rotation. Ce critère assure la diversité du contenu généré.
Scoring par Usage
| Catégorie | Utilisations | Score | Statut |
|---|---|---|---|
| Neuf | 0 | 100 pts | Priorité maximale |
| Peu utilisé | 1-2 | 80 pts | Recommandé |
| Modérément utilisé | 3-5 | 60 pts | Acceptable |
| Très utilisé | 6-10 | 40 pts | Limité |
| Saturé | > 10 | 20 pts | À éviter |
Périodes de Rotation
Le système respecte des périodes de rotation selon le type de source :
rotationPeriods = {
premium: 90, // 3 mois - Sources premium (coût élevé, qualité maximale)
standard: 60, // 2 mois - Sources standard (équilibre qualité/coût)
fallback: 30 // 1 mois - Sources fallback (renouvellement rapide)
}
Ajustements Temporels
Bonus Période de Rotation Respectée (+10 à +20 pts)
// Calcul du bonus temporel
if (daysSinceLastUse >= rotationPeriod) {
const bonus = Math.min(20, daysSinceLastUse - rotationPeriod + 10);
return bonus;
}
Malus Utilisation Récente (-10 à -20 pts)
Articles utilisés dans les 7 derniers jours subissent une pénalité pour favoriser la diversité.
// Malus utilisation récente
if (daysSinceLastUse < 7) {
const penalty = -Math.max(10, 20 - daysSinceLastUse * 2);
return penalty;
}
Ajustements Contextuels
Bonus Client Différent (+10 pts)
Si l'article est utilisé par un client différent du précédent :
if (context.clientId && article.lastClientId &&
context.clientId !== article.lastClientId) {
adjustment += 10;
}
Bonus Contexte Différent (+15 pts max)
Évaluation de la similarité avec le contexte précédent :
calculateContextSimilarity(context1, context2) {
const ctx1Words = context1.toLowerCase().split(/\s+/);
const ctx2Words = context2.toLowerCase().split(/\s+/);
const intersection = ctx1Words.filter(word => ctx2Words.includes(word));
const union = [...new Set([...ctx1Words, ...ctx2Words])];
return intersection.length / union.length;
}
Bonus Contenu Evergreen (+5 pts)
Articles à valeur permanente (guides, conseils) bénéficient d'un bonus de réutilisabilité.
Malus Sur-utilisation Race (-10 pts)
Pénalité si l'article a été trop utilisé pour la même race (≥ 5 utilisations).
Statuts de Rotation
getRotationStatus(lastUsed, sourceType, now) {
const daysSinceLastUse = calculateDaysDifference(lastUsed, now);
const rotationPeriod = this.rotationPeriods[sourceType];
if (daysSinceLastUse >= rotationPeriod) return 'available';
if (daysSinceLastUse >= rotationPeriod * 0.7) return 'soon_available';
return 'in_rotation';
}
Statistiques de Collection
Le calculateur fournit des statistiques globales sur l'état de réutilisation du stock :
getCollectionReuseStats(articles) {
return {
totalArticles: articles.length,
byUsageCategory: { fresh: X, low: Y, ... },
byRotationStatus: { available: A, in_rotation: B, ... },
averageUsage: averageUsageCount,
reuseEfficiency: percentageAvailable,
recommendations: ['action1', 'action2', ...]
};
}
Orchestration par BasicScoringEngine
Calcul Principal
Le BasicScoringEngine coordonne les quatre calculateurs :
async scoreArticle(newsItem, context) {
// Exécution en parallèle pour optimiser les performances
const [specificityResult, freshnessResult, qualityResult, reuseResult] =
await Promise.all([
this.specificityCalculator.calculateSpecificity(newsItem, context),
this.freshnessCalculator.calculateFreshness(newsItem, context),
this.qualityCalculator.calculateQuality(newsItem, context),
this.reuseCalculator.calculateReuse(newsItem, context)
]);
// Application de la formule CDC
const finalScore = Math.round(
(specificityResult.score * 0.4) + // 40%
(freshnessResult.score * 0.3) + // 30%
(qualityResult.score * 0.2) + // 20%
(reuseResult.score * 0.1) // 10%
);
return {
finalScore,
specificityScore: specificityResult.score,
freshnessScore: freshnessResult.score,
qualityScore: qualityResult.score,
reuseScore: reuseResult.score,
scoringDetails: { /* détails complets */ },
scoreCategory: this.categorizeScore(finalScore),
usageRecommendation: this.generateUsageRecommendation(...)
};
}
Catégorisation des Scores
| Catégorie | Plage | Recommandation | Usage |
|---|---|---|---|
| Excellent | 80-100 | priority_use |
Utilisation prioritaire |
| Bon | 65-79 | recommended |
Recommandé |
| Correct | 50-64 | conditional_use |
Usage conditionnel |
| Faible | 30-49 | limited_use |
Usage limité |
| Rejeté | 0-29 | avoid |
À éviter |
Scoring par Lot
Pour optimiser les performances, le système support le scoring en lot avec limitation de concurrence :
async batchScore(newsItems, context) {
const batchSize = 10; // Limitation pour éviter la surcharge
const results = [];
for (let i = 0; i < newsItems.length; i += batchSize) {
const batch = newsItems.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => this.scoreArticle(item, context))
);
results.push(...batchResults);
}
// Tri par score décroissant
return results.sort((a, b) => (b.finalScore || 0) - (a.finalScore || 0));
}
Explication des Scores
Le moteur peut expliquer en détail comment un score a été calculé :
explainScore(scoredArticle) {
return {
scoreBreakdown: {
finalScore: scoredArticle.finalScore,
components: {
specificity: {
score: scoredArticle.specificityScore,
weight: 0.4,
contribution: Math.round(scoredArticle.specificityScore * 0.4),
reason: scoredArticle.scoringDetails.specificity.reason,
details: scoredArticle.scoringDetails.specificity.details
},
// ... autres composants
}
},
strengths: this.identifyStrengths(scoredArticle),
weaknesses: this.identifyWeaknesses(scoredArticle),
improvementSuggestions: this.generateImprovementSuggestions(scoredArticle),
usageGuideline: {
category: scoredArticle.scoreCategory,
recommendation: scoredArticle.usageRecommendation,
confidence: this.calculateConfidence(scoredArticle)
}
};
}
Performance et Optimisation
Exécution Parallèle
Les quatre calculateurs s'exécutent en parallèle pour minimiser la latence :
// ✅ Optimal : 4 calculs en parallèle
const results = await Promise.all([calc1, calc2, calc3, calc4]);
// ❌ Suboptimal : 4 calculs séquentiels
const result1 = await calc1;
const result2 = await calc2;
const result3 = await calc3;
const result4 = await calc4;
Cache et Mémorisation
- Base de données des races : Chargée en mémoire au démarrage
- Sources quality : Index en mémoire pour accès O(1)
- Calculs récents : Cache des scores pour éviter les recalculs
Métriques de Performance
Le système collecte des métriques de performance :
{
totalScored: 1250,
averageScore: 67.3,
scoreDistribution: {
excellent: 156,
good: 234,
fair: 345,
poor: 289,
reject: 226
},
calculationTime: {
total: 45678, // ms
average: 36.5 // ms par article
}
}
Cas d'Usage et Exemples
Exemple 1 : Article Premium Spécialisé
{
"title": "Nouvelle étude génétique sur la dysplasie chez les Bergers Allemands",
"content": "Une équipe de chercheurs de l'École Vétérinaire de Maisons-Alfort...",
"url": "https://centrale-canine.fr/etudes/dysplasie-berger-allemand-2024",
"publishDate": "2024-01-10T08:00:00Z",
"sourceType": "premium",
"sourceDomain": "centrale-canine.fr"
}
// Contexte
{
"raceCode": "352-1", // Berger Allemand
"clientId": "client-123",
"searchDate": "2024-01-12T10:00:00Z"
}
// Résultat de scoring
{
"finalScore": 91,
"specificityScore": 100, // Mention exacte "Bergers Allemands"
"freshnessScore": 95, // 2 jours, très récent
"qualityScore": 100, // centrale-canine.fr = source premium
"reuseScore": 80, // Article neuf, jamais utilisé
"scoreCategory": "excellent",
"usageRecommendation": "priority_use"
}
Exemple 2 : Article Standard Généraliste
{
"title": "5 conseils pour l'alimentation des grands chiens",
"content": "Les chiens de grande taille ont des besoins nutritionnels spécifiques...",
"url": "https://wamiz.com/conseils-alimentation-grands-chiens",
"publishDate": "2023-12-15T14:30:00Z",
"sourceType": "standard",
"sourceDomain": "wamiz.com",
"usageCount": 3,
"lastUsed": "2024-01-05T10:00:00Z"
}
// Contexte
{
"raceCode": "352-1", // Berger Allemand (grand chien)
"clientId": "client-456",
"searchDate": "2024-01-12T10:00:00Z"
}
// Résultat de scoring
{
"finalScore": 64,
"specificityScore": 50, // "grands chiens" = taille similaire
"freshnessScore": 40, // 28 jours, dans la catégorie "fair"
"qualityScore": 80, // wamiz.com = source spécialisée
"reuseScore": 60, // 3 utilisations = modérément utilisé
"scoreCategory": "fair",
"usageRecommendation": "conditional_use"
}
Exemple 3 : Article Fallback Sur-utilisé
{
"title": "Les animaux de compagnie et la famille",
"content": "Avoir un animal de compagnie apporte de nombreux bénéfices...",
"url": "https://blog-perso.com/animaux-famille",
"publishDate": "2023-10-20T16:00:00Z",
"sourceType": "fallback",
"sourceDomain": "blog-perso.com",
"usageCount": 12,
"lastUsed": "2024-01-10T08:00:00Z"
}
// Résultat de scoring
{
"finalScore": 23,
"specificityScore": 10, // "animaux de compagnie" = très généraliste
"freshnessScore": 20, // 84 jours = ancien
"qualityScore": 30, // Blog personnel = faible qualité
"reuseScore": 20, // > 10 utilisations = saturé
"scoreCategory": "reject",
"usageRecommendation": "avoid"
}
Extensibilité et Personnalisation
Ajout de Nouveaux Calculateurs
L'architecture modulaire permet d'ajouter facilement de nouveaux critères :
// Exemple : Calculateur de sentiment
class SentimentCalculator {
async calculateSentiment(article, context) {
// Logique d'analyse de sentiment
return {
score: sentimentScore,
reason: 'positive_sentiment',
details: 'Contenu majoritairement positif'
};
}
}
// Intégration dans BasicScoringEngine
constructor() {
this.sentimentCalculator = new SentimentCalculator();
this.weights = {
specificity: 0.35, // Réduction pour faire place au sentiment
freshness: 0.25,
quality: 0.2,
reuse: 0.1,
sentiment: 0.1 // Nouveau critère
};
}
Personnalisation des Poids
Les poids peuvent être ajustés selon le contexte d'usage :
// Profil "News" : Privilégier fraîcheur et spécificité
const newsWeights = {
specificity: 0.5,
freshness: 0.4,
quality: 0.1,
reuse: 0.0
};
// Profil "Evergreen" : Équilibrer qualité et réutilisabilité
const evergreenWeights = {
specificity: 0.3,
freshness: 0.1,
quality: 0.4,
reuse: 0.2
};
Configuration Dynamique
Le système support la configuration dynamique via le contexte :
const context = {
raceCode: "352-1",
scoringProfile: "premium", // news, evergreen, premium, balanced
qualityThreshold: 70,
freshnessBonus: 1.2,
customWeights: { /* poids spécifiques */ }
};
Monitoring et Observabilité
Logs Structurés
Chaque opération de scoring génère des logs détaillés :
logger.info('Article scored successfully', {
articleId: 'art-123',
finalScore: 85,
breakdown: {
specificity: 90,
freshness: 95,
quality: 80,
reuse: 70
},
calculationTime: 45,
raceCode: '352-1',
category: 'excellent'
});
Métriques Business
- Distribution des scores : Répartition par catégorie
- Performance moyenne : Score moyen par race/source
- Efficacité de réutilisation : Taux d'articles disponibles
- Qualité des sources : Évolution de la qualité du stock
Alertes Automatiques
Le système peut déclencher des alertes :
// Alerte qualité dégradée
if (averageQualityScore < threshold) {
alerting.trigger('quality_degradation', {
currentScore: averageQualityScore,
threshold: threshold,
recommendation: 'Renouveler sources premium'
});
}
Évolutions Futures
Machine Learning
Integration future d'un modèle ML pour affiner les scores :
class MLScoringEngine extends BasicScoringEngine {
constructor() {
super();
this.mlModel = new ContentQualityModel();
}
async scoreArticle(newsItem, context) {
const baseScore = await super.scoreArticle(newsItem, context);
const mlAdjustment = await this.mlModel.predict(newsItem, context);
return {
...baseScore,
finalScore: this.adjustWithML(baseScore.finalScore, mlAdjustment),
mlConfidence: mlAdjustment.confidence
};
}
}
Scoring Adaptatif
Ajustement automatique des poids selon les performances :
class AdaptiveScoringEngine extends BasicScoringEngine {
updateWeights(feedbackData) {
// Apprentissage des poids optimaux selon feedback utilisateur
this.weights = this.optimizeWeights(feedbackData);
}
}
Intégration Multi-langues
Support de scoring multi-langues avec détection automatique :
const languageSpecificCalculators = {
'fr': new FrenchSpecificityCalculator(),
'en': new EnglishSpecificityCalculator(),
'de': new GermanSpecificityCalculator()
};
Conclusion
Le système de scoring SourceFinder offre une évaluation sophistiquée et équilibrée du contenu canin, combinant pertinence thématique, actualité, qualité des sources et optimisation de la réutilisation.
Son architecture modulaire garantit :
- Flexibilité : Ajout facile de nouveaux critères
- Performance : Calculs parallèles et optimisations
- Transparence : Explication détaillée des scores
- Fiabilité : Gestion d'erreurs et logging complet
- Évolutivité : Support de personnalisations avancées
Cette approche multi-critères assure une sélection de contenu optimale pour tous les cas d'usage, de la génération d'actualités urgentes aux guides permanents de référence.