Building a RAG Use Case
Summary
Retrieval-augmented generation (RAG) answers a question with a language model, but grounds the answer in your own data instead of in what the model happens to know. The pattern has three stages: index your records as embedding vectors, retrieve the records that match the question, and pass those records to the model as context.
ADITO ships every part of this. Vector indexing and retrieval run on Indexsearch and Solr, the vectors come from the adito-embed model, and the answer comes from adito-llm. This guide connects those pieces into one working pipeline and points to the reference documentation for each step.
Prerequisites
- ADITO 2026.1 or later. Vector search is integrated into Indexsearch starting with that release; see the upgrade guide for the changed
indexsearchmethod signatures. - A configured Indexsearch module with vector search enabled. See Vector Search via Solr for the full setup and Indexsearch configuration for the individual properties.
- An API key for the ADITO AI Runtime, loaded from an alias or environment configuration rather than hard-coded.
- Familiarity with JDito processes, in particular the
executableandwebserviceprocess variants.
Outcome
After completing this guide, an entity's records are searchable by meaning, a JDito process can retrieve the records most relevant to a natural-language question, and the ADITO-LLM produces an answer that cites only those records.
1. Decide what to retrieve before you build anything
RAG quality is decided by what ends up in the index, not by the prompt. Work through these questions first, because each one changes the record container you build in step 2.
- Which entity holds the answer? One vector index group per entity. Start with a single entity such as
Organisationand add more once retrieval works. - What is one retrievable unit? A whole record, or a section of it? A 40-page contract stored as one vector retrieves poorly, because a single vector has to represent the whole document. Split it.
- What text represents that unit? The embedding is created from text you assemble yourself. Concatenating a title, a short description, and the fields a user would search by usually beats dumping every column into the input.
- Who is allowed to see it? Permission filtering happens at retrieval time, so the fields and filter extensions needed for the permission check must be part of the index group.
The embedding model accepts up to 32,768 tokens per input string, but that is a limit, not a target. Smaller units produce sharper matches, and more of them fit into the prompt in step 4. Chunk long text into passages of a few hundred words with a little overlap between them.
2. Index the source data as vectors
The index side is a standard vector record container. Vector Search via Solr walks through the configuration in detail; the summary for a RAG use case is:
- Add a record container of type
indexto the entity and set its configuration mode toVECTORGROUP_DEFINITION. Only one record container per entity can use this mode. - Map the fields you want to retrieve, plus the system field
#VECTOR. The query must return data in the same order as the field mappings. - Add a
splitDataSubProcessto produce one vector document per chunk. UsesubProcessinstead only if one vector per record is genuinely enough. - Include the fields and filter extensions your permission checks need.
- Deploy the configuration.
The #VECTOR field is backed by the Solr field type knn_vector:
<!-- Fixed dimension and similarity function. Both are part of the index definition, not of your query. -->
<fieldType name="knn_vector" className="solr.DenseVectorField" vectorDimension="1024" similarityFunction="cosine"/>
The #VECTOR field is fixed to 1024 dimensions. Always send dimensions: 1024 in the embedding request, and store the vector as a JSON array string. A vector of any other length is rejected at index time.
Chunking in the splitDataSubProcess
splitDataSubProcess runs once per dataset, receives $local.data and $local.idvalue, and returns a three-dimensional string array — one entry per vector document. This is where chunking belongs: split the source text, request one embedding per chunk, and emit one copy of the dataset per chunk with its own #VECTOR value.
Batch the chunks into a single embedding request. The input parameter accepts an array of strings, and the response returns the vectors in the same order:
// One request for all chunks of a record. The response order matches the input order.
const body = {
model: "adito-embed",
input: ["First passage of the document.", "Second passage of the document."],
dimensions: 1024,
"no-log": true
};
The complete process, including the REST call and the response handling, is in the vector search guide.
splitDataSubProcess runs for every dataset in the index group. One embedding request per chunk instead of one per record turns a full rebuild into thousands of HTTP calls. Batch the chunks, and prefer the incremental indexer for ongoing updates.
3. Retrieve the records that match the question
Retrieval is a vector search with the question as input. Embed the question with the same model and the same dimensions used at index time, then run a nearest-neighbor query against the vector collection.
import { indexsearch } from "@aditosoftware/jdito-types";
/**
* Retrieves the records most similar to a natural-language question.
*
* @param {string} pQuestion - The user's question.
* @param {number} pLimit - Number of records to retrieve.
*
* @returns {object[]} The matching index hits, most similar first.
*/
function retrieveContext(pQuestion, pLimit)
{
// getVectorFromRemote() calls the embedding model and returns the vector as a JSON array string.
// See the appendix of the vector search guide for the full implementation.
let questionVector = getVectorFromRemote(pQuestion);
let query = indexsearch.createIndexQuery()
.setVector(questionVector)
.setTopK(pLimit)
.setRows(pLimit)
.setIndexGroups("Organisation")
.setResultIndexFields(
indexsearch.FIELD_ID,
indexsearch.FIELD_TITLE,
indexsearch.FIELD_DESCRIPTION,
indexsearch.FIELD_SCORE
);
// The collection parameter is mandatory. Against the standard index, setVector() fails.
let searchResult = indexsearch.searchIndex(query, indexsearch.COLLECTION_VECTOR_INDEX);
return searchResult[indexsearch.HITS] || [];
}
setTopK(...) defines how many nearest neighbors Solr selects; the default is 32. setRows(...) defines how many of those are returned. Keep rows equal to or lower than topK, so you never request more documents than the nearest-neighbor query actually selected.
How many you should retrieve is a separate question. Everything you retrieve is sent to the model in step 4 and consumes tokens there, so 5 to 10 well-chunked passages usually beat 50.
setPattern(...), setIncludedTags(...), and setExcludedTags(...) are standard-index features and are not available on the vector index. To restrict a vector search, for example by permission or by record type, use addFilter(...) and build the pattern with indexsearch.buildPattern(patternConfig, indexsearch.COLLECTION_VECTOR_INDEX). See Building vector filters correctly.
indexsearch.FIELD_SCORE returns the similarity score of each hit. Log it while tuning: if the top hit for an obviously answerable question scores poorly, the problem is the chunking in step 2, not the prompt in step 4.
4. Assemble the prompt and generate the answer
Pass the retrieved records to the chat completions endpoint as context, and instruct the model to answer only from that context. The call itself uses the same net.createConfigForRestWebserviceCall() pattern as Building an Agent:
import { auth, indexsearch, net, util } from "@aditosoftware/jdito-types";
/**
* Answers a question using only the retrieved records as context.
*
* @param {string} pQuestion - The user's question.
* @param {object[]} pHits - Index hits returned by retrieveContext().
*
* @returns {string} The raw response body of the chat completions call.
*/
function answerFromContext(pQuestion, pHits)
{
// Number the sources so the model can reference them in its answer.
let context = pHits
.map(function (pHit, pIndex)
{
return "[" + (pIndex + 1) + "] " + pHit[indexsearch.FIELD_TITLE] + "\n" + pHit[indexsearch.FIELD_DESCRIPTION];
})
.join("\n\n");
let messages = [
{
role: "system",
content: "You are a CRM assistant. Answer only from the numbered sources below. "
+ "Cite the sources you used as [1], [2]. If the sources do not contain the answer, say so.\n\n"
+ context
},
{ role: "user", content: pQuestion }
];
let body = {
model: "adito-llm",
messages: messages,
// Low temperature: the answer should follow the sources, not invent around them.
temperature: 0.15
};
let restConfig = net.createConfigForRestWebserviceCall()
.url("https://ai.adito.cloud/chat/completions")
.dataTypeAccept("application/json")
.dataTypeSend("application/json")
.dataTypeJDitoAccept(util.DATA_TEXT)
.dataTypeJDitoSend(util.DATA_TEXT)
.actionType(net.POST)
.requestEntity(JSON.stringify(body));
// Load the token from an approved secret source, never hard-code it in the process.
let authConfig = auth.createConfigForOAuth2().token("myApiToken");
return net.callRestWebservice(restConfig, authConfig);
}
Two instructions in the system message carry most of the weight, and both are worth keeping verbatim: answer only from the sources, and say so when the sources do not contain the answer. Without the second one, the model fills the gap from its own knowledge, which is exactly what RAG is meant to prevent. See Provide context, not just instructions for the general pattern.
If the answer feeds an automated workflow rather than a user, constrain it to a schema with guided_json — for example an object with an answer field and a source_ids array — instead of parsing free text.
Retrieved context counts against both the request timeout and the context window. Retrieving more records is the easiest way to make a working prototype start timing out. Use Token Usage Estimation to size the prompt before rolling out.
5. Keep the index up to date
An answer is only as current as the index behind it. Vector documents are rebuilt by the same indexer as the standard index, with the vector collection passed explicitly.
import { indexsearch } from "@aditosoftware/jdito-types";
// Full rebuild of one vector index group. Resource-intensive: it re-embeds every record.
indexsearch.runIndexer(["Organisation"], indexsearch.COLLECTION_VECTOR_INDEX);
// Incremental update for records that actually changed.
const indexerConfig = indexsearch.createIncrementalIndexerRunConfig()
.group("Organisation")
.addUids(["uid-1", "uid-2"]);
indexsearch.runIncrementalIndexer(indexerConfig, indexsearch.COLLECTION_VECTOR_INDEX);
Because every full rebuild re-embeds every chunk, a vector index is considerably more expensive to rebuild than a standard index. Prefer the incremental indexer for day-to-day updates and schedule full rebuilds deliberately, using the rebuildIndex_serverProcess described in Server-side process configuration. Run it on a background server, not on a frontend pod.
Guardrails
Anything in a CRM record can end up in the system message — including text a customer wrote. A note containing "ignore your previous instructions" is a prompt injection attempt delivered through your own data. Keep retrieved content clearly delimited from your instructions, never let a retrieved record grant permissions, and read Prompt injection defenses before exposing a RAG endpoint to end users.
Treat the generated answer as a suggestion or draft. Per AI Compliance, record changes and other side-effecting actions require explicit input from a user or a configured, auditable process.
Retrieval does not enforce permissions on its own. A vector search returns whatever is in the index group, so a user asking a question can otherwise receive content from records they are not allowed to open. Apply the permission filter in step 3, not after the model has already seen the data.
Prompt content and responses are retained for 90 days in ADITO Cloud in Germany, and CRM data included in a prompt is stored as part of that prompt log. The "no-log": true flag shown in step 2 suppresses logging for a request. See AI Compliance for what is stored and for how long.
See also: Vector Search via Solr | Embeddings | Index Search via JDito | Building an Agent | Prompting Guide