diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserBriefDto.java index 0c9479c9c45bcee887bc8f082d39ad58611f54b7..af5d4f8aea27b5e6126eae14518a60a50c599dd4 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserBriefDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserBriefDto.java @@ -21,10 +21,12 @@ import java.util.UUID; public class UserBriefDto { @NotNull + @Field(name = "id", type = FieldType.Keyword) @Schema(example = "1ffc7b0e-9aeb-4e8b-b8f1-68f3936155b4") private UUID id; @NotNull + @Field(name = "username", type = FieldType.Keyword) @Schema(example = "jcarberry", description = "Only contains lowercase characters") private String username; @@ -32,17 +34,21 @@ public class UserBriefDto { private String name; @JsonProperty("qualified_name") + @Field(name = "qualified_name", type = FieldType.Keyword) @Schema(example = "Josiah Carberry — @jcarberry") private String qualifiedName; + @Field(name = "orcid", type = FieldType.Keyword) @Schema(example = "0000-0002-1825-0097") private String orcid; @JsonProperty("given_name") + @Field(name = "firstname", type = FieldType.Keyword) @Schema(example = "Josiah") private String firstname; @JsonProperty("family_name") + @Field(name = "lastname", type = FieldType.Keyword) @Schema(example = "Carberry") private String lastname; diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java index 1baf6ff415e5c5b47c81974bd9d42a2588300a48..7fe58ca156b1e0821ca55dc4638c3d1e8c89be22 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -89,8 +89,12 @@ public class UserServiceImpl implements UserService { entity.setLastname(data.getLastname()); entity.setAffiliation(data.getAffiliation()); entity.setOrcid(data.getOrcid()); + /* create at metadata database */ final User user = userRepository.save(entity); log.info("Updated user data for user with id {}", user.getId()); + /* save in open search database */ + userIdxRepository.save(userMapper.userToUserDto(user)); + log.info("Created user with id {} in open search database", user.getId()); return user; } diff --git a/dbrepo-search-db/init/indices/user.json b/dbrepo-search-db/init/indices/user.json index 687518d52a514fea5ac837796907a2efc326a763..f8a68910d76ebf23356d646145afb9df4017e7c3 100644 --- a/dbrepo-search-db/init/indices/user.json +++ b/dbrepo-search-db/init/indices/user.json @@ -28,6 +28,9 @@ }, "username": { "type": "keyword" + }, + "qualified_name": { + "type": "keyword" } } }, diff --git a/dbrepo-search-service/app/opensearch_client.py b/dbrepo-search-service/app/opensearch_client.py index 52991e9721db05e1d1aceb55fdbe9c512885bf8d..9423e0379f59c45834a9e1950a7d9c4ea869e9b9 100644 --- a/dbrepo-search-service/app/opensearch_client.py +++ b/dbrepo-search-service/app/opensearch_client.py @@ -125,34 +125,40 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None): """ searchable_indices = ["database", "user", "table", "column", "identifier", "view", "concept", "unit"] index = searchable_indices - field_list = [ - "table.name", - "identifier.titles.title", - "identifier.descriptions.description", - "identifier.publisher", - "identifier.creators.*.firstname", - "identifier.creators.*.lastname", - "identifier.creators.*.creator_name", - "column.column_type", - "column.is_null_allowed", - "column.is_primary_key", - "unit.uri", - "unit.name", - "unit.description", - "concept.uri", - "concept.name", - "concept.description", - "funders", - "title", - "description", - "creator.username", - "author", - "name", - "uri", - "database.*", - "internal_name", - "is_public", - ] + # field_list = [ + # "id", + # "internal_name", + # "table.name", + # "database.is_public", + # "database.container.image.name", + # "database.container.image.version", + # "table.description", + # "identifier.titles.title", + # "identifier.descriptions.description", + # "identifier.publisher", + # "identifier.creators.*.firstname", + # "identifier.creators.*.lastname", + # "identifier.creators.*.creator_name", + # "column.column_type", + # "column.is_null_allowed", + # "column.is_primary_key", + # "unit.uri", + # "unit.name", + # "unit.description", + # "concept.uri", + # "concept.name", + # "concept.description", + # "funders", + # "title", + # "description", + # "creator.username", + # "author", + # "name", + # "uri", + # "database.*", + # "internal_name", + # "is_public", + # ] queries = [] if search_term is not None: logging.debug('query has search_term present') @@ -193,89 +199,98 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None): logging.debug("search for specific index: %s", value) index = value continue - if key in field_list: - if re.match(f"{key}\\.", key): - new_field = key[key.index(".") + 1:len(key)] - logging.debug( - f"field name {key} starts with index name {index}: flattened field name to {new_field}") - key = new_field - if is_range_open_end and re.match(f"unit\\.", key): - logging.debug(f"omit key={key} because query type=open end range and key is somewhat unit") - logging.info(f"add match-query for range ),{t2}]") - musts.append({ - "range": { - "val_max": { - "lte": t2 - } + # if key in field_list: + if re.match(f"{index}\.", key): + new_field = key[key.index(".") + 1:len(key)] + logging.debug( + f"field name {key} starts with index name {index}: flattened field name to {new_field}") + key = new_field + if re.match(".*properties\..*", key): + new_field = key.replace("properties.", "") + logging.debug( + f"field name {key} contains properties keyword: flattened field name to {new_field}") + key = new_field + if is_range_open_end and re.match(f"unit\.", key): + logging.debug(f"omit key={key} because query type=open end range and key is somewhat unit") + logging.info(f"add match-query for range ),{t2}]") + musts.append({ + "range": { + "val_max": { + "lte": t2 } - }) - elif is_range_open_begin and re.match(f"unit\\.", key): - logging.debug(f"omit key={key} because query type=open begin range and key is somewhat unit") - logging.info(f"add match-query for range [{t1},(") - musts.append({ - "range": { - "val_min": { - "gte": t1 - } - } - }) - elif is_range_query and re.match(f"unit\\.", key): - logging.debug(f"omit key={key} because query type=full range and key is somewhat unit") - logging.info(f"add match-query for range [{t1},{t2}]") - musts.append({ - "range": { - "val_min": { - "gte": t1 - } + } + }) + elif is_range_open_begin and re.match(f"unit\.", key): + logging.debug(f"omit key={key} because query type=open begin range and key is somewhat unit") + logging.info(f"add match-query for range [{t1},(") + musts.append({ + "range": { + "val_min": { + "gte": t1 } - }) - musts.append({ - "range": { - "val_max": { - "lte": t2 - } + } + }) + elif is_range_query and re.match(f"unit\.", key): + logging.debug(f"omit key={key} because query type=full range and key is somewhat unit") + logging.info(f"add match-query for range [{t1},{t2}]") + musts.append({ + "range": { + "val_min": { + "gte": t1 } - }) - else: - musts.append({ - "match": { - key: {"query": value, "minimum_should_match": "90%"} + } + }) + musts.append({ + "range": { + "val_max": { + "lte": t2 } - }) + } + }) + else: + precision = "90%" + if key in ["attributes.orcid", "creators.name_identifier"]: + precision = "100%" + logging.debug(f"key {key} needs precision of 100%") + musts.append({ + "match": { + key: {"query": value, "minimum_should_match": precision} + } + }) specific_query = {"bool": {"must": musts}} queries.append(specific_query) body = { - "query": {"bool": {"must": queries}}, - "_source": [ - "_class", - "id", - "table_id", - "database_id", - "name", - "identifier.*", - "column_type", - "description", - "titles", - "descriptions", - "funders", - "licenses", - "creators", - "visibility", - "title", - "type", - "uri", - "username", - "is_public", - "created", - "_score", - "concept", - "unit", - "author", - "docID", - "creator.*", - "owner.*", - "details.*", - ], + "query": {"bool": {"must": queries}} + # "_source": [ + # "_class", + # "id", + # "table_id", + # "database_id", + # "name", + # "identifier.*", + # "column_type", + # "description", + # "titles", + # "descriptions", + # "funders", + # "licenses", + # "creators", + # "visibility", + # "title", + # "type", + # "uri", + # "username", + # "is_public", + # "created", + # "_score", + # "concept", + # "unit", + # "author", + # "docID", + # "creator.*", + # "owner.*", + # "details.*", + # ], } logging.debug('search index: %s', index) logging.debug('search body: %s', body) diff --git a/dbrepo-ui/components/search/AdvancedSearch.vue b/dbrepo-ui/components/search/AdvancedSearch.vue index 539339e01257d8a129123d9f36c886d853d29743..f1cb9faf59d693353a3ec742fb4a0e1b4f05047d 100644 --- a/dbrepo-ui/components/search/AdvancedSearch.vue +++ b/dbrepo-ui/components/search/AdvancedSearch.vue @@ -2,124 +2,132 @@ <div> <v-card flat tile> <v-card-text class="pt-0 pl-4 pb-6 pr-4"> - <v-row dense> - <v-col cols="3"> - <v-select - v-model="advancedSearchData.type" - :items="fieldItems" - item-text="name" - item-value="value" - solo - label="Type" /> - </v-col> - </v-row> - <p>The following fields are <code>AND</code> connected and depend on the type above.</p> - <v-row dense> - <v-col cols="3"> - <v-text-field - v-model="advancedSearchData.id" - clearable - label="ID" /> - </v-col> - <v-col cols="3"> - <v-text-field - v-if="!hideFields.hideNameField" - v-model="advancedSearchData.name" - clearable - label="Name" /> - </v-col> - <v-col cols="3"> - <v-text-field - v-if="!hideFields.hideInternalNameField" - v-model="advancedSearchData.internal_name" - clearable - label="Internal Name" /> - </v-col> - </v-row> - <v-row v-if="loadingFields" dense> - <v-progress-circular color="primary" indeterminate /> - </v-row> - <v-row v-if="!loadingFields && renderedFields" dense> - <v-col v-for="field in renderedFields" :key="`f-${field.attribute_name}`" cols="3"> - <v-select - v-if="field.type === 'boolean'" - v-model="advancedSearchData[generateDynamicVModelKey(field)]" - clearable - :items="booleanItems" - item-text="name" - item-value="value" - :label="generateFriendlyName(field)" /> - <v-text-field - v-if="(field.type === 'keyword' && field.attribute_name !== 'column_type') || field.type === 'text' || field.type === 'date'" - v-model="advancedSearchData[generateDynamicVModelKey(field)]" - type="text" - :label="generateFriendlyName(field)" - clearable /> - <v-select - v-if="field.type === 'keyword' && field.attribute_name === 'column_type'" - v-model="advancedSearchData[generateDynamicVModelKey(field)]" - :items="columnTypes" - item-value="value" - clearable - :label="generateFriendlyName(field)" /> - <v-text-field - v-if="field.type === 'integer'" - v-model="advancedSearchData[generateDynamicVModelKey(field)]" - type="number" - :label="generateFriendlyName(field)" - clearable /> - <v-autocomplete - v-if="field.attribute_name === 'licenses'" - v-model="advancedSearchData[generateDynamicVModelKey(field)]" - :items="fetchLicenses()" - :label="generateFriendlyName(field)" - clearable - multiple /> - </v-col> - </v-row> - <p v-if="isEligibleConceptOrUnitSearch" class="mt-4"> - If you select a <code>concept</code> and <code>unit</code>, you can search across columns regardless of their - unit of measurement. - </p> - <v-row v-if="isEligibleConceptOrUnitSearch" dense> - <v-col cols="3"> - <v-select - v-model="advancedSearchData['concept.uri']" - clearable - :items="concepts" - item-text="name" - item-value="uri" - label="Concept" /> - </v-col> - <v-col cols="3"> - <v-select - v-model="advancedSearchData['unit.uri']" - clearable - :items="units" - item-text="name" - item-value="uri" - label="Unit" /> - </v-col> - <v-col cols="3"> - <v-text-field - v-model="advancedSearchData['t1']" - clearable - type="number" - label="Start Value" /> - </v-col> - <v-col cols="3"> - <v-text-field - v-model="advancedSearchData['t2']" - clearable - type="number" - label="End Value" /> - </v-col> - </v-row> - <v-row dense> - <v-btn class="mr-2" color="primary" :loading="loading" small @click="advancedSearch"> - Search - </v-btn> - </v-row> + <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit"> + <v-row dense> + <v-col cols="3"> + <v-select + v-model="advancedSearchData.type" + :items="fieldItems" + item-text="name" + item-value="value" + solo + label="Type" /> + </v-col> + </v-row> + <p>The following fields are <code>AND</code> connected and depend on the type above.</p> + <v-row dense> + <v-col cols="3"> + <v-text-field + v-model="advancedSearchData.id" + clearable + label="ID" /> + </v-col> + <v-col cols="3"> + <v-text-field + v-if="!hideFields.hideNameField" + v-model="advancedSearchData.name" + clearable + label="Name" /> + </v-col> + <v-col cols="3"> + <v-text-field + v-if="!hideFields.hideInternalNameField" + v-model="advancedSearchData.internal_name" + clearable + label="Internal Name" /> + </v-col> + </v-row> + <v-row v-if="loadingFields" dense> + <v-progress-circular color="primary" indeterminate /> + </v-row> + <v-row v-if="!loadingFields && renderedFields" dense> + <v-col v-for="field in renderedFields" :key="`f-${field.attribute_name}`" cols="3"> + <v-select + v-if="field.type === 'boolean'" + v-model="advancedSearchData[generateDynamicVModelKey(field)]" + clearable + :items="booleanItems" + item-text="name" + item-value="value" + :label="generateFriendlyName(field)" /> + <v-text-field + v-if="(field.type === 'keyword' && field.attribute_name !== 'column_type') || field.type === 'text' || field.type === 'date'" + v-model="advancedSearchData[generateDynamicVModelKey(field)]" + type="text" + :label="generateFriendlyName(field)" + clearable /> + <v-select + v-if="field.type === 'keyword' && field.attribute_name === 'column_type'" + v-model="advancedSearchData[generateDynamicVModelKey(field)]" + :items="columnTypes" + item-value="value" + clearable + :label="generateFriendlyName(field)" /> + <v-text-field + v-if="field.type === 'integer'" + v-model="advancedSearchData[generateDynamicVModelKey(field)]" + type="number" + :label="generateFriendlyName(field)" + clearable /> + <v-autocomplete + v-if="field.attribute_name === 'licenses'" + v-model="advancedSearchData[generateDynamicVModelKey(field)]" + :items="fetchLicenses()" + :label="generateFriendlyName(field)" + clearable + multiple /> + </v-col> + </v-row> + <p v-if="isEligibleConceptOrUnitSearch" class="mt-4"> + If you select a <code>concept</code> and <code>unit</code>, you can search across columns regardless of their + unit of measurement. + </p> + <v-row v-if="isEligibleConceptOrUnitSearch" dense> + <v-col cols="3"> + <v-select + v-model="advancedSearchData['concept.uri']" + clearable + :items="concepts" + item-text="name" + item-value="uri" + label="Concept" /> + </v-col> + <v-col cols="3"> + <v-select + v-model="advancedSearchData['unit.uri']" + clearable + :items="units" + item-text="name" + item-value="uri" + label="Unit" /> + </v-col> + <v-col cols="3"> + <v-text-field + v-model="advancedSearchData['t1']" + clearable + type="number" + label="Start Value" /> + </v-col> + <v-col cols="3"> + <v-text-field + v-model="advancedSearchData['t2']" + clearable + type="number" + label="End Value" /> + </v-col> + </v-row> + <v-row dense> + <v-btn + type="submit" + class="mr-2" + color="primary" + :loading="loading" + small + @click="advancedSearch"> + Search + </v-btn> + </v-row> + </v-form> </v-card-text> </v-card> </div> @@ -133,6 +141,7 @@ import SemanticMapper from '@/api/semantic.mapper' export default { data () { return { + valid: false, loading: false, loadingFields: false, showAdvancedSearch: false, @@ -212,6 +221,9 @@ export default { }) }, methods: { + submit () { + this.$refs.form.validate() + }, /* Removes all advanced search fields when switching the type */ resetAdvancedSearchFields () { Object.keys(this.advancedSearchData) @@ -251,14 +263,14 @@ export default { dynamicFieldsMap () { // Defines a mapping to narrow down the fields rendered for the advanced search return { - database: ['created', 'description', 'is_public'], - table: ['created', 'description', 'is_public'], + database: ['is_public'], + table: ['description', 'is_public'], column: ['column_type', 'is_primary_key', 'is_null_allowed'], - user: ['firstname', 'lastname', 'username'], + user: ['firstname', 'lastname', 'username', 'attributes.properties.orcid'], identifier: [ 'creators.properties.creator_name', 'creators.properties.name_identifier', 'descriptions.properties.description', 'doi', 'funders.properties.funder_identifier', - 'publication_year', 'titles.properties.title', 'visibility' + 'publication_year', 'titles.properties.title' ], view: ['is_public', 'query'], concept: ['uri'], diff --git a/dbrepo-ui/pages/search/index.vue b/dbrepo-ui/pages/search/index.vue index 6db2a9450e284456c8a276094dbee089a610db7f..e08e4b63b846351c4f5d8ccbbfe484c73350c81e 100644 --- a/dbrepo-ui/pages/search/index.vue +++ b/dbrepo-ui/pages/search/index.vue @@ -218,6 +218,8 @@ export default { return null } else if (this.isView(item)) { return item.query + } else if (this.isUser(item)) { + return item.name } return null }, @@ -264,6 +266,11 @@ export default { tags.push({ text: 'Unit' }) } else if (this.isConcept(item)) { tags.push({ text: 'Concept' }) + } else if (this.isUser(item)) { + tags.push({ text: 'User' }) + if ('orcid' in item.attributes && item.attributes.orcid) { + tags.push({ text: 'ORCID', color: 'green' }) + } } return tags },