DEV Community

NEHA JAKATE
NEHA JAKATE

Posted on

Vectors vs. Graphs: Picking the Right Tool for Code Intelligence

The Problem

Ask five teams building AI coding tools how they represent a codebase for an LLM, and you'll get two answers: embeddings, or a dependency graph. Both camps are right — for different questions. Picking the wrong one for your use case is why some "semantic code search" tools return technically-similar-but-practically-useless results.

Architecture: What Each Model Actually Captures

Vectors: Semantic Similarity

embed("parse a JWT token")  embed(function verifyToken(token) {...})
Enter fullscreen mode Exit fullscreen mode

Embeddings place semantically similar functions close together in vector space. The query doesn't need exact keyword matches — "parse a JWT token" finds the right utility even if the function is named decodeAuthPayload.

Best for: finding unfamiliar code by intent, code completion across different styles, spotting duplicate logic, semantic clustering of docs.

Limitation: vectors capture meaning, not structure. Two semantically similar functions might be years apart in age, one deprecated and one live in production — the embedding doesn't know the difference.

Graphs: Structural Truth

callers(auth/middleware.js)  [23 modules]
communities()  [{name: "billing", files: [...]}, {name: "auth", files: [...]}]
Enter fullscreen mode Exit fullscreen mode

Traversing imports, calls, and inheritance answers exact structural questions: reachability, transitive dependencies, ownership boundaries.

Best for: finding what breaks if you change a function, mapping module dependencies, identifying hot-path slices, detecting team boundaries via community detection.

Limitation: graphs find everything structurally reachable, but can't rank by relevance or catch intent-based similarity across unrelated components. A graph traversal will happily return 40 structurally-connected files with no sense of which 3 actually matter for your question.

Implementation: The Combined Pattern

Neither model wins outright — the canonical pattern chains them:

# Step 1: graph-first neighborhood
neighborhood = graph.get_neighborhood(
    target_file,
    include=["imports", "callers", "community"]
)

# Step 2: vector re-ranking inside that neighborhood
results = vector_search(
    query=user_query,
    candidates=neighborhood,   # constrained, not global
    top_k=10
)
Enter fullscreen mode Exit fullscreen mode

This gives you results that are both connected (graph-verified as structurally relevant) and semantically relevant (vector-ranked within that constrained set) — instead of either a noisy global vector search or an unranked pile of everything structurally reachable.

Failure Modes to Watch For

Failure Cause
Vector search returns deprecated/dead code No graph-position awareness — embedding similarity ignores whether code is even reachable from production
Graph traversal returns too much No relevance ranking — "structurally reachable" ≠ "relevant to this question"

Learnings

The decision framework that generalizes: ask what your engineers actually query for.

  • Mostly "find code that does X" → vectors alone are probably sufficient
  • Mostly "what breaks if I change X" or "where are the team boundaries" → you need a graph
  • Most mature codebases eventually need both, chained as above

Resources

  • Spiderbrain uses this exact pattern: a pre-built structural graph with semantic re-ranking applied at query time.
  • Related: Every AI Coding Assistant Sees Your Files, Not Your Architecture (the graph side of this argument in more depth)

🚀 Free on Local · ☁️ Hosted on Pro & Cortex

Link : https://spiderbrain.ai/

Linkedin : https://www.linkedin.com/company/spiderbrain


Discuss: if you're running vector search over a codebase today, are you constraining candidates by any structural signal first, or searching the whole embedding space every time?

Top comments (0)