@@ -9,6 +9,25 @@ namespace CodeContext.Services;
99/// </summary>
1010public class FileRelevanceScorer
1111{
12+ // Scoring weights for different factors
13+ private const double FileNameWeight = 0.30 ;
14+ private const double FilePathWeight = 0.20 ;
15+ private const double ContentWeight = 0.40 ;
16+ private const double ImportanceWeight = 0.10 ;
17+
18+ // Scoring parameters
19+ private const double NeutralScore = 0.5 ;
20+ private const double LowDefaultScore = 0.3 ;
21+ private const int MaxMatchesPerKeyword = 10 ;
22+ private const int ContentLengthNormalizer = 100 ;
23+
24+ // File importance boost values
25+ private const double ReadmeBoost = 0.3 ;
26+ private const double ConfigBoost = 0.2 ;
27+ private const double MainFileBoost = 0.2 ;
28+ private const double IndexFileBoost = 0.15 ;
29+ private const double TestFileBoost = 0.1 ;
30+
1231 private readonly string _projectPath ;
1332
1433 public FileRelevanceScorer ( string projectPath )
@@ -52,15 +71,15 @@ public ScoredFile ScoreFile(string filePath, string content, string query)
5271 breakdown [ "content" ] = contentScore ;
5372
5473 // 4. File importance indicators (10% weight)
55- var importanceScore = ScoreImportance ( filePath ) ;
74+ var importanceScore = ScoreImportance ( filePath , content . Length ) ;
5675 breakdown [ "importance" ] = importanceScore ;
5776
5877 // Calculate weighted total score
5978 var totalScore =
60- ( nameScore * 0.30 ) +
61- ( pathScore * 0.20 ) +
62- ( contentScore * 0.40 ) +
63- ( importanceScore * 0.10 ) ;
79+ ( nameScore * FileNameWeight ) +
80+ ( pathScore * FilePathWeight ) +
81+ ( contentScore * ContentWeight ) +
82+ ( importanceScore * ImportanceWeight ) ;
6483
6584 var tokenCount = TokenCounter . EstimateTokensForFile ( filePath , content ) ;
6685
@@ -96,10 +115,10 @@ private static List<string> ExtractKeywords(string query)
96115 /// </summary>
97116 private static double ScoreFileName ( string filePath , List < string > keywords )
98117 {
99- var fileName = Path . GetFileNameWithoutExtension ( filePath ) . ToLowerInvariant ( ) ;
100118 if ( keywords . Count == 0 )
101- return 0.5 ; // Neutral score if no keywords
119+ return NeutralScore ;
102120
121+ var fileName = Path . GetFileNameWithoutExtension ( filePath ) . ToLowerInvariant ( ) ;
103122 var matchCount = keywords . Count ( keyword => fileName . Contains ( keyword ) ) ;
104123 return Math . Min ( 1.0 , matchCount / ( double ) keywords . Count * 1.5 ) ;
105124 }
@@ -109,58 +128,65 @@ private static double ScoreFileName(string filePath, List<string> keywords)
109128 /// </summary>
110129 private static double ScoreFilePath ( string filePath , List < string > keywords )
111130 {
112- var pathLower = filePath . ToLowerInvariant ( ) ;
113131 if ( keywords . Count == 0 )
114- return 0.5 ;
132+ return NeutralScore ;
115133
134+ var pathLower = filePath . ToLowerInvariant ( ) ;
116135 var matchCount = keywords . Count ( keyword => pathLower . Contains ( keyword ) ) ;
117136 return Math . Min ( 1.0 , matchCount / ( double ) keywords . Count ) ;
118137 }
119138
120139 /// <summary>
121140 /// Scores based on content relevance.
141+ /// Optimized to avoid repeated ToLowerInvariant() calls on large content.
122142 /// </summary>
123143 private static double ScoreContent ( string content , List < string > keywords )
124144 {
125145 if ( string . IsNullOrWhiteSpace ( content ) || keywords . Count == 0 )
126- return 0.3 ; // Low default score
146+ return LowDefaultScore ;
127147
148+ // Cache lowercase conversion once to avoid repeated allocations
128149 var contentLower = content . ToLowerInvariant ( ) ;
150+
129151 var totalMatches = keywords . Sum ( keyword =>
130152 {
131153 var count = Regex . Matches ( contentLower , Regex . Escape ( keyword ) ) . Count ;
132- return Math . Min ( count , 10 ) ; // Cap at 10 matches per keyword to avoid skew
154+ return Math . Min ( count , MaxMatchesPerKeyword ) ;
133155 } ) ;
134156
135157 // Normalize by content length and keyword count
136- var density = totalMatches / ( double ) ( content . Length / 100 + 1 ) ;
158+ var density = totalMatches / ( double ) ( content . Length / ContentLengthNormalizer + 1 ) ;
137159 return Math . Min ( 1.0 , density * keywords . Count ) ;
138160 }
139161
140162 /// <summary>
141163 /// Scores based on file importance indicators.
142164 /// </summary>
143- private static double ScoreImportance ( string filePath )
165+ /// <param name="filePath">Path to the file.</param>
166+ /// <param name="fileSize">Size of the file content in bytes.</param>
167+ private static double ScoreImportance ( string filePath , int fileSize )
144168 {
169+ const int VeryLargeFileThreshold = 50000 ; // 50KB
170+ const double LargeFilePenalty = 0.1 ;
171+
145172 var fileName = Path . GetFileName ( filePath ) . ToLowerInvariant ( ) ;
146- var score = 0.5 ; // Base score
173+ var score = NeutralScore ;
147174
148175 // Boost for important file types
149176 if ( fileName . Contains ( "readme" ) )
150- score += 0.3 ;
177+ score += ReadmeBoost ;
151178 if ( fileName . Contains ( "config" ) || fileName . Contains ( "settings" ) )
152- score += 0.2 ;
179+ score += ConfigBoost ;
153180 if ( fileName . Contains ( "main" ) || fileName . Contains ( "program" ) || fileName . Contains ( "app" ) )
154- score += 0.2 ;
181+ score += MainFileBoost ;
155182 if ( fileName . Contains ( "index" ) || fileName . Contains ( "router" ) )
156- score += 0.15 ;
183+ score += IndexFileBoost ;
157184 if ( fileName . Contains ( "test" ) || fileName . Contains ( "spec" ) )
158- score += 0.1 ; // Tests are useful but secondary
185+ score += TestFileBoost ;
159186
160- // Penalize very long files (might be generated/verbose)
161- // This would need actual file size, using path length as proxy
162- if ( filePath . Length > 100 )
163- score -= 0.1 ;
187+ // Penalize very large files (might be generated/verbose)
188+ if ( fileSize > VeryLargeFileThreshold )
189+ score -= LargeFilePenalty ;
164190
165191 return Math . Clamp ( score , 0.0 , 1.0 ) ;
166192 }
0 commit comments