How to Query a Knowledge Graph with Natural Language
Why Natural Language Querying Matters
Knowledge graphs store information in a structured format that requires formal query languages to access. Cypher (for Neo4j) and SPARQL (for RDF stores) are powerful but require the user to know the schema, the node labels, the relationship types, and the query syntax. This limits graph access to developers who know the query language and the schema, which defeats the purpose of making organizational knowledge accessible to everyone.
Natural language querying removes that barrier. A product manager can ask "what does the checkout service depend on" without knowing that the graph uses the predicate DEPENDS_ON or that the service is stored as a node with label Service. The translation layer handles the mapping from colloquial language to formal queries, making the knowledge graph accessible to anyone who can form a question.
Step-by-Step Implementation
Use an LLM or NER model to identify the entities mentioned in the question. "Which services depend on Redis" contains two entity references: "services" (an entity type) and "Redis" (a specific entity). "Who maintains the payments API" contains "payments API" (a specific entity) and an implicit search for a Person entity type. Entity extraction at query time uses the same techniques as extraction during graph construction, but on shorter text and with tighter latency requirements.
def extract_query_entities(question):
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=500,
messages=[{"role": "user", "content": f"""Extract entities from this question.
Return JSON: {{"entities": [{{"name": "...", "type": "specific|type_query"}}]}}
A "specific" entity is a named thing (Redis, payments API).
A "type_query" is a category being searched (services, databases, people).
Question: {question}"""}]
)
return json.loads(response.content[0].text)["entities"]Match the extracted entity names to their canonical representations in the graph. "Redis" might be stored as "Redis," "Redis Cache," or "Amazon ElastiCache (Redis)." Use a combination of exact match, alias lookup, and fuzzy string matching. If multiple candidates match, rank them by connection density (the most-connected matching node is usually the right one) or ask the LLM to disambiguate.
def link_to_graph(entity_name, graph_db):
# exact match
exact = graph_db.find_node(name=entity_name)
if exact:
return exact
# alias match
alias_match = graph_db.find_by_alias(entity_name)
if alias_match:
return alias_match
# fuzzy match
candidates = graph_db.fuzzy_search(entity_name, threshold=0.8)
if len(candidates) == 1:
return candidates[0]
elif len(candidates) > 1:
# rank by connection count
return max(candidates, key=lambda n: n.degree)
return None # entity not in graphClassify the question into one of four query types. Property queries ask about attributes of a single entity ("what version of PostgreSQL are we running"). Relationship queries ask what is connected to an entity ("what does the checkout service depend on"). Path queries ask how two entities are connected ("how is the payments service related to Redis"). Aggregation queries ask for counts or lists ("how many services use PostgreSQL"). The query type determines the structure of the graph query to generate.
def classify_intent(question, entities):
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=200,
messages=[{"role": "user", "content": f"""Classify this question's intent.
Options: property, relationship, path, aggregation
Question: {question}
Entities found: {json.dumps([e["name"] for e in entities])}
Return JSON: {{"intent": "...", "direction": "outgoing|incoming|both"}}"""}]
)
return json.loads(response.content[0].text)Use the linked entities, query intent, and your graph schema to generate a formal query. For Neo4j, generate Cypher. For programmatic graphs, generate the traversal code. Including your graph schema (node labels, relationship types, property keys) in the LLM prompt dramatically improves query accuracy because the model generates queries using your actual schema rather than guessing.
CYPHER_PROMPT = """Given this graph schema:
Node labels: Service, Database, Person, Team, Technology, API
Relationship types: DEPENDS_ON, USES, MAINTAINED_BY, PART_OF
Generate a Cypher query for this question:
{question}
Linked entities: {entities}
Query intent: {intent}
Return only the Cypher query, no explanation."""
def generate_cypher(question, entities, intent, schema):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=500,
messages=[{"role": "user", "content":
CYPHER_PROMPT.replace("{question}", question)
.replace("{entities}", json.dumps(entities))
.replace("{intent}", intent)}]
)
return response.content[0].text.strip()Run the generated query against your graph database. Handle empty results by generating a natural language explanation of why no results were found ("no services matching 'Redis' were found in the knowledge graph"). For non-empty results, format them as structured data that either goes directly to the user or feeds into an LLM prompt as retrieved context. Include the original query and the entities it resolved to, which helps users understand and verify the results.
The Spreading Activation Alternative
For retrieval applications, there is an alternative to formal query generation: spreading activation. Instead of translating the question into a precise graph query, you identify entities in the question, activate their nodes in the graph, let activation spread through relationships with decay, and use the activation scores to boost retrieval results. This approach is less precise than Cypher for structured queries ("count all services using PostgreSQL") but more robust for open-ended retrieval queries ("tell me about issues with the payments infrastructure") because it does not require the question to map cleanly to a single graph pattern.
Adaptive Recall uses spreading activation as its primary graph query mechanism. When you call the recall tool, entities in your query are identified, their graph nodes are activated, activation spreads through connections, and memories associated with highly activated nodes receive score boosts. This gives you graph-powered retrieval without writing queries or managing query generation.
Query your knowledge graph by just asking questions. Adaptive Recall's spreading activation finds connected information without formal query generation.
Try It Free