diff --git a/benchmarks/sorters_bench.js b/benchmarks/sorters_bench.js new file mode 100644 index 00000000..502a92b4 --- /dev/null +++ b/benchmarks/sorters_bench.js @@ -0,0 +1,45 @@ + +import { createExploreDiversitySorter } from "../js/feedEngine/sorters.js"; + +const NUM_ITEMS = 1000; // Large enough to see the difference between N^3 and N^2 +const TAG_POOL = ["cats", "dogs", "nostr", "bitcoin", "art", "music", "news", "tech", "coding", "python", "javascript", "react", "vue", "angular", "svelte", "deno", "node", "linux", "macos", "windows"]; + +function randomTags() { + const count = Math.floor(Math.random() * 5) + 1; + const tags = new Set(); + while (tags.size < count) { + tags.add(TAG_POOL[Math.floor(Math.random() * TAG_POOL.length)]); + } + return Array.from(tags).map(t => ["t", t]); +} + +function buildItem(i) { + return { + video: { + id: `vid-${i}`, + tags: randomTags(), + rootCreatedAt: 1000 + i, + }, + metadata: { + exploreScore: Math.random(), + }, + }; +} + +async function runBenchmark() { + const items = []; + for (let i = 0; i < NUM_ITEMS; i++) { + items.push(buildItem(i)); + } + + const sorter = createExploreDiversitySorter({ lambda: 0.7 }); + + console.log(`Sorting ${NUM_ITEMS} items...`); + const start = performance.now(); + await sorter(items, { addWhy: () => {} }); + const end = performance.now(); + + console.log(`Time: ${(end - start).toFixed(2)}ms`); +} + +runBenchmark(); diff --git a/js/feedEngine/sorters.js b/js/feedEngine/sorters.js index c4620509..5a264fa8 100644 --- a/js/feedEngine/sorters.js +++ b/js/feedEngine/sorters.js @@ -300,8 +300,36 @@ export function createExploreDiversitySorter({ const selected = []; const remaining = [...candidates]; + const candidateState = new Map(); + + for (const candidate of remaining) { + candidateState.set(candidate, { + maxSimilarity: 0, + similarItemId: "", + }); + } while (remaining.length > 0) { + if (selected.length > 0) { + const lastSelected = selected[selected.length - 1]; + const lastSelectedVector = vectors.get(lastSelected); + const lastSelectedId = stableVideoId(lastSelected); + + for (const candidate of remaining) { + const state = candidateState.get(candidate); + const candidateVector = vectors.get(candidate); + const similarity = cosineSimilarity( + candidateVector, + lastSelectedVector, + ); + + if (similarity > state.maxSimilarity) { + state.maxSimilarity = similarity; + state.similarItemId = lastSelectedId; + } + } + } + let bestCandidate = null; let bestMmr = Number.NEGATIVE_INFINITY; let bestRaw = Number.NEGATIVE_INFINITY; @@ -326,22 +354,9 @@ export function createExploreDiversitySorter({ for (const candidate of remaining) { const rawScore = normalizeScore(candidate?.metadata?.exploreScore); - let penalty = 0; - let similarItemId = ""; - - if (selected.length > 0) { - const candidateVector = vectors.get(candidate); - for (const selectedItem of selected) { - const similarity = cosineSimilarity( - candidateVector, - vectors.get(selectedItem), - ); - if (similarity > penalty) { - penalty = similarity; - similarItemId = stableVideoId(selectedItem); - } - } - } + const state = candidateState.get(candidate); + const penalty = state.maxSimilarity; + const similarItemId = state.similarItemId; const mmrScore = normalizedLambda * rawScore - (1 - normalizedLambda) * penalty; @@ -404,6 +419,7 @@ export function createExploreDiversitySorter({ const index = remaining.indexOf(bestCandidate); if (index >= 0) { remaining.splice(index, 1); + candidateState.delete(bestCandidate); } }