Using a natural language interface, the user can give a description of a parameter and the system makes the connection to the formal parameter, including the assignment of values. The methodology used to accomplish this is Retrieval Augmented Generation (RAG). The goal is to identify within a user’s text the terms being used. The user uses a natural language description of the term. The RAG method identifies the actual term that the user is referring to.
For example, the description of the ThermodynamicBensonRuleDefinitionDataSet (found in rdfs:comment of the annotations) ontology class for the Benson rule definition is:
The Benson rule for the data set. This is all the information associated with a benson rule definition for a dataset.
Within a RAG recognition method, this would be compared to the text. Using RAG means that the user’s text does not have to match the description exactly but be ‘close’ (or ‘best’ compared to the other descriptions given to the RAG).
The RAG, in combination with an agent for this purpose, can also identify a parameter and its value. For example, for the entropy ontology class: ThermodynamicStandardEntropy, the description is:
The standard entropy, 298K, of a thermodynamic structure
So if the text says something like:
The standard entropy is 29.0 kcals
the ‘standard entropy’ text could be paired with ThermodynamicStandardEntropy. The same applies to the units. The RAG determines the term identification and the Agent’s instructions tell the LLM to associate the value with this term.
The RAG methodology starts with a dictionary of terms. There are a set of terms (keywords) and an associated dictionary natural language description of those terms. The RAG methodology finds the term that has the closest natural language description to the phrasing in the user’s text. If there is a value associated with the term, that value is assigned.
The post is a first experiment setting up a Google VertexAI RAG implemention. More specifically, the google-cloud-discoveryengine API is to be used to use a vector of term-description pairs to ‘discover’ the best match with a natural language text (see this github for documentation and source codes). More extensive documentation and examples are found at
Google Home -> Documentation -> AI and ML > Vertex AI Search -> Get search results
The google-cloud-discoveryengine is a mechanism that allows the definitions to be set up in a database and then used for the recognition process. The terms are set up in a Firestore database. This database then automatically updates a ‘datastore‘ database. This database is then pointed to within an ‘search agent’ (a SearchRequest class) whose instructions say how to use the ‘RAG’ vector to identify terms.
The motivation for this use of RAG is to be able to interpret a text and extract parameters. The natural language description used in a text should be, using RAG, coupled with the ontology class that represents that term.
Discovery Engine
The google-cloud-discoveryengineis the engine under the hood, and “Vertex AI Search” is the driver’s seat.
Here is the breakdown of how they relate to each other and to RAG (Retrieval-Augmented Generation).
1. The Branding vs. The Code
Vertex AI Search (formerly Gen AI App Builder): This is the Product Name you see in the Google Cloud Console. It is a high-level “platform-as-a-service” designed to make RAG easy by combining search, website crawling, and LLMs into one interface.
google-cloud-discoveryengine: This is the Technical API/Client Library. When you write Java code to talk to Vertex AI Search, you use the Discovery Engine library. In Google’s backend, “Discovery Engine” is the internal service name for the search and recommendation infrastructure.
2. Where “RAG” Fits In
RAG consists of two main parts: Retrieval (finding the data) and Augmentation/Generation (the LLM explaining the data).
3. The Hierarchy of Objects
The Discovery Engine library uses a specific hierarchy that can be confusing at first. Here is how they stack up:
Project/Location: Your GCP project and the
globalregion.Collection: A logical grouping (usually
default_collection).Data Store: The “Physical” storage. This is where your Firestore data lives and where the schema is defined.
Engine (or “App”): The “Logical” layer. This is where you configure the LLM, the preamble, and search behavior.
Serving Config: The specific “Entry Point” for a search (e.g.,
default_search).
4. Why use this instead of “Raw” Vertex AI?
You could technically build RAG using the “raw” Vertex AI Vector Search (Matching Engine) and a manual Gemini API call. However:
Manual RAG: You have to manually turn text into vectors, store them, query them, grab the text, and feed it to the LLM.
Discovery Engine (This implementation): You just point it at Firestore. It handles the vectorization, the ranking (the
clearbox_escorer_score), and the LLM integration automatically.
Summary
You are using the Discovery Engine Java SDK to interact with the Vertex AI Search service to perform a RAG workflow.
The google-cloud-discoveryengine library is essentially the “API Gateway” that allows your Java application to command the complex AI machinery Google has built.
RAG and ChemConnect
One important use of Retrieval Augmented Generation (RAG) in ChemConnect is to connect natural language descriptions of the ontology class to the ontology class itself. Every ChemConnect class has the form, for example for the class: InitialReadInLocalStorageSystem:

