Skip to main content
This tutorial builds an end-to-end semantic search (a small RAG loop) over your own documents using the Gloo AI Embeddings API. You’ll chunk a corpus, embed every chunk in one batched call, embed a query with the same model, rank chunks by cosine similarity, and feed the top matches into a Guarded Responses call for a grounded, cited answer. It’s the hands-on counterpart to the Embeddings API guide and the RAG lessons in the Learning Center: same concepts, but applied directly to the embeddings endpoint rather than Gloo’s Data Engine.

Own vector store, or Data Engine?

Gloo offers two ways to search your content, and this tutorial is about the first:
  • You run your own vector store. You call POST /ai/v2/direct/embeddings to turn text into vectors, keep them wherever you like, and do retrieval yourself. Use this when you already have a vector database, need full control over chunking and ranking, or want to search content that was never uploaded to Gloo. That’s what we build here.
  • Gloo’s Data Engine does it for you. You upload content to a Publisher and call Search or a grounded endpoint; Gloo embeds, stores, and retrieves. Use this when you’d rather not run retrieval infrastructure. See the Grounded Completions tutorial for that flow.
The decision in one line: direct Embeddings is the right tool when you own your vectors. If Gloo should own the index for you, use the Data Engine instead of this endpoint.
Either way, the generation half can be guided by Gloo’s guardrails, that’s the endpoint-family split we return to at the end.

Prerequisites

Before starting, ensure you have:
  • A Gloo AI Studio account
  • Your API key from the API Credentials page
  • Authentication setup: Complete the Authentication Tutorial first
  • Basic familiarity with REST APIs and a language runtime (Node.js 18+, Python 3.9+, PHP 8+, Go 1.20+, or Java 17+)

Working Code Sample

Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java). Includes the 5 Bezalel sample content files.Setup and testing instructions are provided later.
The code snippets in this tutorial are simplified and self-contained, designed for readability and easy copy-paste. The cookbook examples add full error handling, retry backoff, and multi-language plumbing. Both implement the same APIs and patterns.

What You’ll Build

A single runnable script that runs the full loop with your own vectors:
  • Chunk a small corpus of text files into ~150-word pieces
  • Index every chunk in one batched embeddings call
  • Retrieve the top-3 chunks for a query by cosine similarity, gated by a relevance threshold
  • Augment the prompt with labeled chunks so citations are possible
  • Generate a grounded answer with Guarded Responses

Example: Bezalel Ministries (continued)

We reuse the fictional Bezalel Ministries corpus from the Grounded Completions tutorial: five .txt files covering hiring, research methodology, creation process, educational resources, and vision & mission. The default query, "How does Bezalel choose its artists?", is answered in detail by the hiring-process document, so you can see the right chunks win.

Step 1: Chunk the Corpus

Retrieval quality starts with chunking. Following the Goldilocks chunk-size guidance, we split each file at blank-line (paragraph) boundaries and greedily pack consecutive paragraphs into chunks targeting ~150 words, inside the 200 to 1,000 token range the lesson recommends. Each chunk carries metadata ({chunk_id, source_file, chunk_index}) so we can attribute answers later. Paragraphs are kept whole, so a dense paragraph can push a chunk over the target; that’s expected, and we don’t hide it.

What You’ll See

Key Points

  • Chunk at natural boundaries. Paragraph breaks keep ideas intact; word-target packing keeps chunks a predictable size.
  • Metadata travels with the text. {chunk_id, source_file, chunk_index} is what makes citations possible two steps from now.
  • Production systems often add overlap. We skip it here to keep the logic explainable. A real pipeline typically overlaps chunks by about 10 to 20% so answers split across a boundary are still retrievable.
  • Read the Building a Knowledge Base lesson for the trade-offs behind chunk size and metadata.

Step 2: Index the Corpus in One Batched Call

Now embed every chunk. The embeddings endpoint accepts an array of up to 2,048 strings in a single input, so one call embeds the whole corpus. That’s the whole point of batching: fewer round-trips, less latency, and (since embeddings are billed per input token) a single place to read what you’re billed in usage.
This is a direct endpoint: there’s only one embeddings endpoint, and its job is to hand you vectors for your store. Guarding applies to the generation half, not the vector half; you’ll see that split in Step 5.

What You’ll See

Key Points

  • One call, not 61. The input array batches the whole corpus; that’s fewer round-trips and less overhead than a per-chunk loop.
  • usage is your bill. Embeddings are priced per input token with no output charge, so prompt_tokens here (9,719 for the Bezalel corpus) is exactly what this call costs; watch it in API usage.
  • encoding_format: "float" keeps the math simple. You get plain number arrays back; base64 is an option when your client needs a compact wire format.
  • Vectors from different models aren’t comparable. Pick one embedding model and use it for both the corpus and the query; switching later means re-embedding everything.
  • This demo holds vectors in a list. A real deployment persists them in a vector store keyed by chunk_id; the retrieval logic is the same either way. See the Embeddings API guide for the full request and response shape.

Step 3: Retrieve by Cosine Similarity

To answer a question, embed it with the same model, then score it against every chunk vector with cosine similarity: dot(q, c) / (‖q‖ · ‖c‖). Gloo passes the provider’s vectors through untouched, so they are not normalized; compute the norms explicitly instead of assuming unit length. Then apply two tuning knobs:
  • Top-k (k = 3): take the highest-scoring chunks. One chunk rarely holds a whole answer, and scores are imperfect, so a small set beats a single best match; see the Retrieval Step lesson.
  • Relevance threshold (0.30 cosine): if even the best score falls short, skip generation and say so. That’s the lesson’s warning against confidently presenting irrelevant matches.

What You’ll See

The full ranked list prints first, so you can judge retrieval quality before generation runs. For the default query:

