Search for Related Content from a Product
You need to find content related to a natural-language query from
within a product -- semantically, not by keyword. The vector service
holds the embedding index in memory, manages the embedding endpoint
connection, and exposes a single RPC: SearchContent.
Your product sends text; the service returns ranked resource
identifiers. No embedding API calls, no vector storage, no scoring
logic in the product.
For the full setup including connecting to both the graph and vector services, see Ground Agents in Context.
Prerequisites
-
Completed the
Ground Agents in Context
guide -- you have
@forwardimpact/librpcand@forwardimpact/libtypeinstalled, the vector service is running, andcreateClient("vector")connects successfully. -
A populated vector index at
data/vectors/index.jsonl.
Connect
import { createClient, createTracer } from "@forwardimpact/librpc";
import { createLogger } from "@forwardimpact/libtelemetry";
import { vector } from "@forwardimpact/libtype";
const logger = createLogger("my-product");
const tracer = await createTracer("my-product");
const vectorClient = await createClient("vector", logger, tracer);
Search with a single query
Pass one or more text strings to SearchContent. The
service embeds each string, scores the resulting vectors against the
index using dot-product similarity, and returns the ranked resource
identifiers:
const query = vector.TextQuery.fromObject({
input: ["career progression for senior engineers"],
});
const result = await vectorClient.SearchContent(query);
console.log("Results:", result.identifiers?.length ?? 0);
for (const id of result.identifiers ?? []) {
console.log(String(id));
}
Expected output (identifiers depend on your knowledge base):
Results: 5
common.Message.a1b2c3d4
common.Message.e5f6g7h8
common.Message.i9j0k1l2
common.Message.m3n4o5p6
common.Message.q7r8s9t0
Identifiers are sorted by similarity score descending. The default limit returns all matches above the threshold.
Search with multiple queries
Pass several strings to score against the index in a single call. The service embeds each string and keeps the highest score per item across all queries:
const query = vector.TextQuery.fromObject({
input: [
"incident management",
"on-call rotation",
],
});
const result = await vectorClient.SearchContent(query);
console.log("Results:", result.identifiers?.length ?? 0);
This avoids multiple round trips when the search intent spans several phrasings.
Apply filters
Constrain results using the optional filter field:
const query = vector.TextQuery.fromObject({
input: ["architecture design patterns"],
filter: {
limit: "3",
threshold: "0.6",
prefix: "common.Message",
},
});
const result = await vectorClient.SearchContent(query);
console.log("Top 3 results above 0.6 threshold:");
for (const id of result.identifiers ?? []) {
console.log(String(id));
}
Expected output:
Top 3 results above 0.6 threshold:
common.Message.a1b2c3d4
common.Message.e5f6g7h8
common.Message.i9j0k1l2
Available filter fields:
| Field | Effect |
|---|---|
prefix |
Only return identifiers starting with this string |
limit |
Cap the number of results |
threshold |
Minimum similarity score to include |
max_tokens |
Stop accumulating results when the token budget is exceeded |
All filter values are strings in the protobuf definition. The service parses them internally. Filters apply in order: prefix, then scoring and threshold, then limit, then token budget.
Resolve identifiers to content
The service returns identifiers, not content. Resolve them 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, 150));
console.log();
}
This two-step pattern keeps the vector service stateless: it scores and ranks; the calling product resolves as much content as it needs.
Verify
You have reached the outcome of this guide when:
-
SearchContentwith a single input string returns ranked resource identifiers. - Passing multiple input strings returns results scored against all queries.
-
Applying a
filterwithlimitandthresholdconstrains the result set. -
Resolving returned identifiers through
libresourceproduces the expected content.
Related
- Ground Agents in Context -- the end-to-end setup for connecting to both the graph and vector services.
- Query the Graph -- when you need exact relationship matching rather than ranked similarity.
- Search Semantically -- the library guide for querying the vector index directly without the gRPC service.