diff --git a/worker/repin.js b/worker/repin.js index ea0e76ce..e87a6cbe 100644 --- a/worker/repin.js +++ b/worker/repin.js @@ -1,6 +1,7 @@ function repin ({ models }) { return async function ({ name }) { - console.log(name) + console.log('doing', name) + // get the id const id = name.slice('repin-'.length) if (id.length === 0 || isNaN(id)) { diff --git a/worker/trust.js b/worker/trust.js index b34d7c32..3a15ca80 100644 --- a/worker/trust.js +++ b/worker/trust.js @@ -1,7 +1,181 @@ function trust ({ boss, models }) { return async function () { - return null + console.log('doing trust') + const graph = await getGraph(models) + const trust = await trustGivenGraph(graph, 624) + await storeTrust(models, trust) + console.log('done doing trust') } } +// only explore a path up to this depth from start +const MAX_DEPTH = 6 +const MAX_TRUST = 0.9 + +function pathsOverlap (arr1 = [], arr2 = []) { + const dp = new Array(arr1.length + 1).fill(0).map(() => new Array(arr2.length + 1).fill(0)) + for (let i = arr1.length - 1; i >= 0; i--) { + for (let j = arr2.length - 1; j >= 0; j--) { + if (arr1[i] === arr2[j]) { + dp[i][j] = dp[i + 1][j + 1] + 1 + if (dp[i][j] > 1) { + return true + } + } else { + dp[i][j] = 0 + } + } + } + + return false +} + +/* + This approximates an upper bound of trust given a list of indepent trust + values ... we basically are compressing a trust vector into a single value + without having to compute the trust using the inclusion-exclusion principle +*/ +function boundedTrust (probs) { + const max = Math.max(...probs) + const sum = probs.reduce((a, c) => a + c) + const trust = sum - max * (sum - max) + return Math.min(trust, MAX_TRUST) +} + +/* + Given the paths to each node and the accumulated trust along that path + this function returns an object where the keys are the node ids and + their value is the trust of that node +*/ +function trustGivenPaths (paths) { + const trust = {} + for (const [node, npaths] of Object.entries(paths)) { + console.log(Object.values(npaths)) + trust[node] = boundedTrust(Object.values(npaths)) + } + return trust +} + +/* + Given a graph and start this function returns an object where + the keys are the node id and their value is the trust of that node +*/ +function trustGivenGraph (graph, start) { + const queue = [] // queue of to be visited nodes + queue.push(start) // visit start first + + const depth = {} // store the node depth ... XXX space inefficient + depth[start] = 0 // start node is depth 0 + + const paths = {} // { node : { path to node as stringified json array : trust } } + paths[start] = { '[]': 1 } // the paths to start is an empty path with trust of 1 + + // while we have nodes to visit + while (queue.length > 0) { + const node = queue.shift() + console.log('visiting', node) + if (depth[node] === MAX_DEPTH) break + + if (!graph[node]) { + // node doesn't have outbound edges + continue + } + + // for all of this nodes outbound edges + for (let i = 0; i < graph[node].length; i++) { + const { node: sibling, trust } = graph[node][i] + console.log('sibling', sibling) + let explore = false + + // for all existing paths to this node + for (const [key, value] of Object.entries(paths[node])) { + const parentPath = JSON.parse(key) + if (parentPath.includes(sibling)) { + // sibling already exists on a path to us, ie this would be a cycle + continue + } + + // add this path to sibling + const path = JSON.stringify([...parentPath, node]) + paths[sibling] = paths[sibling] || {} + + // if this sibling has not been visited along this path + if (!paths[sibling][path]) { + // here we exclude paths that aren't disjoint - they mininally contribute + // to trust so we just exclude them, yielding a very small underestimation + // of trust while reducing the number of paths we have to explore + let disjoint = true + // for all the paths to sibling + for (const [key2] of Object.entries(paths[sibling])) { + // if this existing path to sibling contains overlap with the + // path we're exploring, ignore it + const otherPath = JSON.parse(key2) + const parsedPath = JSON.parse(path) + if (pathsOverlap(otherPath, parsedPath)) { + disjoint = false + break + } + } + + // if this path is disjoint with all existing paths to sibling + if (disjoint) { + // accumulate the trust along the path and store it + paths[sibling][path] = value * trust + explore = true + } + } + } + + // if we shouldn't explore this sibling, don't queue it + if (!explore) continue + depth[sibling] = depth[node] + 1 + queue.push(sibling) + console.log('queuing', sibling) + } + } + + return trustGivenPaths(paths) +} + +/* + graph is returned as json in adjacency list where edges are the trust value 0-.9 + graph = { + node1 : [{node : node2, trust: trust12}, {node: node3, trust: trust13}], + node2 : [{node : node1, trust: trust21}], + node3 : [{node : node2, trust: trust32}], + } +*/ +async function getGraph (models) { + const [{ graph }] = await models.$queryRaw` + select json_object_agg(id, hops) as graph + from ( + select id, json_agg(json_build_object('node', oid, 'trust', trust)) as hops + from ( + select "ItemAct"."userId" as id, "Item"."userId" as oid, least(${MAX_TRUST}, count(*)/10.0) as trust + from "ItemAct" + join "Item" on "itemId" = "Item".id and "ItemAct"."userId" <> "Item"."userId" + where "ItemAct".act = 'VOTE' group by "ItemAct"."userId", "Item"."userId" + ) a + group by id + ) b` + console.log(graph) + return graph +} + +async function storeTrust (models, nodeTrust) { + // convert nodeTrust into table literal string + let values = '' + for (const [id, trust] of Object.entries(nodeTrust)) { + if (values) values += ',' + values += `(${id}, ${trust})` + } + + // update the trust of each user in graph + await models.$executeRaw( + `UPDATE users + SET trust = g.trust + FROM (values ${values}) g(id, trust) + WHERE users.id = g.id`) +} + module.exports = { trust }