DEV Community

Cover image for Connect your Strands agent to Amazon OpenSearch Service
Jon Handler
Jon Handler

Posted on Originally published at Medium

Connect your Strands agent to Amazon OpenSearch Service

Your agent is only as good as what it can look up. Point it at OpenSearch Service and give it hybrid search as a tool.

An AI agent reasons well and knows little. The model is frozen at training time, so anything specific to your business, your product catalog, your support history, your internal docs, lives outside it. The agent gets that knowledge the same way an application does: it searches for it. The question is what it searches, and how well.

Amazon OpenSearch Service is the shortest path to that knowledge source. It already holds your text, and it can run lexical and semantic search over the same index, so an agent can retrieve by keyword and by word-context similarity in a single call. This post wires a Strands agent to Amazon OpenSearch Serverless with two short pieces of code: the agent itself, and a hybrid search tool it can call. Serverless means you do not size or manage a cluster; you create a collection, point the agent at it, and let it scale on its own.

The agent is a handful of lines

In Strands, the model is the agent. You hand it a set of tools, and it reasons about when to call them, reads what they return, and keeps going until it has an answer. Amazon Bedrock backs it by default, so there is no model object to construct up front. What you write is small: an import, a system prompt that tells the agent to ground its answers in what it retrieves, the tool it can call, and the question.

from strands import Agent, tool

agent = Agent(
    tools=[opensearch_hybrid_search],
    system_prompt="Answer using the knowledge base. Retrieve before you answer, and cite what you used."
)
agent("What changed in our refund policy last quarter?")
Enter fullscreen mode Exit fullscreen mode

Your application receives a user's question, pulls together whatever it already knows, the session, the user's recent activity, any context it is holding, and hands that to the agent. The agent decides it needs to look something up, calls the knowledge-base tool, reads the passages that come back, and grounds its answer in them rather than in whatever the model happened to memorize at training time. Agents rarely stop at one tool call, though; they loop, calling a tool, reasoning over what it returned, and calling again until they have enough to answer. Think of it as a Unix pipe with judgment: question | retrieve | reason | answer, except the agent runs that loop itself and decides how many times to go around before it responds.

The tool is a hybrid query against OpenSearch Service

The tool runs a hybrid query with two clauses. One is lexical, a BM25 match that locks onto exact terms, product codes, and names. The other is neural, a clause that ranks documents by how closely their word-context patterns align with the query's, so it surfaces relevant passages whose wording differs from the words the user typed. A search pipeline normalizes the two score scales and combines them, so a single request returns one ranked list drawn from both signals.

@tool
def opensearch_hybrid_search(query: str) -> list[str]:
    """Search the knowledge base for passages relevant to the query.
    Use for any question about company-specific information."""
    body = {
      "query": {"hybrid": {"queries": [
        {"match": {"text": {"query": query}}},
        {"neural": {"text_embedding": {
            "query_text": query, "model_id": MODEL_ID, "k": 10}}}
      ]}}
    }
    resp = client.search(index="kb", body=body,
                         params={"search_pipeline": "hybrid-pipeline"})
    return [h["_source"]["text"] for h in resp["hits"]["hits"]]
Enter fullscreen mode Exit fullscreen mode

Two fields on the index make this work: text holds the raw passage for the lexical clause, and text_embedding holds its vector for the neural clause. The search pipeline, created once on the collection, carries the normalization and combination settings so the query body stays this small. The tool hands back a list of passage strings, which is exactly what the agent needs to read and cite.

One point worth stressing, because it is where hybrid search quietly fails: both legs have to be good. Hybrid search amplifies every signal you give it, so a noisy lexical configuration produces noisy combined results. Adding a semantic clause does not paper over a weak BM25 setup. Tune the keyword side as carefully as the vector side, and the combination rewards you; neglect it, and the combination carries the noise straight through.

Getting the embeddings in: the easy path and the direct path

The neural clause needs a vector for each document, and you have two ways to produce them. The easy path is automatic semantic enrichment: you enable it on a text field, and Amazon OpenSearch Service expands that field into a sparse vector with a service-managed model at ingestion, with no embedding pipeline to wire up and no model to host. Because the model runs at ingest and queries are only tokenized, the matching clause for this path is a neural_sparse query rather than the dense neural clause shown above. For most teams this is where to start, because it removes the step that usually stalls a first vector project.

The direct path is to supply your own dense vectors. You run your embedding model, write the vectors into a knn_vector field yourself, and query them with the dense neural clause, keeping full control over the model, its dimensions, and when you re-embed. Reach for it when you have a specific model you need to use or you are already generating embeddings elsewhere in your stack. The hybrid structure is the same either way; the one clause that changes is whether you match the enriched field with neural_sparse or the dense field with neural.

Point the agent at the knowledge source

This post covers the bare-bones interaction between the application and the agent, giving you the idea on how to create and use an agent. In a real production system, the lexical clause would grow filters, boosts, and field weights tuned to your data instead of a bare match. The agent would carry more than one tool: guardrails that keep it inside policy, a step that evaluates whether the retrieved passages are good enough before it answers, and often separate lexical and vector tools with a planning step that decides which to call for a given question. You might split retrieval across several indexes, add reranking, or let the agent reformulate a query that came back thin.

An agent with no knowledge source guesses; an agent with a good one answers. An Amazon OpenSearch Serverless collection holds your knowledge, automatic semantic enrichment gets the vectors in without an embedding pipeline, and one hybrid search tool gives the agent lexical precision and semantic reach in a single call. The agent code is a dozen lines, the tool is a dozen more, and the retrieval underneath them is the part that decides whether the answers are any good. Build the knowledge source well, and the agent gets smarter for free.

Top comments (0)