[Enterprise Search] Adds an endpoint to query documents from an index (#135345)

* WIP: Add internal enterprise search route to fetch a index mapping

This is still WIP because I need to learn how to properly mock things in 
Kibana.

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Update return value for API based on the designs.

* Add request handler context to router mock constructor

* WIP: simple index query for docs.

* Fix and add specs

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Brian McGue 2022-06-30 10:50:25 -07:00 committed by GitHub
parent c3327e4fad
commit b06eee9355
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 268 additions and 0 deletions

View file

@ -0,0 +1,104 @@
/*
* 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 { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
import { fetchSearchResults } from './fetch_search_results';
describe('fetchSearchResults lib function', () => {
const mockClient = {
asCurrentUser: {
search: jest.fn(),
},
};
const indexName = 'search-regular-index';
const query = 'banana';
const regularSearchResultsResponse = {
took: 4,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: null,
hits: [
{
_index: 'search-regular-index',
_id: '5a12292a0f5ae10021650d7e',
_score: 4.437291,
_source: {
name: 'banana',
id: '5a12292a0f5ae10021650d7e',
},
},
],
},
};
const emptySearchResultsResponse = {
took: 4,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 0,
relation: 'eq',
},
max_score: null,
hits: [],
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should return search results with hits', async () => {
mockClient.asCurrentUser.search.mockImplementation(
() => regularSearchResultsResponse as SearchResponseBody
);
await expect(
fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName, query)
).resolves.toEqual(regularSearchResultsResponse);
expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
index: indexName,
q: query,
});
});
it('should return empty search results', async () => {
mockClient.asCurrentUser.search.mockImplementationOnce(
() => emptySearchResultsResponse as SearchResponseBody
);
await expect(
fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName, query)
).resolves.toEqual(emptySearchResultsResponse);
expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
index: indexName,
q: query,
});
});
});

View file

@ -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 { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
export const fetchSearchResults = async (
client: IScopedClusterClient,
indexName: string,
query: string
): Promise<SearchResponseBody> => {
const results = await client.asCurrentUser.search({
index: indexName,
q: query,
});
return results;
};

View file

@ -9,8 +9,10 @@ import { RouteDependencies } from '../../plugin';
import { registerIndexRoutes } from './indices';
import { registerMappingRoute } from './mapping';
import { registerSearchRoute } from './search';
export const registerEnterpriseSearchRoutes = (dependencies: RouteDependencies) => {
registerIndexRoutes(dependencies);
registerMappingRoute(dependencies);
registerSearchRoute(dependencies);
};

View file

@ -0,0 +1,97 @@
/*
* 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 { MockRouter, mockDependencies } from '../../__mocks__';
import { RequestHandlerContext } from '@kbn/core/server';
jest.mock('../../lib/fetch_search_results', () => ({
fetchSearchResults: jest.fn(),
}));
import { fetchSearchResults } from '../../lib/fetch_search_results';
import { registerSearchRoute } from './search';
describe('Elasticsearch Index Mapping', () => {
let mockRouter: MockRouter;
const mockClient = {};
beforeEach(() => {
const context = {
core: Promise.resolve({ elasticsearch: { client: mockClient } }),
} as jest.Mocked<RequestHandlerContext>;
mockRouter = new MockRouter({
context,
method: 'get',
path: '/internal/enterprise_search/{index_name}/search/{query}',
});
registerSearchRoute({
...mockDependencies,
router: mockRouter.router,
});
});
describe('GET /internal/enterprise_search/{index_name}/search/{query}', () => {
it('fails validation without index_name', () => {
const request = { params: { query: 'banana' } };
mockRouter.shouldThrow(request);
});
it('fails validation without query', () => {
const request = { params: { index_name: 'search-banana' } };
mockRouter.shouldThrow(request);
});
it('returns search results for a query', async () => {
const mockData = {
took: 4,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: null,
hits: [
{
_index: 'search-regular-index',
_id: '5a12292a0f5ae10021650d7e',
_score: 4.437291,
_source: {
name: 'banana',
id: '5a12292a0f5ae10021650d7e',
},
},
],
},
};
(fetchSearchResults as jest.Mock).mockImplementationOnce(() => {
return Promise.resolve(mockData);
});
await mockRouter.callRoute({
params: { index_name: 'search-index-name', query: 'banana' },
});
expect(fetchSearchResults).toHaveBeenCalledWith(mockClient, 'search-index-name', 'banana');
expect(mockRouter.response.ok).toHaveBeenCalledWith({
body: mockData,
headers: { 'content-type': 'application/json' },
});
});
});
});

View file

@ -0,0 +1,44 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { fetchSearchResults } from '../../lib/fetch_search_results';
import { RouteDependencies } from '../../plugin';
export function registerSearchRoute({ router }: RouteDependencies) {
router.get(
{
path: '/internal/enterprise_search/{index_name}/search/{query}',
validate: {
params: schema.object({
index_name: schema.string(),
query: schema.string(),
}),
},
},
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
try {
const searchResults = await fetchSearchResults(
client,
request.params.index_name,
request.params.query
);
return response.ok({
body: searchResults,
headers: { 'content-type': 'application/json' },
});
} catch (error) {
return response.customError({
statusCode: 502,
body: 'Error fetching data from Enterprise Search',
});
}
}
);
}