content/frontend/services/elastic_search_api.js (186 lines of code) (raw):
/* global ELASTIC_KEY */
/* global ELASTIC_INDEX */
/* global ELASTIC_CLOUD_ID */
import ElasticsearchAPIConnector from '@elastic/search-ui-elasticsearch-connector';
import { SearchDriver } from '@elastic/search-ui';
import { cleanTitle } from '../search/search_helpers';
// Boost ranking for specific pages for specific terms
const BOOSTED_PAGES = [
{
path: '/ee/user/duo_amazon_q/',
queries: ['q', 'amazon q'],
},
{
path: '/ee/user/gitlab_duo_chat/examples.html',
queries: ['duo slash commands'],
},
];
// Initialize connector to Elastic Search instance.
const connector = new ElasticsearchAPIConnector(
{
cloud: {
id: ELASTIC_CLOUD_ID,
},
index: ELASTIC_INDEX,
apiKey: ELASTIC_KEY,
},
(requestBody, requestState, queryConfig) => {
if (!requestState.searchTerm) return requestBody;
const searchTerm = requestState.searchTerm.toLowerCase();
// Check if current search term matches any of our boosted pages
const boostMatch = BOOSTED_PAGES.find((page) =>
page.queries.some((query) => query.toLowerCase() === searchTerm),
);
const searchFields = queryConfig.search_fields;
const updatedRequestBody = {
...requestBody,
query: {
function_score: {
query: {
bool: {
should: [],
must: [
{
multi_match: {
query: requestState.searchTerm,
fields: Object.keys(searchFields).map((fieldName) => {
const weight = searchFields[fieldName].weight || 1;
return `${fieldName}^${weight}`;
}),
},
},
],
},
},
functions: [
{
filter: {
terms: { gitlab_docs_section: ['contribute', 'none'] },
},
weight: 0.5,
},
],
score_mode: 'multiply',
},
},
highlight: {
type: 'plain',
encoder: 'html',
boundary_scanner: 'sentence',
fragment_size: 200,
no_match_size: 300,
number_of_fragments: 1,
max_analyzed_offset: 999999,
pre_tags: '<strong>',
post_tags: '</strong>',
fields: {
body_content: {},
title: {},
},
},
};
// Add filters.
if (queryConfig?.filters?.length) {
updatedRequestBody.query.function_score.query.bool.should.push({
bool: {
should: [],
},
});
updatedRequestBody.query.function_score.query.bool.minimum_should_match = 1;
const [filter] = queryConfig?.filters || [];
const { field, values } = filter || {};
if (field && values) {
updatedRequestBody.query.function_score.query.bool.should[0].bool.should.push(
...values.map((val) => ({ term: { [field]: val } })),
);
}
}
// If we have a boost match, add a strong boost for the specific page
if (boostMatch) {
updatedRequestBody.query.function_score.functions.push({
filter: {
term: {
url_path: boostMatch.path,
},
},
weight: 1000,
});
}
return updatedRequestBody;
},
);
/**
* Fetch search results from Elastic
*
* @param {String} query
* @param {Array} filters
* @param {Object} pageInfo
* Contains pageNumber(int) and numResults(int)
*
* @returns Array
*/
export const fetchResults = (query, filters, pageInfo) => {
return new Promise((resolve) => {
if (!query || typeof query !== 'string') {
resolve([]);
return;
}
const searchQuery = {
result_fields: {
title: {
raw: {},
snippet: {
size: 100,
fallback: true,
},
},
url_path: { raw: {} },
gitlab_docs_breadcrumbs: { raw: {} },
gitlab_docs_section: { raw: {} },
body_content: {
raw: {},
snippet: {
size: 100,
fallback: true,
},
},
},
search_fields: {
title: { weight: 3 },
headings: { weight: 2 },
body_content: { weight: 1 },
},
filters: [],
};
// Override search query filters.
if (filters.length > 0) {
searchQuery.filters = [
{
field: 'gitlab_docs_section',
values: filters.flatMap((filter) => filter.split(',')),
type: 'any',
},
];
}
const { pageNumber, numResults } = pageInfo || {};
const driver = new SearchDriver({
apiConnector: connector,
searchQuery,
initialState: {
searchTerm: query,
resultsPerPage: numResults,
current: pageNumber,
},
trackUrlState: false,
});
let isInitialUpdate = true;
// Set up a listener for results.
driver.subscribeToStateChanges((state) => {
if (isInitialUpdate) {
isInitialUpdate = false;
return;
}
if (state.isLoading === false && state.wasSearched === true) {
const results = state.results.map((result) => ({
id: result.id.raw,
title: result.title.snippet
? cleanTitle(result.title.snippet[0])
: cleanTitle(result.title.raw),
breadcrumbs: result.gitlab_docs_breadcrumbs.raw,
url_path: result.url_path.raw,
htmlSnippet: result.body_content.snippet[0],
}));
const resultsData = {
results,
pagingStart: state.pagingStart,
pagingEnd: state.pagingEnd,
totalResults: state.totalResults,
};
resolve(resultsData);
driver.unsubscribeToStateChanges();
}
});
// Trigger the search.
driver.setSearchTerm(query);
// Set current page to update results.
driver.setCurrent(pageNumber);
});
};