File size: 5,462 Bytes
33f66e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Client-side model runtime: intercepts the app's fetch() calls and answers
// them from item factors shipped with the page. Generated by export_static.py.
(() => {
  const ALPHA = 20.0, REG = 0.1, F = 96;
  const PERSONAS = [{"name": "Sci-fi night", "titles": ["Matrix, The", "Star Wars: Episode IV - A New Hope", "Terminator 2: Judgment Day", "Alien", "Blade Runner"]}, {"name": "Rom-com classics", "titles": ["Sleepless in Seattle", "You've Got Mail", "Pretty Woman", "Four Weddings and a Funeral", "Notting Hill"]}, {"name": "Animated favorites", "titles": ["Toy Story", "Bug's Life, A", "Aladdin", "Lion King, The", "Beauty and the Beast"]}, {"name": "Horror night", "titles": ["Shining, The", "Halloween", "Exorcist, The", "Scream", "Psycho"]}];
  let catalog = null, V = null, VtV = null, unit = null, byId = null;

  async function load() {
    if (catalog) return;
    catalog = await (await realFetch("catalog.json")).json();
    const buf = await (await realFetch("factors.bin")).arrayBuffer();
    V = new Float32Array(buf);
    byId = new Map(catalog.map((m, i) => [m.movie_id, i]));
    VtV = new Float64Array(F * F);
    const n = catalog.length;
    for (let i = 0; i < n; i++) {
      const o = i * F;
      for (let a = 0; a < F; a++) {
        const va = V[o + a];
        for (let b = a; b < F; b++) VtV[a * F + b] += va * V[o + b];
      }
    }
    for (let a = 0; a < F; a++) for (let b = 0; b < a; b++) VtV[a * F + b] = VtV[b * F + a];
    unit = new Float32Array(V.length);
    for (let i = 0; i < n; i++) {
      const o = i * F;
      let s = 0;
      for (let a = 0; a < F; a++) s += V[o + a] * V[o + a];
      const inv = s > 0 ? 1 / Math.sqrt(s) : 0;
      for (let a = 0; a < F; a++) unit[o + a] = V[o + a] * inv;
    }
  }

  function solve(A, b) {   // Gaussian elimination with partial pivoting
    const n = b.length, x = Float64Array.from(b), M = Float64Array.from(A);
    for (let c = 0; c < n; c++) {
      let p = c;
      for (let r = c + 1; r < n; r++) if (Math.abs(M[r * n + c]) > Math.abs(M[p * n + c])) p = r;
      if (p !== c) {
        for (let k = c; k < n; k++) { const t = M[c * n + k]; M[c * n + k] = M[p * n + k]; M[p * n + k] = t; }
        const t = x[c]; x[c] = x[p]; x[p] = t;
      }
      const piv = M[c * n + c];
      for (let r = c + 1; r < n; r++) {
        const f = M[r * n + c] / piv;
        if (f === 0) continue;
        for (let k = c; k < n; k++) M[r * n + k] -= f * M[c * n + k];
        x[r] -= f * x[c];
      }
    }
    for (let r = n - 1; r >= 0; r--) {
      let s = x[r];
      for (let k = r + 1; k < n; k++) s -= M[r * n + k] * x[k];
      x[r] = s / M[r * n + r];
    }
    return x;
  }

  const movie = i => {
    const m = catalog[i];
    return { movie_id: m.movie_id, title: m.title, year: m.year, genres: m.genres };
  };

  function recommend(movieIds, topN) {
    const idx = [...new Set(movieIds.map(id => byId.get(id)))].filter(i => i !== undefined);
    const A = Float64Array.from(VtV), b = new Float64Array(F);
    for (const i of idx) {
      const o = i * F;
      for (let a = 0; a < F; a++) {
        b[a] += (1 + ALPHA) * V[o + a];
        for (let c = 0; c < F; c++) A[a * F + c] += ALPHA * V[o + a] * V[o + c];
      }
    }
    for (let a = 0; a < F; a++) A[a * F + a] += REG;
    const u = solve(A, b);
    const n = catalog.length, scores = new Float64Array(n);
    for (let i = 0; i < n; i++) {
      const o = i * F;
      let s = 0;
      for (let a = 0; a < F; a++) s += V[o + a] * u[a];
      scores[i] = s;
    }
    const excluded = new Set(idx);
    const order = [...scores.keys()].filter(i => !excluded.has(i))
      .sort((x, y) => scores[y] - scores[x]).slice(0, topN);
    const maxScore = Math.max(scores[order[0]], 1e-9);
    const recs = order.map((i, r) => {
      let best = idx[0], bestSim = -2;
      for (const p of idx) {
        let s = 0;
        for (let a = 0; a < F; a++) s += unit[i * F + a] * unit[p * F + a];
        if (s > bestSim) { bestSim = s; best = p; }
      }
      return { ...movie(i), rank: r + 1, score: Math.round(scores[i] * 1e4) / 1e4,
               match: Math.round(scores[i] / maxScore * 1e4) / 1e4, because: catalog[best].title };
    });
    return { picks: idx.map(movie), recommendations: recs };
  }

  const respond = data => Promise.resolve({ ok: true, status: 200, json: async () => data });
  const realFetch = window.fetch.bind(window);

  window.fetch = async (url, opts) => {
    const u = String(url);
    if (u.startsWith("search?")) {
      await load();
      const q = new URLSearchParams(u.split("?")[1]).get("q").trim().toLowerCase();
      if (q.length < 2) return respond([]);
      const hits = catalog.filter(m => m.title.toLowerCase().includes(q))
        .sort((a, b) => b.n_likes - a.n_likes).slice(0, 20)
        .map(m => movie(byId.get(m.movie_id)));
      return respond(hits);
    }
    if (u.startsWith("sample?")) {
      await load();
      const i = parseInt(new URLSearchParams(u.split("?")[1]).get("index") || "0", 10);
      const p = PERSONAS[i % PERSONAS.length];
      return respond({ name: p.name,
        movies: p.titles.filter(t => catalog.some(m => m.title === t))
          .map(t => movie(byId.get(catalog.find(m => m.title === t).movie_id))) });
    }
    if (u === "recommend") {
      await load();
      const body = JSON.parse(opts.body);
      return respond(recommend(body.movie_ids, body.n || 10));
    }
    return realFetch(url, opts);
  };
})();