dev_docs/tutorials/elasticsearch.mdx
This guide covers how to obtain the Elasticsearch client in both your plugin's start lifecycle and route handler context. It explains the difference between asCurrentUser and asInternalUser, and demonstrates best practices for robust concurrent index initialization.
In your plugin's start method, the core parameter provides access to the Elasticsearch client:
import type { CoreStart, Plugin } from '@kbn/core/server';
export class MyPlugin implements Plugin {
public start(core: CoreStart) {
const esClient = core.elasticsearch.client.asInternalUser;
// You can now use esClient to call Elasticsearch APIs
}
}
asScoped(request) to perform actions as the request-authenticated user.elasticsearch.client.asCurrentUser instead of needing to call asScoped(request).asInternalUser for system-level operations that should bypass user permissions (see security considerations).When defining a server route, you can access the Elasticsearch client from the route handler's context parameter:
router.get(
{ path: '/api/my_plugin/search', validate: false },
async (context, request, response) => {
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
// Use esClient to call Elasticsearch APIs
const result = await esClient.search({
index: 'my-index',
body: { query: { match_all: {} } }
});
return response.ok({ body: result });
}
);
context.core.elasticsearch.client.asCurrentUser executes requests as the user making the HTTP request.asCurrentUser in route handlers to respect user permissions.When working in distributed environments (such as multiple Kibana instances), it is common for several instances to attempt to create the same index at the same time. To ensure the index is ready for ingesting and searching, all instances should:
import type { CoreStart, Plugin } from '@kbn/core/server';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
async function ensureIndexReady(esClient: ElasticsearchClient, indexName: string, mappings: object) {
try {
// Attempt to create the index even if it may already exist
// Use a long timeout and set requestTimeout slightly longer to avoid socket closure
await esClient.indices.create(
{
index: indexName,
mappings,
timeout: '300s',
// Allow for 0 or 1 replicas for higher availability on multi-node clusters, but continue to work on single-node (dev) clusters
auto_expand_replicas: '0-1',
},
{ requestTimeout: 310_000 }
);
} catch (error) {
if (error?.body?.error?.type !== 'resource_already_exists_exception') {
throw error;
}
// Index already exists - this is expected in multi-instance environments
// Do NOT log this as an error since it's an expected scenario that we
// gracefully handle
}
// Wait for the index to be ready (green status)
// Use a long timeout and set requestTimeout slightly longer to avoid socket closure
// Note: On serverless, the health API is only available to the internal user
await esClient.cluster.health(
{
index: indexName,
wait_for_status: 'green',
timeout: '300s',
},
{ requestTimeout: 310_000 }
);
}
export class MyPlugin implements Plugin {
public start(core: CoreStart) {
const esClient = core.elasticsearch.client.asInternalUser;
const indexName = 'my-index';
const mappings = {
properties: {
title: { type: 'text' },
description: { type: 'text' },
},
};
ensureIndexReady(esClient, indexName, mappings)
.then(() => {
// Index is ready for ingesting and searching
})
.catch((err) => {
this.logger.error(`Failed to initialize index ${indexName} in start: ${err?.message || err}`);
});
}
}
acknowledged: true, shard_acknowledged: false).When implementing index initialization:
resource_already_exists_exception as an error - this is completely expected when multiple Kibana instances start simultaneously// Good: Only log actual errors
} catch (error) {
if (error?.body?.error?.type !== 'resource_already_exists_exception') {
this.logger.error(`Failed to create index ${indexName}: ${error?.message || error}`);
throw error;
}
// Don't log - this is expected behavior
}
asScoped or asCurrentUser for user-initiated actions to enforce securityasInternalUser only for trusted, internal operations that must not be restricted by user permissions.asInternalUser in route handlers that respond to user requests.