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/librpc and @forwardimpact/libtype installed, the vector service is running, and createClient("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:

  • SearchContent with a single input string returns ranked resource identifiers.
  • Passing multiple input strings returns results scored against all queries.
  • Applying a filter with limit and threshold constrains the result set.
  • Resolving returned identifiers through libresource produces the expected content.
  • 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.