For the RAG implementation, the rdfs:comment is a natural language description of the class. This is what will be used as the comparison for the RAG vector. Given text within a prompt, the text that comes closest to matching the comment will be chosen as representing the class. This is using the LLM as a natural language processor to find similar text to match the comment. This means that the user doesn’t need to know the exact class, but it should suffice with a description.
The structure of the RAG vector that will be implemented is:
- id (dcterms:identifier): A unique identifier for the class
- term: This is the classname (without namespace) to be identified
- description (rdfs:comment) The natural language description that will be used for the RAG comparison
Setting up Datastore
The Google ‘Data Store‘ is where the terms and the description reside for the RAG method. The data in the Data Store actually mimics the data collection in a Google Firestore database. The implementation occupies the Firestore collection and then the Data Store is automatically updated.
The Console page
The datastore setup follows the instructions on this page. In the ‘AI Applications’ console page, the ‘Data Stores’ link has the current data stores. To create a new datastore, press the ‘create datastore button’. There are four stages of setup:
- Source: this is the data that is to be coupled with the datastore. In this case it is the Firestore application.
- Data: This is the information that is needed to point to the firestore application: projectId-> database Id -> collection
- Configuration: This is the configuration of the datastore. this is the location and the name (the basic name gets appended with a unique number).
- Pricing: check how the pricing is arranged.
Two parts that gave me trouble were:
- Location: it has to be a location that is compatible with the application and with the firestore. Unless there is legal issues as to where the data is stored, ‘global’ works.
- Firestore Data and Schema: if the firestore data exists, it will be immediately uploaded and a schema will be generated. this might be okay, but it could be that the generated schema has the wrong values. The image in section Schema outlines the permissions.
Database Definition



Schema
The Schema of the data is how the Data Store/Firestore data is structured. It is important that the properties have the correct accessability, namely that they be ‘Searchable’, ‘Indexable’ and ‘Retrievable’



Data
The data in the Data Store mimics the data in the Firestore collection pointed to in the setup



Firebase Firestore data
There is a one-to-one correspondence between the Firestore collection dictionary_terms and the datastore elements. This collection is pointed to by the following:
- projectID: ‘blurock-database’ this is the project where all the code and data resides.
- database ID: ‘(default)’: there is only one database defined
- Collection Name: ‘dictionary_terms’: This is the top level collection where the dictionary terms are stored.






Datastore hierarchy
To understand the path projects/{project}/locations/{location}/collections/{collection}/dataStores/{dataStore}/branches/{branch}, think of it as a physical library address. Each part narrows down exactly where the “books” (your dictionary terms) are stored.
1. Breaking Down the Terms
| Term | Meaning | Typical Value |
collection | A logical grouping for multiple Engines/Data Stores. It is currently a placeholder for future multi-tenancy. | default_collection (99% of use cases). |
dataStore | The actual “container” or “database” holding your data. This is where your dictionary lives. | Your chosen ID, e.g., blurock-dictionary-v1. |
branch | An isolated version of the data within a Data Store. | 0 or default_branch. |
Deep Dive: What is a “Branch” in AI?
Just like in Git, branches allow you to have different versions of your data.
0(ordefault_branch): This is the “Live” branch. When your Agent answers questions, it only looks at branch0.1or2: You can use these to “stage” data. For example, you could import a massive new dictionary into branch1, verify it, and then “switch” the Agent to look at branch1instead of0.
2. Should you use different Data Stores?
Yes, but only for specific reasons. You shouldn’t create a new Data Store for every small change, but you should use different ones if the Data Type or Search Goal changes.
When to create separate Data Stores:
Different Data Structures: * Store 1: Dictionary (Structured JSON with terms/values).
Store 2: Technical Manuals (Unstructured PDFs).
Reason: The AI needs to index “Terms” differently than it indexes “Paragraphs.”
Security and Access:
Store 1: Public Terms (Anyone can see).
Store 2: Internal Configs (Only Admins can see).
Reason: Permissions are often set at the Data Store level.
Language Separation:
Store 1:
english-dictionaryStore 2:
swedish-dictionaryReason: Vertex AI Search performs better when it knows the specific language of the index.
3. Recommendation for your RAG Application
For a “Dictionary Interpreter,” one Data Store is usually enough. However, you can connect multiple Data Stores to one Agent.
Example: You could have a
hardware-dictionarystore and asoftware-dictionarystore. Your Agent can search across both simultaneously to find the correct value for a user’s prompt.
Summary Checklist
Project/Location: Use
blurock-databaseandglobal(oreu).Collection: Always use
default_collection.Branch: Always use
0(for your JavaBranchNamebuilder, usedefault_branch).DataStore: Use unique IDs for distinct types of content.
Google Cloud Resource Hierarchy Explained
This video is useful because it explains the foundational “Project > Location > Resource” hierarchy used across all Google Cloud services, which helps clarify how these specific AI identifiers fit into the bigger picture.
DictionaryAgentService
This is the JAVA class that uses the Data Store that was set up to analyse the prompt.
Connection to AI Application
To do the search, the SearchRequest has to be coupled with a Agent which in turn is connected to the datastore. To create the agent one uses the Google Console under the project that will be used (in this application it is blurock-database). Here there is a ‘create app’ button.



