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); }); };