Merge pull request #12172 from backstage/emmai/search-error-handling
[Search] improve search error handling when missing index
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Exports `MissingIndexError` that can be used by the search engines for better error handling when missing index.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
---
|
||||
|
||||
If error is `MissingIndexError` we return a 400 response with a more clear error message.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': patch
|
||||
---
|
||||
|
||||
Throws `MissingIndexError` when no index of type exist.
|
||||
+23
@@ -793,6 +793,29 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
|
||||
elasticSearchQuerySpy.mockClear();
|
||||
});
|
||||
|
||||
it('should throws missing index error', async () => {
|
||||
jest.spyOn(clientWrapper, 'search').mockRejectedValue({
|
||||
meta: {
|
||||
body: {
|
||||
error: {
|
||||
type: 'index_not_found_exception',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
async () =>
|
||||
await testSearchEngine.query({
|
||||
term: 'testTerm',
|
||||
types: ['unknown'],
|
||||
filters: {},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Missing index for unknown__search. This means there are no documents to search through.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('indexer', () => {
|
||||
|
||||
+9
-2
@@ -23,6 +23,7 @@ import {
|
||||
SearchEngine,
|
||||
SearchQuery,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { MissingIndexError } from '@backstage/plugin-search-backend-node';
|
||||
import esb from 'elastic-builder';
|
||||
import { isEmpty, isNaN as nan, isNumber } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -329,10 +330,16 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
nextPageCursor,
|
||||
previousPageCursor,
|
||||
};
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
if (error.meta?.body?.error?.type === 'index_not_found_exception') {
|
||||
throw new MissingIndexError(
|
||||
`Missing index for ${queryIndices}. This means there are no documents to search through.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
this.logger.error(
|
||||
`Failed to query documents for indices ${queryIndices}`,
|
||||
e,
|
||||
error,
|
||||
);
|
||||
return Promise.reject({ results: [] });
|
||||
}
|
||||
|
||||
@@ -113,6 +113,12 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
initialize(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class MissingIndexError extends Error {
|
||||
constructor(message?: string, cause?: Error | unknown);
|
||||
readonly cause?: Error | undefined;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class NewlineDelimitedJsonCollatorFactory
|
||||
implements DocumentCollatorFactory
|
||||
|
||||
@@ -949,6 +949,19 @@ describe('LunrSearchEngine', () => {
|
||||
previousPageCursor: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throws missing index error', async () => {
|
||||
await expect(
|
||||
async () =>
|
||||
await testLunrSearchEngine.query({
|
||||
term: 'testTerm',
|
||||
types: ['unknown'],
|
||||
filters: {},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Missing index for unknown. This could be because the index hasn't been created yet or there was a problem during index creation.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return previous page cursor if on another page', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
QueryTranslator,
|
||||
SearchEngine,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { MissingIndexError } from '../errors';
|
||||
import lunr from 'lunr';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
@@ -163,30 +164,38 @@ export class LunrSearchEngine implements SearchEngine {
|
||||
|
||||
const results: LunrResultEnvelope[] = [];
|
||||
|
||||
const indexKeys = Object.keys(this.lunrIndices).filter(
|
||||
type => !documentTypes || documentTypes.includes(type),
|
||||
);
|
||||
|
||||
if (documentTypes?.length && !indexKeys.length) {
|
||||
throw new MissingIndexError(
|
||||
`Missing index for ${documentTypes?.toString()}. This could be because the index hasn't been created yet or there was a problem during index creation.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Iterate over the filtered list of this.lunrIndex keys.
|
||||
Object.keys(this.lunrIndices)
|
||||
.filter(type => !documentTypes || documentTypes.includes(type))
|
||||
.forEach(type => {
|
||||
try {
|
||||
results.push(
|
||||
...this.lunrIndices[type].query(lunrQueryBuilder).map(result => {
|
||||
return {
|
||||
result: result,
|
||||
type: type,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
// if a field does not exist on a index, we can see that as a no-match
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.message.startsWith('unrecognised field')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
indexKeys.forEach(type => {
|
||||
try {
|
||||
results.push(
|
||||
...this.lunrIndices[type].query(lunrQueryBuilder).map(result => {
|
||||
return {
|
||||
result: result,
|
||||
type: type,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
// if a field does not exist on a index, we can see that as a no-match
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.message.startsWith('unrecognised field')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort results.
|
||||
results.sort((doc1, doc2) => {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* Failed to query documents for index that does not exist.
|
||||
* @public
|
||||
*/
|
||||
export class MissingIndexError extends Error {
|
||||
/**
|
||||
* An inner error that caused this error to be thrown, if any.
|
||||
*/
|
||||
readonly cause?: Error | undefined;
|
||||
|
||||
constructor(message?: string, cause?: Error | unknown) {
|
||||
super(message);
|
||||
|
||||
Error.captureStackTrace?.(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.cause = isError(cause) ? cause : undefined;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export type {
|
||||
RegisterCollatorParameters,
|
||||
RegisterDecoratorParameters,
|
||||
} from './types';
|
||||
export * from './errors';
|
||||
export * from './indexing';
|
||||
export * from './test-utils';
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { z } from 'zod';
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ErrorResponseBody, InputError } from '@backstage/errors';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
@@ -129,7 +129,10 @@ export async function createRouter(
|
||||
const router = Router();
|
||||
router.get(
|
||||
'/query',
|
||||
async (req: express.Request, res: express.Response<SearchResultSet>) => {
|
||||
async (
|
||||
req: express.Request,
|
||||
res: express.Response<SearchResultSet | ErrorResponseBody>,
|
||||
) => {
|
||||
const parseResult = requestSchema.safeParse(req.query);
|
||||
|
||||
if (!parseResult.success) {
|
||||
@@ -154,9 +157,14 @@ export async function createRouter(
|
||||
const resultSet = await engine?.query(query, { token });
|
||||
|
||||
res.send(filterResultSet(toSearchResults(resultSet)));
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (error.name === 'MissingIndexError') {
|
||||
// re-throw and let the default error handler middleware captures it and serializes it with the right response code on the standard form
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`There was a problem performing the search query. ${err}`,
|
||||
`There was a problem performing the search query. ${error}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user