The first decision is the type of app to be created. In our case, under Search and Assistant the Custom Search (general) app was chosen.



The next step is to choose the configuration:
- Generative Responses: this type of app is important for the search that is intended, namely using a RAG search and some interpretation of the terms that are found.
- App Name: Here the ‘real’ app name will be this name with a number attached (see below field). This full name (with number attached) is what is used to refer to this app from the JAVA code.
- Company Name:
- Location: In this case, this has to be compatible with the other services (Firestore, Data Store). ‘global’ is a safe bet (unless there are legal restrictions.



The next step is to connect the app with the Data Store. The list of currently available Data Store databases are listed.
The last step is to determine the pricing. And a this point one has an agent that can be referred to by the search.
Connection to App in the JAVA code
The location of the Data Store to be used in the Agent is given as a combination of project, location, and agent Id (the long name):
String agentId = “analyzeterm_1772469548697”;
String servingConfigS = String.format(
“projects/%s/locations/global/collections/default_collection/engines/%s/servingConfigs/default_search”,
“blurock-database”,
agentId
);
This is used in the search setup (SearchRequest):
.setServingConfig(servingConfigS)
Data Store Filter
There is only one Data Store dictionary that has all the terms. However, there could be different types of terms for different purposes. This is determined by the metadata within the dictionary term, specifically the datatype.
This filter is used to isolate only those dictionary terms having the proper datatype:
String filterExpression = “metadata.datatype: ANY(\”” + datatype + “\”) “;
This is used within the SearchRequest setup with:
.setFilter(filterExpression)
Instruction String
The instruction string of the agent tells the agent how to use the Data Store values. In this experiment, the goal was actually just to identify the task and to identify the name of the task. The task is found with the RAG procedure and the value is determined through the ‘Generative Response’ type that was enable in the configuration.
The instructions are given to the agent on how to use the RAG and the prompt
ROLE: You are a Data Extraction Assistant.
TASK: You are provided with a USER QUERY and a list of DICTIONARY TERMS (Search Results).
Your goal is to find the best matching ‘term’ from the results and extract the specific value associated with it from the user’s query.
RULES:
1. KEY: Select the ‘term’ from the search results that matches the user’s intent.
2. VALUE: Extract the value/name (e.g., ‘SimpleBenson’) directly from the USER QUERY.
3. OUTPUT: Return only JSON. If no value is found in the query, use null.
USER QUERY: {query}
FORMAT:
{“term_name”: “extracted_value”}
SearchRequest Definition
This configuration is put together with the SearchRequest:
SearchRequest request = SearchRequest.newBuilder()
.setServingConfig(servingConfigS)
.setQuery(userPrompt)
.setFilter(filterExpression)
.setContentSearchSpec(SearchRequest.ContentSearchSpec.newBuilder()
.setSummarySpec(SearchRequest.ContentSearchSpec.SummarySpec.newBuilder()
.setSummaryResultCount(1)
.setIgnoreLowRelevantContent(true)
.setModelPromptSpec(SearchRequest.ContentSearchSpec.SummarySpec.ModelPromptSpec.newBuilder()
.setPreamble(jsonInstructionString)
.build())
.build())
.build())
.setPageSize(1) // Limit to top 5 identified terms
.build();
Using the SearchRequest for the response
The response is found in the SearchResponse:
SearchPagedResponse pagedResponse = client.search(request);
SearchResponse response = pagedResponse.getPage().getResponse();
System.out.println(response); // The LLM’s interpretation)
Interpreting the Response
Within the SearchResponse, the result is under the summary:
String rawJsonOutput = response.getSummary().getSummaryText();
A typical summary result is:
summary {
summary_text: ““`json\n{\”InitialReadInLocalStorageSystem\”: \”SimpleBenson\”}\n“`”
summary_with_metadata {
summary: ““`json\n{\”InitialReadInLocalStorageSystem\”: \”SimpleBenson\”}\n“`”
citation_metadata {
}
}
}
From this the JSON value should be isolated:
String cleanJson = rawJsonOutput.replaceAll(““`json|“`”, “”).trim();
package esblurock.info.JavaAIAgent.workflow.rag;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.cloud.discoveryengine.v1.SearchRequest;
import com.google.cloud.discoveryengine.v1.SearchResponse;
import com.google.cloud.discoveryengine.v1.SearchServiceClient;
import com.google.cloud.discoveryengine.v1.SearchServiceClient.SearchPagedResponse;
import esblurock.info.JavaAIAgent.workflow.WorkflowConstants;
import com.google.cloud.discoveryengine.v1.ServingConfigName;
import com.google.protobuf.Struct;
public class DictionaryAgentService {
public static void identifyTerms(String userPrompt, String datatype) throws Exception {
// 1. Initialize the client
try (SearchServiceClient client = SearchServiceClient.create()) {
String agentId = "analyzeterm_1772469548697";
String servingConfigS = String.format(
"projects/%s/locations/global/collections/default_collection/engines/%s/servingConfigs/default_search",
"blurock-database",
agentId
);
String filterExpression = "metadata.datatype: ANY(\"" + datatype + "\") ";
String jsonInstructionString = """
ROLE: You are a Data Extraction Assistant.
TASK: You are provided with a USER QUERY and a list of DICTIONARY TERMS (Search Results).
Your goal is to find the best matching 'term' from the results and extract the specific value associated with it from the user's query.
RULES:
1. KEY: Select the 'term' from the search results that matches the user's intent.
2. VALUE: Extract the value/name (e.g., 'SimpleBenson') directly from the USER QUERY.
3. OUTPUT: Return only JSON. If no value is found in the query, use null.
USER QUERY: {query}
FORMAT:
{"term_name": "extracted_value"}
""";
// 3. Configure the search request
SearchRequest request = SearchRequest.newBuilder()
.setServingConfig(servingConfigS)
.setQuery(userPrompt)
.setFilter(filterExpression)
.setContentSearchSpec(SearchRequest.ContentSearchSpec.newBuilder()
.setSummarySpec(SearchRequest.ContentSearchSpec.SummarySpec.newBuilder()
.setSummaryResultCount(1)
.setIgnoreLowRelevantContent(true)
.setModelPromptSpec(SearchRequest.ContentSearchSpec.SummarySpec.ModelPromptSpec.newBuilder()
.setPreamble(jsonInstructionString)
.build())
.build())
.build())
.setPageSize(1) // Limit to top 5 identified terms
.build();
// 4. Execute and interpret
SearchPagedResponse pagedResponse = client.search(request);
SearchResponse response = pagedResponse.getPage().getResponse();
// 2. Check if a summary was actually generated
if (response.hasSummary()) {
// This contains your JSON string: {"InitialReadInLocalStorageSystem": "SimpleBenson"}
String rawJsonOutput = response.getSummary().getSummaryText();
System.out.println("LLM Extracted JSON: " + rawJsonOutput);
// Optional: If the LLM adds markdown triple backticks (```json ... ```), clean them
String cleanJson = rawJsonOutput.replaceAll("```json|```", "").trim();
System.out.println("JSON: " + cleanJson);
// Now you can use a library like Jackson or Gson to turn this into a Map
} else {
// 3. Debugging: If no summary, find out why (e.g., LLM_ADDON_NOT_ENABLED)
System.out.println("Summary skipped. Reasons: " +
response.getSummary().getSummarySkippedReasonsList());
}
}
}
}package esblurock.info.JavaAIAgent.workflow.rag;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class TestRAGSearchInExamples {
@Test
void test() {
String datatypeString = "exampledatasettransactions";
String queryString = "Initial read of a dataset file from file system"
+ "";
try {
DictionaryAgentService.identifyTerms(queryString, datatypeString);
} catch (Exception e) {
e.printStackTrace();
}
}
}