Initial commit

This commit is contained in:
Luka Romih
2022-12-07 04:43:36 +01:00
commit 257f3c354f
496 changed files with 55373 additions and 0 deletions
@@ -0,0 +1,282 @@
const fs = require('fs')
const xmlFlow = require('xml-flow')
const xss = require('xss')
const debug = require('debug')('termPortal:models/helpers/dictionary')
const db = require('../../db')
const { intoDbArray } = require('..')
const JOBS_MAX = 50
const JOBS_MIN = 15
// Import given file into given dictionary.
exports.readFileIntoDb = (
userId,
dictionaryId,
importFilePath,
entryStatus,
dbClient
) => {
return new Promise((resolve, reject) => {
const progress = {
jobCount: 0,
totalCount: 0,
validCount: 0,
isWholeFileRead: false,
importError: null
}
entryStatus = entryStatus === 'complete' ? 'complete' : 'in_edit'
const importFileReader = fs.createReadStream(importFilePath)
const xmlStream = xmlFlow(importFileReader, {
strict: true,
trim: false,
preserveMarkup: xmlFlow.ALWAYS,
simplifyNodes: false,
useArrays: xmlFlow.ALWAYS
})
const handleError = handleImportError(xmlStream, progress, reject)
const handleEntry = handleEntryXml(
userId,
dictionaryId,
entryStatus,
progress,
dbClient,
resolve,
handleError
)
xmlStream
.on('error', () => {})
.once('error', handleError)
.on('end', () => (progress.isWholeFileRead = true))
.on('tag:entry', handleEntry)
})
}
function handleImportError(xmlStream, progress, reject) {
return function handleError(error) {
if (progress.importError) return
progress.importError = error
xmlStream.removeAllListeners('tag:entry')
reject(error)
}
}
function handleEntryXml(
userId,
dictionaryId,
entryStatus,
progress,
dbClient,
resolve,
handleError
) {
return async function handleEntry(entry) {
if (progress.importError) return
try {
if (++progress.jobCount > JOBS_MAX) this.pause()
const term = toMixedBasic(
entry.$markup.find(el => el.$name === 'term')?.$markup
)
const hwGrp = entry.$markup.find(el => el.$name === 'hwGrp')
const wordforms = hwGrp?.$attrs.wfs
const accent = hwGrp?.$attrs.acc
const pronunciation = hwGrp?.$attrs.pron
const domainLabels = entry.$markup
.find(el => el.$name === 'domainLabels')
?.$markup.filter(childEl => childEl.$name === 'domainLabel')
.map(domainLabel => toText(domainLabel.$markup))
const label = toMixedExtended(
entry.$markup.find(el => el.$name === 'label')?.$markup
)
const definition = toMixedExtended(
entry.$markup.find(el => el.$name === 'def')?.$markup
)
const synonyms = entry.$markup
.find(el => el.$name === 'syns')
?.$markup.filter(childEl => childEl.$name === 'syn')
.map(synonym => toMixedBasic(synonym.$markup))
const links = entry.$markup
.find(el => el.$name === 'links')
?.$markup.filter(childEl => childEl.$name === 'link')
.map(link => ({
link: toMixedBasic(link.$markup),
type: link.$attrs.type
}))
const other = toMixedOther(
entry.$markup.find(el => el.$name === 'other')?.$markup
)
let hasForeignTerms = false
const foreignLanguageContent = entry.$markup
.find(el => el.$name === 'fLangs')
?.$markup.filter(childEl => childEl.$name === 'fLang')
.map(fLang => ({
language: fLang.$attrs.lang,
terms: fLang.$markup
.find(el => el.$name === 'fTerms')
?.$markup.filter(childEl => childEl.$name === 'fTerm')
.map(term => {
const content = toMixedBasic(term.$markup)
if (content.length) hasForeignTerms = true
return content
}),
definition:
toMixedExtended(
fLang.$markup.find(el => el.$name === 'fDef')?.$markup
) || null,
synonyms: fLang.$markup
.find(el => el.$name === 'fSyns')
?.$markup.filter(childEl => childEl.$name === 'fSyn')
.map(synonym => toMixedBasic(synonym.$markup))
}))
const multimedia = entry.$markup.find(el => el.$name === 'mm')?.$markup
const images = []
const audio = []
const videos = []
multimedia?.forEach(mm => {
switch (mm.$name) {
case 'image':
images.push(toText(mm.$markup))
break
case 'audio':
audio.push(toText(mm.$markup))
break
case 'video':
videos.push(toText(mm.$markup))
}
})
const isValid = !!term && (!!definition || hasForeignTerms)
if (!entry.$markup.length) return
const values = [
dictionaryId,
isValid,
entryStatus,
term || null,
userId,
null,
wordforms,
accent,
pronunciation,
intoDbArray(domainLabels, 'always'),
label || null,
definition || null,
intoDbArray(synonyms),
intoDbArray(links, 'always'),
other || null,
intoDbArray(foreignLanguageContent, 'always'),
images.length ? images : null,
audio.length ? audio : null,
videos.length ? videos : null
]
const text = `SELECT entry_new (${db.genParamStr(values)})`
await dbClient.query(text, values)
progress.totalCount++
if (isValid) progress.validCount++
} catch (error) {
handleError(error)
} finally {
progress.jobCount--
if (progress.jobCount < JOBS_MIN) this.resume()
if (
!progress.jobCount &&
progress.isWholeFileRead &&
!progress.importError
) {
debug(`TOTAL ENTRIES: ${progress.totalCount}`)
debug(`VALID ENTRIES: ${progress.validCount}`)
resolve()
}
}
}
}
const markupFilter = {
noMixed: new xss.FilterXSS({
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}),
mixedBasic: new xss.FilterXSS({
whiteList: {
sup: [],
sub: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}),
mixedExtended: new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: [],
a: ['href']
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
onTag: customTagHandler
}),
mixedOther: new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: [],
a: ['href'],
br: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
onTag: customTagHandler
})
}
function customTagHandler(tag, html, { isWhite, isClosing }) {
// Special treatment only for whitelisted opening anchor tags.
if (tag !== 'a' || !isWhite || isClosing) return
const matchUrl = html.match(/href="?(?<url>https?:\/\/.*?)"?[\s>]/)
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
return `<a href${url ? `="${url}" target="_blank"` : ''}>`
}
function toText(markupObj) {
return markupFilter.noMixed
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
function toMixedBasic(markupObj) {
return markupFilter.mixedBasic
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
function toMixedExtended(markupObj) {
return markupFilter.mixedExtended
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
function toMixedOther(markupObj) {
return markupFilter.mixedOther
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
}
+196
View File
@@ -0,0 +1,196 @@
const { removeHtmlTags } = require('../../helpers')
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
exports.deserialize = {
primaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
nameSl: domain.name_sl,
nameEn: domain.name_en
}
return deserializedDomain
},
secondaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
isApproved: domain.approved,
nameSl: domain.name_sl,
nameEn: domain.name_en
}
return deserializedDomain
},
approvedSecondaryDomain(domain) {
const deserializedDomain = {
id: domain.id,
nameSl: domain.name_sl,
nameEn: domain.name_en
}
return deserializedDomain
},
language(language) {
const deserializedLanguage = {
id: language.id,
code: language.code,
nameSl: language.name_sl,
nameEn: language.name_en
}
return deserializedLanguage
},
editDescription(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
nameEn: dictionary.name_en,
nameSlShort: dictionary.name_sl_short,
author: dictionary.author,
domainPrimary: dictionary.domain_primary_id,
description: dictionary.description,
issn: dictionary.issn
}
return deserializedDictionary
},
editUsers(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
terminologyReviewFlag: dictionary.entries_have_terminology_review_flag,
languageReviewFlag: dictionary.entries_have_language_review_flag,
status: dictionary.status
}
return deserializedDictionary
},
editStructure(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
hasDomainLabels: dictionary.entries_have_domain_labels,
hasLabel: dictionary.entries_have_label,
hasDefinition: dictionary.entries_have_definition,
hasSynonyms: dictionary.entries_have_synonyms,
hasLinks: dictionary.entries_have_links,
hasOther: dictionary.entries_have_other,
hasForeignLanguages: dictionary.entries_have_foreign_languages,
hasForeignDefinitions: dictionary.entries_have_foreign_definitions,
hasForeignSynonyms: dictionary.entries_have_foreign_synonyms,
hasImages: dictionary.entries_have_images,
hasAudio: dictionary.entries_have_audio,
hasVideo: dictionary.entries_have_videos
}
return deserializedDictionary
},
editDomainLabels(domainLabel) {
const deserializedDomainLabel = {
id: domainLabel.id,
name: domainLabel.name,
isVisible: domainLabel.is_visible
}
return deserializedDomainLabel
},
imports(oneImport) {
const deserializedImports = {
timeStarted: oneImport.time_started,
status: oneImport.status,
deleteExisting: oneImport.delete_existing_entries,
fileFormat: oneImport.file_format,
countValidEntries: oneImport.count_valid_entries
}
return deserializedImports
}
}
exports.bulkIndex = async (entries, primaryDomain, dictionary, source) => {
const entriesCount = entries.length
if (!entries.length) return
// Construct request body.
const bulkBody = new Array(entriesCount * 2)
for (let i = 0; i < entriesCount; i++) {
let entry = prepareEntryForIndexing(entries[i])
bulkBody[i * 2] = { index: { _index: ENTRY_INDEX, _id: entry.id } }
entry.primaryDomain = primaryDomain
entry.dictionary = dictionary
entry.source = source
entry = removeHtmlTags(JSON.stringify(entry))
bulkBody[i * 2 + 1] = entry
}
// Send the request.
const bulkResponse = await searchEngineClient.bulk({ body: bulkBody })
// Log possible errors.
if (bulkResponse.errors) {
const erroredDocuments = []
bulkResponse.items.forEach((action, i) => {
const operation = Object.keys(action)[0]
if (action[operation].error) {
erroredDocuments.push({
// If the status is 429 it means that you can retry the document,
// otherwise it's very likely a mapping error, and you should
// fix the document before to try it again.
status: action[operation].status,
error: action[operation].error,
operation: action[operation].body[i * 2],
document: action[operation].body[i * 2 + 1]
})
}
})
console.log('Errors indexing documents:') // eslint-disable-line no-console
console.log(erroredDocuments) // eslint-disable-line no-console
}
}
function prepareEntryForIndexing(entry) {
// Snake case property names into camel case.
entry.isValid = entry.is_valid
delete entry.is_valid
entry.isPublished = entry.is_published
delete entry.is_published
entry.isTerminologyReviewed = entry.is_terminology_reviewed
delete entry.is_terminology_reviewed
entry.isLanguageReviewed = entry.is_language_reviewed
delete entry.is_language_reviewed
entry.homonymSort = entry.homonym_sort
delete entry.homonym_sort
entry.timeMostRecentComment = entry.time_most_recent_comment
delete entry.time_most_recent_comment
entry.domainLabels = entry.domain_labels
delete entry.domain_labels
entry.foreignEntries = entry.foreign_entries
delete entry.foreign_entries
// Remove (top-level) properies with null or empty array values.
entry = Object.fromEntries(
Object.entries(entry).filter(
([_, v]) => v !== null && (!Array.isArray(v) || v.length)
)
)
return entry
}
exports.prepareEntryForIndexing = prepareEntryForIndexing
+62
View File
@@ -0,0 +1,62 @@
const { readdir, stat } = require('fs/promises')
const { partial } = require('filesize')
const { DATA_FILES_PATH } = require('../../config/settings')
const formatFileSize = partial({ separator: ',' })
exports.deserialize = {
extraction(extraction) {
const deserializedExtraction = {
id: extraction.id,
name: extraction.name,
status: extraction.status,
corpusId: extraction.corpus_id,
timeStarted: extraction.time_started,
timeFinished: extraction.time_finished,
ossParams: extraction.oss_params
}
return deserializedExtraction
}
}
exports.getExtractionFilesPath = getExtractionFilesPath
exports.getDocumentsPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/documents`
}
exports.getStopTermsPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/stop_terms`
}
exports.getConllusPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/conllu`
}
exports.getTermCandidatesPath = extractionId => {
return `${getExtractionFilesPath(extractionId)}/term_candidates.json`
}
exports.getFileNamesInFolder = async folderPath => {
const filenames = await readdir(folderPath)
return filenames
}
exports.getFileStatsInFolder = async folderPath => {
const filenames = await readdir(folderPath)
const fileStats = await Promise.all(
filenames.map(async filename => {
const filePath = `${folderPath}/${filename}`
const { mtimeMs: timeModified, size } = await stat(filePath)
const sizeHumanReadable = formatFileSize(size)
const fileStats = { filename, size: sizeHumanReadable, timeModified }
return fileStats
})
)
return fileStats
}
function getExtractionFilesPath(extractionId) {
return `${DATA_FILES_PATH}/extraction/${extractionId}`
}
+73
View File
@@ -0,0 +1,73 @@
const xss = require('xss')
const cache = require('../cache')
const Portal = require('../portal')
const INSTANCE_SETTING_NAMESPACE = 'instance-setting'
const INSTANCE_SETTING_CACHE_EXPIRE_TIME = 60 * 60 // 1 hour.
const SLOVENIAN_LANGUAGE_ID_CACHE_EXPIRE_TIME = 60 * 60 // 1 hour.
exports.intoDbArray = (inputData, mode) => {
if (Array.isArray(inputData)) {
return inputData
} else if (inputData === undefined || inputData === '') {
if (mode === 'always') return []
if (mode === 'undefined') return undefined
else return null
// return mode === 'always' ? [] : null
}
return [inputData]
}
exports.removeHtmlTags = text => {
return tagFilter.process(text)
}
// Returns setting value by its name from table instance_settings.
exports.getInstanceSetting = async settingName => {
const settingCacheName = `${INSTANCE_SETTING_NAMESPACE}:${settingName}`
let settingValue = await cache.get(settingCacheName)
if (!settingValue) {
settingValue = await Portal.getInstanceSettingValue(settingName)
await cache.set(
settingCacheName,
settingValue,
'EX',
INSTANCE_SETTING_CACHE_EXPIRE_TIME
)
}
return settingValue
}
// Deletes cached instance settings.
exports.clearCachedInstanceSettings = async () => {
const settingNames = await Portal.fetchAllInstanceSettingNames()
const settingCacheNames = settingNames.map(
settingName => `${INSTANCE_SETTING_NAMESPACE}:${settingName}`
)
await cache.unlink(...settingCacheNames)
}
// Returns slovenian language id and keeps it cached.
exports.getSlovenianLanguageId = async () => {
const settingCacheName = 'slovenian-language-id'
let id = await cache.get(settingCacheName)
if (!id) {
id = (await Portal.getSlovenianLanguageId()).toString()
await cache.set(
settingCacheName,
id,
'EX',
SLOVENIAN_LANGUAGE_ID_CACHE_EXPIRE_TIME
)
}
return id
}
const tagFilter = new xss.FilterXSS({
whiteList: {},
stripIgnoreTag: true
})
+67
View File
@@ -0,0 +1,67 @@
exports.aggregateSettings = settings => {
const deserializedSettings = settings.reduce((agg, setting) => {
agg[setting.name] = setting.value
return agg
}, {})
return deserializedSettings
}
exports.deserialize = {
settings(settings) {
const deserializedSettings = {
name: settings.portal_name,
description: settings.portal_description,
code: settings.portal_code,
isExtractionEnabled: settings.is_extraction_enabled,
isDictionariesEnabled: settings.is_dictionaries_enabled,
isConsultancyEnabled: settings.is_consultancy_enabled
}
return deserializedSettings
},
dictSettings(settings) {
const deserializedSettings = {
minEntriesPerDictionary: settings.min_entries_per_dictionary,
dictionaryPublishApproval: settings.dictionary_publish_approval,
// keepNumOfExportsPerDict: settings.keep_num_of_exports_per_dictionary,
// dictionaryAutoSaveFrequency: settings.dictionary_auto_save_frequency,
numOfHistoryEntriesPerEntry: settings.num_of_history_entires_per_entry,
canPublishEntriesInEdit: settings.can_publish_entries_in_edit
}
return deserializedSettings
},
consultSettings(settings) {
const deserializedSettings = {
consultancyType: settings.consultancy_type,
zrcEmail: settings.zrc_email,
zrcURL: settings.zrc_url
}
return deserializedSettings
},
connections(connection) {
const deserializedConnections = {
id: connection.id,
name: connection.name,
indexURL: connection.url_index,
code: connection.code,
isEnabled: connection.is_enabled,
synced: connection.time_last_synced,
isLinked: connection.is_linked,
URLupdate: connection.url_update
}
return deserializedConnections
},
dictionaries(dictionary) {
const deserializeDictionaries = {
id: dictionary.id,
name: dictionary.name,
code: dictionary.code,
isEnabled: dictionary.is_enabled
}
return deserializeDictionaries
}
}
@@ -0,0 +1,40 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,76 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
match_phrase: {
title: searchString
}
},
boost: 3
}
},
{
constant_score: {
filter: {
match_phrase: {
question: searchString
}
},
boost: 2
}
},
{
constant_score: {
filter: {
match_phrase: {
answer: searchString
}
},
boost: 1
}
}
]
}
},
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,70 @@
const { queryForField } = require('../..')
const whitespace = /\s+/
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (searchString, filters, hitsPerPage, page) {
const searchTokens = searchString.split(whitespace)
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: queryForField('title', searchTokens),
boost: 3
}
},
{
constant_score: {
filter: queryForField('question', searchTokens),
boost: 2
}
},
{
constant_score: {
filter: queryForField('answer', searchTokens),
boost: 1
}
}
]
}
},
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,85 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
match: {
title: {
query: searchString,
operator: 'and'
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
match: {
question: {
query: searchString,
operator: 'and'
}
}
},
boost: 2
}
},
{
constant_score: {
filter: {
match: {
answer: {
query: searchString,
operator: 'and'
}
}
},
boost: 1
}
}
]
}
},
filter: []
}
},
sort: ['_score', 'timeCreated']
}
if (filters.status) {
queryDsl.query.bool.filter.push({
term: {
status: filters.status
}
})
}
if (filters.assignedConsultant) {
queryDsl.query.bool.filter.push({
term: {
'assignedConsultants.id': filters.assignedConsultant
}
})
}
if (filters.primaryDomain) {
queryDsl.query.bool.filter.push({
term: {
'primaryDomain.id': filters.primaryDomain
}
})
}
return queryDsl
}
@@ -0,0 +1,129 @@
const { EDITOR_MAX_HITS } = require('../../../../../config/settings')
module.exports = function (dictionaryId, filters, searchFieldFilters) {
const queryDsl = {
_source: ['id', 'isValid', 'isPublished', 'term'],
fields: ['foreignEntries.terms'],
script_fields: {
commentActivityIndicator: {
script: {
source: `
if (!doc.containsKey('timeMostRecentComment') || doc['timeMostRecentComment'].empty) return "";
ZonedDateTime mostRecentCommentTime = doc['timeMostRecentComment'].value;
long nowMilli = params['now'];
Instant nowInstant = Instant.ofEpochMilli(nowMilli);
ZonedDateTime now = ZonedDateTime.ofInstant(nowInstant, ZoneId.of('Z'));
long ageInDays = ChronoUnit.DAYS.between(mostRecentCommentTime, now);
if (ageInDays < 7) {
return "T"
} else if (ageInDays < 30) {
return "M"
} else if (ageInDays < 365) {
return "L"
} else {
return ""
}
`,
params: {
now: Date.now()
}
}
}
},
size: EDITOR_MAX_HITS,
query: {
bool: {
filter: [
{
term: {
'dictionary.id': dictionaryId
}
}
]
}
},
sort: ['term.sort', 'homonymSort']
}
if (searchFieldFilters) queryDsl.query.bool.filter.push(searchFieldFilters)
if (filters) {
if (filters.isValid !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isValid: filters.isValid
}
})
}
if (filters.isPublished !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isPublished: filters.isPublished
}
})
}
if (filters.hasComments !== undefined) {
if (filters.hasComments === true) {
queryDsl.query.bool.filter.push({
exists: {
field: 'timeMostRecentComment'
}
})
} else {
queryDsl.query.bool.filter.push({
bool: {
must_not: {
exists: {
field: 'timeMostRecentComment'
}
}
}
})
}
}
if (filters.isComplete !== undefined) {
if (filters.isComplete === true) {
queryDsl.query.bool.filter.push({
term: {
status: 'complete'
}
})
} else {
queryDsl.query.bool.filter.push({
bool: {
must_not: {
term: {
status: 'complete'
}
}
}
})
}
}
if (filters.isTerminologyReviewed !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isTerminologyReviewed: filters.isTerminologyReviewed
}
})
}
if (filters.isLanguageReviewed !== undefined) {
queryDsl.query.bool.filter.push({
term: {
isLanguageReviewed: filters.isLanguageReviewed
}
})
}
}
return queryDsl
}
@@ -0,0 +1,103 @@
const { fieldMap } = require('../..')
const generateQuery = require('./all')
module.exports = function (dictionaryId, searchField, filters) {
const searchFieldFilters = generateSearchFieldFilters(searchField)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField) {
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: {
exists: {
field: mappedName
}
}
}
}
}
return {
exists: {
field: mappedName
}
}
}
return {
dis_max: {
queries: [
{
exists: {
field: 'term'
}
},
{
exists: {
field: 'synonyms'
}
},
{
exists: {
field: 'label'
}
},
{
exists: {
field: 'definition'
}
},
{
exists: {
field: 'other'
}
},
{
exists: {
field: 'domainLabels'
}
},
{
exists: {
field: 'links'
}
},
{
nested: {
path: 'foreignEntries',
query: {
dis_max: {
queries: [
{
exists: {
field: 'foreignEntries.terms'
}
},
{
exists: {
field: 'foreignEntries.synonyms'
}
},
{
exists: {
field: 'foreignEntries.definition'
}
}
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,76 @@
const { fieldMap } = require('../..')
const generateQuery = require('./all')
module.exports = function (dictionaryId, searchField, searchString, filters) {
const searchFieldFilters = generateSearchFieldFilters(
searchField,
searchString
)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField, searchString) {
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
[mappedName]: searchString
}
}
}
}
}
return {
match_phrase: {
[mappedName]: searchString
}
}
}
return {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,62 @@
const { fieldMap, queryForField } = require('../..')
const generateQuery = require('./all')
const whitespace = /\s+/
module.exports = function (dictionaryId, searchField, searchString, filters) {
const searchFieldFilters = generateSearchFieldFilters(
searchField,
searchString
)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField, searchString) {
const searchTokens = searchString.split(whitespace)
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: queryForField(mappedName, searchTokens)
}
}
}
return queryForField(mappedName, searchTokens)
}
return {
dis_max: {
queries: [
queryForField('term', searchTokens),
queryForField('synonyms', searchTokens),
queryForField('label', searchTokens),
queryForField('definition', searchTokens),
queryForField('other', searchTokens),
queryForField('domainLabels', searchTokens),
queryForField('links', searchTokens),
{
nested: {
path: 'foreignEntries',
query: {
dis_max: {
queries: [
queryForField('foreignEntries.terms', searchTokens),
queryForField('foreignEntries.synonyms', searchTokens),
queryForField('foreignEntries.definition', searchTokens)
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,82 @@
const { fieldMap } = require('../..')
const generateQuery = require('./all')
module.exports = function (dictionaryId, searchField, searchString, filters) {
const searchFieldFilters = generateSearchFieldFilters(
searchField,
searchString
)
const queryDsl = generateQuery(dictionaryId, filters, searchFieldFilters)
return queryDsl
}
function generateSearchFieldFilters(searchField, searchString) {
const mappedName = fieldMap[searchField]
if (mappedName) {
if (mappedName.startsWith('foreignEntries')) {
return {
nested: {
path: 'foreignEntries',
query: {
match: {
[mappedName]: {
query: searchString,
operator: 'and'
}
}
}
}
}
}
return {
match: {
[mappedName]: {
query: searchString,
operator: 'and'
}
}
}
}
return {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
}
}
]
}
}
}
@@ -0,0 +1,183 @@
const debug = require('debug')('termPortal:generate-query')
const genAllHitQuery = require('./main/all')
const genAllAggQuery = require('./main/aggregate-all')
const genPhraseHitQuery = require('./main/phrase')
const genPhraseAggQuery = require('./main/aggregate-phrase')
const genWildcardHitQuery = require('./main/wildcard')
const genWildcardAggQuery = require('./main/aggregate-wildcard')
const genMultiWordHitQuery = require('./main/word-multi')
const genWordAggQuery = require('./main/aggregate-word')
const genSingleWordHitQuery = require('./main/word-single')
const genEditorAllQuery = require('./editor/all')
const genEditorExistsQuery = require('./editor/exists')
const genEditorPhraseQuery = require('./editor/phrase')
const genEditorWildcardQuery = require('./editor/wildcard')
const genEditorWordsQuery = require('./editor/words')
const genConsultancyAllQuery = require('./consultancy/all')
const genConsultancyPhraseQuery = require('./consultancy/phrase')
const genConsultancyWildcardQuery = require('./consultancy/wildcard')
const genConsultancyWordsQuery = require('./consultancy/words')
const notOnlyAsterisks = /[^*"\s]/
const hasWildcards = /[*?]/
const hasWhitespace = /\s/
// Determines query type, constructs and returns appropriate search engine query(es).
exports.main = async (
searchString,
filters,
hitsPerPage,
page,
withAggregation
) => {
let hitsQuery
let aggregateQuery
const firstChar = searchString[0]
const lastChar = searchString[searchString.length - 1]
// TODO Aditional sanitization/transformation?
if (!notOnlyAsterisks.test(searchString)) {
debug('query type: all')
hitsQuery = genAllHitQuery(filters, hitsPerPage, page)
if (withAggregation) {
aggregateQuery = genAllAggQuery(hitsQuery)
}
} else if (firstChar === '"' && lastChar === '"') {
debug('query type: phrase')
searchString = searchString.slice(1, -1)
hitsQuery = await genPhraseHitQuery(
searchString,
filters,
hitsPerPage,
page
)
if (withAggregation) {
aggregateQuery = genPhraseAggQuery(searchString, hitsQuery)
}
} else if (hasWildcards.test(searchString)) {
debug('query type: wildcard')
let slovenianFieldQueries, foreignFieldQueries
;[hitsQuery, slovenianFieldQueries, foreignFieldQueries] =
await genWildcardHitQuery(searchString, filters, hitsPerPage, page)
if (withAggregation) {
aggregateQuery = genWildcardAggQuery(
hitsQuery,
slovenianFieldQueries,
foreignFieldQueries
)
}
} else if (hasWhitespace.test(searchString)) {
debug('query type: multi word')
hitsQuery = await genMultiWordHitQuery(
searchString,
filters,
hitsPerPage,
page
)
if (withAggregation) {
aggregateQuery = genWordAggQuery(searchString, hitsQuery)
}
} else {
debug('query type: single word')
hitsQuery = await genSingleWordHitQuery(
searchString,
filters,
hitsPerPage,
page
)
if (withAggregation) {
aggregateQuery = genWordAggQuery(searchString, hitsQuery)
}
}
return withAggregation ? [hitsQuery, aggregateQuery] : hitsQuery
}
// Determines query type, constructs and returns appropriate search engine query for editor.
exports.editor = (dictionaryId, searchField, searchString, filters) => {
let hitsQuery
const firstChar = searchString[0]
const lastChar = searchString[searchString.length - 1]
// TODO Aditional sanitization/transformation?
if (!searchString) {
debug('query type: all')
hitsQuery = genEditorAllQuery(dictionaryId, filters)
} else if (!notOnlyAsterisks.test(searchString)) {
debug('query type: exists')
hitsQuery = genEditorExistsQuery(dictionaryId, searchField, filters)
} else if (firstChar === '"' && lastChar === '"') {
debug('query type: phrase')
searchString = searchString.slice(1, -1)
hitsQuery = genEditorPhraseQuery(
dictionaryId,
searchField,
searchString,
filters
)
} else if (hasWildcards.test(searchString)) {
debug('query type: wildcard')
hitsQuery = genEditorWildcardQuery(
dictionaryId,
searchField,
searchString,
filters
)
} else {
debug('query type: words')
hitsQuery = genEditorWordsQuery(
dictionaryId,
searchField,
searchString,
filters
)
}
return hitsQuery
}
// Determines query type, constructs and returns appropriate search engine query for consultancy.
exports.consultancy = (searchString, filters, hitsPerPage, page) => {
let hitsQuery
const firstChar = searchString[0]
const lastChar = searchString[searchString.length - 1]
// TODO Aditional sanitization/transformation?
if (!notOnlyAsterisks.test(searchString)) {
debug('query type: all')
hitsQuery = genConsultancyAllQuery(filters, hitsPerPage, page)
} else if (firstChar === '"' && lastChar === '"') {
debug('query type: phrase')
searchString = searchString.slice(1, -1)
hitsQuery = genConsultancyPhraseQuery(
searchString,
filters,
hitsPerPage,
page
)
} else if (hasWildcards.test(searchString)) {
debug('query type: wildcard')
hitsQuery = genConsultancyWildcardQuery(
searchString,
filters,
hitsPerPage,
page
)
} else {
debug('query type: words')
hitsQuery = genConsultancyWordsQuery(
searchString,
filters,
hitsPerPage,
page
)
}
return hitsQuery
}
@@ -0,0 +1,42 @@
module.exports = function (hitsQuery) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,80 @@
module.exports = function (searchString, hitsQuery) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
slovenianHits: {
filter: {
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
},
foreignHits: {
filter: {
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
},
aggs: {
sourceLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,68 @@
module.exports = function (
hitsQuery,
slovenianFieldQueries,
foreignFieldQueries
) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
slovenianHits: {
filter: {
dis_max: {
queries: slovenianFieldQueries
}
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
},
foreignHits: {
filter: {
dis_max: {
queries: foreignFieldQueries
}
},
aggs: {
sourceLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,80 @@
module.exports = function (searchString, hitsQuery) {
// A shallow copy will prevent hitsQuery from being modified.
const queryDsl = { ...hitsQuery }
queryDsl.from = 0
queryDsl.size = 0
queryDsl.aggs = {
primaryDomains: {
terms: {
field: 'primaryDomain.id',
size: 100
}
},
dictionaries: {
terms: {
field: 'dictionary.id',
size: 200
}
},
sources: {
terms: {
field: 'source.code'
}
},
slovenianHits: {
filter: {
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
}
},
foreign: {
nested: {
path: 'foreignEntries'
},
aggs: {
targetLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
},
foreignHits: {
filter: {
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
},
aggs: {
sourceLanguages: {
terms: {
field: 'foreignEntries.lang.id',
size: 50
}
}
}
}
}
}
}
return queryDsl
}
@@ -0,0 +1,64 @@
// TODO Consider alternative paging when hits exceed 10000.
module.exports = function (filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
return queryDsl
}
@@ -0,0 +1,247 @@
const { separateSlovenianLanguage } = require('../..')
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
term: {
'term.keyword': {
value: searchString
}
}
},
boost: 6
}
},
{
constant_score: {
filter: {
match_phrase: {
term: searchString
}
},
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
term: {
'foreignEntries.terms.keyword': {
value: searchString
}
}
}
}
},
boost: 4
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
'foreignEntries.terms': searchString
}
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
match_phrase: {
synonyms: searchString
}
},
{
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
'foreignEntries.synonyms': searchString
}
}
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
match_phrase: {
'foreignEntries.definition': searchString
}
}
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push({
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
})
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
multi_match: {
query: searchString,
type: 'phrase',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return queryDsl
}
@@ -0,0 +1,207 @@
const { separateSlovenianLanguage, queryForField } = require('../..')
const whitespace = /\s+/
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const searchTokens = searchString.split(whitespace)
const termQuery = queryForField('term', searchTokens)
const foreignTermQuery = queryForField('foreignEntries.terms', searchTokens)
const synonymsQuery = queryForField('synonyms', searchTokens)
const foreignSynonymsQuery = queryForField(
'foreignEntries.synonyms',
searchTokens
)
const labelQuery = queryForField('label', searchTokens)
const definitionQuery = queryForField('definition', searchTokens)
const otherQuery = queryForField('other', searchTokens)
const domainLabelsQuery = queryForField('domainLabels', searchTokens)
const linksQuery = queryForField('links', searchTokens)
const foreignDefinitionQuery = queryForField(
'foreignEntries.definition',
searchTokens
)
const slovenianFieldQueries = [
termQuery,
synonymsQuery,
labelQuery,
definitionQuery,
otherQuery,
domainLabelsQuery,
linksQuery
]
const foreignFieldQueries = [
foreignTermQuery,
foreignSynonymsQuery,
foreignDefinitionQuery
]
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: termQuery,
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: foreignTermQuery
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
synonymsQuery,
{
nested: {
path: 'foreignEntries',
query: foreignSynonymsQuery
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
labelQuery,
definitionQuery,
otherQuery,
domainLabelsQuery,
linksQuery,
{
nested: {
path: 'foreignEntries',
query: foreignDefinitionQuery
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push(...slovenianFieldQueries)
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
dis_max: {
queries: foreignFieldQueries
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return [queryDsl, slovenianFieldQueries, foreignFieldQueries]
}
@@ -0,0 +1,233 @@
const { separateSlovenianLanguage } = require('../..')
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
match: {
term: {
query: searchString,
operator: 'and'
}
}
},
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.terms': {
query: searchString,
operator: 'and'
}
}
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
match: {
synonyms: {
query: searchString,
operator: 'and'
}
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.synonyms': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.definition': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push({
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
})
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return queryDsl
}
@@ -0,0 +1,262 @@
const { separateSlovenianLanguage } = require('../..')
// TODO Consider alternative paging when hits exceed 10000.
module.exports = async function (searchString, filters, hitsPerPage, page) {
const queryDsl = {
from: hitsPerPage * (page - 1),
size: hitsPerPage,
query: {
bool: {
must: {
dis_max: {
queries: [
{
constant_score: {
filter: {
term: {
'term.keyword': {
value: searchString
}
}
},
boost: 6
}
},
{
constant_score: {
filter: {
match: {
term: {
query: searchString,
operator: 'and'
}
}
},
boost: 5
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
term: {
'foreignEntries.terms.keyword': {
value: searchString
}
}
}
}
},
boost: 4
}
},
{
constant_score: {
filter: {
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.terms': {
query: searchString,
operator: 'and'
}
}
}
}
},
boost: 3
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
match: {
synonyms: {
query: searchString,
operator: 'and'
}
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.synonyms': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 2
}
},
{
constant_score: {
filter: {
dis_max: {
queries: [
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
},
{
nested: {
path: 'foreignEntries',
query: {
match: {
'foreignEntries.definition': {
query: searchString,
operator: 'and'
}
}
}
}
}
]
}
},
boost: 1
}
}
]
}
},
filter: [
{
term: {
isPublished: true
}
},
{
term: {
'dictionary.status': 'published'
}
}
]
}
},
sort: ['_score', 'term.sort', 'homonymSort']
}
if (filters.primaryDomains.length) {
queryDsl.query.bool.filter.push({
terms: {
'primaryDomain.id': filters.primaryDomains
}
})
}
if (filters.dictionaries.length) {
queryDsl.query.bool.filter.push({
terms: {
'dictionary.id': filters.dictionaries
}
})
}
if (filters.sources.length) {
queryDsl.query.bool.filter.push({
terms: {
'source.code': filters.sources
}
})
}
if (filters.targetLanguages.length) {
queryDsl.query.bool.filter.push({
nested: {
path: 'foreignEntries',
query: {
terms: {
'foreignEntries.lang.id': filters.targetLanguages
}
}
}
})
}
if (filters.sourceLanguages.length) {
const [foreignLanguages, isSlovenianSource] =
await separateSlovenianLanguage(filters.sourceLanguages)
const sourceLanguageFilter = {
dis_max: {
queries: []
}
}
if (isSlovenianSource) {
sourceLanguageFilter.dis_max.queries.push({
multi_match: {
query: searchString,
operator: 'and',
fields: [
'term',
'synonyms',
'label',
'definition',
'other',
'domainLabels',
'links'
]
}
})
}
if (foreignLanguages.length) {
sourceLanguageFilter.dis_max.queries.push({
nested: {
path: 'foreignEntries',
query: {
bool: {
must: [
{
terms: {
'foreignEntries.lang.id': foreignLanguages
}
},
{
multi_match: {
query: searchString,
operator: 'and',
fields: [
'foreignEntries.terms',
'foreignEntries.synonyms',
'foreignEntries.definition'
]
}
}
]
}
}
}
})
}
queryDsl.query.bool.filter.push(sourceLanguageFilter)
}
return queryDsl
}
+225
View File
@@ -0,0 +1,225 @@
const Portal = require('../../portal')
const { getSlovenianLanguageId } = require('..')
const slCollator = new Intl.Collator('sl', { sensitivity: 'base' })
// Return a new array, with slovenian language removed and isSlovenianSource set to true if present.
exports.separateSlovenianLanguage = async languages => {
const slovenianLanguageId = await getSlovenianLanguageId()
let isSlovenianSource = false
const foreignLanguages = languages.filter(language => {
if (language !== slovenianLanguageId) return true
isSlovenianSource = true
return false
})
return [foreignLanguages, isSlovenianSource]
}
// Simplify and enhance search engine's hits output into useful entries object.
exports.prepareEntries = hits => {
const entriesByCategory = hits.body.hits.hits.reduce(
(agg, hit) => {
const entry = hit._source
switch (hit._score) {
case 6:
case 5:
agg.byTerm.push(entry)
break
case 4:
case 3:
agg.byForeignTerm.push(entry)
break
case 2:
case 1:
agg.byOther.push(entry)
break
default:
agg.uncategorized.push(entry)
}
return agg
},
{ byTerm: [], byForeignTerm: [], byOther: [], uncategorized: [] }
)
return entriesByCategory
}
// Transform search engine's aggregation raw output into correct and friendly format.
exports.prepareAggregation = async aggregationRaw => {
const { aggregations, hits } = aggregationRaw.body
const hitsCount = hits.total.value
if (!hitsCount) return
const primaryDomainIds = aggregations.primaryDomains.buckets.map(
bucket => bucket.key
)
const dictionaryIds = aggregations.dictionaries.buckets.map(
bucket => bucket.key
)
const languageIdSet = new Set()
const refinedSourceLanguageBuckets = []
aggregations.foreign.targetLanguages.buckets.forEach(bucket =>
languageIdSet.add(bucket.key)
)
const slovenianHitCount = aggregations.slovenianHits?.doc_count
// "Match all" aggregation returns no slovenian hits and source languages.
if (slovenianHitCount) {
const slovenianLanguageId = await getSlovenianLanguageId()
const slovenianBucket = {
key: slovenianLanguageId,
doc_count: slovenianHitCount
}
let wasSlovenianBucketInserted = false
aggregations.foreign.foreignHits.sourceLanguages.buckets.forEach(bucket => {
if (!wasSlovenianBucketInserted && bucket.doc_count < slovenianHitCount) {
languageIdSet.add(slovenianLanguageId)
refinedSourceLanguageBuckets.push(slovenianBucket)
wasSlovenianBucketInserted = true
}
languageIdSet.add(bucket.key)
refinedSourceLanguageBuckets.push(bucket)
})
if (!wasSlovenianBucketInserted) {
languageIdSet.add(slovenianLanguageId)
refinedSourceLanguageBuckets.push(slovenianBucket)
}
} else if (slovenianHitCount === 0) {
aggregations.foreign.foreignHits.sourceLanguages.buckets.forEach(bucket => {
languageIdSet.add(bucket.key)
refinedSourceLanguageBuckets.push(bucket)
})
}
const languageIds = [...languageIdSet]
const names = await Portal.getSearchAggregateNames(
primaryDomainIds,
dictionaryIds,
languageIds
)
const aggregation = {
sourceLanguages: refinedSourceLanguageBuckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.languages[id], hits }
}
),
targetLanguages: aggregations.foreign.targetLanguages.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.languages[id], hits }
}
),
primaryDomains: aggregations.primaryDomains.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.primaryDomains[id], hits }
}
),
dictionaries: aggregations.dictionaries.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: names.dictionaries[id], hits }
}
),
sources: aggregations.sources.buckets.map(
({ key: id, doc_count: hits }) => {
return { id, name: id, hits }
}
)
}
sortAggregation(aggregation)
return aggregation
}
// Enhance aggregation for display in search filters.
exports.prepareSeachFilterData = (aggregation, filters) => {
const enhancedAggregation = {}
if (aggregation) {
for (const category of Object.keys(aggregation)) {
const enhancedCategoryMembers = aggregation[category].map(member => {
if (filters[category].includes(member.id)) member.checked = true
return member
})
enhancedAggregation[category] = enhancedCategoryMembers
}
}
return enhancedAggregation
}
// Maps editor search api field names into search engine ones.
exports.fieldMap = {
term: 'term',
synonyms: 'synonyms',
label: 'label',
definition: 'definition',
other: 'other',
domainLabels: 'domainLabels',
links: 'links',
foreignTerms: 'foreignEntries.terms',
foreignSynonyms: 'foreignEntries.synonyms',
foreignDefinition: 'foreignEntries.definition'
}
// Generate search engine DSL wildcard query fragment for a single field.
exports.queryForField = (field, searchTokens) => {
return {
bool: {
filter: searchTokens.map(token => {
return {
wildcard: {
[field]: {
value: token
}
}
}
})
}
}
}
// Simplify and enhance search engine's editor hits output into useful entries list.
exports.prepareEditorEntries = hits => {
const entries = hits.body.hits.hits.map(hit => {
const entry = hit._source
entry.foreignTerm = hit.fields['foreignEntries.terms']?.[0]
entry.commentActivityIndicator = hit.fields.commentActivityIndicator[0]
return entry
})
return entries
}
function sortAggregation(aggregation) {
for (const categoryBuckets of Object.values(aggregation)) {
categoryBuckets.sort(bucketCompareFn)
}
}
function bucketCompareFn(bucketA, bucketB) {
if (bucketA.hits !== bucketB.hits) return 0
return slCollator.compare(bucketA.name, bucketB.name)
}
// Simplify search engine's consultancy hits output into useful entries list.
exports.prepareConsultancyEntries = hits => {
const entries = hits.body.hits.hits.map(hit => hit._source)
return entries
}
+42
View File
@@ -0,0 +1,42 @@
exports.deserialize = {
userById(user) {
const deserializedUser = {
id: user.id,
userName: user.username,
firstName: user.first_name,
lastName: user.last_name,
email: user.email,
hitsPerPage: user.hits_per_page,
userRoles: user.user_roles,
assignedConsultancyEntries: user.assigned_consultancy_entries
}
return deserializedUser
},
user(userData) {
const deserializedData = {
id: userData.id,
userName: userData.username,
firstName: userData.first_name,
lastName: userData.last_name,
email: userData.email,
password: userData.password
}
return deserializedData
},
userRights(userData) {
const deserializedRights = {
id: userData.id,
userName: userData.username,
email: userData.email,
hasAdministration: userData.administration,
hasEditing: userData.editing,
hasTerminologyReview: userData.terminology_review,
hasLanguageReview: userData.language_review
}
return deserializedRights
}
}