Repository navigation
fix: weigh download counts on a log scale - #2175
Conversation
b65cc76 to
8ecca46
Compare
|
Using a linear scale for the download counts is highly misleading as it is always calculated based on the most downloaded extension. That leads to a situation where the download count becomes irrelevant as criteria, replacing it to a log scale make it more sane though it will need more improvements. Once the explain page is available it will be easier to assess the results and adjust. |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes global search ranking behavior (relevance scoring) and should get a final human review to validate the intended impact and guardrails beyond unit tests.
Pull request overview
Updates the search relevance calculation so extension download counts contribute on a logarithmic scale, improving ranking discrimination across the long-tail of download counts while keeping the normalization relative to the registry maximum.
Changes:
- Change the downloads component of relevance from linear normalization to
log1p(downloads) / log1p(maxDownloads). - Extend
SearchStatswith a precomputeddownloadRefLogto avoid recomputing the reference log per entry. - Add a unit test that asserts the expected (log-scaled) download-term value, not just monotonic ordering.
File summaries
| File | Description |
|---|---|
| server/src/main/java/org/eclipse/openvsx/search/RelevanceService.java | Switches downloads relevance term to log scale and adds downloadRefLog to SearchStats. |
| server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java | Adds an assertion test for the exact log-scaled downloads contribution. |
Review details
Suppressed comments (1)
server/src/test/java/org/eclipse/openvsx/search/ElasticSearchServiceTest.java:232
- This test tries to isolate the download term by giving the extension 0 reviews, but
calculateRatinguses IMDB-style smoothing againstrepositories.getAverageReviewRating(). Unless that average is explicitly stubbed to 0 here, the rating term may contribute and make this assertion flaky if the repository mock setup changes.
var index = mockIndex(true);
// After mockIndex, which stubs a maximum of its own.
Mockito.when(repositories.getMaxExtensionDownloadCount()).thenReturn(1_000_000);
var oldest = LocalDateTime.parse("2020-01-01T00:00");
var extension = mockExtension("foo", "n1", "u1", 0.0, 0, 100_000, oldest, false, false);
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
8ecca46 to
37cbcb2
Compare
|
Rebased and the review comment addressed. The conflict was with the stack, not with The one real conflict was in var downloadsValue = Math.log1p(extension.getDownloadCount()) / stats.downloadRefLog;rather than The Copilot comment was right, and pointed at something slightly worse than it described: the Verified after the rebase: 93 tests across the search suites pass, the formatter reports both touched Still stacked on #2174 — merge that first, or say the word and I will retarget this at |
37cbcb2 to
fdb098a
Compare
fdb098a to
8754a78
Compare
The download term of the relevance score is the extension's count over the largest count in the registry, clamped to one. Download counts span several orders of magnitude, so dividing by the largest of them leaves everything outside the top few percent indistinguishable from nothing: against a registry maximum of twenty million, an extension with six hundred thousand downloads scores 0.03 of the one this term can contribute - against a relevance that also carries a rating and a recency term, each worth up to one in their own right. Which is how EclipseFdn/open-vsx.org#13014 comes to describe a result list whose entries have "a small fraction" of the download count of the extension that is missing from it. Popularity was in the formula but not, at any realistic scale, in the score. Taken logarithmically the same extension scores 0.79. The comparison is still against the registry maximum; what changes is that the distance between a thousand downloads and a million is no longer rounding error. Note it compresses the low end in exchange - a thousand downloads scores 0.41 where linear gave 0.0001 - so this buys discrimination in the middle of the range at the cost of some at the bottom. sqrt would split the difference and percentile-of-the-distribution would spread it evenly, at the cost of computing the distribution; log is the one that needs nothing that SearchStats does not already have. Relevance is computed when a document is indexed, and the daily soft update recomputes it for every active extension - which is what that job is for, since relevance already depends on timestamps relative to now. So this needs no reindex of its own and applies within a day of deploying. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test's javadoc credited the extension having no rating, but the formula smooths a rating towards the registry average, so an extension with no reviews scores that average rather than nothing. What held the term at zero was the average itself - and only via Mockito's default for an unstubbed double, so a later stub of getAverageReviewRating in the shared setup would have moved this test's expected value without touching it. Now stated, and stubbed explicitly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both recounted the linear scale and what it scored, which is the shape #2180 rules out: history belongs in the commit message and the pull request, and both already carry it. What is left is the reason the code reads as it does - that download counts span orders of magnitude, so measuring one against the largest tells you almost nothing about the rest. The javadoc on downloadRefLog stays as it is. It explains why the raw maximum is kept alongside rather than replaced, which is a constraint on the code as it stands and not an account of how it used to be. The test's javadoc keeps the part that says how the other two terms are held at zero, since nothing about reading the test makes that apparent, and drops the arithmetic - which the test computes anyway, so stating it in prose was narration of the assertion two lines below it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8754a78 to
9d7b003
Compare
|
Rebased and the comments revised. The conflict was the stack again, not On #2180. Two of the comments here were exactly the shape that clause rules out. The one on the formula recounted the linear scale and what it used to score: // Log scale, not linear. Both scales measure this against the largest download count in the
// registry, but download counts span several orders of magnitude, and dividing by the largest of
// them leaves everything outside the top few percent indistinguishable from nothing: against a
// maximum of twenty million, an extension with six hundred thousand downloads scores 0.03 of the
// one this term can contribute, so its popularity is worth roughly nothing next to a rating or a
// recent release. Taken logarithmically the same extension scores 0.79, ...Nine lines, most of them about code that will not exist once this merges. Now three, keeping only the reason the line reads as it does: // Log scale: download counts span several orders of magnitude, so measuring one against the
// largest in the registry leaves everything outside the top few percent indistinguishable
// from nothing. See EclipseFdn/open-vsx.org#13014.The before-and-after arithmetic is still in the commit message and in the table above, which is where someone asking "why did this change" is looking. The test javadoc lost the same arithmetic — the test computes One I deliberately kept: the javadoc on Re-verified after the trim, since a comment change should not alter behaviour but the rebase moved real code: |
Follow-up to #2174 (now merged), from the same investigation of EclipseFdn/open-vsx.org#13014.
Popularity was in the formula but not in the score
The download term of the relevance score is the extension's count over the largest count in the registry, clamped to one:
Download counts span several orders of magnitude, so dividing by the largest of them leaves everything outside the top few percent indistinguishable from nothing:
(against a registry maximum of 20,000,000)
Relevance is the sum of three equally-weighted terms — rating, downloads, recency — each able to contribute up to 1.0. So an extension with 637k downloads was getting 0.03 for its popularity, next to a rating worth up to 1.0 and a release date worth up to 1.0. That is how #13014 comes to describe a result list whose every entry has "a small fraction" of the download count of the extension missing from it: the downloads were being counted, just not enough to matter.
The change
Still measured against the registry maximum; what changes is that the distance between a thousand downloads and a million is no longer rounding error.
The trade, stated plainly: log compresses the low end in exchange — 1,000 downloads scores 0.41 where linear gave 0.0001 — so this buys discrimination in the middle of the range at the cost of some at the bottom.
sqrtsplits the difference (0.179 for 637k), and percentile-of-the-distribution would spread it evenly at the cost of computing the distribution. Log is the one that needs nothingSearchStatsdoesn't already gather. Easy to swap if you'd rather have a different curve — the table above is the whole basis for the choice.downloadRefLogis kept alongside the rawdownloadRefrather than replacing it, because the raw maximum is what appears in the diagnostics written when a relevance comes outNaN, where a log would say much less about the registry.No reindex needed
Relevance is computed when a document is indexed, and the daily soft update recomputes it for every active extension — that job exists precisely because relevance depends on timestamps relative to
now. So this applies within a day of deploying, with no manual rebuild.Tests
weighsDownloadsOnALogScaleasserts what the term is worth, not merely that more downloads beat fewer — which the existingtestRelevanceDownloadCountalready covers and which a linear scale satisfies just as well. It isolates the term by giving the extension no rating and the oldest timestamp in the registry, zeroing the other two so relevance equals the download term, then asserts the log ratio to within 0.001. It fails on the linear formula, which I checked.Full server suite green (1184 tests), formatter clean.
🤖 Generated with Claude Code