Ontology

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

ParameterMeaning
relationFilter string(s). Repeated entries are ANDed.
relation_depthMax hops to traverse when resolving a filter (13, default 1). Does not limit how large your ontology schema can be.
include_relationsWhen 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 concept attribute= 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.

On this page