diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/index.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/index.svg
new file mode 100644
index 0000000000000..6af8b7e92afe9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/index.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts
index 7a764e9ef6fb5..8d493c0a4d59b 100644
--- a/x-pack/plugins/enterprise_search/server/plugin.ts
+++ b/x-pack/plugins/enterprise_search/server/plugin.ts
@@ -80,6 +80,7 @@ import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/t
import { GlobalConfigService } from './services/global_config_service';
import { uiSettings as enterpriseSearchUISettings } from './ui_settings';
+import { getIndicesSearchResultProvider } from './utils/indices_search_result_provider';
import { getSearchResultProvider } from './utils/search_result_provider';
import { ConfigType } from '.';
@@ -343,6 +344,7 @@ export class EnterpriseSearchPlugin implements Plugin {
if (globalSearch) {
globalSearch.registerResultProvider(getSearchResultProvider(http.basePath, config));
+ globalSearch.registerResultProvider(getIndicesSearchResultProvider(http.basePath));
}
}
diff --git a/x-pack/plugins/enterprise_search/server/utils/indices_search_result_provider.test.ts b/x-pack/plugins/enterprise_search/server/utils/indices_search_result_provider.test.ts
new file mode 100644
index 0000000000000..facae46be72a4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/utils/indices_search_result_provider.test.ts
@@ -0,0 +1,212 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { NEVER, lastValueFrom } from 'rxjs';
+
+import { IScopedClusterClient } from '@kbn/core/server';
+
+import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../common/constants';
+
+import { getIndicesSearchResultProvider } from './indices_search_result_provider';
+
+describe('Enterprise Search - indices search provider', () => {
+ const basePathMock = {
+ prepend: (input: string) => `/kbn${input}`,
+ } as any;
+
+ const indicesSearchResultProvider = getIndicesSearchResultProvider(basePathMock);
+
+ const regularIndexResponse = {
+ 'search-github-api': {
+ aliases: {},
+ },
+ 'search-msft-sql-index': {
+ aliases: {},
+ },
+ };
+
+ const mockClient = {
+ asCurrentUser: {
+ count: jest.fn().mockReturnValue({ count: 100 }),
+ indices: {
+ get: jest.fn(),
+ stats: jest.fn(),
+ },
+ security: {
+ hasPrivileges: jest.fn(),
+ },
+ },
+ asInternalUser: {},
+ };
+ const client = mockClient as unknown as IScopedClusterClient;
+ mockClient.asCurrentUser.indices.get.mockResolvedValue(regularIndexResponse);
+
+ const githubIndex = {
+ id: 'search-github-api',
+ score: 75,
+ title: 'search-github-api',
+ type: 'Index',
+ url: {
+ path: `${ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL}/search_indices/search-github-api`,
+ prependBasePath: true,
+ },
+ icon: '/kbn/plugins/enterpriseSearch/assets/source_icons/index.svg',
+ };
+
+ const msftIndex = {
+ id: 'search-msft-sql-index',
+ score: 75,
+ title: 'search-msft-sql-index',
+ type: 'Index',
+ url: {
+ path: `${ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL}/search_indices/search-msft-sql-index`,
+ prependBasePath: true,
+ },
+ icon: '/kbn/plugins/enterpriseSearch/assets/source_icons/index.svg',
+ };
+
+ beforeEach(() => {});
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('find', () => {
+ it('returns formatted results', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'search-github-api' },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([{ ...githubIndex, score: 100 }]);
+ });
+
+ it('returns all matched results', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'search' },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([
+ { ...githubIndex, score: 90 },
+ { ...msftIndex, score: 90 },
+ ]);
+ });
+
+ it('returns all indices on empty string', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: '' },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toHaveLength(0);
+ });
+
+ it('respect maximum results', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'search' },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 1,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([{ ...githubIndex, score: 90 }]);
+ });
+
+ describe('returns empty results', () => {
+ it('when term does not match with created indices', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'sample' },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([]);
+ });
+
+ it('if client is undefined', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'sample' },
+ {
+ aborted$: NEVER,
+ client: undefined,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([]);
+ });
+
+ it('if tag is specified', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'search', tags: ['tag'] },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([]);
+ });
+
+ it('if unknown type is specified', async () => {
+ const results = await lastValueFrom(
+ indicesSearchResultProvider.find(
+ { term: 'search', types: ['tag'] },
+ {
+ aborted$: NEVER,
+ client,
+ maxResults: 100,
+ preference: '',
+ },
+ {} as any
+ )
+ );
+ expect(results).toEqual([]);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/utils/indices_search_result_provider.ts b/x-pack/plugins/enterprise_search/server/utils/indices_search_result_provider.ts
new file mode 100644
index 0000000000000..c826be87fcdfc
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/utils/indices_search_result_provider.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { from, takeUntil } from 'rxjs';
+
+import { IBasePath } from '@kbn/core-http-server';
+import {
+ GlobalSearchProviderResult,
+ GlobalSearchResultProvider,
+} from '@kbn/global-search-plugin/server';
+import { i18n } from '@kbn/i18n';
+
+import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../common/constants';
+
+import { getIndexData } from '../lib/indices/utils/get_index_data';
+
+export function getIndicesSearchResultProvider(basePath: IBasePath): GlobalSearchResultProvider {
+ return {
+ find: ({ term, types, tags }, { aborted$, client, maxResults }) => {
+ if (!client || !term || tags || (types && !types.includes('indices'))) {
+ return from([[]]);
+ }
+ const fetchIndices = async (): Promise => {
+ const { indexNames } = await getIndexData(client, false, false, term);
+
+ const searchResults: GlobalSearchProviderResult[] = indexNames
+ .map((indexName) => {
+ let score = 0;
+ const searchTerm = (term || '').toLowerCase();
+ const searchName = indexName.toLowerCase();
+ if (!searchTerm) {
+ score = 80;
+ } else if (searchName === searchTerm) {
+ score = 100;
+ } else if (searchName.startsWith(searchTerm)) {
+ score = 90;
+ } else if (searchName.includes(searchTerm)) {
+ score = 75;
+ }
+
+ return {
+ id: indexName,
+ title: indexName,
+ icon: basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/index.svg'),
+ type: i18n.translate('xpack.enterpriseSearch.searchIndexProvider.type.name', {
+ defaultMessage: 'Index',
+ }),
+ url: {
+ path: `${ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL}/search_indices/${indexName}`,
+ prependBasePath: true,
+ },
+ score,
+ };
+ })
+ .filter(({ score }) => score > 0)
+ .slice(0, maxResults);
+ return searchResults;
+ };
+ return from(fetchIndices()).pipe(takeUntil(aborted$));
+ },
+ getSearchableTypes: () => ['indices'],
+ id: 'enterpriseSearchIndices',
+ };
+}
diff --git a/x-pack/plugins/global_search/common/types.ts b/x-pack/plugins/global_search/common/types.ts
index fabae7ea01e8f..676cb421c4a15 100644
--- a/x-pack/plugins/global_search/common/types.ts
+++ b/x-pack/plugins/global_search/common/types.ts
@@ -7,6 +7,7 @@
import { Observable } from 'rxjs';
import { Serializable } from '@kbn/utility-types';
+import { IScopedClusterClient } from '@kbn/core/server';
/**
* Options provided to {@link GlobalSearchResultProvider | a result provider}'s `find` method.
@@ -26,6 +27,11 @@ export interface GlobalSearchProviderFindOptions {
* this can (and should) be used to cancel any pending asynchronous task and complete the result observable from within the provider.
*/
aborted$: Observable;
+ /**
+ * A ES client of type IScopedClusterClient is passed to the `find` call.
+ * When performing calls to ES, the interested provider can utilize this parameter to identify the specific cluster.
+ */
+ client?: IScopedClusterClient;
/**
* The total maximum number of results (including all batches, not per emission) that should be returned by the provider for a given `find` request.
* Any result emitted exceeding this quota will be ignored by the service and not emitted to the consumer.
diff --git a/x-pack/plugins/global_search/public/services/types.ts b/x-pack/plugins/global_search/public/services/types.ts
index 169b1538d13d9..7cc622b82fba8 100644
--- a/x-pack/plugins/global_search/public/services/types.ts
+++ b/x-pack/plugins/global_search/public/services/types.ts
@@ -6,6 +6,7 @@
*/
import { Observable } from 'rxjs';
+import { IScopedClusterClient } from '@kbn/core/server';
/**
* Options for the server-side {@link GlobalSearchPluginStart.find | find API}
@@ -25,4 +26,9 @@ export interface GlobalSearchFindOptions {
* If/when provided and emitting, the result observable will be completed and no further result emission will be performed.
*/
aborted$?: Observable;
+ /**
+ * A ES client of type IScopedClusterClient is passed to the `find` call.
+ * When performing calls to ES, the interested provider can utilize this parameter to identify the specific cluster.
+ */
+ client?: IScopedClusterClient;
}
diff --git a/x-pack/plugins/global_search/server/routes/find.ts b/x-pack/plugins/global_search/server/routes/find.ts
index ec491454f7a88..64aa76ee64a70 100644
--- a/x-pack/plugins/global_search/server/routes/find.ts
+++ b/x-pack/plugins/global_search/server/routes/find.ts
@@ -33,8 +33,9 @@ export const registerInternalFindRoute = (router: GlobalSearchRouter) => {
const { params, options } = req.body;
try {
const globalSearch = await ctx.globalSearch;
+ const { client } = (await ctx.core).elasticsearch;
const allResults = await globalSearch
- .find(params, { ...options, aborted$: req.events.aborted$ })
+ .find(params, { ...options, aborted$: req.events.aborted$, client })
.pipe(
map((batch) => batch.results),
reduce((acc, results) => [...acc, ...results])
diff --git a/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts
index fc53c8d01ebfa..0551e4338c1d3 100644
--- a/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts
+++ b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts
@@ -76,6 +76,7 @@ describe('POST /internal/global_search/find', () => {
{
preference: 'custom-pref',
aborted$: expect.any(Object),
+ client: expect.any(Object),
}
);
});
diff --git a/x-pack/plugins/global_search/server/types.ts b/x-pack/plugins/global_search/server/types.ts
index 10a7bafe850dd..21de10af6a72f 100644
--- a/x-pack/plugins/global_search/server/types.ts
+++ b/x-pack/plugins/global_search/server/types.ts
@@ -13,6 +13,7 @@ import type {
Capabilities,
IRouter,
CustomRequestHandlerContext,
+ IScopedClusterClient,
} from '@kbn/core/server';
import {
GlobalSearchBatchedResults,
@@ -92,6 +93,11 @@ export interface GlobalSearchFindOptions {
* If/when provided and emitting, no further result emission will be performed and the result observable will be completed.
*/
aborted$?: Observable;
+ /**
+ * A ES client of type IScopedClusterClient is passed to the `find` call.
+ * When performing calls to ES, the interested provider can utilize this parameter to identify the specific cluster.
+ */
+ client?: IScopedClusterClient;
}
/**
diff --git a/x-pack/plugins/global_search_bar/public/lib/result_to_option.test.ts b/x-pack/plugins/global_search_bar/public/lib/result_to_option.test.ts
index d60453ca37d85..92589202cd16a 100644
--- a/x-pack/plugins/global_search_bar/public/lib/result_to_option.test.ts
+++ b/x-pack/plugins/global_search_bar/public/lib/result_to_option.test.ts
@@ -52,6 +52,15 @@ describe('resultToOption', () => {
);
});
+ it('uses icon for `index` type', () => {
+ const input = createSearchResult({ type: 'index', icon: 'index-icon' });
+ expect(resultToOption(input, [])).toEqual(
+ expect.objectContaining({
+ icon: { type: 'index-icon' },
+ })
+ );
+ });
+
it('does not use icon for other types', () => {
const input = createSearchResult({ type: 'dashboard', icon: 'dash-icon' });
expect(resultToOption(input, [])).toEqual(
diff --git a/x-pack/plugins/global_search_bar/public/lib/result_to_option.tsx b/x-pack/plugins/global_search_bar/public/lib/result_to_option.tsx
index 88fa86edbd395..555434a85404f 100644
--- a/x-pack/plugins/global_search_bar/public/lib/result_to_option.tsx
+++ b/x-pack/plugins/global_search_bar/public/lib/result_to_option.tsx
@@ -25,7 +25,8 @@ export const resultToOption = (
type === 'application' ||
type === 'integration' ||
type.toLowerCase() === 'enterprise search' ||
- type.toLowerCase() === 'search';
+ type.toLowerCase() === 'search' ||
+ type.toLowerCase() === 'index';
const option: EuiSelectableTemplateSitewideOption = {
key: id,
label: title,