Skip to content

Commit

Permalink
[Serverless Search] Index documents can be browsed in index management (
Browse files Browse the repository at this point in the history
elastic#172635)

This PR adds functionality to browse index documents in a index
management app.

## Screen Recording


https://github.com/elastic/kibana/assets/55930906/d7e565b4-f893-4c56-8e51-3a535cea09ff



### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
  • Loading branch information
saarikabhasi authored Dec 11, 2023
1 parent 594731d commit 543a047
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 35 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pageLoadAssetSize:
securitySolutionServerless: 62488
serverless: 16573
serverlessObservability: 68747
serverlessSearch: 71995
serverlessSearch: 72995
sessionView: 77750
share: 71239
snapshotRestore: 79032
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@
* Side Public License, v 1.
*/

import { IScopedClusterClient } from '@kbn/core/server';
import { ElasticsearchClient } from '@kbn/core/server';
import { DEFAULT_DOCS_PER_PAGE } from '../types';

import { fetchSearchResults } from './fetch_search_results';

const DEFAULT_FROM_VALUE = 0;
describe('fetchSearchResults lib function', () => {
const mockClient = {
asCurrentUser: {
search: jest.fn(),
},
search: jest.fn(),
};

const indexName = 'search-regular-index';
Expand Down Expand Up @@ -78,15 +76,13 @@ describe('fetchSearchResults lib function', () => {
});

it('should return search results with hits', async () => {
mockClient.asCurrentUser.search.mockImplementation(() =>
Promise.resolve(mockSearchResponseWithHits)
);
mockClient.search.mockImplementation(() => Promise.resolve(mockSearchResponseWithHits));

await expect(
fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName, query)
fetchSearchResults(mockClient as unknown as ElasticsearchClient, indexName, query)
).resolves.toEqual(regularSearchResultsResponse);

expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
expect(mockClient.search).toHaveBeenCalledWith({
from: DEFAULT_FROM_VALUE,
index: indexName,
q: query,
Expand All @@ -95,21 +91,19 @@ describe('fetchSearchResults lib function', () => {
});

it('should escape quotes in queries and return results with hits', async () => {
mockClient.asCurrentUser.search.mockImplementation(() =>
Promise.resolve(mockSearchResponseWithHits)
);
mockClient.search.mockImplementation(() => Promise.resolve(mockSearchResponseWithHits));

await expect(
fetchSearchResults(
mockClient as unknown as IScopedClusterClient,
mockClient as unknown as ElasticsearchClient,
indexName,
'"yellow banana"',
0,
DEFAULT_DOCS_PER_PAGE
)
).resolves.toEqual(regularSearchResultsResponse);

expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
expect(mockClient.search).toHaveBeenCalledWith({
from: DEFAULT_FROM_VALUE,
index: indexName,
q: '\\"yellow banana\\"',
Expand All @@ -118,23 +112,21 @@ describe('fetchSearchResults lib function', () => {
});

it('should return search results with hits when no query is passed', async () => {
mockClient.asCurrentUser.search.mockImplementation(() =>
Promise.resolve(mockSearchResponseWithHits)
);
mockClient.search.mockImplementation(() => Promise.resolve(mockSearchResponseWithHits));

await expect(
fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName)
fetchSearchResults(mockClient as unknown as ElasticsearchClient, indexName)
).resolves.toEqual(regularSearchResultsResponse);

expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
expect(mockClient.search).toHaveBeenCalledWith({
from: DEFAULT_FROM_VALUE,
index: indexName,
size: DEFAULT_DOCS_PER_PAGE,
});
});

it('should return empty search results', async () => {
mockClient.asCurrentUser.search.mockImplementationOnce(() =>
mockClient.search.mockImplementationOnce(() =>
Promise.resolve({
...mockSearchResponseWithHits,
hits: {
Expand All @@ -149,10 +141,10 @@ describe('fetchSearchResults lib function', () => {
);

await expect(
fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName, query)
fetchSearchResults(mockClient as unknown as ElasticsearchClient, indexName, query)
).resolves.toEqual(emptySearchResultsResponse);

expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
expect(mockClient.search).toHaveBeenCalledWith({
from: DEFAULT_FROM_VALUE,
index: indexName,
q: query,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
*/

import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
import { ElasticsearchClient } from '@kbn/core/server';
import { DEFAULT_DOCS_PER_PAGE, Paginate } from '../types';
import { escapeLuceneChars } from '../utils/escape_lucene_charts';
import { fetchWithPagination } from '../utils/fetch_with_pagination';

export const fetchSearchResults = async (
client: IScopedClusterClient,
client: ElasticsearchClient,
indexName: string,
query?: string,
from: number = 0,
size: number = DEFAULT_DOCS_PER_PAGE
): Promise<Paginate<SearchHit>> => {
const result = await fetchWithPagination(
async () =>
await client.asCurrentUser.search({
await client.search({
from,
index: indexName,
size,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ jest.mock('@kbn/search-index-documents/lib', () => ({

describe('Elasticsearch Search', () => {
let mockRouter: MockRouter;
const mockClient = {};

const mockClient = {
asCurrentUser: jest.fn(),
};
beforeEach(() => {
const context = {
core: Promise.resolve({ elasticsearch: { client: mockClient } }),
} as jest.Mocked<RequestHandlerContext>;
core: Promise.resolve({
elasticsearch: { client: mockClient },
}),
} as unknown as jest.Mocked<RequestHandlerContext>;

mockRouter = new MockRouter({
context,
Expand Down Expand Up @@ -90,7 +93,7 @@ describe('Elasticsearch Search', () => {
beforeEach(() => {
const context = {
core: Promise.resolve({ elasticsearch: { client: mockClient } }),
} as jest.Mocked<RequestHandlerContext>;
} as unknown as jest.Mocked<RequestHandlerContext>;

mockRouterNoQuery = new MockRouter({
context,
Expand Down Expand Up @@ -137,7 +140,7 @@ describe('Elasticsearch Search', () => {
});

expect(fetchSearchResults).toHaveBeenCalledWith(
mockClient,
mockClient.asCurrentUser,
'search-index-name',
'banana',
0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function registerSearchRoute({ router, log }: RouteDependencies) {
elasticsearchErrorHandler(log, async (context, request, response) => {
const indexName = decodeURIComponent(request.params.index_name);
const searchQuery = request.body.searchQuery;
const { client } = (await context.core).elasticsearch;
const client = (await context.core).elasticsearch.client.asCurrentUser;
const { page = 0, size = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } = request.query;
const from = page * size;
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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 React, { useEffect, useState } from 'react';

import {
DocumentList,
DocumentsOverview,
INDEX_DOCUMENTS_META_DEFAULT,
} from '@kbn/search-index-documents';

import { i18n } from '@kbn/i18n';
import { useIndexDocumentSearch } from '../../hooks/api/use_index_documents';
import { useIndexMappings } from '../../hooks/api/use_index_mappings';

const DEFAULT_PAGINATION = {
pageIndex: INDEX_DOCUMENTS_META_DEFAULT.pageIndex,
pageSize: INDEX_DOCUMENTS_META_DEFAULT.pageSize,
totalItemCount: INDEX_DOCUMENTS_META_DEFAULT.totalItemCount,
};

interface IndexDocumentsProps {
indexName: string;
}

export const IndexDocuments: React.FC<IndexDocumentsProps> = ({ indexName }) => {
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
const [searchQuery, setSearchQuery] = useState('');
const searchQueryCallback = (query: string) => {
setSearchQuery(query);
};
const { results: indexDocuments, meta: documentsMeta } = useIndexDocumentSearch(
indexName,
pagination,
searchQuery
);

const { data: mappingData } = useIndexMappings(indexName);

const docs = indexDocuments?.data ?? [];

useEffect(() => {
setSearchQuery('');
setPagination(DEFAULT_PAGINATION);
}, [indexName]);
return (
<DocumentsOverview
dataTelemetryIdPrefix="serverless-view-index-documents"
searchQueryCallback={searchQueryCallback}
documentComponent={
<>
{docs.length === 0 &&
i18n.translate('xpack.serverlessSearch.indexManagementTab.documents.noMappings', {
defaultMessage: 'No documents found for index',
})}
{docs?.length > 0 && (
<DocumentList
dataTelemetryIdPrefix="serverless-view-index-documents"
docs={docs}
docsPerPage={pagination.pageSize ?? 10}
isLoading={false}
mappings={mappingData?.mappings?.properties ?? {}}
meta={documentsMeta ?? DEFAULT_PAGINATION}
onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })}
setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })}
/>
)}
</>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { IndexDetailsTab } from '@kbn/index-management-plugin/common/constants';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { ServerlessSearchPluginStartDependencies } from '../../../types';
import { IndexDocuments } from './documents';

export const createIndexOverviewContent = (
core: CoreStart,
services: ServerlessSearchPluginStartDependencies
): IndexDetailsTab => {
return {
id: 'documents',
name: (
<FormattedMessage
defaultMessage="Documents"
id="xpack.serverlessSearch.indexManagementTab.documents"
/>
),
order: 11,
renderTabContent: ({ index }) => {
const queryClient = new QueryClient();
return (
<KibanaContextProvider services={{ ...core, ...services }}>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<IndexDocuments indexName={index.name} />
</QueryClientProvider>
</KibanaContextProvider>
);
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 { Pagination } from '@elastic/eui';
import { SearchHit } from '@kbn/es-types';
import { pageToPagination, Paginate } from '@kbn/search-index-documents';
import { useQuery } from '@tanstack/react-query';
import { useKibanaServices } from '../use_kibana';

interface IndexDocuments {
meta: Pagination;
results: Paginate<SearchHit>;
}
const DEFAULT_PAGINATION = {
from: 0,
has_more_hits_than_total: false,
size: 10,
total: 0,
};
export const useIndexDocumentSearch = (
indexName: string,
pagination: Omit<Pagination, 'totalItemCount'>,
searchQuery?: string
) => {
const { http } = useKibanaServices();
const response = useQuery({
queryKey: ['fetchIndexDocuments', pagination, searchQuery],
queryFn: async () =>
http.post<IndexDocuments>(`/internal/serverless_search/indices/${indexName}/search`, {
body: JSON.stringify({
searchQuery,
}),
query: {
page: pagination.pageIndex,
size: pagination.pageSize,
},
}),
});
return {
results: response?.data?.results,
meta: pageToPagination(response?.data?.results?._meta?.page ?? DEFAULT_PAGINATION),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types';
import { useQuery } from '@tanstack/react-query';
import { useKibanaServices } from '../use_kibana';

export const useIndexMappings = (indexName: string) => {
const { http } = useKibanaServices();
return useQuery({
queryKey: ['fetchIndexMappings'],
queryFn: async () =>
http.fetch<IndicesGetMappingIndexMappingRecord>(
`/internal/serverless_search/mappings/${indexName}`
),
});
};
Loading

0 comments on commit 543a047

Please sign in to comment.