Skip to content

Commit

Permalink
fix(#755): Cannot use facetGroup conjunction/disjunction/negation on …
Browse files Browse the repository at this point in the history
…non managed referenced groups
  • Loading branch information
novoj committed Dec 4, 2024
1 parent 077523b commit 5192916
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
*
* _ _ ____ ____
* _____ _(_) |_ __ _| _ \| __ )
* / _ \ \ / / | __/ _` | | | | _ \
* | __/\ V /| | || (_| | |_| | |_) |
* \___| \_/ |_|\__\__,_|____/|____/
*
* Copyright (c) 2024
*
* Licensed under the Business Source License, Version 1.1 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/FgForrest/evitaDB/blob/master/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.evitadb.api.exception;


import io.evitadb.exception.EvitaInvalidUsageException;

import javax.annotation.Nonnull;
import java.io.Serial;

/**
* Exception is thrown when the code needs the entity type to be managed by evitaDB, but it is not.
*
* @author Jan Novotný (novotny@fg.cz), FG Forrest a.s. (c) 2024
*/
public class EntityNotManagedException extends EvitaInvalidUsageException {
@Serial private static final long serialVersionUID = 2826263371602773442L;

public EntityNotManagedException(@Nonnull String entityType) {
super(
"Cannot execute the operation, entity type `" + entityType + "` is not managed by evitaDB!"
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import io.evitadb.index.bitmap.BaseBitmap;
import io.evitadb.index.bitmap.Bitmap;
import io.evitadb.index.bitmap.EmptyBitmap;
import io.evitadb.index.facet.FacetIndex;
import io.evitadb.index.hierarchy.predicate.HierarchyFilteringPredicate;
import io.evitadb.utils.Assert;
import io.evitadb.utils.CollectionUtils;
Expand Down Expand Up @@ -824,13 +825,22 @@ public boolean isFacetGroupConjunction(@Nonnull ReferenceSchemaContract referenc
referencedGroupType != null,
() -> "Referenced group type must be defined for facet group conjunction of `" + referenceName + "`!"
);
return new FilteringFormulaPredicate(
this,
getScopes(),
filterBy,
referencedGroupType,
() -> "Facet group conjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
if (referenceSchema.isReferencedGroupTypeManaged()) {
return new FilteringFormulaPredicate(
this,
getScopes(),
filterBy,
referencedGroupType,
() -> "Facet group conjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
} else {
return new FilteringFormulaPredicate(
this,
getThrowingGlobalIndexesForNonManagedEntityTypeGroup(referenceName, referencedGroupType),
filterBy,
() -> "Facet group conjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
}
}
)
.test(groupId);
Expand All @@ -841,6 +851,37 @@ public boolean isFacetGroupConjunction(@Nonnull ReferenceSchemaContract referenc
}
}

/**
* Creates a list of global entity indexes for the given non-managed entity type. Global indexes contains only
* primary keys of groups retrieved from {@link FacetIndex} of the given reference.
*
* @param referenceName name of the reference to retrieve groups from
* @param referencedGroupType type of the referenced group
* @return list of fake global entity indexes
*/
@Nonnull
private List<GlobalEntityIndex> getThrowingGlobalIndexesForNonManagedEntityTypeGroup(
@Nonnull String referenceName,
@Nonnull String referencedGroupType
) {
return getScopes().stream()
.map(scope -> {
final Optional<Index<EntityIndexKey>> refTypeIndex = getIndex(new EntityIndexKey(EntityIndexType.GLOBAL, scope));
return refTypeIndex
.map(GlobalEntityIndex.class::cast)
.map(index -> index.getFacetingEntities().get(referenceName))
.map(facetIndex -> GlobalEntityIndex.createThrowingStub(
referencedGroupType,
new EntityIndexKey(EntityIndexType.GLOBAL, scope),
facetIndex.getGroupsAsMap().keySet()
)
)
.orElse(null);
})
.filter(Objects::nonNull)
.toList();
}

/**
* Returns true if passed `groupId` of `referenceName` is requested to be joined with other facet groups by
* disjunction (OR) instead of default conjunction (AND).
Expand All @@ -865,13 +906,22 @@ public boolean isFacetGroupDisjunction(@Nonnull ReferenceSchemaContract referenc
referencedGroupType != null,
() -> "Referenced group type must be defined for facet group disjunction of `" + referenceName + "`!"
);
return new FilteringFormulaPredicate(
this,
getScopes(),
filterBy,
referencedGroupType,
() -> "Facet group disjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
if (referenceSchema.isReferencedGroupTypeManaged()) {
return new FilteringFormulaPredicate(
this,
getScopes(),
filterBy,
referencedGroupType,
() -> "Facet group disjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
} else {
return new FilteringFormulaPredicate(
this,
getThrowingGlobalIndexesForNonManagedEntityTypeGroup(referenceName, referencedGroupType),
filterBy,
() -> "Facet group disjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
}
}
).test(groupId);
}
Expand Down Expand Up @@ -905,13 +955,22 @@ public boolean isFacetGroupNegation(@Nonnull ReferenceSchemaContract referenceSc
referencedGroupType != null,
() -> "Referenced group type must be defined for facet group negation of `" + referenceName + "`!"
);
return new FilteringFormulaPredicate(
this,
getScopes(),
filterBy,
referencedGroupType,
() -> "Facet group negation of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
if (referenceSchema.isReferencedGroupTypeManaged()) {
return new FilteringFormulaPredicate(
this,
getScopes(),
filterBy,
referencedGroupType,
() -> "Facet group negation of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
} else {
return new FilteringFormulaPredicate(
this,
getThrowingGlobalIndexesForNonManagedEntityTypeGroup(referenceName, referencedGroupType),
filterBy,
() -> "Facet group negation of `" + referenceSchema.getName() + "` filter: " + facetFilterBy
);
}
}
).test(groupId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ static NestedContextSorter createFacetGroupSorter(
() -> "Facet groups of reference `" + referenceSchema.getName() + "` cannot be sorted because they relate to " +
"non-managed entity type `" + referenceSchema.getReferencedGroupType() + "`."
);
} else if (!referenceSchema.isReferencedGroupTypeManaged()) {
} else if (referenceSchema.getReferencedGroupType() == null || !referenceSchema.isReferencedGroupTypeManaged()) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
import io.evitadb.core.query.algebra.deferred.DeferredFormula;
import io.evitadb.core.query.algebra.deferred.FormulaWrapper;
import io.evitadb.dataType.Scope;
import io.evitadb.index.GlobalEntityIndex;
import io.evitadb.index.bitmap.Bitmap;
import lombok.Getter;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Set;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
Expand Down Expand Up @@ -89,6 +91,39 @@ public FilteringFormulaPredicate(
this.filteringFormula.initialize(queryContext.getInternalExecutionContext());
}

public FilteringFormulaPredicate(
@Nonnull QueryPlanningContext queryContext,
@Nonnull List<GlobalEntityIndex> indexes,
@Nonnull FilterBy filterBy,
@Nonnull Supplier<String> stepDescriptionSupplier
) {
this.filterBy = filterBy;
// create a deferred formula that will log the execution time to query telemetry
this.filteringFormula = new DeferredFormula(
new FormulaWrapper(
createFormulaForTheFilter(
queryContext,
GlobalEntityIndex.class,
indexes,
filterBy,
null,
null,
stepDescriptionSupplier
),
(executionContext, formula) -> {
try {
executionContext.pushStep(QueryPhase.EXECUTION_FILTER_NESTED_QUERY, stepDescriptionSupplier);
return formula.compute();
} finally {
executionContext.popStep();
}
}
)
);
// we need to initialize formula immediately with new execution context - the results are needed in planning phase already
this.filteringFormula.initialize(queryContext.getInternalExecutionContext());
}

@Override
public boolean test(int entityPrimaryKey) {
return filteringFormula.compute().contains(entityPrimaryKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,41 @@ public static Formula createFormulaForTheFilter(
@Nullable FilterBy rootFilterBy,
@Nonnull String entityType,
@Nonnull Supplier<String> stepDescriptionSupplier
) {
return createFormulaForTheFilter(
queryContext,
GlobalEntityIndex.class,
// now analyze the filter by in a nested context with exchanged primary entity index
requestedScopes
.stream()
.flatMap(scope -> queryContext.getGlobalEntityIndexIfExists(entityType, scope).stream())
.toList(),
filterBy,
rootFilterBy,
queryContext.getSchema(entityType),
stepDescriptionSupplier
);
}

/**
* Method creates a new formula that looks for entity primary keys in global index of `entityType` collection that
* match the `filterBy` constraint.
*
* @param queryContext used for accessing global index, global cache and recording query telemetry
* @param filterBy the filter constraints the entities must match
* @param entitySchema the entity schema of the entity that is looked up
* @param stepDescriptionSupplier the message supplier for the query telemetry
* @return output {@link Formula} that is able to produce the matching entity primary keys
*/
@Nonnull
public static <T extends EntityIndex> Formula createFormulaForTheFilter(
@Nonnull QueryPlanningContext queryContext,
@Nonnull Class<T> indexType,
@Nonnull List<T> indexesToUse,
@Nonnull FilterBy filterBy,
@Nullable FilterBy rootFilterBy,
@Nullable EntitySchemaContract entitySchema,
@Nonnull Supplier<String> stepDescriptionSupplier
) {
final Formula theFormula;
try {
Expand All @@ -308,23 +343,19 @@ public static Formula createFormulaForTheFilter(
);

// now analyze the filter by in a nested context with exchanged primary entity index
final List<GlobalEntityIndex> globalIndexesToUse = requestedScopes
.stream()
.flatMap(scope -> queryContext.getGlobalEntityIndexIfExists(entityType, scope).stream())
.toList();
if (globalIndexesToUse.isEmpty()) {
if (indexesToUse.isEmpty()) {
return EmptyFormula.INSTANCE;
} else {
theFormula = queryContext.analyse(
theFilterByVisitor.executeInContextAndIsolatedFormulaStack(
GlobalEntityIndex.class,
() -> globalIndexesToUse,
indexType,
() -> indexesToUse,
null,
queryContext.getSchema(entityType),
entitySchema,
null,
null,
null,
new AttributeSchemaAccessor(queryContext.getCatalogSchema(), queryContext.getSchema(entityType)),
new AttributeSchemaAccessor(queryContext.getCatalogSchema(), entitySchema),
(entityContract, attributeName, locale) -> Stream.of(entityContract.getAttributeValue(attributeName, locale)),
() -> {
// initialize root constraint for the execution
Expand Down Expand Up @@ -974,7 +1005,7 @@ public final <T, S extends Index<?>> T executeInContextAndIsolatedFormulaStack(
@Nonnull Class<S> indexType,
@Nonnull Supplier<List<S>> targetIndexSupplier,
@Nullable EntityContentRequire requirements,
@Nonnull EntitySchemaContract entitySchema,
@Nullable EntitySchemaContract entitySchema,
@Nullable ReferenceSchemaContract referenceSchema,
@Nullable Function<FilterConstraint, FilterConstraint> nestedQueryFormulaEnricher,
@Nullable EntityNestedQueryComparator entityNestedQueryComparator,
Expand Down Expand Up @@ -1060,7 +1091,7 @@ public final <T, S extends Index<?>> T executeInContext(
@Nonnull Class<S> indexType,
@Nonnull Supplier<List<S>> targetIndexSupplier,
@Nullable EntityContentRequire requirements,
@Nonnull EntitySchemaContract entitySchema,
@Nullable EntitySchemaContract entitySchema,
@Nullable ReferenceSchemaContract referenceSchema,
@Nullable Function<FilterConstraint, FilterConstraint> nestedQueryFormulaEnricher,
@Nullable EntityNestedQueryComparator entityNestedQueryComparator,
Expand Down Expand Up @@ -1530,7 +1561,7 @@ public ProcessingScope(
@Nonnull Supplier<List<T>> targetIndexSupplier,
@Nonnull Set<Scope> requiredScopes,
@Nullable EntityContentRequire requirements,
@Nonnull EntitySchemaContract entitySchema,
@Nullable EntitySchemaContract entitySchema,
@Nullable ReferenceSchemaContract referenceSchema,
@Nullable Function<FilterConstraint, FilterConstraint> nestedQueryFormulaEnricher,
@Nullable EntityNestedQueryComparator entityNestedQueryComparator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public Formula translate(@Nonnull FacetHaving facetHaving, @Nonnull FilterByVisi
return entityIndex.getFacetReferencingEntityIdsFormula(
facetHaving.getReferenceName(),
(groupId, theFacetIds, recordIdBitmaps) -> {
if ((referenceSchema.isReferencedGroupTypeManaged() || groupId == null) && filterByVisitor.isFacetGroupConjunction(referenceSchema, groupId)) {
if (filterByVisitor.isFacetGroupConjunction(referenceSchema, groupId)) {
// AND relation is requested for facet of this group
return new FacetGroupAndFormula(
facetHaving.getReferenceName(), groupId, theFacetIds, recordIdBitmaps
Expand Down
Loading

0 comments on commit 5192916

Please sign in to comment.