This commit is contained in:
Brennan Wilkes (Text Groove) 2026-02-01 11:25:40 -08:00
parent 04d181da35
commit 2356eb8f8f

View file

@ -51,6 +51,10 @@ export function topSuggestions(allAgg, limit, otherPinnedSku, mappedSkus) {
return scored.slice(0, limit).map((x) => x.it); return scored.slice(0, limit).map((x) => x.it);
} }
// viz/app/linker/suggestions.js
// (requires fnv1a32u(str) helper to exist in this file)
export function recommendSimilar( export function recommendSimilar(
allAgg, allAgg,
pinned, pinned,
@ -76,55 +80,52 @@ export function recommendSimilar(
const pinnedSmws = smwsKeyFromName(pinned.name || ""); const pinnedSmws = smwsKeyFromName(pinned.name || "");
// ---- Tuning knobs ---- // ---- Tuning knobs ----
const MAX_SCAN = 5000; // total work cap const MAX_SCAN = 5000; // cap for huge catalogs
const MAX_CHEAP_KEEP = 320; const FULL_SCAN_UNDER = 12000; // ✅ scan everything if catalog is "small"
const MAX_FINE = 70; const MAX_CHEAP_KEEP = 320; // keep top candidates from cheap stage
const WINDOWS = 4; // scan several windows to cover the catalog const MAX_FINE = 70; // expensive score only on top-N
// ---------------------- // ----------------------
// Faster "topK" keeper: only sorts occasionally.
function pushTopK(arr, item, k) { function pushTopK(arr, item, k) {
arr.push(item); arr.push(item);
if (arr.length > k) { if (arr.length >= k * 2) {
arr.sort((a, b) => b.s - a.s); arr.sort((a, b) => b.s - a.s);
arr.length = k; arr.length = k;
} }
} }
const cheap = []; const cheap = [];
const nAll = allAgg.length || 0; const nAll = allAgg.length || 0;
if (!nAll) return []; if (!nAll) return [];
// Multi-window starts: deterministic, spread around the array // ✅ scan whole catalog when it's not huge
const h = fnv1a32u(pinnedSku || pinNorm); const scanN = nAll <= FULL_SCAN_UNDER ? nAll : Math.min(MAX_SCAN, nAll);
const starts = [
h % nAll,
(Math.imul(h ^ 0x9e3779b9, 0x85ebca6b) >>> 0) % nAll,
(Math.imul(h ^ 0xc2b2ae35, 0x27d4eb2f) >>> 0) % nAll,
((h + (nAll >>> 1)) >>> 0) % nAll,
];
const scanN = Math.min(MAX_SCAN, nAll); // ✅ rotate start to avoid alphabetical bias, but still cover scanN sequentially
const perWin = Math.max(1, Math.floor(scanN / WINDOWS)); const start = (fnv1a32u(pinnedSku || pinNorm) % nAll) >>> 0;
// Optional debug: // Optional debug: uncomment to verify were actually hitting the region you expect
console.log("[linker] recommendSimilar scan", { pinnedSku, nAll, scanN, perWin, starts: starts.map(s => allAgg[s]?.name) }); // console.log("[linker] recommendSimilar scan2", { pinnedSku, nAll, scanN, start, startName: allAgg[start]?.name });
let scanned = 0; for (let i = 0; i < scanN; i++) {
const it = allAgg[(start + i) % nAll];
function consider(it) { if (!it) continue;
if (!it) return;
const itSku = String(it.sku || ""); const itSku = String(it.sku || "");
if (!itSku) return; if (!itSku) continue;
if (itSku === pinnedSku) return; if (itSku === pinnedSku) continue;
if (otherSku && itSku === otherSku) return; if (otherSku && itSku === otherSku) continue;
// HARD BLOCKS ONLY: // HARD BLOCKS ONLY:
if (typeof sameStoreFn === "function" && sameStoreFn(pinnedSku, itSku)) return; if (typeof sameStoreFn === "function" && sameStoreFn(pinnedSku, itSku)) continue;
if (typeof isIgnoredPairFn === "function" && isIgnoredPairFn(pinnedSku, itSku)) return; if (typeof isIgnoredPairFn === "function" && isIgnoredPairFn(pinnedSku, itSku)) continue;
if (typeof sameGroupFn === "function" && sameGroupFn(pinnedSku, itSku)) return; if (typeof sameGroupFn === "function" && sameGroupFn(pinnedSku, itSku)) continue;
// (Optional) original mapped exclusion lives here in your codebase.
// Keep it if you want, but it wasn't your issue:
if (mappedSkus && mappedSkus.has(itSku)) continue;
// SMWS exact NUM.NUM match => keep at top // SMWS exact NUM.NUM match => keep at top
if (pinnedSmws) { if (pinnedSmws) {
@ -137,16 +138,16 @@ export function recommendSimilar(
{ it, s: 1e9 + stores * 10 + hasPrice, itNorm: "", itRawToks: null }, { it, s: 1e9 + stores * 10 + hasPrice, itNorm: "", itRawToks: null },
MAX_CHEAP_KEEP MAX_CHEAP_KEEP
); );
return; continue;
} }
} }
const itNorm = normSearchText(it.name || ""); const itNorm = normSearchText(it.name || "");
if (!itNorm) return; if (!itNorm) continue;
const itRawToks = tokenizeQuery(itNorm); const itRawToks = tokenizeQuery(itNorm);
const itToks = filterSimTokens(itRawToks); const itToks = filterSimTokens(itRawToks);
if (!itToks.length) return; if (!itToks.length) continue;
const itBrand = itToks[0] || ""; const itBrand = itToks[0] || "";
const firstMatch = pinBrand && itBrand && pinBrand === itBrand; const firstMatch = pinBrand && itBrand && pinBrand === itBrand;
@ -182,19 +183,9 @@ export function recommendSimilar(
pushTopK(cheap, { it, s: s0, itNorm, itRawToks }, MAX_CHEAP_KEEP); pushTopK(cheap, { it, s: s0, itNorm, itRawToks }, MAX_CHEAP_KEEP);
} }
// Scan several windows, total capped at MAX_SCAN // Final trim/sort for cheap stage
for (let w = 0; w < WINDOWS && scanned < scanN; w++) {
const start = starts[w % starts.length];
const take = Math.min(perWin, scanN - scanned);
for (let i = 0; i < take; i++) {
const it = allAgg[(start + i) % nAll];
consider(it);
}
scanned += take;
}
cheap.sort((a, b) => b.s - a.s); cheap.sort((a, b) => b.s - a.s);
if (cheap.length > MAX_CHEAP_KEEP) cheap.length = MAX_CHEAP_KEEP;
// Fine stage: expensive scoring only on top candidates // Fine stage: expensive scoring only on top candidates
const fine = []; const fine = [];
@ -233,14 +224,14 @@ export function recommendSimilar(
if (pinnedSku.startsWith("u:") || itSku.startsWith("u:")) s *= 1.12; if (pinnedSku.startsWith("u:") || itSku.startsWith("u:")) s *= 1.12;
if (s > 0) fine.push({ it, s }); fine.push({ it, s });
} }
fine.sort((a, b) => b.s - a.s); fine.sort((a, b) => b.s - a.s);
const out = fine.slice(0, limit).map((x) => x.it); const out = fine.slice(0, limit).map((x) => x.it);
if (out.length) return out; if (out.length) return out;
// Fallback: hard blocks only // Fallback (unchanged)
const fallback = []; const fallback = [];
for (const it of allAgg) { for (const it of allAgg) {
if (!it) continue; if (!it) continue;
@ -265,6 +256,8 @@ export function recommendSimilar(
} }
export function computeInitialPairsFast(allAgg, mappedSkus, limitPairs, isIgnoredPairFn, sameStoreFn) { export function computeInitialPairsFast(allAgg, mappedSkus, limitPairs, isIgnoredPairFn, sameStoreFn) {
const itemsAll = allAgg.filter((it) => !!it); const itemsAll = allAgg.filter((it) => !!it);