Querying the Knowledge Graph¶
The query layer combines vector similarity with graph traversal to surface structurally relevant results. You can explore the graph from any result and submit feedback that updates retrieval weights.
Architecture¶
graph LR
Q[Query] --> VEC[Vector Search]
Q --> PPR[Graph PPR]
VEC --> COMBINE[Combined Scoring]
PPR --> COMBINE
COMBINE --> RESULTS[Ranked Results + Rules]
RESULTS --> EXPLORE[explore(node_id)]
RESULTS --> FEEDBACK[feedback(outcomes)]
FEEDBACK -->|adjusts weights| PPR
The query pipeline combines vector similarity with graph structure. Unlike flat vector stores (Chroma, Pinecone, FAISS), qortex uses Personalized PageRank over typed edges to surface concepts that are structurally important, not just textually similar.
QortexClient¶
QortexClient is the protocol that all consumers target. It has one in-process implementation (LocalQortexClient) and one over-the-wire implementation (MCP server).
from qortex.client import LocalQortexClient
from qortex.core.memory import InMemoryBackend
from qortex.vec.index import NumpyVectorIndex
# Set up
vector_index = NumpyVectorIndex(dimensions=384)
backend = InMemoryBackend(vector_index=vector_index)
backend.connect()
client = LocalQortexClient(
vector_index=vector_index,
backend=backend,
embedding_model=my_embedding, # Any model with .embed(texts) -> list[list[float]]
mode="graph", # "vec" for vector-only, "graph" for graph-enhanced
)
Modes¶
| Mode | What It Does | When to Use |
|---|---|---|
"vec" |
Pure vector similarity search | Simple use cases, testing, Level 0 parity |
"graph" |
Vec + PPR combined scoring | Production (the differentiator) |
Searching: query()¶
result = client.query(
context="How to implement authentication?",
domains=["security"],
top_k=5,
min_confidence=0.0,
)
# Result shape
print(result.query_id) # Unique ID for this query (used in feedback)
print(len(result.items)) # Number of results
for item in result.items:
print(item.id) # Unique item ID
print(item.content) # "OAuth2: OAuth2 authorization framework for..."
print(item.score) # 0.0–1.0 relevance score
print(item.domain) # "security"
print(item.node_id) # Graph node ID (use for explore())
print(item.metadata) # Dict of additional metadata
# Rules auto-surfaced in query results
for rule in result.rules:
print(rule.id) # "rule:use-oauth"
print(rule.text) # "Always use OAuth2 for third-party API access"
print(rule.relevance) # How relevant this rule is to the query
print(rule.source_concepts) # Which query results linked to this rule
Rules are surfaced automatically with zero consumer effort. If the query activates concepts that have linked rules, those rules appear in the response.
Exploring: explore()¶
After a query, you can traverse the graph from any result node:
# Take a node_id from query results
node_id = result.items[0].node_id
explore = client.explore(node_id, depth=1)
# The node itself
print(explore.node.name) # "OAuth2"
print(explore.node.description) # Full description
# Typed edges (structurally related, not just textually similar)
for edge in explore.edges:
print(f"{edge.source_id} --{edge.relation_type}--> {edge.target_id}")
# "sec:oauth --REQUIRES--> sec:jwt"
# "sec:oauth --USES--> sec:rbac"
# Neighbor nodes
for neighbor in explore.neighbors:
print(f"{neighbor.name}: {neighbor.description}")
# Rules linked to this concept
for rule in explore.rules:
print(f"[{rule.category}] {rule.text}")
graph TD
OAUTH[OAuth2] -->|REQUIRES| JWT[JWT]
OAUTH -->|USES| RBAC[RBAC]
MFA[MFA] -->|SUPPORTS| OAUTH
OAUTH -.->|rule| R1["Always use OAuth2 for<br/>third-party API access"]
OAUTH -.->|rule| R2["Rotate JWT signing keys<br/>every 90 days"]
JWT -.->|rule| R2
Depth¶
explore() supports depth 1–3:
| Depth | What You Get | Use Case |
|---|---|---|
| 1 (default) | Immediate neighbors | Quick context |
| 2 | 2-hop neighborhood | Understand a cluster |
| 3 | 3-hop neighborhood | Full subgraph exploration |
If the node doesn't exist, explore() returns None.
Projecting Rules: rules()¶
Get rules from the knowledge graph, filtered by domain, concept, or category:
# All rules
all_rules = client.rules()
# Rules for specific concepts (e.g., activated by a query)
activated_ids = [item.node_id for item in result.items]
relevant_rules = client.rules(concept_ids=activated_ids)
# Rules by category
arch_rules = client.rules(categories=["architectural"])
# Rules by domain
security_rules = client.rules(domains=["security"])
The result includes metadata:
print(relevant_rules.projection) # "rules"
print(relevant_rules.domain_count) # Number of distinct domains
for rule in relevant_rules.rules:
print(rule.id) # "rule:rbac-first"
print(rule.text) # "Define RBAC roles before writing authorization code"
print(rule.domain) # "security"
print(rule.category) # "architectural"
print(rule.source_concepts) # ["sec:rbac"]
print(rule.relevance) # Score based on concept activation
Feedback: Closing the Loop¶
After using query results, tell qortex what worked:
client.feedback(
query_id=result.query_id,
outcomes={
result.items[0].id: "accepted", # This was useful
result.items[-1].id: "rejected", # This was not
},
source="my-app",
)
graph LR
QUERY[query()] --> USE[Use Results]
USE --> FEEDBACK[feedback(outcomes)]
FEEDBACK --> TELEPORT[Update PPR<br/>Teleportation Factors]
TELEPORT --> QUERY
style FEEDBACK fill:#2d5,stroke:#333
style TELEPORT fill:#25d,stroke:#333
In graph mode, feedback adjusts the Personalized PageRank teleportation factors. Accepted concepts get higher teleportation probability on future queries. Over time, the system learns which concepts are actually useful, not just textually similar.
| Outcome | Effect |
|---|---|
"accepted" |
Increases concept's teleportation factor |
"rejected" |
Decreases concept's teleportation factor |
This is a continuous signal, not a one-time benchmark. Every query-feedback pair makes future queries better.
Over MCP¶
All client methods are available as MCP tools for cross-language consumers:
| Client Method | MCP Tool | Use Case |
|---|---|---|
client.query() |
qortex_query |
Search |
client.explore() |
qortex_explore |
Graph traversal |
client.rules() |
qortex_rules |
Rule projection |
client.feedback() |
qortex_feedback |
Learning loop |
client.status() |
qortex_status |
Health check |
client.domains() |
qortex_domains |
List domains |
client.ingest() |
qortex_ingest |
Ingest content |
Configure qortex as an MCP server:
{
"mcpServers": {
"qortex": {
"command": "uv",
"args": ["run", "--directory", "/path/to/qortex", "qortex-mcp"]
}
}
}
Then any MCP client (Claude Desktop, Mastra TS, custom agents) can use the full query pipeline.
See API Reference for detailed tool schemas.
Framework Adapters¶
Instead of using QortexClient directly, use framework-native adapters:
| Framework | Adapter | What It Replaces |
|---|---|---|
| LangChain | QortexVectorStore |
Chroma, FAISS, Pinecone |
| LangChain | QortexRetriever |
Any BaseRetriever |
| Mastra | MCP tools | Any MastraVector impl |
| CrewAI | QortexKnowledgeStorage |
ChromaDB default |
| Agno | QortexKnowledge |
Any KnowledgeProtocol |
All adapters expose the same qortex extras: explore(), rules(), and feedback().
Next Steps¶
- API Reference: full protocol and type reference
- LangChain VectorStore: drop-in for Chroma/FAISS
- Mastra MCP: cross-language integration
- Architecture Overview: how it all fits together