feat: Session 11 finale — similarity_nodes, node2vec, Cypher layer

- similarity_nodes(graph, metric): Jaccard / Adamic-Adar similarity between all node pairs
- node2vec_embed(graph, dimensions): Random-walk based graph embeddings
- cypher(query): Translate Cypher MATCH...RETURN → BaraQL GRAPH_TABLE SQL
- Fixed recursive lock deadlock in similarity functions
- Fixed graph start node parsing (pattern variable vs numeric ID)
- Added sequtils import to graph engine
- All 340+ existing tests pass
This commit is contained in:
2026-05-17 16:00:15 +03:00
parent 898f108963
commit a0c5ce8598
3 changed files with 340 additions and 9 deletions
+60 -8
View File
@@ -30,6 +30,7 @@ import ../graph/community as gcomm
import ../ai/chunk as chunkmod
import ../ai/embed as embedmod
import ../ai/llm as llmmod
import ../graph/cypher as cyphermod
type
IndexEntry* = ref object
@@ -1402,13 +1403,62 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
for pol in ctx.policies[table]:
result.add("-- CREATE POLICY " & pol.name & " FOR " & pol.command & "\n")
# Foreign keys
if tbl.foreignKeys.len > 0:
result.add("\n-- Foreign Keys:\n")
for fk in tbl.foreignKeys:
result.add("-- " & fk.refTable & "(" & fk.refColumn & ") ON DELETE " & fk.onDelete & "\n")
return result
of "similarity_nodes":
if expr.irFuncArgs.len < 1: return "[]"
let graphName = evalExpr(expr.irFuncArgs[0], row, ctx)
let metric = if expr.irFuncArgs.len >= 2: evalExpr(expr.irFuncArgs[1], row, ctx).toLower() else: "jaccard"
if graphName notin ctx.graphs:
return "[]"
let g = ctx.graphs[graphName]
let simMetric = if metric == "adamic_adar" or metric == "adamic-adar": gengine.smAdamicAdar else: gengine.smJaccard
let pairs = gengine.similarityNodes(g, simMetric)
var arr = newJArray()
for (a, b, sim) in pairs:
arr.add(%*{"node_a": uint64(a), "node_b": uint64(b), "similarity": sim})
return $(arr)
of "node2vec_embed":
if expr.irFuncArgs.len < 1: return "[]"
let graphName = evalExpr(expr.irFuncArgs[0], row, ctx)
let dims = if expr.irFuncArgs.len >= 2:
try: parseInt(evalExpr(expr.irFuncArgs[1], row, ctx)) except: 64
else: 64
if graphName notin ctx.graphs:
return "[]"
let g = ctx.graphs[graphName]
let embeddings = gengine.node2vec(g, dims, 10, 5)
var obj = newJObject()
for nid, emb in embeddings:
var vecStr = "["
for i, v in emb:
if i > 0: vecStr.add(",")
vecStr.add($v)
vecStr.add("]")
obj[$(uint64(nid))] = %vecStr
return $(obj)
of "cypher":
if expr.irFuncArgs.len < 1: return "[]"
let cypherQuery = evalExpr(expr.irFuncArgs[0], row, ctx)
let sql = cyphermod.cypherToSql(cypherQuery)
if sql.len == 0: return "[]"
let tokens = qlex.tokenize(sql)
let astNode = qpar.parse(tokens)
if astNode.stmts.len == 0: return "[]"
let res = executeQuery(ctx, astNode)
if not res.success:
return "Error: " & res.message
var jsonRows = newJArray()
for r in res.rows:
var jsonRow = newJObject()
for col in res.columns:
jsonRow[col] = if col in r: %r[col] else: newJNull()
jsonRows.add(jsonRow)
return $(jsonRows)
of "datetime":
if expr.irFuncArgs.len > 0:
let arg = evalExpr(expr.irFuncArgs[0], row, ctx).toLower()
@@ -3501,10 +3551,12 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
let algo = plan.graphAlgo.toLowerAscii()
let returnCols = plan.graphReturnCols
let firstNodeId = if g.nodes.len > 0: g.nodes.keys.toSeq[0] else: gengine.NodeId(0)
let explicitStart = try: parseUInt(plan.graphStartNode) except: 0'u64
let explicitEnd = try: parseUInt(plan.graphEndNode) except: 0'u64
case algo
of "bfs":
let startId = if plan.graphStartNode.len > 0: gengine.NodeId(parseUInt(plan.graphStartNode)) else: firstNodeId
let startId = if explicitStart > 0: gengine.NodeId(explicitStart) else: firstNodeId
let maxDepth = if plan.graphMaxDepth >= 0: plan.graphMaxDepth else: -1
let traverseResult = gengine.bfs(g, startId, maxDepth)
for nodeId in traverseResult:
@@ -3524,7 +3576,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
result.add(row)
of "dfs":
let startId = if plan.graphStartNode.len > 0: gengine.NodeId(parseUInt(plan.graphStartNode)) else: firstNodeId
let startId = if explicitStart > 0: gengine.NodeId(explicitStart) else: firstNodeId
let maxDepth = if plan.graphMaxDepth >= 0: plan.graphMaxDepth else: -1
let traverseResult = gengine.dfs(g, startId, maxDepth)
for nodeId in traverseResult:
@@ -3565,9 +3617,9 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
result.add(row)
of "shortest_path", "shortestpath":
if plan.graphStartNode.len > 0 and plan.graphEndNode.len > 0:
let startId = gengine.NodeId(parseUInt(plan.graphStartNode))
let endId = gengine.NodeId(parseUInt(plan.graphEndNode))
if explicitStart > 0 and explicitEnd > 0:
let startId = gengine.NodeId(explicitStart)
let endId = gengine.NodeId(explicitEnd)
let path = gengine.shortestPath(g, startId, endId)
for nodeId in path:
var row = initTable[string, string]()
@@ -3584,8 +3636,8 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
return @[]
of "dijkstra":
if plan.graphStartNode.len > 0:
let startId = gengine.NodeId(parseUInt(plan.graphStartNode))
if explicitStart > 0:
let startId = gengine.NodeId(explicitStart)
let dists = gengine.dijkstra(g, startId)
for nodeId, dist in dists:
var row = initTable[string, string]()