linkpredict

package
v1.1.0-preview Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 15 Imported by: 0

README

Topological link prediction algorithms for NornicDB, providing Neo4j Graph Data Science (GDS) compatibility.

Overview

This package adds canonical graph-based link prediction to complement NornicDB's existing semantic/behavioral inference engine. It implements the standard Neo4j GDS link prediction procedures.

Neo4j GDS Compatibility

All algorithms are accessible via Neo4j GDS-compatible Cypher procedures:

-- Adamic-Adar (weights rare connections)
CALL gds.linkPrediction.adamicAdar.stream({
  sourceNode: id(n),
  topK: 10
})
YIELD node1, node2, score

-- Common Neighbors (simple count)
CALL gds.linkPrediction.commonNeighbors.stream({
  sourceNode: id(n),
  topK: 10
})
YIELD node1, node2, score

-- Resource Allocation (inverse degree weighting)
CALL gds.linkPrediction.resourceAllocation.stream({
  sourceNode: id(n),
  topK: 10
})
YIELD node1, node2, score

-- Preferential Attachment (degree product)
CALL gds.linkPrediction.preferentialAttachment.stream({
  sourceNode: id(n),
  topK: 10
})
YIELD node1, node2, score

-- Jaccard Coefficient (normalized overlap)
CALL gds.linkPrediction.jaccard.stream({
  sourceNode: id(n),
  topK: 10
})
YIELD node1, node2, score

-- Hybrid (topology + semantic)
CALL gds.linkPrediction.predict.stream({
  sourceNode: id(n),
  algorithm: 'adamic_adar',
  topologyWeight: 0.6,
  semanticWeight: 0.4,
  topK: 20
})
YIELD node1, node2, score, topology_score, semantic_score, reason

Algorithms Implemented

1. Common Neighbors
  • Formula: |N(u) ∩ N(v)|
  • Use: Simplest heuristic, counts shared neighbors
  • Good for: Social networks, collaboration graphs
2. Jaccard Coefficient
  • Formula: |N(u) ∩ N(v)| / |N(u) ∪ N(v)|
  • Use: Normalized similarity, reduces degree bias
  • Good for: Comparing nodes with different degrees
3. Adamic-Adar
  • Formula: Σ(1 / log(|N(z)|)) for z in common neighbors
  • Use: Weights rare connections higher
  • Good for: Citation networks, knowledge graphs
  • Reference: Adamic & Adar (2003)
4. Resource Allocation
  • Formula: Σ(1 / |N(z)|) for z in common neighbors
  • Use: Simpler variant of Adamic-Adar
  • Good for: Similar to Adamic-Adar
5. Preferential Attachment
  • Formula: |N(u)| * |N(v)|
  • Use: Models "rich get richer" phenomenon
  • Good for: Scale-free networks, growth models
  • Reference: Barabási & Albert (1999)

Hybrid Scoring

Combines topological and semantic signals:

import "github.com/orneryd/nornicdb/pkg/linkpredict"

// Create hybrid scorer
config := linkpredict.HybridConfig{
    TopologyWeight:    0.6,  // Structural signals
    SemanticWeight:    0.4,  // Embedding similarity
    TopologyAlgorithm: "adamic_adar",
    NormalizeScores:   true,
    MinThreshold:      0.3,
}
scorer := linkpredict.NewHybridScorer(config)

// Set semantic scorer (embedding similarity)
scorer.SetSemanticScorer(func(ctx context.Context, source, target storage.NodeID) float64 {
    // Return cosine similarity between embeddings
    return linkpredict.CosineSimilarity(sourceEmb, targetEmb)
})

// Get predictions
predictions := scorer.Predict(ctx, graph, sourceNode, 10)
for _, pred := range predictions {
    fmt.Printf("%s: %.3f (topo: %.3f, sem: %.3f)\n",
        pred.TargetID, pred.Score, pred.TopologyScore, pred.SemanticScore)
}

When to Use Each Algorithm

Algorithm Social Networks Knowledge Graphs Citation Networks AI Agent Memory
Common Neighbors ⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐
Jaccard ⭐⭐⭐ ⭐⭐ ⭐⭐ ⭐⭐
Adamic-Adar ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Resource Allocation ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Preferential Attachment ⭐⭐ ⭐⭐⭐
Hybrid (Topology + Semantic) ⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐

Comparison: Topological vs Semantic

Topological Link Prediction (this package):

  • Uses only graph structure (neighbors, degrees, paths)
  • Fast, deterministic
  • Captures social/organizational patterns
  • Examples: "You both know Alice", "Popular people connect to popular people"

