Traverse Knowledge and Search Semantically
You build a product feature that needs relationship traversal or
semantic search. You do not want each product to stand up its own
graph database or vector store. Two gRPC services (graph
and vector) provide those capabilities to any product
through a shared backend. Connect once with the generated client
code from npx fit-codegen generate --all. Every product
in the monorepo can then query relationships and search content
through the same typed interface.
In this guide you connect to both services. You run a representative call against each one. You then check that the responses match what the underlying indexes contain. By the end, your product connects to both services. You also understand the request and response shapes well enough to build features on top of them.
Prerequisites
- Node.js 18+
-
Generated client code available (run
npx fit-codegen generate --allif not) - Services running (
npx fit-rc start) -
A populated knowledge base:
data/graphs/index.jsonlfor the graph service,data/vectors/index.jsonlfor the vector service. If these indexes do not exist yet, see the Ground Agents in Context library guide for the ingestion pipeline.
Install the transport and type packages:
npm install @forwardimpact/librpc @forwardimpact/libtype
Architecture overview
Both services sit behind gRPC and expose typed RPC methods. Products never read the JSONL index files directly. They call the service. The service holds the index in memory and runs the queries. This separation means that a single index load serves all connected products. Individual products carry no index management code.
Product A ──┐ ┌── data/graphs/index.jsonl
├── gRPC ── graph ──┤
Product B ──┘ └── data/graphs/ontology.ttl
Product A ──┐ ┌── data/vectors/index.jsonl
├── gRPC ── vector ──┤
Product B ──┘ └── gRPC ── embedding ── inference backend
The graph service owns three RPCs:
| RPC | Purpose | Request type |
|---|---|---|
QueryByPattern |
Match triples by subject, predicate, and object | graph.PatternQuery |
GetSubjects |
List all entity URIs, optionally filtered by type | graph.SubjectsQuery |
GetOntology |
Return the SHACL ontology as Turtle RDF | common.Empty |
The vector service owns one RPC:
| RPC | Purpose | Request type |
|---|---|---|
SearchContent |
Semantic similarity search over indexed content | vector.TextQuery |
Both return tool.ToolCallResult, which carries either a
content string or an identifiers array of
resource identifiers.
Connect to the graph service
Create a graph client with the generated
GraphClient class. The client reads its connection
details from config/config.json automatically:
import { createClient, createTracer } from "@forwardimpact/librpc";
import { createLogger } from "@forwardimpact/libtelemetry";
const logger = createLogger("my-product");
const tracer = await createTracer("my-product");
const graphClient = await createClient("graph", logger, tracer);
createClient("graph") resolves the host and
port from the service configuration. It creates a
GraphClient instance. It then establishes the gRPC
channel with automatic retry. The client retries up to 10 times on
transient errors, with exponential backoff that starts at 1 second
with jitter.
Query the graph
Run a triple-pattern query to find all resources that contain a
schema:worksFor relationship:
import { graph } from "@forwardimpact/libtype";
const query = graph.PatternQuery.fromObject({
subject: "?",
predicate: "schema:worksFor",
object: "?",
});
const result = await graphClient.QueryByPattern(query);
console.log("Matched identifiers:", result.identifiers?.length ?? 0);
Expected output (numbers depend on your knowledge base):
Matched identifiers: 3
The ? wildcard matches any value in that position. To
constrain a position, pass a full URI or a prefixed name:
const specific = graph.PatternQuery.fromObject({
subject: "?",
predicate: "schema:worksFor",
object: "https://acme.example/org/engineering",
});
const result = await graphClient.QueryByPattern(specific);
List subjects by type
To see every entity of a given RDF type:
const subjectsQuery = graph.SubjectsQuery.fromObject({
type: "schema:Person",
});
const result = await graphClient.GetSubjects(subjectsQuery);
console.log(result.content);
Expected output:
https://acme.example/people/alice https://schema.org/Person
https://acme.example/people/bob https://schema.org/Person
Read the ontology
The ontology describes all types and predicates observed in the graph. Agents typically read this to learn what relationships exist before they write queries:
import { common } from "@forwardimpact/libtype";
const ontology = await graphClient.GetOntology(common.Empty.fromObject({}));
console.log(ontology.content.substring(0, 200));
The response is a Turtle RDF string that contains SHACL shape definitions.
Connect to the vector service
The vector service follows the same client pattern:
const vectorClient = await createClient("vector", logger, tracer);
Search for related content
Pass natural-language text to SearchContent. The
service embeds the text with its configured embedding endpoint. It
scores the vectors against the index. It then returns the ranked
resource identifiers:
import { vector } from "@forwardimpact/libtype";
const searchQuery = vector.TextQuery.fromObject({
input: ["career progression for senior engineers"],
});
const result = await vectorClient.SearchContent(searchQuery);
console.log("Matched identifiers:", result.identifiers?.length ?? 0);
for (const id of result.identifiers ?? []) {
console.log(String(id));
}
Expected output (identifiers depend on your knowledge base):
Matched identifiers: 5
common.Message.a1b2c3d4
common.Message.e5f6g7h8
common.Message.i9j0k1l2
common.Message.m3n4o5p6
common.Message.q7r8s9t0
Apply filters
Both services accept a QueryFilter on applicable RPCs.
The filter constrains results by prefix, count limit, or token
budget:
import { tool } from "@forwardimpact/libtype";
const filtered = vector.TextQuery.fromObject({
input: ["incident management procedures"],
filter: { limit: "3", prefix: "common.Message" },
});
const result = await vectorClient.SearchContent(filtered);
Available filter fields:
| Field | Type | Effect |
|---|---|---|
prefix |
string | Only return identifiers starting with this string |
limit |
string | Cap the number of results |
threshold |
string | Minimum similarity score (vector only) |
max_tokens |
string | Stop when cumulative token count exceeds this budget |
Filter values are strings in the protobuf definition. The service parses them internally.
Resolve identifiers to content
Both services return resource identifiers. They do not return the
content itself. To retrieve the actual content chunks, resolve
identifiers through libresource:
import { createResourceIndex } from "@forwardimpact/libresource";
const resources = createResourceIndex("resources");
const ids = result.identifiers.map((id) => String(id));
const items = await resources.get(ids);
for (const item of items) {
console.log(`--- ${item.id.type}.${item.id.name} ---`);
console.log(item.content.substring(0, 100));
}
This two-step pattern keeps the services stateless. You query the service, then you resolve the identifiers. The product that calls the service resolves the resources locally.
Verify
You reach the outcome of this guide when:
-
createClient("graph")connects without error andgraphClient.GetSubjects(...)returns a content string with subject URIs. -
createClient("vector")connects without error andvectorClient.SearchContent(...)returns an identifiers array. -
You can apply a
QueryFilterto constrain results by limit or prefix. -
You can resolve returned identifiers to content through
libresource.
If any connection fails, confirm the services run with
npx fit-rc status. Then check that
config/config.json lists the correct host and port for
each service.
What's next
Answer Relationship Questions from a Product
Answer relationship questions from any product. Send triple patterns to the shared graph service and write no join logic.
Search for Related Content from a Product
Find related content by meaning from any product. The shared vector service manages the embeddings, so your product stores none.