Application search does not always require a managed Elasticsearch service. For teams that want more control over infrastructure, deployment, configuration, and operating costs, OpenSearch can be deployed and managed directly.
OpenSearch provides search and indexing capabilities that can support product search, document search, log analysis, internal portals, and other applications that need more than a traditional database query.
The important part is understanding what you take responsibility for when you operate OpenSearch yourself.
A self-managed search system gives you control, but you also become responsible for capacity planning, backups, upgrades, security, monitoring, and failure recovery.
This article explains how to build an application search setup with OpenSearch without depending on a managed Elasticsearch service.
What Self-Managed OpenSearch Means
With a managed search service, the provider usually handles much of the underlying infrastructure.
With self-managed OpenSearch, your team operates the search cluster.
A simplified architecture looks like this:
Application
|
v
Search API
|
v
OpenSearch Cluster
|
+-- Indexes
+-- Shards
+-- Replicas
+-- StorageThe application should normally communicate with OpenSearch through a controlled service boundary rather than allowing every part of the application to access the cluster directly.
For example:
Web Application
|
v
Search Service
|
v
OpenSearchThis keeps search-specific logic in one place.
When OpenSearch Makes Sense
OpenSearch can be a good choice when an application needs:
Full-text search
Filtering
Sorting
Search suggestions
Faceted navigation
Aggregations
Large searchable document collections
Flexible indexing
Search ranking
It can also be useful when the team already has infrastructure for running stateful services.
However, self-managing OpenSearch is not automatically simpler than using a managed service.
The operational requirements should be considered before choosing the architecture.
Basic OpenSearch Architecture
A production deployment normally contains more than one OpenSearch node.
A simplified cluster might look like:
Application
|
v
Search Service
|
v
Load Balancer
|
+----------+----------+
| | |
v v v
OpenSearch OpenSearch OpenSearch
Node 1 Node 2 Node 3
| | |
+----------+----------+
|
v
Persistent StorageMultiple nodes provide better availability and allow data and search workload to be distributed across the cluster.
The exact topology depends on workload size and availability requirements.
Start With a Development Environment
Before deploying a cluster, start with a small local environment.
A containerized setup is convenient for development because it allows the application team to run OpenSearch without installing the entire stack directly on the operating system.
For example:
services:
opensearch:
image: opensearchproject/opensearch:latest
environment:
- discovery.type=single-node
- DISABLE_SECURITY_PLUGIN=true
- OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "9200:9200"The exact image version should be pinned for repeatable development environments.
Avoid using an unpinned latest tag in production.
Connect a Rails Application
A Rails application can communicate with OpenSearch through an HTTP client or a compatible Ruby library.
A simple service object can keep search logic separate from controllers.
class ProductSearch
def initialize(client)
@client = client
end
def search(query)
@client.search(
index: "products",
body: {
query: {
multi_match: {
query: query,
fields: ["name", "description"]
}
}
}
)
end
endThe exact client depends on the application's OpenSearch integration.
The important design principle is to avoid scattering raw search requests throughout controllers and models.
Connect a .NET Application
A .NET application can also isolate OpenSearch access behind a dedicated service.
For example:
public class ProductSearchService
{
private readonly HttpClient _client;
public ProductSearchService(HttpClient client)
{
_client = client;
}
public async Task<string> SearchAsync(string query)
{
var request = new
{
query = new
{
multi_match = new
{
query,
fields = new[]
{
"name",
"description"
}
}
}
};
var response = await _client.PostAsJsonAsync(
"products/_search",
request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}In a production application, map the response into application-specific models instead of returning raw JSON throughout the application.
Create an Index Mapping
Search quality depends heavily on how documents are indexed.
For example:
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"category": {
"type": "keyword"
},
"price": {
"type": "double"
},
"createdAt": {
"type": "date"
}
}
}
}The distinction between text and keyword fields is important.
A text field is generally used for full-text search.
A keyword field is generally useful for exact matching, filtering, grouping, and sorting.
Choosing the wrong field type can make application queries harder or less efficient later.
Keep Database Data and Search Data Separate
OpenSearch should usually be treated as a search-oriented representation of application data.
For example:
PostgreSQL
|
| Source of truth
v
Application
|
| Indexing pipeline
v
OpenSearch
|
v
Search ResultsDo not automatically treat OpenSearch as the authoritative transactional database.
Your application database may contain relationships, transactions, constraints, and business rules that do not belong in the search index.
The search index should contain the data required to answer search requests efficiently.
Index Documents From the Application
Suppose the application stores products in a relational database.
A search document might contain:
{
"id": 101,
"name": "Wireless Keyboard",
"description": "Compact keyboard for office use",
"category": "accessories",
"price": 79.99
}The application can index this document after the product is created or updated.
A common architecture is:
Database Transaction
|
v
Application Event
|
v
Background Job
|
v
OpenSearch IndexUsing a background job prevents search indexing from unnecessarily slowing down the user's request.
Handle Search Indexing Failures
The database and search index can temporarily become inconsistent.
For example:
Database update
|
v
Successful
OpenSearch update
|
v
FailedThe application should have a recovery strategy.
Possible approaches include:
Retry queues
Dead-letter queues
Periodic reconciliation
Reindexing jobs
Event-based indexing
Change tracking
Do not assume that one successful indexing request means the systems will remain synchronized forever.
Design for Idempotent Indexing
An indexing operation should ideally be safe to repeat.
For example, use a stable document identifier:
Product ID 101
|
v
OpenSearch document ID 101If the indexing job runs twice, the second operation should update the same document rather than create an unintended duplicate.
This becomes particularly important when background jobs are retried.
Use Bulk Indexing for Large Data Sets
Indexing records individually can create unnecessary network overhead.
For initial data loads or large synchronization jobs, use bulk operations.
Conceptually:
Product 1 ─┐
Product 2 ─┤
Product 3 ─┼──> Bulk Request ──> OpenSearch
Product 4 ─┤
Product 5 ─┘Bulk indexing can significantly reduce request overhead.
However, inspect individual item failures in the bulk response.
A successful HTTP response does not necessarily mean every document was indexed successfully.
Join the conversation! Your thoughts help the community grow.