Key Points

  • Print the ranked list, not just the winners. When a RAG answer is wrong, the first thing to inspect is what was retrieved, before blaming the model.
  • Normalize explicitly. Gloo returns the provider’s vectors as-is; cosine similarity needs the norms, so compute them rather than assuming unit length.
  • k > 1 on purpose. Answers spread across chunks and scores are noisy, so the top few together beat the single best match.
  • 0.30 is a named tuning knob, not magic. It’s a starting value for this corpus; tune it (and k) against your own content. The Retrieval Step lesson covers similarity search, top-K, and ranking in depth.

Step 4: Augment with Labeled Chunks

Now build the prompt that turns retrieved chunks into a grounded answer. We follow the anatomy from the Augmentation Step lesson: instructions first (role, use-only-context, admit-gaps), context in the middle, question last. Each chunk is prefixed with a [Source: file, chunk N] label so the model can attribute what it uses, which is what makes citations possible.

Key Points

  • Instructions first, question last. The model sees the grounding rules before the context and the actual ask, matching the lesson’s prompt anatomy.
  • Labels make citations possible. [Source: file, chunk N] lets the model (and you) point back to specific retrieved chunks, not just a document.
  • Context stays tiny. Three chunks at ~150 to 300 words is a few hundred tokens, nowhere near a context-window concern, which is why we can keep this discussion brief.
  • Read the Augmentation Step lesson for prompt templates and how to tell the model to use (and not misuse) the context you pass.

Step 5: Generate a Grounded Answer

The last call goes to Guarded Responses (POST /ai/v2/guarded/responses) to leverage Gloo’s full safety and values pipeline. The instructions field carries the grounding rules (the Responses equivalent of a system message); input carries the labeled context and question. We parse the typed output[] array for the answer text.
The endpoint-family split is the real design decision. The retrieval half is yours: direct Embeddings plus your own vectors, the whole reason this recipe exists. The generation half goes guarded, so Gloo’s guardrails, output moderation, and the optional tradition values-alignment protect the final answer. That matches the “which endpoint should I use” guidance for new integrations. Contrast with Grounded Completions, where Gloo’s Data Engine does retrieval for you.

Key Points

  • Use each endpoint for its half. Direct Embeddings for retrieval over your vectors; Guarded Responses for a protected, cited answer.
  • temperature: 0.2 favors faithfulness. A lower temperature keeps the answer close to the provided context.
  • tradition is a Gloo extra. The commented-out knob requests a values-aligned response for a specific tradition, something the direct surface can’t offer.
  • Watch the guardrail path. A 403 here means guardrails refused the content, not that your key is wrong; handle it as its own case (see Troubleshooting).
  • Read the Generation Step lesson for how retrieved context becomes a final, grounded answer.

Testing Your Implementation

  1. Set up your environment:
  2. Run the code:
  3. Try your own query: pass one as an argument:

What You’ll See

A complete run of the Python example, live-tested against the Bezalel corpus with the default query:
The answer is drawn from the retrieved chunks and cites the [Source: ...] labels the model was given; the hiring-process document won because the query matches it, exactly as the ranking predicted.
Numbers like the 9,719 embedding tokens and the specific scores vary with the corpus and the embedding model. The shape of the output (counts, ranked chunks, top-k, then the answer) is what every language implementation produces.

Working Code Sample

View Complete Code

Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions and the Bezalel content files.

Next Steps

Now that you’ve built semantic search on your own vectors:

Embeddings API Guide

Full request and response reference for the direct embeddings endpoint.

Building a Knowledge Base

Go deeper on chunking, metadata, and the Goldilocks chunk-size problem.

Grounded Completions

The Data Engine alternative: let Gloo do retrieval for content you upload.

Supported Models

Embedding models, vector widths, input limits, and live pricing.

Troubleshooting

Retrieval returns nothing relevant

Symptom: Generation is skipped and the script prints No sufficiently relevant chunks found. Cause: No chunk scored at or above RELEVANCE_THRESHOLD (0.30 cosine). This is the threshold doing its job; it prevents confidently answering from irrelevant matches. Solution: Try an off-corpus query to see the gate fire. For example, "What is the weather on Mars?" scores roughly 0.10 to 0.21 against every Bezalel chunk, so the best match still falls below 0.30:
If in-corpus queries also miss, lower RELEVANCE_THRESHOLD (try 0.2), raise TOP_K, or revisit your chunking.

Guarded Responses returns 403

Cause: The guarded surface refused the content, a guardrails hard-block, not an authentication failure. (An auth 401/403 means a bad key or missing organization access.) Solution: Treat the guardrail 403 as its own case and surface it gracefully rather than as a crash: the cookbook exposes it as guardrails hard-blocked this request (403) with the message body. Re-check the instructions and input you’re sending, and don’t retry unchanged.

400 Bad Request on the embeddings call

Cause: A non-embedding model ID, an empty or whitespace-only string, more than 2,048 inputs, or a chunk longer than the model’s input limit. Solution: Use an embedding model (gloo-openai-text-embedding-3-small), make sure no input is blank, and keep each chunk within the model’s max_input_tokens. The response’s detail.param names the offending field; see the Embeddings error table.

Missing GLOO_API_KEY

Cause: The environment variable isn’t set. Solution: Create a .env from .env.example (see Testing Your Implementation), add GLOO_API_KEY=..., and re-run. The script fails fast with the variable name in the message.

503 from embeddings

Cause: A transient provider outage or rate limit. Embeddings requests aren’t failed over to a second provider, so a 503 returns with retryable: true. Solution: Retry with bounded exponential backoff. The cookbook retries 1s → 2s → 4s (three attempts) before surfacing the error. If the same request keeps failing, change the request or model rather than retrying unchanged.

Additional Resources