diff --git a/doc/04_Searching_For_Data_In_Index/05_Search_Modifiers/README.md b/doc/04_Searching_For_Data_In_Index/05_Search_Modifiers/README.md index 41af4986..661a49cb 100644 --- a/doc/04_Searching_For_Data_In_Index/05_Search_Modifiers/README.md +++ b/doc/04_Searching_For_Data_In_Index/05_Search_Modifiers/README.md @@ -12,24 +12,27 @@ $search->addModifier(new ParentIdFilter(1)) ### Filters -| Modifier | Modifier Category | Description | -|--------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [IdFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/IdFilter.php) | Basic filters | Filter by element ID | -| [IdsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/IdsFilter.php) | Basic filters | Filter by multiple element IDs | -| [BooleanFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/Basic/BooleanFilter.php) | Basic filters | Filter boolean fields based on the value with [PQL field name resolution support](#pql-field-name-resolution) | -| [IntegerFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/IntegerFilter.php) | Basic filters | Filter integer fields based on the value with [PQL field name resolution support](#pql-field-name-resolution) | -| [ExcludeFoldersFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/ExcludeFoldersFilter.php) | Basic filters | Exclude folders from search result | -| [ParentIdsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/ParentIdsFilter.php) | Tree related filters | Filter by parent ID | -| [PathFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/PathFilter.php) | Tree related filters | Filter by path (depending on use case for all levels or direct children only and with or without the parent item included) | -| [ClassIdsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/ClassIdsFilter.php) | Tree related filters | Filter object items by class IDs (depending on use case the folders can be included in the result). Setting parameter `$useClassName` to `true` allows filtering based on the classNames instead | -| [TagFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/TagFilter.php) | Tree related filters | Filter by tag IDs (it is also possible to include child tags) | -| [AssetMetaDataFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Asset/AssetMetaDataFilter.php) | Asset filters | Filter by asset meta data attribute. The format of the `$data` which needs to be passed depends on the type of the meta data attribute and is handled by its [field definition adapter](https://github.com/pimcore/generic-data-index-bundle/tree/1.x/src/SearchIndexAdapter/DefaultSearch/Asset/FieldDefinitionAdapter). | -| [WorkspaceQuery](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Workspaces/WorkspaceQuery.php) | Workspace related filters | Filter based on the user workspaces and permissions for a defined element type (this query is added to the asset/document/data object search by default) | -| [ElementWorkspacesQuery](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Workspaces/WorkspaceQuery.php) | Workspace related filters | Filter based on the user workspaces and permissions respecting all element types (this query is added to the element search by default) | -| [MultiSelectFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/FieldType/MultiSelectFilter.php) | Field type filters | Filter text fields by a list of exact strings. Supports [PQL field name resolution](#pql-field-name-resolution). | -| [DateFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/FieldType/DateFilter.php) | Field type filters | Filter date fields based on an exact date or a range of dates. Supports [PQL field name resolution](#pql-field-name-resolution). | -| [ClassificationStoreFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/FieldType/ClassificationStoreFilter.php) | Nested filters | Filter based on the classification store field values. Requires sub-modifier based on the filtered field type. Only fields types, which are supported by classificaiton store can be used for sub-modifier. | -| [NestedFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/FieldType/NestedFilter.php) | Nested filters | Filter for nested fields. Requires sub-modifier based on the field type of nested field. | +| Modifier | Modifier Category | Description | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [IdFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/IdFilter.php) | Basic filters | Filter by element ID | +| [IdsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/IdsFilter.php) | Basic filters | Filter by multiple element IDs | +| [BooleanFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/Basic/BooleanFilter.php) | Basic filters | Filter boolean fields based on the value with [PQL field name resolution support](#pql-field-name-resolution) | +| [IntegerFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/IntegerFilter.php) | Basic filters | Filter integer fields based on the value with [PQL field name resolution support](#pql-field-name-resolution) | +| [NumberFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/Basic/NumberFilter.php) | Basic filters | Filter number fields based on the value with [PQL field name resolution support](#pql-field-name-resolution) | +| [ExcludeFoldersFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Basic/ExcludeFoldersFilter.php) | Basic filters | Exclude folders from search result | +| [ExcludeVariantsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/Basic/ExcludeVariantsFilter.php) | Basic filters | Exclude data object variants from search result | +| [ParentIdsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/ParentIdsFilter.php) | Tree related filters | Filter by parent ID | +| [PathFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/PathFilter.php) | Tree related filters | Filter by path (depending on use case for all levels or direct children only and with or without the parent item included) | +| [ClassIdsFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/ClassIdsFilter.php) | Tree related filters | Filter object items by class IDs (depending on use case the folders can be included in the result). Setting parameter `$useClassName` to `true` allows filtering based on the classNames instead | +| [TagFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Tree/TagFilter.php) | Tree related filters | Filter by tag IDs (it is also possible to include child tags) | +| [AssetMetaDataFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Asset/AssetMetaDataFilter.php) | Asset filters | Filter by asset meta data attribute. The format of the `$data` which needs to be passed depends on the type of the meta data attribute and is handled by its [field definition adapter](https://github.com/pimcore/generic-data-index-bundle/tree/1.x/src/SearchIndexAdapter/DefaultSearch/Asset/FieldDefinitionAdapter). | +| [WorkspaceQuery](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Workspaces/WorkspaceQuery.php) | Workspace related filters | Filter based on the user workspaces and permissions for a defined element type (this query is added to the asset/document/data object search by default) | +| [ElementWorkspacesQuery](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/Workspaces/WorkspaceQuery.php) | Workspace related filters | Filter based on the user workspaces and permissions respecting all element types (this query is added to the element search by default) | +| [MultiSelectFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/FieldType/MultiSelectFilter.php) | Field type filters | Filter text fields by a list of exact strings. Supports [PQL field name resolution](#pql-field-name-resolution). | +| [BooleanMultiSelectFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/FieldType/BooleanMultiSelectFilter.php) | Field type filters | Filter boolean fields by a list of values (true, false, null). Supports [PQL field name resolution](#pql-field-name-resolution). | +| [DateFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.0/src/Model/Search/Modifier/Filter/FieldType/DateFilter.php) | Field type filters | Filter date fields based on an exact date or a range of dates. Supports [PQL field name resolution](#pql-field-name-resolution). | +| [ClassificationStoreFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/FieldType/ClassificationStoreFilter.php) | Nested filters | Filter based on the classification store field values. Requires sub-modifier based on the filtered field type. Only fields types, which are supported by classificaiton store can be used for sub-modifier. | +| [NestedFilter](https://github.com/pimcore/generic-data-index-bundle/blob/2.x/src/Model/Search/Modifier/Filter/FieldType/NestedFilter.php) | Nested filters | Filter for nested fields. Requires sub-modifier based on the field type of nested field. | ### Full Text Search Queries diff --git a/src/Model/DefaultSearch/Query/MultiBoolQuery.php b/src/Model/DefaultSearch/Query/MultiBoolQuery.php new file mode 100644 index 00000000..36056668 --- /dev/null +++ b/src/Model/DefaultSearch/Query/MultiBoolQuery.php @@ -0,0 +1,62 @@ +value => [ + QueryType::BOOL->value => [ + ConditionType::SHOULD->value => [ + (new BoolExistsQuery($this->field))->toArrayAsSubQuery(), + (new TermsFilter($this->field, $this->terms))->toArrayAsSubQuery(), + ], + 'minimum_should_match' => 1, + ], + ], + ]); + } + + public function getField(): string + { + return $this->field; + } + + /** @return (bool)[] */ + public function getTerms(): array + { + return $this->terms; + } + + public function toArrayAsSubQuery(): array + { + return [ + QueryType::BOOL->value => [ + ConditionType::SHOULD->value => [ + (new BoolExistsQuery($this->field))->toArrayAsSubQuery(), + (new TermsFilter($this->field, $this->terms))->toArrayAsSubQuery(), + ], + 'minimum_should_match' => 1, + ], + ]; + } +} diff --git a/src/Model/DefaultSearch/Query/TermsFilter.php b/src/Model/DefaultSearch/Query/TermsFilter.php index 3c7bae54..cc733073 100644 --- a/src/Model/DefaultSearch/Query/TermsFilter.php +++ b/src/Model/DefaultSearch/Query/TermsFilter.php @@ -19,7 +19,7 @@ final class TermsFilter extends BoolQuery implements AsSubQueryInterface { public function __construct( private readonly string $field, - /** @var (int|string)[] */ + /** @var (int|string|bool)[] */ private readonly array $terms, ) { parent::__construct([ @@ -36,7 +36,7 @@ public function getField(): string return $this->field; } - /** @return (int|string)[] */ + /** @return (int|string|bool)[] */ public function getTerms(): array { return $this->terms; diff --git a/src/Model/Search/Modifier/Filter/Basic/ExcludeVariantsFilter.php b/src/Model/Search/Modifier/Filter/Basic/ExcludeVariantsFilter.php new file mode 100644 index 00000000..aebafd80 --- /dev/null +++ b/src/Model/Search/Modifier/Filter/Basic/ExcludeVariantsFilter.php @@ -0,0 +1,20 @@ +fieldName; + } + + public function getSearchTerm(): int|float + { + return $this->searchTerm; + } + + public function isPqlFieldNameResolutionEnabled(): bool + { + return $this->enablePqlFieldNameResolution; + } +} diff --git a/src/Model/Search/Modifier/Filter/FieldType/BooleanMultiSelectFilter.php b/src/Model/Search/Modifier/Filter/FieldType/BooleanMultiSelectFilter.php new file mode 100644 index 00000000..ff8b1b5c --- /dev/null +++ b/src/Model/Search/Modifier/Filter/FieldType/BooleanMultiSelectFilter.php @@ -0,0 +1,57 @@ +validate(); + } + + public function getField(): string + { + return $this->field; + } + + public function getValues(): array + { + return $this->values; + } + + public function isPqlFieldNameResolutionEnabled(): bool + { + return $this->enablePqlFieldNameResolution; + } + + private function validate(): void + { + foreach ($this->values as $value) { + if (!is_bool($value) && !is_null($value)) { + throw new ValueError( + sprintf( + 'Provided array must contain only boolean or null values. (%s given)', + gettype($value) + ), + ); + } + } + } +} diff --git a/src/Model/Search/Modifier/Filter/FieldType/ClassificationStoreFilter.php b/src/Model/Search/Modifier/Filter/FieldType/ClassificationStoreFilter.php index 281243b9..7a1f054a 100644 --- a/src/Model/Search/Modifier/Filter/FieldType/ClassificationStoreFilter.php +++ b/src/Model/Search/Modifier/Filter/FieldType/ClassificationStoreFilter.php @@ -15,6 +15,7 @@ use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\BooleanFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IntegerFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\NumberFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\FullTextSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\WildcardSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\SearchModifierInterface; @@ -25,8 +26,8 @@ public function __construct( private string $fieldName, private string $group, - private BooleanFilter|DateFilter|FullTextSearch|IntegerFilter|MultiSelectFilter|NumberRangeFilter| - WildcardSearch $subModifier, + private BooleanFilter|DateFilter|FullTextSearch|IntegerFilter|MultiSelectFilter|BooleanMultiSelectFilter| + NumberFilter|NumberRangeFilter|WildcardSearch $subModifier, private string $locale = MappingProperty::NOT_LOCALIZED_KEY, ) { } @@ -42,7 +43,7 @@ public function getGroup(): string } public function getSubModifier(): BooleanFilter|DateFilter|FullTextSearch|IntegerFilter| - MultiSelectFilter|NumberRangeFilter|WildcardSearch + MultiSelectFilter|BooleanMultiSelectFilter|NumberFilter|NumberRangeFilter|WildcardSearch { return $this->subModifier; } diff --git a/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/BasicFilters.php b/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/BasicFilters.php index 8753988c..8ac5cedf 100644 --- a/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/BasicFilters.php +++ b/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/BasicFilters.php @@ -14,6 +14,7 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DefaultSearch\Search\Modifier\Filter; use Pimcore\Bundle\GenericDataIndexBundle\Attribute\Search\AsSearchModifierHandler; +use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\DefaultSearch\ConditionType; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory\SystemField; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Modifier\SearchModifierContextInterface; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\BoolExistsQuery; @@ -23,10 +24,13 @@ use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Interfaces\SearchInterface; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\BooleanFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\ExcludeFoldersFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\ExcludeVariantsFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IdFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IdsFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IntegerFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\NumberFilter; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\SearchPqlFieldNameTransformationServiceInterface; +use Pimcore\Model\DataObject\AbstractObject; /** * @internal @@ -53,26 +57,34 @@ public function handleIdFilter(IdFilter $idFilter, SearchModifierContextInterfac public function handleIntegerFilter(IntegerFilter $integerFilter, SearchModifierContextInterface $context): void { $context->getSearch()->addQuery( - $this->getIntegerQuery($integerFilter, null, $context->getOriginalSearch()) + $this->getNumberQuery($integerFilter, null, $context->getOriginalSearch()) ); } - public function getIntegerQuery( - IntegerFilter $integerFilter, + #[AsSearchModifierHandler] + public function handleNumberFilter(NumberFilter $numberFilter, SearchModifierContextInterface $context): void + { + $context->getSearch()->addQuery( + $this->getNumberQuery($numberFilter, null, $context->getOriginalSearch()) + ); + } + + public function getNumberQuery( + IntegerFilter|NumberFilter $filter, ?string $prefix = null, ?SearchInterface $search = null ): TermFilter { - $fieldName = $integerFilter->getFieldName(); + $fieldName = $filter->getFieldName(); if ($prefix) { $fieldName = $prefix . '.' . $fieldName; } - if ($search && $integerFilter->isPqlFieldNameResolutionEnabled()) { + if ($search && $filter->isPqlFieldNameResolutionEnabled()) { $fieldName = $this->fieldNameTransformationService->transformFieldnameForSearch($search, $fieldName); } return new TermFilter( field: $fieldName, - term: $integerFilter->getSearchTerm(), + term: $filter->getSearchTerm(), ); } @@ -125,13 +137,26 @@ public function handleExcludeFoldersFilter( ExcludeFoldersFilter $excludeFoldersFilter, SearchModifierContextInterface $context ): void { - $context->getSearch()->addQuery(new BoolQuery([ - 'must_not' => [ + $context->getSearch()->addQuery($this->excludeTypeQuery(AbstractObject::OBJECT_TYPE_FOLDER)); + } + + #[AsSearchModifierHandler] + public function handleExcludeVariantsFilter( + ExcludeVariantsFilter $excludeVariantsFilter, + SearchModifierContextInterface $context + ): void { + $context->getSearch()->addQuery($this->excludeTypeQuery(AbstractObject::OBJECT_TYPE_VARIANT)); + } + + private function excludeTypeQuery(string $type): BoolQuery + { + return new BoolQuery([ + ConditionType::MUST_NOT->value => [ new TermFilter( field: SystemField::TYPE->getPath(), - term: 'folder', + term: $type, ), ], - ])); + ]); } } diff --git a/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/FieldTypeFilters.php b/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/FieldTypeFilters.php index ddda2271..2a2d3f22 100644 --- a/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/FieldTypeFilters.php +++ b/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/FieldTypeFilters.php @@ -15,10 +15,13 @@ use Pimcore\Bundle\GenericDataIndexBundle\Attribute\Search\AsSearchModifierHandler; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Modifier\SearchModifierContextInterface; +use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\BoolExistsQuery; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\DateFilter as DateFilterQuery; +use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\MultiBoolQuery; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\Query as QueryFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\TermsFilter as TermsFilterQuery; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Interfaces\SearchInterface; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\BooleanMultiSelectFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\DateFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\MultiSelectFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\NumberRangeFilter; @@ -97,6 +100,44 @@ public function getMultiSelectQuery( ); } + #[AsSearchModifierHandler] + public function handleBooleanMultiSelectFilter( + BooleanMultiSelectFilter $filter, + SearchModifierContextInterface $context + ): void { + $context->getSearch()->addQuery( + $this->getBooleanMultiSelectFilter($filter, null, $context->getOriginalSearch()) + ); + } + + public function getBooleanMultiSelectFilter( + BooleanMultiSelectFilter $filter, + ?string $prefix = null, + ?SearchInterface $search = null + ): null|BoolExistsQuery|MultiBoolQuery|TermsFilterQuery { + if (count($filter->getValues()) === 0) { + return null; + } + + $fieldName = $filter->getField(); + if ($prefix) { + $fieldName = $prefix . '.' . $fieldName; + } + + if ($search && $filter->isPqlFieldNameResolutionEnabled()) { + $fieldName = $this->fieldNameTransformationService->transformFieldnameForSearch($search, $fieldName); + } + + $hasNull = in_array(null, $filter->getValues(), true); + $nonNullValues = array_filter($filter->getValues(), static fn ($v) => $v !== null); + + return match (true) { + $hasNull && $nonNullValues !== [] => new MultiBoolQuery($fieldName, $nonNullValues), + $hasNull => new BoolExistsQuery($fieldName), + default => new TermsFilterQuery($fieldName, $nonNullValues), + }; + } + #[AsSearchModifierHandler] public function handleNumberRangeFilter( NumberRangeFilter $numberRangeFilter, @@ -125,8 +166,8 @@ public function getNumberRangeFilter( 'range', [ $fieldName => [ - 'gte' => $numberRangeFilter->getMin(), - 'lte' => $numberRangeFilter->getMax(), + 'gt' => $numberRangeFilter->getMin(), + 'lt' => $numberRangeFilter->getMax(), ], ] ); diff --git a/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/NestedTypeFilters.php b/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/NestedTypeFilters.php index 8e5c4d14..032c3e31 100644 --- a/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/NestedTypeFilters.php +++ b/src/SearchIndexAdapter/DefaultSearch/Search/Modifier/Filter/NestedTypeFilters.php @@ -14,12 +14,15 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DefaultSearch\Search\Modifier\Filter; use Pimcore\Bundle\GenericDataIndexBundle\Attribute\Search\AsSearchModifierHandler; +use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Modifier\SearchModifierContextInterface; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\NestedFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\DefaultSearch\Query\SimpleQueryStringFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Interfaces\SearchInterface; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\BooleanFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IntegerFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\NumberFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\BooleanMultiSelectFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\ClassificationStoreFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\DateFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\MultiSelectFilter; @@ -83,8 +86,8 @@ private function getSubQuery(ClassificationStoreFilter|NestedFilterParam $filter $modifier instanceof BooleanFilter => $this->basicFilters->getBooleanQuery($modifier, $fieldName, $search)->toArrayAsSubQuery(), - $modifier instanceof IntegerFilter => - $this->basicFilters->getIntegerQuery($modifier, $fieldName, $search)->toArrayAsSubQuery(), + $modifier instanceof IntegerFilter, $modifier instanceof NumberFilter => + $this->basicFilters->getNumberQuery($modifier, $fieldName, $search)->toArrayAsSubQuery(), $modifier instanceof DateFilter => $this->fieldTypeFilters->getDateFilterQuery($modifier, $fieldName, $search)->toArray(true), @@ -92,6 +95,9 @@ private function getSubQuery(ClassificationStoreFilter|NestedFilterParam $filter $modifier instanceof MultiSelectFilter => $this->fieldTypeFilters->getMultiSelectQuery($modifier, $fieldName, $search)->toArrayAsSubQuery(), + $modifier instanceof BooleanMultiSelectFilter => + $this->fieldTypeFilters->getBooleanMultiSelectFilter($modifier, $fieldName, $search)?->toArrayAsSubQuery(), + $modifier instanceof NumberRangeFilter => $this->fieldTypeFilters->getNumberRangeFilter($modifier, $fieldName, $search)->toArray(true), @@ -107,13 +113,14 @@ private function getSubQuery(ClassificationStoreFilter|NestedFilterParam $filter private function buildFieldPrefix(string $fieldName): string { - return 'standard_fields.' . $fieldName; + return FieldCategory::STANDARD_FIELDS->value . '.' . $fieldName; } private function buildStoreFieldPrefix(ClassificationStoreFilter $filter): string { return sprintf( - 'standard_fields.%s.%s.%s', + '%s.%s.%s.%s', + FieldCategory::STANDARD_FIELDS->value, $filter->getFieldName(), $filter->getGroup(), $filter->getLocale(), diff --git a/tests/Functional/Search/Modifier/Filter/BasicFiltersTest.php b/tests/Functional/Search/Modifier/Filter/BasicFiltersTest.php index ca59eabf..ae2134de 100644 --- a/tests/Functional/Search/Modifier/Filter/BasicFiltersTest.php +++ b/tests/Functional/Search/Modifier/Filter/BasicFiltersTest.php @@ -14,12 +14,15 @@ use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\BooleanFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\ExcludeFoldersFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\ExcludeVariantsFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IdFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IdsFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\IntegerFilter; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Basic\NumberFilter; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\Asset\AssetSearchServiceInterface; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\DataObject\DataObjectSearchServiceInterface; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\SearchProviderInterface; +use Pimcore\Model\DataObject\AbstractObject; use Pimcore\Tests\Support\Util\TestHelper; class BasicFiltersTest extends \Codeception\Test\Unit @@ -70,6 +73,31 @@ public function testExcludeFolders() $this->assertNotEquals('folder', $searchResult->getItems()[0]->getType()); } + // tests + public function testExcludeVariants() + { + $object = TestHelper::createEmptyObject(save: false); + $object->setType(AbstractObject::OBJECT_TYPE_VARIANT); + $object->save(); + + TestHelper::createEmptyObject(); + + /** @var DataObjectSearchServiceInterface $searchService */ + $searchService = $this->tester->grabService(DataObjectSearchServiceInterface::class); + /** @var SearchProviderInterface $searchProvider */ + $searchProvider = $this->tester->grabService(SearchProviderInterface::class); + + $search = $searchProvider->createDataObjectSearch(); + $searchResult = $searchService->search($search); + $this->assertCount(2, $searchResult->getItems()); + + $search = $searchProvider->createDataObjectSearch(); + $search->addModifier(new ExcludeVariantsFilter()); + $searchResult = $searchService->search($search); + $this->assertCount(1, $searchResult->getItems()); + $this->assertNotEquals(AbstractObject::OBJECT_TYPE_VARIANT, $searchResult->getItems()[0]->getType()); + } + public function testIdFilter() { $asset = TestHelper::createImageAsset(); @@ -139,12 +167,43 @@ public function testIntegerFilter() $assetSearch = $searchProvider ->createAssetSearch() - ->addModifier(new IntegerFilter('system_fields.userOwner', $asset->getUserOwner())) - ->addModifier(new IntegerFilter('system_fields.userModification', $asset->getUserModification())) + ->addModifier(new IntegerFilter('userOwner', $asset->getUserOwner())) + ->addModifier(new IntegerFilter('userModification', $asset->getUserModification())) ; $searchResult = $searchService->search($assetSearch); $this->assertCount(1, $searchResult->getItems()); $this->assertEquals($asset->getId(), $searchResult->getItems()[0]->getId()); + + $assetSearch = $searchProvider + ->createAssetSearch() + ->addModifier(new IntegerFilter('userOwner', $asset->getUserOwner(), false)) + ; + $searchResult = $searchService->search($assetSearch); + $this->assertEmpty($searchResult->getItems()); + } + + public function testNumberFilter() + { + $number = 124; + $object = $this->tester->createFullyFledgedObjectUnittest(); + $object2 = $this->tester->createFullyFledgedObjectUnittest(); + $object2->setNumber(420)->save(); + + /** @var DataObjectSearchServiceInterface $searchService */ + $searchService = $this->tester->grabService(DataObjectSearchServiceInterface::class); + /** @var SearchProviderInterface $searchProvider */ + $searchProvider = $this->tester->grabService(SearchProviderInterface::class); + + $search = $searchProvider->createDataObjectSearch(); + $search->addModifier(new NumberFilter('number', $number)); + $searchResult = $searchService->search($search); + $this->assertCount(1, $searchResult->getItems()); + $this->assertEquals($object->getId(), $searchResult->getItems()[0]->getId()); + + $search = $searchProvider->createDataObjectSearch(); + $search->addModifier(new NumberFilter('number', $number, false)); + $searchResult = $searchService->search($search); + $this->assertEmpty($searchResult->getItems()); } public function testBooleanFilter() diff --git a/tests/Functional/Search/Modifier/Filter/FieldTypeFiltersTest.php b/tests/Functional/Search/Modifier/Filter/FieldTypeFiltersTest.php index 0c2e3d5d..bd656cd4 100644 --- a/tests/Functional/Search/Modifier/Filter/FieldTypeFiltersTest.php +++ b/tests/Functional/Search/Modifier/Filter/FieldTypeFiltersTest.php @@ -13,6 +13,7 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\Tests\Functional\Search\Modifier\Filter; use Carbon\Carbon; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\BooleanMultiSelectFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\DateFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\FieldType\MultiSelectFilter; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\Asset\AssetSearchServiceInterface; @@ -82,6 +83,52 @@ public function testMultiSelectFilter() $this->assertIdArrayEquals([], $searchResult->getIds()); } + // tests + public function testBooleanMultiSelectFilter() + { + /** + * @var Unittest $object1 + * @var Unittest $object2 + */ + $object1 = TestHelper::createEmptyObject()->setKey('object1')->setCheckbox(true)->save(); + $object2 = TestHelper::createEmptyObject()->setKey('object2')->save(); + + /** @var DataObjectSearchServiceInterface $searchService */ + $searchService = $this->tester->grabService(DataObjectSearchServiceInterface::class); + /** @var SearchProviderInterface $searchProvider */ + $searchProvider = $this->tester->grabService(SearchProviderInterface::class); + + $elementSearch = $searchProvider + ->createDataObjectSearch() + ->addModifier(new BooleanMultiSelectFilter('checkbox', [true, null])) + ; + $searchResult = $searchService->search($elementSearch); + $this->assertIdArrayEquals([$object1->getId(), $object2->getId()], $searchResult->getIds()); + + $elementSearch = $searchProvider + ->createDataObjectSearch() + ->addModifier(new BooleanMultiSelectFilter('checkbox', [true])) + ; + $searchResult = $searchService->search($elementSearch); + $this->assertCount(1, $searchResult->getIds()); + $this->assertEquals($object1->getId(), $searchResult->getIds()[0]); + + $object2->setCheckbox(false)->save(); + $elementSearch = $searchProvider + ->createDataObjectSearch() + ->addModifier(new BooleanMultiSelectFilter('checkbox', [true, false])) + ; + $searchResult = $searchService->search($elementSearch); + $this->assertIdArrayEquals([$object1->getId(), $object2->getId()], $searchResult->getIds()); + + $elementSearch = $searchProvider + ->createDataObjectSearch() + ->addModifier(new BooleanMultiSelectFilter('checkbox', [true, false], false)) + ; + $searchResult = $searchService->search($elementSearch); + $this->assertIdArrayEquals([], $searchResult->getIds()); + } + public function testDateFilterAsset() { $asset1 = TestHelper::createImageAsset() diff --git a/tests/Unit/Model/DefaultSearch/Query/MultiBoolQueryTest.php b/tests/Unit/Model/DefaultSearch/Query/MultiBoolQueryTest.php new file mode 100644 index 00000000..6b6305d1 --- /dev/null +++ b/tests/Unit/Model/DefaultSearch/Query/MultiBoolQueryTest.php @@ -0,0 +1,184 @@ +getField()); + self::assertSame($terms, $multiBoolQuery->getTerms()); + } + + public function testToArray(): void + { + $field = 'active'; + $terms = [true, false]; + + $multiBoolQuery = new MultiBoolQuery($field, $terms); + + $expected = [ + 'bool' => [ + 'filter' => [ + 'bool' => [ + 'should' => [ + [ + 'bool' => [ + 'must_not' => [ + 'exists' => ['field' => $field], + ], + ], + ], + [ + 'terms' => [$field => $terms], + ], + ], + 'minimum_should_match' => 1, + ], + ], + ], + ]; + + self::assertSame($expected, $multiBoolQuery->toArray(true)); + } + + public function testToArrayWithoutBool(): void + { + $field = 'published'; + $terms = [true]; + + $multiBoolQuery = new MultiBoolQuery($field, $terms); + + $expected = [ + 'filter' => [ + 'bool' => [ + 'should' => [ + [ + 'bool' => [ + 'must_not' => [ + 'exists' => ['field' => $field], + ], + ], + ], + [ + 'terms' => [$field => $terms], + ], + ], + 'minimum_should_match' => 1, + ], + ], + ]; + + self::assertSame($expected, $multiBoolQuery->toArray()); + } + + public function testToArrayAsSubQuery(): void + { + $field = 'enabled'; + $terms = [false]; + + $multiBoolQuery = new MultiBoolQuery($field, $terms); + $expected = [ + 'bool' => [ + 'should' => [ + [ + 'bool' => [ + 'must_not' => [ + 'exists' => ['field' => $field], + ], + ], + ], + [ + 'terms' => [$field => $terms], + ], + ], + 'minimum_should_match' => 1, + ], + ]; + + self::assertSame($expected, $multiBoolQuery->toArrayAsSubQuery()); + } + + public function testWithMultipleTerms(): void + { + $field = 'visibility'; + $terms = [true, false, null]; + + $multiBoolQuery = new MultiBoolQuery($field, $terms); + + self::assertSame($field, $multiBoolQuery->getField()); + self::assertSame($terms, $multiBoolQuery->getTerms()); + + $result = $multiBoolQuery->toArray(true); + self::assertArrayHasKey('bool', $result); + self::assertArrayHasKey('filter', $result['bool']); + self::assertArrayHasKey('bool', $result['bool']['filter']); + self::assertArrayHasKey('should', $result['bool']['filter']['bool']); + self::assertCount(2, $result['bool']['filter']['bool']['should']); + self::assertSame(1, $result['bool']['filter']['bool']['minimum_should_match']); + } + + public function testWithSingleTerm(): void + { + $field = 'is_active'; + $terms = [true]; + + $multiBoolQuery = new MultiBoolQuery($field, $terms); + + $result = $multiBoolQuery->toArray(); + self::assertArrayHasKey('filter', $result); + self::assertArrayHasKey('bool', $result['filter']); + self::assertArrayHasKey('should', $result['filter']['bool']); + + // Should contain BoolExistsQuery and TermsFilter + $shouldConditions = $result['filter']['bool']['should']; + self::assertCount(2, $shouldConditions); + + // The first condition should be BoolExistsQuery (must_not exists) + self::assertArrayHasKey('bool', $shouldConditions[0]); + self::assertArrayHasKey('must_not', $shouldConditions[0]['bool']); + + // The second condition should be TermsFilter + self::assertArrayHasKey('terms', $shouldConditions[1]); + self::assertSame($terms, $shouldConditions[1]['terms'][$field]); + } + + public function testWithStringField(): void + { + $field = 'category.subcategory.name'; + $terms = [true]; + + $multiBoolQuery = new MultiBoolQuery($field, $terms); + + self::assertSame($field, $multiBoolQuery->getField()); + + $result = $multiBoolQuery->toArrayAsSubQuery(); + $shouldConditions = $result['bool']['should']; + + // Verify field is used correctly in both conditions + self::assertSame($field, $shouldConditions[0]['bool']['must_not']['exists']['field']); + self::assertArrayHasKey($field, $shouldConditions[1]['terms']); + } +} diff --git a/tests/Unit/Model/Modifier/Filter/Basic/BooleanMultiSelectFilterTest.php b/tests/Unit/Model/Modifier/Filter/Basic/BooleanMultiSelectFilterTest.php new file mode 100644 index 00000000..77cdacfc --- /dev/null +++ b/tests/Unit/Model/Modifier/Filter/Basic/BooleanMultiSelectFilterTest.php @@ -0,0 +1,100 @@ +expectException(ValueError::class); + $this->expectExceptionMessage('Provided array must contain only boolean or null values. (string given)'); + new BooleanMultiSelectFilter('field', [true, false, 'string']); + } + + public function testBooleanMultiSelectFilterWithInteger(): void + { + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Provided array must contain only boolean or null values. (integer given)'); + new BooleanMultiSelectFilter('field', [true, false, 1]); + } + + public function testGetField(): void + { + $filter = new BooleanMultiSelectFilter('test_field', [true, false]); + $this->assertSame('test_field', $filter->getField()); + } + + public function testGetValues(): void + { + $values = [true, false, null]; + $filter = new BooleanMultiSelectFilter('field', $values); + $this->assertSame($values, $filter->getValues()); + } + + public function testIsPqlFieldNameResolutionEnabledDefault(): void + { + $filter = new BooleanMultiSelectFilter('field', [true, false]); + $this->assertTrue($filter->isPqlFieldNameResolutionEnabled()); + } + + public function testIsPqlFieldNameResolutionEnabledTrue(): void + { + $filter = new BooleanMultiSelectFilter('field', [true, false], true); + $this->assertTrue($filter->isPqlFieldNameResolutionEnabled()); + } + + public function testIsPqlFieldNameResolutionEnabledFalse(): void + { + $filter = new BooleanMultiSelectFilter('field', [true, false], false); + $this->assertFalse($filter->isPqlFieldNameResolutionEnabled()); + } + + public function testValidBooleanValues(): void + { + $filter = new BooleanMultiSelectFilter('field', [true, false]); + $this->assertSame([true, false], $filter->getValues()); + } + + public function testValidBooleanAndNullValues(): void + { + $filter = new BooleanMultiSelectFilter('field', [true, false, null]); + $this->assertSame([true, false, null], $filter->getValues()); + } + + public function testValidSingleBooleanValue(): void + { + $filter = new BooleanMultiSelectFilter('field', [true]); + $this->assertSame([true], $filter->getValues()); + } + + public function testValidSingleNullValue(): void + { + $filter = new BooleanMultiSelectFilter('field', [null]); + $this->assertSame([null], $filter->getValues()); + } + + public function testComplexFieldName(): void + { + $fieldName = 'category.subcategory.active'; + $filter = new BooleanMultiSelectFilter($fieldName, [true, false]); + $this->assertSame($fieldName, $filter->getField()); + } +}