Skip to content

Vector search or graph search?

  • Lesson
  • Beginner
  • 14 min read
  • Checked 15 Aug 2026

Split the question before choosing a database

Section titled “Split the question before choosing a database”

Suppose a recruiter asks:

Find a Python developer with cloud and data-engineering experience who has worked with one of my colleagues.

This is not one retrieval problem. It contains four different tests:

Part of the request Best starting tool Why
“experience like Python, cloud, and data engineering” Vector search Ranks profiles by proximity in one embedding model’s learned representation
“GCP,” “CKA,” a name, or an error code Lexical search or an exact field lookup Scores word overlap; a non-analyzed field preserves an exact identifier
“available,” “India,” and “the viewer may see this profile” Structured filters and authorization rules Treats required conditions as pass/fail, not as soft similarity
“worked with my colleague” or “within two connections” Graph traversal Follows stored relationship types and paths

A database product can support more than one row in this table. Choose by query behavior, not by product label.

Lexical behavior depends on the field’s analyzer. A full-text field may lowercase, stem, or split text, while a keyword or structured field can preserve a normalized exact value.1

An embedding model converts the request and each profile into vectors. A vector index retrieves nearby vectors under a configured similarity function.2

That can place these profiles close together even when their words differ:

request: Python developer with cloud and data-pipeline experience
profile: Built ETL workflows with PySpark on Google Cloud

The model does not literally understand the person’s career, verify a skill, or prove job fit. It supplies coordinates learned during training. Evaluate the ranking with representative recruiter judgments.

Vector similarity is also the wrong place for hard rules. A highly similar profile with available = false must still fail an availability filter.

Graph search follows recorded relationships

Section titled “Graph search follows recorded relationships”

A property graph represents entities as nodes and their connections as typed relationships with optional properties.3 For example:

(You)-[:KNOWS]->(Maya)-[:WORKED_WITH]->(Asha)

A traversal visits nodes by following allowed relationships. It can answer “How is Asha connected to me?” because those edges exist in the data. It cannot infer that Asha is a strong Python developer unless the graph also contains suitable properties, skills, or linked evidence.

Cypher expresses a bounded connection query as a path pattern:

MATCH (me:Person {id: $viewerId})-[:KNOWS]->(colleague:Person)
MATCH (colleague)-[:WORKED_WITH]-(candidate:Person)
WHERE candidate.openToWork = true
AND candidate.region = $region
RETURN DISTINCT candidate

The path pattern handles connectivity; WHERE handles exact properties.4 Production authorization still belongs in enforced access-control checks, not only in generated query text.

One word called “graph” can mean two things

Section titled “One word called “graph” can mean two things”

HNSW also uses a graph, but its edges are index machinery: they connect nearby vectors so approximate nearest-neighbor search can navigate the vector space. A domain graph stores business relationships such as KNOWS, WORKED_AT, or HAS_SKILL.

HNSW edge: vector A ──near in embedding space── vector B
Domain edge: Maya ──WORKED_WITH── Asha

Do not use an HNSW path as proof that two people know each other.

Combine retrieval methods when the question combines constraints

Section titled “Combine retrieval methods when the question combines constraints”

A practical pipeline can use all four tools:

authorize viewer
apply tenant, region, availability, and exact-skill filters
rank eligible profiles by vector similarity
follow approved relationship types and path lengths
return evidence: matched fields, score, and connection path

The order is workload-dependent. A selective graph traversal can create the candidate set before vector ranking. A broad graph can make vector-first retrieval cheaper. Push authorization into every stage that reads data.

Beware of a small vector top_k: the best connected candidate may never reach the graph stage. Test filtered recall, over-retrieval, latency, and the final recruiter outcome before fixing the order.

