mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-29 18:59:11 +01:00
Add additional help to quick search and an advanced search documentation page (#1757)
Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
committed by
GitHub
parent
f2249cea73
commit
ddd2fc1055
@@ -143,6 +143,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
builder.single(link("importLog", resourceLinks.repository().importLog("IMPORT_LOG_ID").replace("IMPORT_LOG_ID", "{logId}")));
|
||||
|
||||
builder.array(searchLinks());
|
||||
builder.single(link("searchableTypes", resourceLinks.search().searchableTypes()));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -93,5 +93,7 @@ public class MapperModule extends AbstractModule {
|
||||
bind(RepositoryImportDtoToRepositoryImportParametersMapper.class).to(Mappers.getMapperClass(RepositoryImportDtoToRepositoryImportParametersMapper.class));
|
||||
|
||||
bind(QueryResultMapper.class).to(Mappers.getMapperClass(QueryResultMapper.class));
|
||||
bind(SearchableTypeMapper.class).to(Mappers.getMapperClass(SearchableTypeMapper.class));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1128,6 +1128,7 @@ class ResourceLinks {
|
||||
public String query(String type) {
|
||||
return searchLinkBuilder.method("query").parameters(type).href();
|
||||
}
|
||||
public String searchableTypes() { return searchLinkBuilder.method("searchableTypes").parameters().href(); }
|
||||
}
|
||||
|
||||
public InitialAdminAccountLinks initialAdminAccount() {
|
||||
|
||||
@@ -42,7 +42,9 @@ import javax.ws.rs.BeanParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path(SearchResource.PATH)
|
||||
@OpenAPIDefinition(tags = {
|
||||
@@ -53,12 +55,14 @@ public class SearchResource {
|
||||
static final String PATH = "v2/search";
|
||||
|
||||
private final SearchEngine engine;
|
||||
private final QueryResultMapper mapper;
|
||||
private final QueryResultMapper queryResultMapper;
|
||||
private final SearchableTypeMapper searchableTypeMapper;
|
||||
|
||||
@Inject
|
||||
public SearchResource(SearchEngine engine, QueryResultMapper mapper) {
|
||||
public SearchResource(SearchEngine engine, QueryResultMapper mapper, SearchableTypeMapper searchableTypeMapper) {
|
||||
this.engine = engine;
|
||||
this.mapper = mapper;
|
||||
this.queryResultMapper = mapper;
|
||||
this.searchableTypeMapper = searchableTypeMapper;
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -110,6 +114,34 @@ public class SearchResource {
|
||||
return search(params);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("searchableTypes")
|
||||
@Produces(VndMediaType.SEARCHABLE_TYPE_COLLECTION)
|
||||
@Operation(
|
||||
summary = "Searchable types",
|
||||
description = "Returns a collection of all searchable types.",
|
||||
tags = "Search",
|
||||
operationId = "searchable_types"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.SEARCHABLE_TYPE_COLLECTION
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Collection<SearchableTypeDto> searchableTypes() {
|
||||
return engine.getSearchableTypes().stream().map(searchableTypeMapper::map).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private QueryResultDto search(SearchParameters params) {
|
||||
QueryResult result = engine.forType(params.getType())
|
||||
.search()
|
||||
@@ -117,7 +149,7 @@ public class SearchResource {
|
||||
.limit(params.getPageSize())
|
||||
.execute(params.getQuery());
|
||||
|
||||
return mapper.map(params, result);
|
||||
return queryResultMapper.map(params, result);
|
||||
}
|
||||
|
||||
private QueryResultDto count(SearchParameters params) {
|
||||
@@ -125,7 +157,7 @@ public class SearchResource {
|
||||
.search()
|
||||
.count(params.getQuery());
|
||||
|
||||
return mapper.map(params, new QueryResult(result.getTotalHits(), result.getType(), Collections.emptyList()));
|
||||
return queryResultMapper.map(params, new QueryResult(result.getTotalHits(), result.getType(), Collections.emptyList()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SearchableFieldDto {
|
||||
private String name;
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we need no equals for dtos
|
||||
public class SearchableTypeDto extends HalRepresentation {
|
||||
private String name;
|
||||
private Collection<SearchableFieldDto> fields;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import sonia.scm.search.SearchableField;
|
||||
import sonia.scm.search.SearchableType;
|
||||
import sonia.scm.search.TypeCheck;
|
||||
|
||||
@Mapper
|
||||
public abstract class SearchableTypeMapper {
|
||||
|
||||
@Mapping(target = "attributes", ignore = true)
|
||||
public abstract SearchableTypeDto map(SearchableType searchableType);
|
||||
|
||||
public abstract SearchableFieldDto map(SearchableField searchableField);
|
||||
|
||||
public String map(Class<?> type) {
|
||||
if (TypeCheck.isString(type)) {
|
||||
return "string";
|
||||
} else if (TypeCheck.isInstant(type) || TypeCheck.isNumber(type)) {
|
||||
return "number";
|
||||
} else if (TypeCheck.isBoolean(type)) {
|
||||
return "boolean";
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,14 +66,14 @@ public class AnalyzerFactory {
|
||||
Analyzer defaultAnalyzer = create(options);
|
||||
|
||||
Map<String, Analyzer> analyzerMap = new HashMap<>();
|
||||
for (SearchableField field : type.getFields()) {
|
||||
for (LuceneSearchableField field : type.getFields()) {
|
||||
addFieldAnalyzer(analyzerMap, field);
|
||||
}
|
||||
|
||||
return new PerFieldAnalyzerWrapper(defaultAnalyzer, analyzerMap);
|
||||
}
|
||||
|
||||
private void addFieldAnalyzer(Map<String, Analyzer> analyzerMap, SearchableField field) {
|
||||
private void addFieldAnalyzer(Map<String, Analyzer> analyzerMap, LuceneSearchableField field) {
|
||||
if (field.getAnalyzer() != Indexed.Analyzer.DEFAULT) {
|
||||
analyzerMap.put(field.getName(), new NonNaturalLanguageAnalyzer());
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@Getter
|
||||
class SearchableField {
|
||||
class LuceneSearchableField implements SearchableField {
|
||||
|
||||
private final String name;
|
||||
private final Class<?> type;
|
||||
@@ -43,7 +43,7 @@ class SearchableField {
|
||||
private final PointsConfig pointsConfig;
|
||||
private final Indexed.Analyzer analyzer;
|
||||
|
||||
SearchableField(Field field, Indexed indexed) {
|
||||
LuceneSearchableField(Field field, Indexed indexed) {
|
||||
this.name = name(field, indexed);
|
||||
this.type = field.getType();
|
||||
this.valueExtractor = ValueExtractors.create(name, type);
|
||||
@@ -65,4 +65,5 @@ class SearchableField {
|
||||
}
|
||||
return field.getName();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import com.google.common.base.Strings;
|
||||
import lombok.Value;
|
||||
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -42,13 +43,13 @@ public class LuceneSearchableType implements SearchableType {
|
||||
Class<?> type;
|
||||
String name;
|
||||
String permission;
|
||||
List<SearchableField> fields;
|
||||
List<LuceneSearchableField> fields;
|
||||
String[] fieldNames;
|
||||
Map<String, Float> boosts;
|
||||
Map<String, PointsConfig> pointsConfig;
|
||||
TypeConverter typeConverter;
|
||||
|
||||
LuceneSearchableType(Class<?> type, IndexedType annotation, List<SearchableField> fields) {
|
||||
|
||||
public LuceneSearchableType(Class<?> type, IndexedType annotation, List<LuceneSearchableField> fields) {
|
||||
this.type = type;
|
||||
this.name = name(type, annotation);
|
||||
this.permission = Strings.emptyToNull(annotation.permission());
|
||||
@@ -72,16 +73,16 @@ public class LuceneSearchableType implements SearchableType {
|
||||
return nameFromAnnotation;
|
||||
}
|
||||
|
||||
private String[] fieldNames(List<SearchableField> fields) {
|
||||
private String[] fieldNames(List<LuceneSearchableField> fields) {
|
||||
return fields.stream()
|
||||
.filter(SearchableField::isDefaultQuery)
|
||||
.map(SearchableField::getName)
|
||||
.filter(LuceneSearchableField::isDefaultQuery)
|
||||
.map(LuceneSearchableField::getName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private Map<String, Float> boosts(List<SearchableField> fields) {
|
||||
private Map<String, Float> boosts(List<LuceneSearchableField> fields) {
|
||||
Map<String, Float> map = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
for (LuceneSearchableField field : fields) {
|
||||
if (field.isDefaultQuery() && field.getBoost() != DEFAULT_BOOST) {
|
||||
map.put(field.getName(), field.getBoost());
|
||||
}
|
||||
@@ -89,9 +90,9 @@ public class LuceneSearchableType implements SearchableType {
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private Map<String, PointsConfig> pointsConfig(List<SearchableField> fields) {
|
||||
private Map<String, PointsConfig> pointsConfig(List<LuceneSearchableField> fields) {
|
||||
Map<String, PointsConfig> map = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
for (LuceneSearchableField field : fields) {
|
||||
PointsConfig config = field.getPointsConfig();
|
||||
if (config != null) {
|
||||
map.put(field.getName(), config);
|
||||
@@ -99,4 +100,8 @@ public class LuceneSearchableType implements SearchableType {
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
public Collection<LuceneSearchableField> getFields() {
|
||||
return Collections.unmodifiableCollection(fields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +77,13 @@ public class QueryResultFactory {
|
||||
private Hit createHit(ScoreDoc scoreDoc) throws IOException, InvalidTokenOffsetsException {
|
||||
Document document = searcher.doc(scoreDoc.doc);
|
||||
Map<String, Hit.Field> fields = new HashMap<>();
|
||||
for (SearchableField field : searchableType.getFields()) {
|
||||
for (LuceneSearchableField field : searchableType.getFields()) {
|
||||
field(document, field).ifPresent(f -> fields.put(field.getName(), f));
|
||||
}
|
||||
return new Hit(document.get(FieldNames.ID), document.get(FieldNames.REPOSITORY), scoreDoc.score, fields);
|
||||
}
|
||||
|
||||
private Optional<Hit.Field> field(Document document, SearchableField field) throws IOException, InvalidTokenOffsetsException {
|
||||
private Optional<Hit.Field> field(Document document, LuceneSearchableField field) throws IOException, InvalidTokenOffsetsException {
|
||||
Object value = field.value(document);
|
||||
if (value != null) {
|
||||
if (field.isHighlighted()) {
|
||||
@@ -97,7 +97,7 @@ public class QueryResultFactory {
|
||||
return empty();
|
||||
}
|
||||
|
||||
private String[] createFragments(SearchableField field, String value) throws InvalidTokenOffsetsException, IOException {
|
||||
private String[] createFragments(LuceneSearchableField field, String value) throws InvalidTokenOffsetsException, IOException {
|
||||
return highlighter.getBestFragments(analyzer, field.getName(), value, 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ final class SearchableTypes {
|
||||
}
|
||||
|
||||
static LuceneSearchableType create(Class<?> type) {
|
||||
List<SearchableField> fields = new ArrayList<>();
|
||||
List<LuceneSearchableField> fields = new ArrayList<>();
|
||||
IndexedType annotation = type.getAnnotation(IndexedType.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalArgumentException(
|
||||
@@ -45,7 +45,7 @@ final class SearchableTypes {
|
||||
return new LuceneSearchableType(type, annotation, fields);
|
||||
}
|
||||
|
||||
private static void collectFields(Class<?> type, List<SearchableField> fields) {
|
||||
private static void collectFields(Class<?> type, List<LuceneSearchableField> fields) {
|
||||
Class<?> parent = type.getSuperclass();
|
||||
if (parent != null) {
|
||||
collectFields(parent, fields);
|
||||
@@ -53,7 +53,7 @@ final class SearchableTypes {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
Indexed indexed = field.getAnnotation(Indexed.class);
|
||||
if (indexed != null) {
|
||||
fields.add(new SearchableField(field, indexed));
|
||||
fields.add(new LuceneSearchableField(field, indexed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ package sonia.scm.search;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
final class TypeCheck {
|
||||
public final class TypeCheck {
|
||||
|
||||
private TypeCheck() {
|
||||
}
|
||||
@@ -50,4 +50,6 @@ final class TypeCheck {
|
||||
public static boolean isString(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
|
||||
public static boolean isNumber(Class<?> type) { return isLong(type) || isInteger(type); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user