Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/feedthejoe / src/core/vector/vector.js |
| Date | Not specified in source |
| Repository | Janady13/feedthejoe |
| Revision | 0e608a2b515a959bf35d2783428766624400ca3c |
| Branch | main |
| Path or archive member | src/core/vector/vector.js |
| Original SHA-256 | 042103b775693e063b69d5cdaf5fe433a785254a735aada9f6ab31215e4382d2 |
| Git blob SHA | 506211e0ede9a46ecdddb2817f2d0ae986a728c5 |
| Reading-copy SHA-256 | 042103b775693e063b69d5cdaf5fe433a785254a735aada9f6ab31215e4382d2 |
Recovered source transcript
Download complete source text ↓138 lines · 4.1 KB
/**
* Dense-vector arithmetic for the interest model.
*
* Every function here is pure and allocation-explicit. Vectors are plain
* Float32Array — no wrapper class — because the embeddings arrive from the ONNX
* runtime in exactly that form and copying them into objects would be waste.
*
* Dimensionality is a caller contract: all inputs to a given call must share a
* length. We check it rather than trusting it, because a silent dimension
* mismatch produces a plausible-looking wrong number, which is the worst
* failure mode a ranking system can have.
*/
/** @typedef {Float32Array} Vec */
/**
* @param {Vec} a
* @param {Vec} b
* @param {string} where Call-site label, for a useful error message.
*/
function assertSameLength(a, b, where) {
if (a.length !== b.length) {
throw new RangeError(`${where}: dimension mismatch (${a.length} vs ${b.length})`);
}
}
/**
* Euclidean (L2) norm.
* @param {Vec} v
* @returns {number}
*/
export function norm(v) {
let sum = 0;
for (let i = 0; i < v.length; i += 1) sum += v[i] * v[i];
return Math.sqrt(sum);
}
/**
* Return a unit-length copy. A zero vector is returned unchanged rather than
* producing NaN — an empty interest vector is a legitimate cold-start state.
*
* @param {Vec} v
* @returns {Vec} A new array; the input is not mutated.
*/
export function normalize(v) {
const n = norm(v);
if (n === 0) return new Float32Array(v);
const out = new Float32Array(v.length);
for (let i = 0; i < v.length; i += 1) out[i] = v[i] / n;
return out;
}
/**
* Dot product.
* @param {Vec} a
* @param {Vec} b
* @returns {number}
*/
export function dot(a, b) {
assertSameLength(a, b, 'dot');
let sum = 0;
for (let i = 0; i < a.length; i += 1) sum += a[i] * b[i];
return sum;
}
/**
* Cosine similarity in [-1, 1]. Returns 0 when either vector is zero-length,
* which reads as "no information", not "maximally dissimilar".
*
* Note: when both inputs are already unit vectors — which is the case for
* everything the embedding adapter produces, since it requests
* `normalize: true` — this is just `dot`. We still divide, because this
* function is also used on raw interest vectors mid-update.
*
* @param {Vec} a
* @param {Vec} b
* @returns {number}
*/
export function cosine(a, b) {
assertSameLength(a, b, 'cosine');
const na = norm(a);
const nb = norm(b);
if (na === 0 || nb === 0) return 0;
return dot(a, b) / (na * nb);
}
/**
* Exponential moving average toward `target`, returned normalized.
*
* This is the whole interest model:
*
* v <- normalize( (1 - alpha) * v + alpha * weight * target )
*
* `alpha` is the session-recency knob. High alpha means the last thing read
* dominates; low alpha means the vector holds its shape. `weight` carries the
* signal strength (and its sign — a skip pushes the vector *away* from the
* item, which is what makes negative feedback actually work rather than merely
* being absent).
*
* @param {Vec} current
* @param {Vec} target
* @param {number} alpha Learning rate in [0, 1].
* @param {number} weight Signal weight; may be negative.
* @returns {Vec} A new normalized vector.
*/
export function emaToward(current, target, alpha, weight) {
assertSameLength(current, target, 'emaToward');
if (!(alpha >= 0 && alpha <= 1)) {
throw new RangeError(`emaToward: alpha must be in [0,1], got ${alpha}`);
}
const out = new Float32Array(current.length);
const keep = 1 - alpha;
const push = alpha * weight;
for (let i = 0; i < current.length; i += 1) {
out[i] = keep * current[i] + push * target[i];
}
return normalize(out);
}
/**
* Arithmetic mean of a non-empty list of equal-length vectors.
* Used to derive a topic centroid from its member items.
*
* @param {Vec[]} vectors
* @returns {Vec}
*/
export function centroid(vectors) {
if (vectors.length === 0) throw new RangeError('centroid: needs at least one vector');
const dim = vectors[0].length;
const out = new Float32Array(dim);
for (const v of vectors) {
assertSameLength(out, v, 'centroid');
for (let i = 0; i < dim; i += 1) out[i] += v[i];
}
for (let i = 0; i < dim; i += 1) out[i] /= vectors.length;
return out;
}