src/examples/rag/vector_graph_search.py
"""Compare lexical, vector, filter, and graph retrieval without dependencies."""
from dataclasses import dataclass
from math import sqrt
Vector = tuple[float, ...]
@dataclass(frozen=True)
class Profile:
summary: str
skills: frozenset[str]
region: str
available: bool
embedding: Vector
# These three-dimensional vectors are hand-written teaching data. A real system
# would pin an embedding model and create vectors from the query and profiles.
PROFILES = {
"Asha": Profile(
summary="Built ETL workflows with PySpark on Google Cloud.",
skills=frozenset({"python", "pyspark", "gcp", "etl"}),
region="India",
available=True,
embedding=(0.90, 0.80, 0.75),
),
"Ben": Profile(
summary="Developed Python API services on AWS.",
skills=frozenset({"python", "fastapi", "aws"}),
region="India",
available=True,
embedding=(0.95, 0.75, 0.10),
),
"Chen": Profile(
summary="Designed Spark pipelines and a warehouse on Azure.",
skills=frozenset({"spark", "azure", "data-engineering"}),
region="India",
available=True,
embedding=(0.25, 0.80, 0.90),
),
"Dina": Profile(
summary="Built Python batch jobs on GCP for analytics.",
skills=frozenset({"python", "gcp", "data-engineering"}),
region="India",
available=False,
embedding=(0.90, 0.65, 0.85),
),
}
# These typed edges are product-domain facts. They are not HNSW edges.
RELATIONSHIPS = {
"you": {("KNOWS", "Maya"), ("KNOWS", "Ravi")},
"Maya": {("KNOWS", "you"), ("WORKED_WITH", "Asha")},
"Ravi": {("KNOWS", "you"), ("KNOWS", "Noor")},
"Noor": {("KNOWS", "Ravi")},
"Asha": {("WORKED_WITH", "Maya")},
"Ben": set(),
"Chen": set(),
"Dina": set(),
}
def cosine_similarity(left: Vector, right: Vector) -> float:
"""Return the normalized dot product of two non-zero vectors."""
if len(left) != len(right):
raise ValueError("vectors must have the same dimensions")
dot_product = sum(a * b for a, b in zip(left, right))
left_length = sqrt(sum(value * value for value in left))
right_length = sqrt(sum(value * value for value in right))
if left_length == 0 or right_length == 0:
raise ValueError("cosine similarity is undefined for a zero vector")
return dot_product / (left_length * right_length)
def lexical_matches(term: str) -> list[str]:
"""Find an exact text term after case normalization."""
needle = term.casefold()
return [
name for name, profile in PROFILES.items()
if needle in profile.summary.casefold()
]
def semantic_ranking(query: Vector, names: list[str]) -> list[tuple[str, float]]:
"""Rank candidates by cosine similarity to the teaching query vector."""
scored = [
(name, cosine_similarity(query, PROFILES[name].embedding))
for name in names
]
return sorted(scored, key=lambda item: item[1], reverse=True)
def follows_typed_path(
start: str,
target: str,
relationship_types: tuple[str, ...],
) -> bool:
"""Return whether an exact sequence of relationship types reaches target."""
frontier = {start}
for relationship_type in relationship_types:
frontier = {
neighbor
for node in frontier
for edge_type, neighbor in RELATIONSHIPS.get(node, set())
if edge_type == relationship_type
}
return target in frontier
if __name__ == "__main__":
query_vector = (1.0, 1.0, 1.0) # Python + cloud + data engineering
print("Lexical match for 'GCP':", lexical_matches("GCP"))
print("\nSemantic ranking (no hard constraints):")
for name, score in semantic_ranking(query_vector, list(PROFILES)):
print(f" {name:5} cosine={score:.3f}")
# Exact product constraints do not belong inside an embedding score.
eligible = [
name
for name, profile in PROFILES.items()
if profile.available
and profile.region == "India"
and "python" in profile.skills
]
required_path = ("KNOWS", "WORKED_WITH")
print("\nHybrid result (eligible and matched KNOWS -> WORKED_WITH):")
for name, score in semantic_ranking(query_vector, eligible):
if follows_typed_path("you", name, required_path):
print(f" {name:5} cosine={score:.3f} path={' -> '.join(required_path)}")

The example uses hand-written three-dimensional vectors so the control flow stays visible. They are teaching data, not real embeddings.

Run it on Windows from an activated Python 3.10+ virtual environment:

Terminal window
python --version
python src/examples/rag/vector_graph_search.py

On macOS or Linux:

Terminal window
python3 --version
python3 src/examples/rag/vector_graph_search.py

It demonstrates three different facts:

  1. lexical search for GCP matches the literal text but misses “Google Cloud”;
  2. vector ranking puts the cloud-and-data profiles near the teaching query;
  3. hard filters plus the typed KNOWS → WORKED_WITH path leave only the eligible profile who worked with a colleague.
If the question asks… Start with… Add when needed…
“Which profiles describe similar work?” Vector search lexical signals, metadata filters, and reranking
“Who has this exact certification or identifier?” Lexical or structured lookup synonyms only when the product defines them
“Who is connected to this person or company?” Graph traversal path length, relationship type, time, and permission filters
“Who fits this work and is in my network?” A hybrid pipeline measure both retrieval stages and expose the supporting path

Combining vectors and a domain graph is a general architecture pattern. Microsoft GraphRAG is a specific indexing pipeline that extracts entities, relationships, claims, community summaries, and embeddings from unstructured text.5 A product with an existing people-and-company graph does not need that pipeline merely because it combines graph and vector retrieval.

Next: choose a retrieval store or review vector search foundations.

  1. Elastic, how full-text search works, describes analysis, inverted indexes, term matching, and BM25 ranking.

  2. Neo4j, vector indexes, describes retrieval of nodes or relationships by similarity between stored and query vectors.

  3. Neo4j, graph database concepts, defines property-graph nodes, typed relationships, properties, paths, and traversal.

  4. Neo4j, path patterns and graph patterns, documents how Cypher matches paths through a graph.

  5. Microsoft, GraphRAG indexing overview, lists its standard extraction, community, summary, and embedding stages.