diff --git a/pom.xml b/pom.xml index 579e667e3..155afe513 100644 --- a/pom.xml +++ b/pom.xml @@ -56,31 +56,8 @@ true - - - true - - - false - - central - https://jcenter.bintray.com - - - - - false - - - true - - central - https://jcenter.bintray.com - - - UTF-8 UTF-8 @@ -102,6 +79,11 @@ graphql-java 16.2 + + org.webjars.npm + n1ru4l__push-pull-async-iterable-iterator + 2.1.2 + @@ -179,7 +161,7 @@ org.webjars.npm graphiql - 1.4.1 + 1.4.2 diff --git a/src/main/java/com/jaeksoft/opensearchserver/GraphQLFunctions.java b/src/main/java/com/jaeksoft/opensearchserver/GraphQLFunctions.java index ac90e5437..2382d69e5 100644 --- a/src/main/java/com/jaeksoft/opensearchserver/GraphQLFunctions.java +++ b/src/main/java/com/jaeksoft/opensearchserver/GraphQLFunctions.java @@ -19,10 +19,25 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.MapSerializer; import com.qwazr.search.analysis.SmartAnalyzerSet; +import com.qwazr.search.field.FieldDefinition; import com.qwazr.search.field.SmartFieldDefinition; import com.qwazr.search.index.IndexServiceInterface; +import com.qwazr.search.index.IndexSettingsDefinition; import com.qwazr.search.index.PostDefinition; +import com.qwazr.search.index.QueryBuilder; +import com.qwazr.search.index.QueryDefinition; +import com.qwazr.search.index.ResultDefinition; +import com.qwazr.search.index.ResultDocumentMap; +import com.qwazr.search.query.AbstractClassicQueryParser; +import com.qwazr.search.query.AbstractQueryParser; +import com.qwazr.search.query.MultiFieldQueryParser; +import com.qwazr.search.query.QueryInterface; +import com.qwazr.search.query.QueryParser; +import com.qwazr.search.query.QueryParserOperator; +import com.qwazr.search.query.SimpleQueryParser; import static graphql.Scalars.GraphQLFloat; import static graphql.Scalars.GraphQLInt; import static graphql.Scalars.GraphQLString; @@ -81,10 +96,16 @@ List getIndexes(final DataFetchingEnvironment environment) { Boolean createIndex(final DataFetchingEnvironment environment) { final String name = getStringArgument(environment, "indexName"); - if (name == null) + if (name == null || name.isEmpty() || name.isBlank()) return false; + final String indexName = name.trim(); + final IndexSettingsDefinition indexSettings = IndexSettingsDefinition + .of() + .primaryKey("") + .recordField(FieldDefinition.RECORD_FIELD) + .build(); graphQlService.refreshSchema(() -> { - indexService.createUpdateIndex(name.trim()); + indexService.createUpdateIndex(indexName, indexSettings); return true; }); return true; @@ -187,6 +208,89 @@ Integer ingestDocuments(final String indexName, final DataFetchingEnvironment en return indexService.postMappedDocuments(indexName, PostDefinition.Documents.of(list, null)); } + private QueryResult search(final String indexName, final QueryInterface queryInterface, final DataFetchingEnvironment environment) { + final QueryBuilder queryDefinitionBuilder = QueryDefinition.of(queryInterface) + .start(environment.getArgument("start")) + .rows(environment.getArgument("rows")); + final List returnedFields = environment.getArgument("returnedFields"); + if (returnedFields == null || returnedFields.isEmpty()) { + queryDefinitionBuilder.returnedFields("*"); + } else { + queryDefinitionBuilder.returnedFields(returnedFields); + } + final ResultDefinition.WithMap result = indexService.searchQuery(indexName, queryDefinitionBuilder.build(), false); + return new QueryResult(result); + } + + private void commonQueryParserParameters(final Map params, final AbstractQueryParser.AbstractBuilder builder) { + builder + .setEnableGraphQueries((Boolean) params.get("enableGraphQueries")) + .setEnablePositionIncrements((Boolean) params.get("enablePositionIncrements")) + .setAutoGenerateMultiTermSynonymsPhraseQuery((Boolean) params.get("autoGenerateMultiTermSynonymsPhraseQuery")) + .setQueryString((String) params.get("queryString")); + ; + + } + + private QueryParserOperator getDefaultOperator(final Map params) { + final String defaultOperator = (String) params.get("defaultOperator"); + return defaultOperator == null ? null : QueryParserOperator.valueOf(defaultOperator); + } + + private void commonClassicQueryParserParameters(final Map params, final AbstractClassicQueryParser.AbstractParserBuilder builder) { + commonQueryParserParameters(params, builder); + + builder + .setAllowLeadingWildcard((Boolean) params.get("allowLeadingWildcard")) + .setAutoGeneratePhraseQuery((Boolean) params.get("autoGeneratePhraseQuery")) + .setFuzzyMinSim((Float) params.get("fuzzyMinSim")) + .setFuzzyPrefixLength((Integer) params.get("fuzzyPrefixLength")) + .setSplitOnWhitespace((Boolean) params.get("splitOnWhitespace")) + .setMaxDeterminizedStates((Integer) params.get("maxDeterminizedStates")) + .setDefaultOperator(getDefaultOperator(params)) + .setPhraseSlop((Integer) params.get("phraseSlop")); + } + + QueryResult searchWithMultiFieldQueryParser(final String indexName, final DataFetchingEnvironment environment) { + final MultiFieldQueryParser.Builder builder = MultiFieldQueryParser.of(); + final Map params = environment.getArgument("params"); + commonClassicQueryParserParameters(params, builder); + final Map fieldBoostMap = environment.getArgument("fieldBoost"); + if (fieldBoostMap != null) { + fieldBoostMap.forEach((field, boost) -> builder.addBoost(field, boost.floatValue())); + } + return search(indexName, builder.build(), environment); + } + + QueryResult searchWithStandardQueryParser(final String indexName, final DataFetchingEnvironment environment) { + final QueryParser.Builder builder = QueryParser.of(environment.getArgument("defaultField")); + final Map params = environment.getArgument("params"); + commonQueryParserParameters(params, builder); + return search(indexName, builder.build(), environment); + } + + QueryResult searchWithSimpleQueryParser(final String indexName, final DataFetchingEnvironment environment) { + final SimpleQueryParser.Builder builder = SimpleQueryParser.of(); + final Map params = environment.getArgument("params"); + commonQueryParserParameters(params, builder); + final List> fieldBoosts = (List>) params.get("fieldBoosts"); + if (fieldBoosts != null) { + for (Map fieldBoost : fieldBoosts) { + final String field = (String) fieldBoost.get("field"); + final Number boost = (Number) fieldBoost.get("boost"); + builder.addBoost(field, boost.floatValue()); + } + } + builder.setDefaultOperator(getDefaultOperator(params)); + for (SimpleOperator operator : SimpleOperator.values()) { + final Boolean enabled = environment.getArgument(operator.name()); + if (enabled != null && enabled) { + builder.addOperator(operator.operator); + } + } + return search(indexName, builder.build(), environment); + } + @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonAutoDetect( setterVisibility = JsonAutoDetect.Visibility.NONE, @@ -296,4 +400,54 @@ GraphQLScalarType getGraphScalarType() { return GraphQLFloat; } } + + public enum SimpleOperator { + + enableAndOperator(SimpleQueryParser.Operator.and), + enableEscapeOperator(SimpleQueryParser.Operator.escape), + enableFuzzyOperator(SimpleQueryParser.Operator.fuzzy), + enableNearOperator(SimpleQueryParser.Operator.near), + enableNotOperator(SimpleQueryParser.Operator.not), + enableOrOperator(SimpleQueryParser.Operator.or), + enablePhraseOperator(SimpleQueryParser.Operator.phrase), + enablePrecedenceOperator(SimpleQueryParser.Operator.precedence), + enablePrefixOperator(SimpleQueryParser.Operator.prefix), + enableWhitespaceOperator(SimpleQueryParser.Operator.whitespace); + + final SimpleQueryParser.Operator operator; + + SimpleOperator(SimpleQueryParser.Operator operator) { + this.operator = operator; + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonAutoDetect( + setterVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, + creatorVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY) + public static class QueryResult { + + @JsonProperty + public final int totalHits; + + @JsonProperty + @JsonSerialize(keyUsing = MapSerializer.class) + public final List> documents; + + QueryResult(ResultDefinition.WithMap result) { + totalHits = result.totalHits > Integer.MAX_VALUE ? null : (int) result.totalHits; + if (result.documents != null) { + documents = new ArrayList<>(result.documents.size()); + for (final ResultDocumentMap resultDocumentMap : result.documents) { + documents.add(resultDocumentMap.fields); + } + } else + documents = List.of(); + } + + } + } diff --git a/src/main/java/com/jaeksoft/opensearchserver/GraphQLSchemaBuilder.java b/src/main/java/com/jaeksoft/opensearchserver/GraphQLSchemaBuilder.java index 8bc420560..2782e5c5d 100644 --- a/src/main/java/com/jaeksoft/opensearchserver/GraphQLSchemaBuilder.java +++ b/src/main/java/com/jaeksoft/opensearchserver/GraphQLSchemaBuilder.java @@ -17,9 +17,12 @@ package com.jaeksoft.opensearchserver; import com.qwazr.search.analysis.SmartAnalyzerSet; +import com.qwazr.search.query.QueryParserOperator; import com.qwazr.utils.StringUtils; import graphql.GraphQL; import static graphql.Scalars.GraphQLBoolean; +import static graphql.Scalars.GraphQLFloat; +import static graphql.Scalars.GraphQLInt; import static graphql.Scalars.GraphQLString; import graphql.schema.DataFetcher; import static graphql.schema.FieldCoordinates.coordinates; @@ -31,6 +34,7 @@ import static graphql.schema.GraphQLEnumType.newEnum; import graphql.schema.GraphQLFieldDefinition; import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; +import graphql.schema.GraphQLInputObjectField; import static graphql.schema.GraphQLInputObjectField.newInputObjectField; import graphql.schema.GraphQLInputObjectType; import static graphql.schema.GraphQLInputObjectType.newInputObject; @@ -51,6 +55,9 @@ public class GraphQLSchemaBuilder { private final static String indexDocumentPrefix = "Index"; private final static String indexDocumentSuffix = "Document"; + private final static String queryDocumentPrefix = "Query"; + private final static String queryDocumentSuffix = "Document"; + private final GraphQLFunctions functions; private final List rootQueries; @@ -59,6 +66,7 @@ public class GraphQLSchemaBuilder { private final IndexContext indexContext; private final FieldContext fieldContext; + private final QueryContext queryContext; private static > GraphQLEnumType createEnum(String name, String description, T... values) { final GraphQLEnumType.Builder builder = newEnum() @@ -80,12 +88,23 @@ private static > GraphQLEnumType createEnum(String name, Strin indexContext = new IndexContext(); fieldContext = new FieldContext(); + queryContext = new QueryContext(); } public GraphQL build() { indexContext.build(); fieldContext.build(); + queryContext.build(); + + for (GraphQLFunctions.Index index : functions.getIndexList(null, 0, Integer.MAX_VALUE)) { + final String sourceIndexName = index.name.trim(); + final String capitalizedIndexName = StringUtils.capitalize(sourceIndexName); + final List fields = functions.getFieldList(sourceIndexName); + indexContext.buildPerIndex(sourceIndexName, capitalizedIndexName, fields); + queryContext.buildPerIndex(sourceIndexName, capitalizedIndexName, fields); + } + final GraphQLSchema.Builder builder = GraphQLSchema.newSchema(); @@ -176,47 +195,41 @@ private void createIndexFunctions() { } - private void createDocumentTypes() { + private void build() { + createIndexFunctions(); + } - for (final GraphQLFunctions.Index index : functions.getIndexList(null, 0, Integer.MAX_VALUE)) { + private void buildPerIndex(String sourceIndexName, String capitalizedIndexName, List fields) { - final String sourceIndexName = index.name.trim(); - final String capitalizedIndexName = StringUtils.capitalize(sourceIndexName); + final GraphQLInputObjectType.Builder indexDocumentBuilder = newInputObject() + .name(indexDocumentPrefix + capitalizedIndexName + indexDocumentSuffix) + .description("A document following the schema of the index \"" + sourceIndexName + "\""); - final GraphQLInputObjectType.Builder indexDocumentBuilder = newInputObject() - .name(indexDocumentPrefix + capitalizedIndexName + indexDocumentSuffix) - .description("A document following the schema of the index \"" + sourceIndexName + "\""); + for (final GraphQLFunctions.Field field : fields) { + indexDocumentBuilder.field(newInputObjectField() + .name(field.name) + .type(field.getGraphScalarType()) + .build()); + } - for (final GraphQLFunctions.Field field : functions.getFieldList(sourceIndexName)) { - indexDocumentBuilder.field(newInputObjectField() - .name(field.name) - .type(field.getGraphScalarType()) - .build()); - } - - final GraphQLInputObjectType indexDocument = indexDocumentBuilder.build(); - if (indexDocument.getFields().isEmpty()) - continue; - final String ingestFunctionName = "ingest" + capitalizedIndexName; - final GraphQLFieldDefinition ingestDocuments = newFieldDefinition() - .name(ingestFunctionName) - .description("Ingest a document into \"" + sourceIndexName + "\"") - .argument(newArgument().name("docs").type(GraphQLList.list(indexDocument))) - .type(GraphQLBoolean) - .build(); + final GraphQLInputObjectType indexDocument = indexDocumentBuilder.build(); + if (indexDocument.getFields().isEmpty()) + return; + final String ingestFunctionName = "ingest" + capitalizedIndexName; + final GraphQLFieldDefinition ingestDocuments = newFieldDefinition() + .name(ingestFunctionName) + .description("Ingest a document into \"" + sourceIndexName + "\"") + .argument(newArgument().name("docs").type(GraphQLList.list(indexDocument))) + .type(GraphQLBoolean) + .build(); - rootMutations.add(ingestDocuments); + rootMutations.add(ingestDocuments); - codeRegistry.dataFetcher( - coordinates("Mutation", ingestFunctionName), - (DataFetcher) env -> functions.ingestDocuments(sourceIndexName, env)); - } + codeRegistry.dataFetcher( + coordinates("Mutation", ingestFunctionName), + (DataFetcher) env -> functions.ingestDocuments(sourceIndexName, env)); } - private void build() { - createIndexFunctions(); - createDocumentTypes(); - } } private class FieldContext { @@ -454,4 +467,186 @@ private void build() { } } + private class QueryContext { + + private GraphQLInputObjectType standardQueryParserInputType; + private GraphQLInputObjectType multiFieldQueryParserInputType; + private GraphQLInputObjectType simpleQueryParserInputType; + private GraphQLInputObjectType fieldBoost; + + private void build() { + fieldBoost = GraphQLInputObjectType.newInputObject() + .name("FieldBoost") + .field(newInputObjectField().name("field").type(GraphQLString).build()) + .field(newInputObjectField().name("boost").type(GraphQLFloat).build()) + .build(); + final List commonQueryParserInputFields = buildCommonQueryInputFields(); + final List commonClassicQueryParserInputFields = buildCommonClassicQueryInputFields(); + + standardQueryParserInputType = GraphQLInputObjectType.newInputObject() + .name("StandardQueryParserParameters") + .description("StandardQueryParser parameters") + .fields(commonQueryParserInputFields) + .fields(commonClassicQueryParserInputFields) + .build(); + + multiFieldQueryParserInputType = GraphQLInputObjectType.newInputObject() + .name("MultiFieldQueryParserParameters") + .description("StandardQueryParser parameters") + .fields(commonQueryParserInputFields) + .fields(commonClassicQueryParserInputFields) + .field(newInputObjectField().name("fields").type(GraphQLList.list(GraphQLString))) + .field(newInputObjectField().name("fieldBoosts").type(GraphQLList.list(fieldBoost))) + .build(); + + final GraphQLInputObjectType.Builder simpleQueryParserInputTypeBuilder = GraphQLInputObjectType.newInputObject() + .name("SimpleQueryParserParameters") + .description("SimpleQueryParser parameters") + .fields(commonQueryParserInputFields) + .field(newInputObjectField().name("fieldBoosts").type(GraphQLList.list(fieldBoost))); + for (GraphQLFunctions.SimpleOperator operator : GraphQLFunctions.SimpleOperator.values()) { + simpleQueryParserInputTypeBuilder.field( + newInputObjectField() + .name(operator.name()) + .type(GraphQLBoolean) + .build()); + } + simpleQueryParserInputType = simpleQueryParserInputTypeBuilder.build(); + + } + + private List buildCommonQueryInputFields() { + return List.of( + newInputObjectField() + .name("queryString") + .type(GraphQLString) + .build(), + newInputObjectField() + .name("enableGraphQueries") + .type(GraphQLBoolean) + .build(), + newInputObjectField() + .name("enablePositionIncrements") + .type(GraphQLBoolean) + .build(), + newInputObjectField() + .name("autoGenerateMultiTermSynonymsPhraseQuery") + .type(GraphQLBoolean) + .build()); + } + + private List buildCommonClassicQueryInputFields() { + return List.of( + newInputObjectField() + .name("allowLeadingWildcard") + .type(GraphQLBoolean) + .build(), + newInputObjectField() + .name("autoGeneratePhraseQuery") + .type(GraphQLBoolean) + .build(), + newInputObjectField() + .name("fuzzyMinSim") + .type(GraphQLFloat) + .build(), + newInputObjectField() + .name("fuzzyPrefixLength") + .type(GraphQLInt) + .build(), + newInputObjectField() + .name("splitOnWhitespace") + .type(GraphQLBoolean) + .build(), + newInputObjectField() + .name("maxDeterminizedStates") + .type(GraphQLInt) + .build(), + newInputObjectField() + .name("defaultOperator") + .type(createEnum("QueryOperator", "Query operator", QueryParserOperator.values())) + .build(), + newInputObjectField() + .name("phraseSlop") + .type(GraphQLInt) + .build()); + } + + private void createSimpleQuery(String capitalizedIndexName, String sourceIndexName, GraphQLObjectType queryResultType) { + final String queryFunctionName = "simpleQuery" + capitalizedIndexName; + + final GraphQLFieldDefinition.Builder builder = newFieldDefinition() + .name(queryFunctionName) + .description("Simple query from \"" + sourceIndexName + "\"") + .argument(newArgument().name("params").type(simpleQueryParserInputType)) + .type(queryResultType); + queryParams(builder); + rootQueries.add(builder.build()); + codeRegistry.dataFetcher( + coordinates("Query", queryFunctionName), + (DataFetcher) env -> functions.searchWithSimpleQueryParser(sourceIndexName, env)); + } + + private void createMultiFieldQuery(String capitalizedIndexName, String sourceIndexName, GraphQLObjectType queryResultType) { + final String queryFunctionName = "multiFieldQuery" + capitalizedIndexName; + final GraphQLFieldDefinition.Builder builder = newFieldDefinition() + .name(queryFunctionName) + .description("Multi-field query from \"" + sourceIndexName + "\"") + .argument(newArgument().name("params").type(multiFieldQueryParserInputType)) + .type(queryResultType); + queryParams(builder); + rootQueries.add(builder.build()); + codeRegistry.dataFetcher( + coordinates("Query", queryFunctionName), + (DataFetcher) env -> functions.searchWithMultiFieldQueryParser(sourceIndexName, env)); + } + + private void queryParams(GraphQLFieldDefinition.Builder builder) { + builder + .argument(newArgument().name("start").type(GraphQLInt).build()) + .argument(newArgument().name("rows").type(GraphQLInt).build()); + // .argument(newArgument().name("returnedFields").type(GraphQLList.list(GraphQLString)).build()); + } + + private void createStandardQuery(String capitalizedIndexName, String sourceIndexName, GraphQLObjectType queryResultType) { + final String queryFunctionName = "standardFieldQuery" + capitalizedIndexName; + final GraphQLFieldDefinition.Builder builder = newFieldDefinition() + .name(queryFunctionName) + .description("Standard query from \"" + sourceIndexName + "\"") + .argument(newArgument().name("params").type(standardQueryParserInputType)) + .type(queryResultType); + queryParams(builder); + rootQueries.add(builder.build()); + codeRegistry.dataFetcher( + coordinates("Query", queryFunctionName), + (DataFetcher) env -> functions.searchWithStandardQueryParser(sourceIndexName, env)); + } + + private void buildPerIndex(String sourceIndexName, String capitalizedIndexName, List fields) { + if (fields == null || fields.isEmpty()) + return; + + final GraphQLObjectType.Builder queryDocumentTypeBuilder = newObject() + .name(queryDocumentPrefix + capitalizedIndexName + queryDocumentSuffix) + .description("A document following the schema of the index \"" + sourceIndexName + "\""); + + for (final GraphQLFunctions.Field field : fields) { + queryDocumentTypeBuilder.field(newFieldDefinition() + .name(field.name) + .type(field.getGraphScalarType()) + .build()); + } + + final GraphQLObjectType queryDocumentType = queryDocumentTypeBuilder.build(); + + final GraphQLObjectType queryResultType = newObject() + .name("QueryResult" + capitalizedIndexName) + .field(newFieldDefinition().name("totalHits").type(GraphQLNonNull.nonNull(GraphQLInt)).build()) + .field(newFieldDefinition().name("documents").type(GraphQLNonNull.nonNull(GraphQLList.list(queryDocumentType)))) + .build(); + + createSimpleQuery(capitalizedIndexName, sourceIndexName, queryResultType); + createMultiFieldQuery(capitalizedIndexName, sourceIndexName, queryResultType); + createStandardQuery(capitalizedIndexName, sourceIndexName, queryResultType); + } + } } diff --git a/src/main/resources/com/jaeksoft/opensearchserver/front/index.html b/src/main/resources/com/jaeksoft/opensearchserver/front/index.html index 9b8c1b630..65396784b 100644 --- a/src/main/resources/com/jaeksoft/opensearchserver/front/index.html +++ b/src/main/resources/com/jaeksoft/opensearchserver/front/index.html @@ -37,13 +37,13 @@ copy them directly into your environment, or perhaps include them in your favored resource bundler. --> - +
Loading...