import { navigate } from '@reach/router'; const relatedToRegex = /(?:^|\s)(\+?related_to:[0-9]+)(?:$|\s)/; const select = `all( all(group(source) order(-count()) each(output(count()))) all(group(journal) max(10) order(-count()) each(output(count()))) all(group(authors.name) max(10) order(-count()) each(output(count())) as(author)) all(group(time.year(timestamp)) max(10) order(-max(time.year(timestamp))) each(output(count())) as(year)) all(group(has_full_text) each(output(count()))) )` .split('\n') .map(s => s.trim()) .join(''); // Combines an array of possible values for a given field into an OR expression that is ANDed with other filters const orCombiner = (field, array, range = false) => array.length ? '+(' + array .map(s => (range ? `${field}:[${s}]` : `${field}:"${s}"`)) .join(' ') + ')' : null; const timestampStartOfYearUtc = year => Date.UTC(year, 0, 1) / 1000; const generateApiQueryParams = () => { const { journal, source, year, author, has_full_text } = getSearchState(); const timestampRanges = year .map(y => parseInt(y)) .map( y => timestampStartOfYearUtc(y) + ';' + (timestampStartOfYearUtc(y + 1) - 1) ); const filter = [ orCombiner('journal', journal), orCombiner('source', source), orCombiner('timestamp', timestampRanges, true), orCombiner('authors.name', author), orCombiner('has_full_text', has_full_text), ] .filter(s => s) .join(' '); const query = new URLSearchParams(window.location.search); const ranking = query.get('ranking'); const fieldset = query.get('fieldset'); // Remove query parameters used in the UI, these are either sent to backend under a different name // or as part of an expression (filters) [ 'journal', 'source', 'year', 'author', 'has_full_text', 'ranking', 'fieldset', ].forEach(q => query.delete(q)); if (filter) query.set('filter', filter); if (ranking) query.set('ranking.profile', ranking); else query.set('ranking.profile','bm25t5'); if (fieldset) query.set('model.defaultIndex', fieldset); query.set('select', select); return query; }; const onSearch = params => { const urlParams = new URLSearchParams(window.location.search); for (let [key, value] of Object.entries(params)) { urlParams.delete(key); if (Array.isArray(value)) value.forEach(v => urlParams.append(key, v)); else if (value) urlParams.set(key, value); } // Offset must be reset whenever result set changes, which we assume may be // every time the URL changes due to other interactions than with pagination. if (!params.hasOwnProperty('offset')) urlParams.delete('offset'); // No query or filters specified if (urlParams.entries().next().done) return; navigate('/search?' + urlParams); }; const getRelatedId = urlParams => { const query = urlParams.get('query'); if (!query) return null; const match = query.match(relatedToRegex); if (!match) return null; return match[1].split(':')[1]; }; const getSearchState = () => { const urlParams = new URLSearchParams(window.location.search); return { query: urlParams.get('query') || '', journal: urlParams.getAll('journal'), source: urlParams.getAll('source'), year: urlParams.getAll('year'), author: urlParams.getAll('author'), has_full_text: urlParams.getAll('has_full_text'), use_specter: urlParams.getAll('use_specter'), ranking: urlParams.get('ranking'), fieldset: urlParams.get('fieldset'), relatedId: getRelatedId(urlParams), }; }; export { generateApiQueryParams, getSearchState, onSearch, relatedToRegex };