diff --git a/docs/UBahn_API.postman_collection.json b/docs/UBahn_API.postman_collection.json index a86efbb..683d90b 100644 --- a/docs/UBahn_API.postman_collection.json +++ b/docs/UBahn_API.postman_collection.json @@ -113,6 +113,58 @@ }, "response": [] }, + { + "name": "{{HOST}}/search/skills", + "event": [ + { + "listen": "test", + "script": { + "id": "d56349bd-a797-47cf-b6bf-2ecfbd342815", + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token}}" + }, + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{HOST}}/search/userAchievements?organizationId=36ed815b-3da1-49f1-a043-aaed0a4e81ad&keyword=Topcoder", + "host": [ + "{{HOST}}" + ], + "path": [ + "search", + "userAchievements" + ], + "query": [ + { + "key": "organizationId", + "value": "36ed815b-3da1-49f1-a043-aaed0a4e81ad" + }, + { + "key": "keyword", + "value": "Topcoder" + } + ] + } + }, + "response": [] + }, { "name": "{{HOST}}/search/users", "event": [ diff --git a/src/common/es-helper.js b/src/common/es-helper.js index 09c7f46..a4474f4 100644 --- a/src/common/es-helper.js +++ b/src/common/es-helper.js @@ -676,33 +676,53 @@ function hasNonAlphaNumeric (text) { * @param keyword the search keyword * @returns array of skillIds */ -async function searchSkills (keyword) { +async function searchSkills (keyword, skillProviderIds) { const queryDoc = DOCUMENTS.skill keyword = escapeRegex(keyword) const query = hasNonAlphaNumeric(keyword) ? `\\*${keyword}\\*` : `*${keyword}*` + const keywordSearchClause = { + query_string: { + default_field: 'name', + minimum_should_match: '100%', + query + } + } + + const searchClause = { + query: {} + } + + if (skillProviderIds == null) { + searchClause.query = keywordSearchClause + searchClause._source = 'id' + } else { + searchClause.query = { + bool: { + filter: [{ + terms: { + [`${RESOURCE_FILTER.skill.skillProviderId.queryField}.keyword`]: skillProviderIds + } + }], + must: [keywordSearchClause] + } + } + } + const esQuery = { index: queryDoc.index, type: queryDoc.type, - body: { - query: { - query_string: { - default_field: 'name', - minimum_should_match: '100%', - query - } - }, - _source: 'id' - } + body: searchClause } logger.debug(`ES query for searching skills: ${JSON.stringify(esQuery, null, 2)}`) const results = await esClient.search(esQuery) - return results.hits.hits.map(hit => hit._source.id) + + return results.hits.hits.map(hit => hit._source) } async function setUserSearchClausesToEsQuery (boolClause, keyword) { - const skillIds = await searchSkills(keyword) + const skillIds = (await searchSkills(keyword)).map(skill => skill.id) boolClause.should.push({ query_string: { fields: ['firstName', 'lastName', 'handle'], @@ -866,6 +886,27 @@ function buildEsQueryToGetAttributeValues (attributeId, attributeValue, size) { return esQuery } +function buildEsQueryToGetSkillProviderIds (organizationId) { + const queryDoc = DOCUMENTS.organization + + const esQuery = { + index: queryDoc.index, + type: queryDoc.type, + body: { + size: 1000, + query: { + term: { + 'id.keyword': { + value: organizationId + } + } + } + } + } + + return esQuery +} + async function resolveUserFilterFromDb (filter, { handle }, organizationId) { const DBHelper = require('../models/index').DBHelper @@ -1391,6 +1432,33 @@ async function searchUsers (authUser, filter, params) { } } +/** + * Search for skills matching the given keyword and are part of the given organization + * @param {Object} param0 the organizationId and keyword + */ + +async function searchSkillsInOrganization ({ organizationId, keyword }) { + const esQueryToGetSkillProviders = buildEsQueryToGetSkillProviderIds(organizationId) + logger.debug(`ES query to get skill provider ids: ${JSON.stringify(esQueryToGetSkillProviders, null, 2)}`) + + const esResultOfQueryToGetSkillProviders = await esClient.search(esQueryToGetSkillProviders) + logger.debug(`ES result: ${JSON.stringify(esResultOfQueryToGetSkillProviders, null, 2)}`) + + const skillProviderIds = _.flatten(esResultOfQueryToGetSkillProviders.hits.hits.map(hit => hit._source.skillProviders == null ? [] : hit._source.skillProviders.map(sp => sp.id))) + logger.debug(`Organization ${organizationId} yielded skillProviderIds: ${JSON.stringify(skillProviderIds, null, 2)}`) + + const skills = await searchSkills(keyword, skillProviderIds) + + return { + result: skills.map(skill => ({ + name: skill.name, + skillId: skill.id, + skillProviderId: skill.skillProviderId + // skillProviderName: 'TODO' + })) + } +} + /** * Searches for matching values for the given attribute value, under the given attribute id * @param {Object} param0 The attribute id and the attribute value properties @@ -1427,19 +1495,19 @@ async function searchAchievementValues ({ organizationId, keyword }) { const esResult = await esClient.search(esQuery) logger.debug(`ES response ${JSON.stringify(esResult, null, 2)}`) const result = esResult.aggregations.achievements.buckets.map(a => { - let achievementName = a.key + const achievementName = a.key let achievementId = null - - for (let achievement of a.ids.hits.hits[0]._source.achievements) { - if (achievement.name == achievementName) { + + for (const achievement of a.ids.hits.hits[0]._source.achievements) { + if (achievement.name === achievementName) { achievementId = achievement.id - break; + break } } return { id: achievementId, name: achievementName - }; + } }) return { @@ -1451,6 +1519,7 @@ module.exports = { searchElasticSearch, getFromElasticSearch, searchUsers, + searchSkillsInOrganization, searchAttributeValues, searchAchievementValues } diff --git a/src/modules/search/controller.js b/src/modules/search/controller.js index da40ca7..b29bfcf 100644 --- a/src/modules/search/controller.js +++ b/src/modules/search/controller.js @@ -13,6 +13,14 @@ async function searchUsers (req, res) { res.send(result.result) } +/** + * Search for skills in organization + */ +async function searchSkills (req, res) { + const result = await esHelper.searchSkillsInOrganization(req.query) + res.send(result.result) +} + /** * Search for attribute values */ @@ -31,6 +39,7 @@ async function searchAchievementValues (req, res) { module.exports = { searchUsers, + searchSkills, searchAttributeValues, searchAchievementValues } diff --git a/src/modules/search/route.js b/src/modules/search/route.js index 6fa3899..f2219ee 100644 --- a/src/modules/search/route.js +++ b/src/modules/search/route.js @@ -14,6 +14,14 @@ module.exports = { scopes: ['read:user', 'all:user'] } }, + '/search/skills': { + get: { + method: Controller.searchSkills + auth: 'jwt', + access: consts.AdminUser, + scopes: ['create:userAttribute', 'all:userAttribute'] + } + }, '/search/userAttributes': { get: { method: Controller.searchAttributeValues,