Semantic Link Prediction (existing inference engine):

  • Uses embeddings, co-access, temporal signals
  • Captures meaning and behavior
  • Examples: "Documents about similar topics", "Accessed together frequently"

Hybrid (best of both):

  • Combines structural and semantic signals
  • More robust predictions
  • Examples: "Similar interests AND mutual friends", "Related topics AND cited together"

Performance

Benchmarked on 1000-node graph with average degree 10:

BenchmarkCommonNeighbors-8         10000    ~100 µs/op
BenchmarkJaccard-8                  5000    ~200 µs/op
BenchmarkAdamicAdar-8               5000    ~250 µs/op
BenchmarkResourceAllocation-8       5000    ~250 µs/op
BenchmarkPreferentialAttachment-8  20000    ~50 µs/op
BenchmarkHybridPredict-8            3000    ~400 µs/op

Testing

# Run all tests
cd nornicdb && go test ./pkg/linkpredict/... -v

# Run benchmarks
go test ./pkg/linkpredict/... -bench=. -benchmem

# Test Neo4j GDS compatibility
go test ./pkg/cypher/... -v -run=TestLinkPrediction

Integration with Existing Inference Engine

The topological algorithms complement NornicDB's existing /pkg/inference/ engine:

┌─────────────────────────────────────────────┐
│         Link Prediction Strategy             │
├─────────────────────────────────────────────┤
│  Topological (this pkg)  │  Semantic (inference) │
│  • Adamic-Adar           │  • Embedding similarity│
│  • Jaccard               │  • Co-access patterns │
│  • Common Neighbors      │  • Temporal proximity │
│  • Resource Allocation   │  • Transitive inference│
│  • Preferential Attach   │                      │
├─────────────────────────────────────────────┤
│         Hybrid Scorer (this pkg)             │
│   Blends topology + semantic with weights    │
└─────────────────────────────────────────────┘

Use topological when:

  • Structure matters (social networks, org charts)
  • Fast predictions needed (no embedding lookups)
  • Graph topology is well-formed

Use semantic when:

  • Meaning matters (documents, knowledge)
  • Embeddings are available
  • Behavioral patterns are important

Use hybrid when:

  • Both dimensions matter (AI agent memory)
  • Maximum prediction quality needed
  • Willing to pay compute cost

References

  • Liben-Nowell & Kleinberg (2007). "The link-prediction problem for social networks"
  • Adamic & Adar (2003). "Friends and neighbors on the Web"
  • Zhou et al. (2009). "Predicting missing links via local information"
  • Barabási & Albert (1999). "Emergence of scaling in random networks"
  • Neo4j GDS Documentation: https://neo4j.com/docs/graph-data-science/

License

MIT (same as NornicDB parent project)

Documentation

Overview

graph_builder.go - Optimized parallel graph construction with caching and persistence

This file provides high-performance graph construction for link prediction:

  • Streaming construction with chunked processing (fixes memory spikes)
  • Parallel edge fetching with worker pool (4-8x speedup)
  • Context cancellation and progress callbacks
  • Disk-based graph caching (gob serialization)
  • Incremental graph updates (delta changes)

Memory Optimization:

  • Processes nodes in chunks to avoid loading all at once
  • Explicit GC hints between chunks
  • Pre-allocated maps with capacity hints

Performance:

  • Parallel edge fetching with configurable worker count
  • Lock-free score accumulation for algorithms
  • Cached graph persistence to avoid rebuilds

Package linkpredict provides topological link prediction algorithms for NornicDB.

This package implements canonical graph-based link prediction heuristics that complement NornicDB's existing semantic/behavioral inference engine.

Algorithms Implemented:

  • Common Neighbors: |N(u) ∩ N(v)|
  • Jaccard Coefficient: |N(u) ∩ N(v)| / |N(u) ∪ N(v)|
  • Adamic-Adar: Σ(1 / log(|N(z)|)) for z in common neighbors
  • Preferential Attachment: |N(u)| * |N(v)|
  • Resource Allocation: Σ(1 / |N(z)|) for z in common neighbors

Usage Example:

// Build graph from storage
graph := linkpredict.BuildGraphFromEngine(ctx, storageEngine)

// Get predictions for a specific node
sourceID := storage.NodeID("user-123")
predictions := linkpredict.AdamicAdar(graph, sourceID, 10)

for _, pred := range predictions {
	fmt.Printf("Suggest edge to %s (score: %.3f)\n", pred.TargetID, pred.Score)
}

// Hybrid scoring: blend topology + semantic
topologyScore := predictions[0].Score
semanticScore := getEmbeddingSimilarity(sourceID, predictions[0].TargetID)
hybridScore := 0.5*topologyScore + 0.5*semanticScore

