Filtering documents by relations
Narrow Search and Ask with ontology relation filters — query by what a document is connected to.
This tutorial uses POST /api/v1/search (and the same fields on Ask). The full schema lives in the API reference.
Your files are linked into the ontology graph. Now your app needs to query them: not only by what the text says, but by what they're connected to.
Imagine you've uploaded incident reports and linked each to a resident and a facility. Your app needs: "Search incidents that involve a resident at CasablancaSite." Here's how to build that query.
Ontology filter parameters
| Parameter | Meaning |
|---|---|
relation | Filter string(s). Repeated entries are ANDed. |
relation_depth | Max hops to traverse when resolving a filter (1–3, default 1). Does not limit how large your ontology schema can be. |
include_relations | When true, attach matched relations on each result |
These compose with existing scope: workspace_id, tag_id, file_id (same mutual-exclusion rules as plain Search).
Filter syntax
relation=<relation_key>.<ConceptTypeKey>
relation=<relation_key>.<ConceptTypeKey>(<attr>:<value>,<attr>:<value>)- Parenthesized attributes are optional — omit them for an existence check ("any concept of this type via this relation").
- Inside one attribute value,
|is OR (same convention as conceptattribute=filters). - Commas separate AND attribute clauses inside the parentheses.
- Chain hops with dots for multi-hop paths (must fit
relation_depth):
relation=cites.Document.cites.Document(status:closed)Keys are matched case-insensitively against stored type keys (organization, Organization).
Examples
Documents that reference any organization:
const response = await fetch("https://api.context212.com/api/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "renewal terms",
workspace_id: [12],
relation: ["references.Organization"],
}),
});Enrich results with relations
Without include_relations, you can filter by the graph but never see the edges on each hit. Set it to true when the UI should show linked concepts:
const data = await fetch("https://api.context212.com/api/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "renewal terms",
workspace_id: [12],
relation: ["references.Organization(region:EU)"],
include_relations: true,
}),
}).then((r) => r.json());
for (const hit of data.results) {
console.log(hit.source.filename);
for (const edge of hit.relations ?? []) {
console.log(` ${edge.relation_key} → ${edge.target_label} (${edge.target_concept_type})`);
}
}Example relations item:
{
"relation_key": "references",
"relation_label": "References",
"target_concept_id": "…",
"target_concept_type": "organization",
"target_label": "Acme Corp",
"target_attributes": { "region": "EU" }
}Ask with the same filters
Ask forwards relation, relation_depth, and include_relations into retrieval:
{
query: "Which EU counterparties appear in our NDAs?",
workspace_id: [12],
relation: ["references.Organization(region:EU)"],
include_relations: true,
}How execution works
- Depth 1 — joins from documents → relations → concepts (cheap, indexable).
- Depth 2–3 — same public API; multi-hop resolution walks concept→concept edges behind one internal resolver.
Callers never choose the SQL strategy — only relation_depth and the filter string.
Next
Hit an error? See Rules & constraints.