How Topological vs Semantic Differ:

**Topological** (this package):

  • Uses only graph structure (neighbors, paths, degrees)
  • Captures social/organizational/citation patterns
  • Example: Two people with many mutual friends should connect
  • Fast (no embedding lookups), deterministic

**Semantic** (existing inference engine):

  • Uses embeddings, co-access, temporal signals
  • Captures meaning and behavior
  • Example: Two documents about similar topics should link
  • Requires embeddings, captures semantic relatedness

**When to Use Each:**

  • Social networks → Topology dominates
  • Knowledge/document graphs → Semantic dominates
  • Citation networks → Both valuable
  • AI agent memory → Both valuable (hybrid)

ELI12 (Explain Like I'm 12):

Imagine you're at a new school and want to make friends:

**Common Neighbors**: "You and Sarah both know Alex and Jamie. You should probably meet Sarah!"

**Jaccard**: "You and Sarah share 2 friends, but you each know 10 people total. That's 2/(10+10-2) = 11% overlap - moderate connection."

**Adamic-Adar**: "Your mutual friend Alex only knows 3 people (rare connection!), but Jamie knows 50 people. Alex is a stronger signal you should know Sarah."

**Preferential Attachment**: "You know 20 people, Sarah knows 30. Popular people connect to popular people (20*30 = 600 'popularity score')."

**Resource Allocation**: "Each mutual friend 'votes' for your connection, weighted by how exclusive that friend is."

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CosineSimilarity

func CosineSimilarity(a, b []float32) float64

CosineSimilarity computes cosine similarity between two embeddings.

This is a utility function for semantic scoring. Returns value in [-1, 1], where 1 = identical, 0 = orthogonal, -1 = opposite.

Uses SIMD-accelerated implementation for maximum performance.

Example:

scorer := func(ctx context.Context, source, target storage.NodeID) float64 {
	sourceEmb := getEmbedding(source)
	targetEmb := getEmbedding(target)
	return linkpredict.CosineSimilarity(sourceEmb, targetEmb)
}

func ExportToWriter

func ExportToWriter(graph Graph, w io.Writer) error

ExportToWriter writes the graph in a simple text format.

func ParallelAdamicAdar

func ParallelAdamicAdar(
	ctx context.Context,
	graph Graph,
	sources []storage.NodeID,
	topK int,
	config *ParallelScoreConfig,
) map[storage.NodeID][]Prediction

ParallelAdamicAdar computes Adamic-Adar scores in parallel.

func ParallelCommonNeighbors

func ParallelCommonNeighbors(
	ctx context.Context,
	graph Graph,
	sources []storage.NodeID,
	topK int,
	config *ParallelScoreConfig,
) map[storage.NodeID][]Prediction

ParallelCommonNeighbors computes common neighbors scores in parallel.

This is useful when computing scores for many source nodes at once.

func ParallelJaccard

func ParallelJaccard(
	ctx context.Context,
	graph Graph,
	sources []storage.NodeID,
	topK int,
	config *ParallelScoreConfig,
) map[storage.NodeID][]Prediction

ParallelJaccard computes Jaccard scores in parallel.

Types

type BuildConfig

type BuildConfig struct {
	// ChunkSize controls how many nodes are processed at once.
	// Smaller = less memory, larger = faster.
	// Default: 1000
	ChunkSize int

	// WorkerCount controls parallel edge fetching.
	// Default: runtime.NumCPU()
	WorkerCount int

	// Undirected treats edges as bidirectional.
	// Default: true
	Undirected bool

	// GCAfterChunk triggers garbage collection after each chunk.
	// Reduces peak memory at cost of some speed.
	// Default: true
	GCAfterChunk bool

	// ProgressCallback is called after each chunk with progress info.
	// Optional - set to nil to disable.
	ProgressCallback func(processed, total int, elapsed time.Duration)

	// CachePath is the directory for persisting cached graphs.
	// Empty string disables caching.
	CachePath string

	// CacheTTL is how long a cached graph is valid.
	// Default: 1 hour
	CacheTTL time.Duration
}

BuildConfig controls graph construction behavior.

func DefaultBuildConfig

func DefaultBuildConfig() *BuildConfig

DefaultBuildConfig returns sensible defaults.

type BuildStats

type BuildStats struct {
	LastBuildTime   time.Duration
	LastBuildNodes  int
	LastBuildEdges  int
	BuildsCompleted int64
	CacheHits       int64
	CacheMisses     int64
}

BuildStats contains statistics about graph building.

type CachedGraph

type CachedGraph struct {
	Graph     map[string][]string // Simplified format for gob
	Timestamp time.Time
	NodeCount int
	EdgeCount int
}

CachedGraph is the serialized format for graph caching.

type EdgeChange

type EdgeChange struct {
	From storage.NodeID
	To   storage.NodeID
}

EdgeChange represents an edge addition or removal.

type Graph

type Graph map[storage.NodeID]NodeSet

Graph represents a graph as an adjacency map for efficient link prediction.

The graph is undirected by default (for most social/knowledge graphs), but can handle directed graphs by only populating adjacency in one direction.

Example:

graph := Graph{
	"alice": {"bob": {}, "charlie": {}},
	"bob":   {"alice": {}, "diana": {}},
}

This represents:

  • alice -- bob
  • alice -- charlie
  • bob -- diana

func BuildGraphFromEngine

func BuildGraphFromEngine(ctx context.Context, engine storage.Engine, undirected bool) (Graph, error)

BuildGraphFromEngine constructs a Graph from a storage engine.

This creates an in-memory adjacency representation optimized for link prediction algorithms. For large graphs (>1M nodes), consider sampling or using candidate generation to limit memory usage.

OPTIMIZED: This function now uses streaming construction with:

  • Chunked processing to avoid memory spikes
  • Parallel edge fetching for 4-8x speedup
  • GC hints between chunks
  • Context cancellation support

Parameters:

  • ctx: Context for cancellation
  • engine: Storage engine implementing GetOutgoingEdges
  • undirected: If true, treats edges as bidirectional

Example:

engine := storage.NewMemoryEngine()
// ... populate with nodes and edges ...

// Build undirected graph (social network)
graph := linkpredict.BuildGraphFromEngine(ctx, engine, true)

// Build directed graph (citation network)
graph = linkpredict.BuildGraphFromEngine(ctx, engine, false)

func BuildGraphFromEngineOptimized

func BuildGraphFromEngineOptimized(
	ctx context.Context,
	engine storage.Engine,
	config *BuildConfig,
) (Graph, error)

BuildGraphFromEngineOptimized is the optimized replacement for BuildGraphFromEngine.

This version provides:

  • Streaming construction (fixes memory spikes)
  • Parallel edge fetching (4-8x speedup)
  • Context cancellation support
  • Optional progress callbacks

Parameters:

  • ctx: Context for cancellation
  • engine: Storage engine
  • config: Build configuration (nil uses defaults)

Example:

config := &linkpredict.BuildConfig{
	ChunkSize:   500,
	WorkerCount: 4,
	Undirected:  true,
	ProgressCallback: func(p, t int, e time.Duration) {
		fmt.Printf("Progress: %d/%d (%.1fs)\n", p, t, e.Seconds())
	},
}
graph, err := linkpredict.BuildGraphFromEngineOptimized(ctx, engine, config)

func ImportFromReader

func ImportFromReader(r io.Reader) (Graph, error)

ImportFromReader reads a graph from simple text format (tab-separated node pairs).

func (Graph) Degree

func (g Graph) Degree(node storage.NodeID) int

Degree returns the degree (number of neighbors) for a node.

func (Graph) Neighbors

func (g Graph) Neighbors(node storage.NodeID) NodeSet

Neighbors returns the neighbor set for a node.

type GraphBuilder

type GraphBuilder struct {
	// contains filtered or unexported fields
}

GraphBuilder provides optimized graph construction with caching.

func NewGraphBuilder

func NewGraphBuilder(engine storage.Engine, config *BuildConfig) *GraphBuilder

NewGraphBuilder creates a new optimized graph builder.

func (*GraphBuilder) ApplyDelta

func (b *GraphBuilder) ApplyDelta(graph Graph, delta *GraphDelta) Graph

ApplyDelta updates an existing graph with changes.

This is much faster than rebuilding when only a few changes occurred. For large deltas (>10% of graph), consider rebuilding instead.

func (*GraphBuilder) Build

func (b *GraphBuilder) Build(ctx context.Context) (Graph, error)

Build constructs a graph with all optimizations.

This method:

  1. Checks for valid cached graph (if caching enabled)
  2. Streams nodes in chunks to avoid memory spikes
  3. Fetches edges in parallel with worker pool
  4. Respects context cancellation
  5. Saves graph to cache (if caching enabled)

Returns the constructed graph or error if cancelled/failed.

func (*GraphBuilder) InvalidateCache

func (b *GraphBuilder) InvalidateCache() error

InvalidateCache removes the cached graph.

func (*GraphBuilder) Stats

func (b *GraphBuilder) Stats() BuildStats

Stats returns build statistics.

type GraphDelta

type GraphDelta struct {
	AddedNodes   []storage.NodeID
	RemovedNodes []storage.NodeID
	AddedEdges   []EdgeChange
	RemovedEdges []EdgeChange
}

GraphDelta represents changes to apply to an existing graph.

type GraphStreamer

type GraphStreamer struct {
	// contains filtered or unexported fields
}

GraphStreamer provides node-by-node graph iteration without loading all into memory.

func NewGraphStreamer

func NewGraphStreamer(engine storage.Engine, config *BuildConfig) *GraphStreamer

NewGraphStreamer creates a streamer for very large graphs.

func (*GraphStreamer) StreamEdges

func (s *GraphStreamer) StreamEdges(ctx context.Context, nodeID storage.NodeID, fn func(edge *storage.Edge) error) error

StreamEdges iterates over all edges for a node.

func (*GraphStreamer) StreamNodes

func (s *GraphStreamer) StreamNodes(ctx context.Context, fn func(node *storage.Node) error) error

StreamNodes iterates over all nodes without loading all into memory.

type HybridConfig

type HybridConfig struct {
	// Weight for topological signal (0.0-1.0)
	TopologyWeight float64

	// Weight for semantic signal (0.0-1.0)
	SemanticWeight float64

	// Which topology algorithm to use
	// Options: "common_neighbors", "jaccard", "adamic_adar",
	//          "preferential_attachment", "resource_allocation"
	TopologyAlgorithm string

	// Use ensemble of all topology algorithms (ignores TopologyAlgorithm)
	UseEnsemble bool

	// Normalize scores to [0, 1] before blending
	NormalizeScores bool

	// Minimum threshold for combined score (0.0-1.0)
	MinThreshold float64
}

HybridConfig configures hybrid scoring behavior.

Weights control the blend between topological and semantic signals:

  • TopologyWeight = 1.0, SemanticWeight = 0.0 → Pure topology
  • TopologyWeight = 0.0, SemanticWeight = 1.0 → Pure semantic
  • TopologyWeight = 0.5, SemanticWeight = 0.5 → Balanced hybrid
  • TopologyWeight = 0.7, SemanticWeight = 0.3 → Topology-dominant

Example:

// Social network (structure matters more)
config := HybridConfig{
	TopologyWeight:    0.7,
	SemanticWeight:    0.3,
	TopologyAlgorithm: "adamic_adar",
}

// Knowledge graph (semantics matter more)
config = HybridConfig{
	TopologyWeight:    0.3,
	SemanticWeight:    0.7,
	TopologyAlgorithm: "jaccard",
}

// Balanced AI agent memory
config = HybridConfig{
	TopologyWeight:    0.5,
	SemanticWeight:    0.5,
	TopologyAlgorithm: "adamic_adar",
	UseEnsemble:       true, // Use all topology algorithms
}

func DefaultHybridConfig

func DefaultHybridConfig() HybridConfig

DefaultHybridConfig returns balanced configuration for general use.

type HybridPrediction

type HybridPrediction struct {
	TargetID       storage.NodeID
	Score          float64 // Combined hybrid score
	TopologyScore  float64 // Raw topology score
	SemanticScore  float64 // Raw semantic score
	TopologyMethod string  // Which topology algorithm was used
	Reason         string  // Human-readable explanation
}

HybridPrediction represents a prediction combining topology and semantics.

type HybridScorer

type HybridScorer struct {
	// contains filtered or unexported fields
}

HybridScorer combines topological and semantic link prediction signals.

This is where topological algorithms (Jaccard, Adamic-Adar, etc.) meet NornicDB's existing semantic inference (embedding similarity, co-access).

Usage Example:

scorer := linkpredict.NewHybridScorer(linkpredict.HybridConfig{
	TopologyWeight: 0.5,
	SemanticWeight: 0.5,
	TopologyAlgorithm: "adamic_adar",
})

// Set semantic scoring function (from inference engine)
scorer.SetSemanticScorer(func(ctx context.Context, source, target storage.NodeID) float64 {
	return getEmbeddingSimilarity(source, target)
})

// Get hybrid predictions
predictions := scorer.Predict(ctx, graph, "user-123", 10)

Why Hybrid Matters:

**Topology alone** captures structure but misses semantics:

  • Two papers with many mutual citations (high topology score)
  • But about completely different topics (low semantic score)
  • Hybrid catches this mismatch

**Semantics alone** captures meaning but misses social patterns:

  • Two people with similar interests (high semantic score)
  • But in completely different social circles (low topology score)
  • Hybrid catches this too

**Best of both worlds:**

  • High topology + high semantic = very confident prediction
  • High topology + low semantic = structurally likely, verify content fit
  • Low topology + high semantic = semantically related, but socially distant
  • Low topology + low semantic = no connection

ELI12:

Imagine recommending friends at school:

**Topology**: "You and Sarah have 5 mutual friends" (structural signal) **Semantic**: "You and Sarah both love soccer and coding" (interest signal) **Hybrid**: "You should definitely meet Sarah!" (both signals agree)

But also: **Topology only**: "You and Jake have 8 mutual friends" (strong structure) **Semantic**: "But Jake is really into art, not sports" (weak interest match) **Hybrid**: "Maybe you know Jake through school, but not sure you'd be close"

func NewHybridScorer

func NewHybridScorer(config HybridConfig) *HybridScorer

NewHybridScorer creates a new hybrid scorer with the given configuration.

func (*HybridScorer) Predict

func (h *HybridScorer) Predict(ctx context.Context, graph Graph, source storage.NodeID, topK int) []HybridPrediction

Predict computes hybrid link predictions for a source node.

This method:

  1. Runs topological algorithm(s) to get structural candidates
  2. Computes semantic scores for those candidates
  3. Blends scores according to configured weights
  4. Returns top-K predictions sorted by hybrid score

Parameters:

  • ctx: Context for cancellation
  • graph: Graph structure for topology algorithms
  • source: Source node to predict edges from
  • topK: Maximum number of predictions to return

Returns:

  • Sorted list of predictions with hybrid scores

Example:

predictions := scorer.Predict(ctx, graph, "user-123", 10)

for _, pred := range predictions {
	fmt.Printf("→ %s: %.3f (topology: %.3f, semantic: %.3f)\n",
		pred.TargetID, pred.Score,
		pred.Metadata["topology_score"],
		pred.Metadata["semantic_score"])
}

func (*HybridScorer) SetSemanticScorer

func (h *HybridScorer) SetSemanticScorer(fn SemanticScorerFunc)

SetSemanticScorer sets the semantic scoring function.

This must be called before using Predict(), or semantic scores will be 0.

Example:

scorer.SetSemanticScorer(func(ctx context.Context, source, target storage.NodeID) float64 {
	return inferenceEngine.GetSimilarity(source, target)
})

type NodeSet

type NodeSet map[storage.NodeID]struct{}

NodeSet represents a set of node IDs (adjacent nodes).

func (NodeSet) Contains

func (ns NodeSet) Contains(id storage.NodeID) bool

Contains checks if a node exists in a set.

func (NodeSet) Size

func (ns NodeSet) Size() int

Size returns the number of nodes in the set.

type ParallelScoreConfig

type ParallelScoreConfig struct {
	WorkerCount int
	BatchSize   int
}

ParallelScoreConfig controls parallel score computation.

func DefaultParallelScoreConfig

func DefaultParallelScoreConfig() *ParallelScoreConfig

DefaultParallelScoreConfig returns defaults for parallel scoring.

type Prediction

type Prediction struct {
	TargetID  storage.NodeID
	Score     float64
	Algorithm string
	Reason    string
}

Prediction represents a predicted edge with confidence score.

Scores are algorithm-specific and not directly comparable across algorithms:

  • Common Neighbors: Integer count (0-N)
  • Jaccard: Similarity ratio (0.0-1.0)
  • Adamic-Adar: Weighted sum (0.0-∞)
  • Preferential Attachment: Product of degrees (0-∞)
  • Resource Allocation: Weighted sum (0.0-∞)

To compare across algorithms, normalize scores to [0, 1] range.

func AdamicAdar

func AdamicAdar(graph Graph, source storage.NodeID, topK int) []Prediction

AdamicAdar computes link predictions using Adamic-Adar index.

Algorithm: score(u, v) = Σ(1 / log(|N(z)|)) for z in N(u) ∩ N(v)

Weights common neighbors by their rarity. A common neighbor with few connections is a stronger signal than a highly-connected one.

Intuition: If you and I both know a person who knows everyone, that's weak evidence we should connect. But if we both know someone exclusive, that's strong evidence.

Pros:

  • Weights rare connections higher (less noise)
  • Often outperforms simpler methods
  • Well-studied in literature

Cons:

  • Requires degree calculation
  • Undefined for degree-1 nodes (uses log)

Example:

predictions := linkpredict.AdamicAdar(graph, "user-789", 15)
for _, p := range predictions {
	fmt.Printf("→ %s: %.3f weighted score\n", p.TargetID, p.Score)
}

Reference: Adamic & Adar (2003), "Friends and neighbors on the Web"

func CommonNeighbors

func CommonNeighbors(graph Graph, source storage.NodeID, topK int) []Prediction

CommonNeighbors computes link predictions based on common neighbor count.

Algorithm: score(u, v) = |N(u) ∩ N(v)|

This is the simplest structural heuristic. Two nodes with many shared neighbors are likely to connect.

Pros:

  • Very fast (O(deg(u) * deg(v)))
  • Intuitive and explainable
  • Works well for social networks

Cons:

  • Biased toward high-degree nodes
  • Doesn't account for neighbor importance

Example 1 - Friend Recommendations:

graph := linkpredict.BuildGraphFromEngine(ctx, engine, true)
predictions := linkpredict.CommonNeighbors(graph, "alice", 5)

fmt.Println("People Alice should meet:")
for _, p := range predictions {
	fmt.Printf("  → %s: %d mutual friends\n", p.TargetID, int(p.Score))
}
// Output:
//   → diana: 3 mutual friends
//   → emily: 2 mutual friends

Example 2 - Research Collaboration Network:

// Build citation graph
graph := linkpredict.BuildGraphFromEngine(ctx, engine, false)

// Find potential collaborators for a researcher
predictions := linkpredict.CommonNeighbors(graph, "researcher-123", 10)

for _, p := range predictions {
	// High common neighbor count = strong collaboration potential
	if p.Score >= 3 {
		fmt.Printf("Strong collaboration signal: %s\n", p.TargetID)
		sendCollabInvite(p.TargetID)
	}
}

Example 3 - Document Clustering:

// Documents connected by citations or references
graph := buildDocumentGraph(documents)

// Find related documents
predictions := linkpredict.CommonNeighbors(graph, "paper-456", 20)

relatedDocs := make([]string, 0)
for _, p := range predictions {
	if p.Score >= 2 { // At least 2 common references
		relatedDocs = append(relatedDocs, string(p.TargetID))
	}
}

createCluster(relatedDocs)

ELI12:

Imagine you're at a party and want to find new friends. Common Neighbors says: "Count how many friends you have IN COMMON with each person."

  • You and Sarah both know: Alex, Jamie, Charlie → 3 common friends
  • You and Mike both know: Alex → 1 common friend
  • Prediction: You should meet Sarah! (higher score)

It's like Facebook's "People You May Know" - they show you people who have lots of mutual friends with you, because you probably move in similar circles!

Real-world Example:

  • You know: [Alice, Bob, Charlie, Diana]
  • Sarah knows: [Bob, Charlie, Diana, Emily]
  • Common: [Bob, Charlie, Diana] = 3 friends
  • Score: 3 (raw count)

Pros & Cons:

✅ Simple and intuitive
✅ Works well for social networks
✅ Fast to compute
❌ Biased toward popular nodes (everyone knows them!)
❌ Doesn't normalize by total friends

Performance:

  • O(|neighbors| × avg_degree) per source node
  • Fast for sparse graphs (most real-world graphs)
  • Memory: O(candidates) for score tracking

func Jaccard

func Jaccard(graph Graph, source storage.NodeID, topK int) []Prediction

Jaccard computes link predictions using Jaccard coefficient.

Algorithm: score(u, v) = |N(u) ∩ N(v)| / |N(u) ∪ N(v)|

Normalizes common neighbors by total neighborhood size, reducing bias toward high-degree nodes.

Score Range: [0.0, 1.0]

  • 0.0: No common neighbors
  • 1.0: Identical neighborhoods

Pros:

  • Normalized (comparable across node pairs)
  • Less biased than raw common neighbors
  • Standard similarity measure

Cons:

  • May underweight important connections
  • Sensitive to degree imbalance

Example 1 - Document Similarity:

graph := buildDocumentGraph(documents)
predictions := linkpredict.Jaccard(graph, "paper-123", 10)

fmt.Println("Similar documents:")
for _, p := range predictions {
	fmt.Printf("  → %s: %.1f%% overlap\n", p.TargetID, p.Score*100)
}
// Output:
//   → paper-789: 45.2% overlap (high similarity)
//   → paper-456: 12.8% overlap (moderate)

Example 2 - Balanced Friend Recommendations:

graph := linkpredict.BuildGraphFromEngine(ctx, engine, true)

// Jaccard normalizes for degree (better than raw common neighbors)
predictions := linkpredict.Jaccard(graph, "user-123", 5)

for _, p := range predictions {
	// 0.3 = 30% of combined neighborhood overlaps
	if p.Score > 0.3 {
		fmt.Printf("Strong match: %s (%.0f%% similar networks)\n",
			p.TargetID, p.Score*100)
	}
}

Example 3 - Tag-Based Content Recommendation:

// Items connected by shared tags
graph := Graph{
	"movie-action-1": {"action": {}, "scifi": {}, "thriller": {}},
	"movie-action-2": {"action": {}, "scifi": {}, "drama": {}},
	"movie-comedy-1": {"comedy": {}, "romance": {}},
}

predictions := linkpredict.Jaccard(graph, "movie-action-1", 10)

for _, p := range predictions {
	// High Jaccard = similar tag profiles
	fmt.Printf("Recommend %s: %.2f similarity\n", p.TargetID, p.Score)
}
// movie-action-2 will rank high (2/4 = 0.5 Jaccard)

ELI12:

Jaccard is like comparing your playlist with a friend's:

Your songs: [A, B, C, D, E] = 5 songs
Friend's songs: [C, D, E, F, G] = 5 songs
In common: [C, D, E] = 3 songs

Jaccard = 3 / (5 + 5 - 3) = 3/7 = 42.9% similar taste!

Why subtract 3? Because C, D, E appear in BOTH lists, so we don't count them twice in the total. That's the "union" part.

Compare to Common Neighbors:

  • Common Neighbors: "You share 3 songs" (raw count)
  • Jaccard: "42.9% of your combined music overlaps" (percentage)

Jaccard is BETTER when comparing things of different sizes:

  • You: 100 friends, share 10 with Alice

  • You: 20 friends, share 10 with Bob

    Common Neighbors: Both score 10 (seems equal) Jaccard: Alice = 10/110 = 9%, Bob = 10/30 = 33% (Bob is closer!)

Real-world Use:

  • Content recommendation (movies, articles, products)
  • Duplicate detection (similar documents, profiles)
  • Community detection (overlapping friend groups)

Pros & Cons:

✅ Normalized (0-1 range, comparable)
✅ Handles different network sizes fairly
✅ Standard similarity metric
❌ Sensitive to rare connections (small denominator)
❌ Slower than raw common neighbors

Performance:

  • O(|N(u)| + |N(v)|) per candidate pair
  • Requires set intersection/union calculation
  • Memory: O(candidates) for tracking

func PreferentialAttachment

func PreferentialAttachment(graph Graph, source storage.NodeID, topK int) []Prediction

PreferentialAttachment computes link predictions based on node degree product.

Algorithm: score(u, v) = |N(u)| * |N(v)|

Models the "rich get richer" phenomenon: high-degree nodes tend to acquire more connections. Common in scale-free networks (social media, citation networks, the web).

Pros:

  • Extremely fast (O(1) per candidate)
  • No neighbor intersection needed
  • Captures growth dynamics

Cons:

  • Strongly biased toward hubs
  • Ignores local structure
  • Not similarity-based

Example:

predictions := linkpredict.PreferentialAttachment(graph, "paper-123", 10)
for _, p := range predictions {
	fmt.Printf("→ %s: %.0f degree product\n", p.TargetID, p.Score)
}

Reference: Barabási & Albert (1999), "Emergence of scaling in random networks"

func ResourceAllocation

func ResourceAllocation(graph Graph, source storage.NodeID, topK int) []Prediction

ResourceAllocation computes link predictions using resource allocation index.

Algorithm: score(u, v) = Σ(1 / |N(z)|) for z in N(u) ∩ N(v)

Similar to Adamic-Adar but uses linear weighting instead of logarithmic. Models resource transmission through common neighbors.

Intuition: Each common neighbor sends a "resource" unit to the target. If the neighbor has many connections, the resource is divided among them.

Pros:

  • Simpler than Adamic-Adar (no log)
  • Often performs comparably
  • Intuitive "spreading" interpretation

Cons:

  • Similar limitations to Adamic-Adar
  • Undefined for degree-0 nodes

Example:

predictions := linkpredict.ResourceAllocation(graph, "concept-456", 20)
for _, p := range predictions {
	fmt.Printf("→ %s: %.3f resource score\n", p.TargetID, p.Score)
}

Reference: Zhou et al. (2009), "Predicting missing links via local information"

type SemanticScorerFunc

type SemanticScorerFunc func(ctx context.Context, source, target storage.NodeID) float64

SemanticScorerFunc computes semantic similarity between two nodes.

This is typically implemented using embedding similarity, but could also incorporate co-access patterns, temporal proximity, etc.

Parameters:

  • ctx: Context for cancellation
  • source: Source node ID
  • target: Target node ID

Returns:

  • Similarity score (typically 0.0-1.0, but implementation-dependent)

Example:

semanticScorer := func(ctx context.Context, source, target storage.NodeID) float64 {
	sourceNode, _ := engine.GetNode(ctx, source)
	targetNode, _ := engine.GetNode(ctx, target)

	if len(sourceNode.Embedding) == 0 || len(targetNode.Embedding) == 0 {
		return 0.0
	}

	return cosineSimilarity(sourceNode.Embedding, targetNode.Embedding)
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL