From 2b66844b0d4ee4b685107e7fe43d4806b25f00f6 Mon Sep 17 00:00:00 2001 From: Enes Erk Date: Wed, 28 Sep 2022 11:30:41 +0200 Subject: [PATCH] FEATURE: Add Node-Type Filter - Creates a method which allows to create a nodetype filter to include and exclude specific nodetypes - Adds a test for the new filter function - Adjusts existing tests to match the new circumstances --- Classes/Eel/ElasticSearchQueryBuilder.php | 50 +++++++++++++++++++ Configuration/Testing/NodeTypes.yaml | 4 ++ README.md | 35 ++++++------- .../Functional/Eel/ElasticSearchQueryTest.php | 20 ++++++-- .../ContentRepositoryNodeCreationTrait.php | 8 +++ 5 files changed, 96 insertions(+), 21 deletions(-) diff --git a/Classes/Eel/ElasticSearchQueryBuilder.php b/Classes/Eel/ElasticSearchQueryBuilder.php index 6c9c4917..dbd2963e 100644 --- a/Classes/Eel/ElasticSearchQueryBuilder.php +++ b/Classes/Eel/ElasticSearchQueryBuilder.php @@ -131,6 +131,56 @@ public function nodeType(string $nodeType): QueryBuilderInterface return $this->queryFilter('term', ['neos_type_and_supertypes' => $nodeType]); } + /** + * Filter multiple node types + * + * @param array $expectedNodeTypes NodeTypes that should be expected + * @param array $excludedNodeTypes NodeTypes that should be excluded + * @return ElasticSearchQueryBuilder + * @throws QueryBuildingException + * @api + */ + public function nodeTypeFilter(array $expectedNodeTypes, array $excludedNodeTypes = []): QueryBuilderInterface + { + $excludeTerms = []; + foreach ($excludedNodeTypes as $nodeType) { + $excludeTerms[] = [ + 'term' => [ + 'neos_type_and_supertypes' => $nodeType + ] + ]; + } + if (!empty($excludeTerms)) { + $this->request->queryFilter( + 'bool', + [ + 'should' => $excludeTerms + ], + 'must_not' + ); + } + + $includeTerms = []; + foreach ($expectedNodeTypes as $nodeType) { + $includeTerms[] = [ + 'term' => [ + 'neos_type_and_supertypes' => $nodeType + ] + ]; + } + if (!empty($includeTerms)) { + $this->request->queryFilter( + 'bool', + [ + 'should' => $includeTerms + ], + 'must' + ); + } + + return $this; + } + /** * Sort descending by $propertyName * diff --git a/Configuration/Testing/NodeTypes.yaml b/Configuration/Testing/NodeTypes.yaml index 9d4b19bb..dee5c4b9 100644 --- a/Configuration/Testing/NodeTypes.yaml +++ b/Configuration/Testing/NodeTypes.yaml @@ -25,6 +25,10 @@ main: type: 'Neos.Neos:ContentCollection' +'Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document2': + superTypes: + 'Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document': true + 'Flowpack.ElasticSearch.ContentRepositoryAdaptor:Content': superTypes: 'Neos.Neos:Content': true diff --git a/README.md b/README.md index 63ab9150..a62f2b52 100644 --- a/README.md +++ b/README.md @@ -427,23 +427,24 @@ Furthermore, the following operators are supported: As **value**, the following methods accept a simple type, a node object or a DateTime object. -| Query Operator | Description | -|----------------|-------------| -|`nodeType('Your.Node:Type')` |Filters on the given NodeType| -|`exactMatch('propertyName', value)` |Supports simple types: `exactMatch('tag', 'foo')`, or node references: `exactMatch('author', authorNode)`| -|`exclude('propertyName', value)` |Excludes results by property - the negation of exactMatch. -|`greaterThan('propertyName', value, [clauseType])` |Range filter with property values greater than the given value| -|`greaterThanOrEqual('propertyName', value, [clauseType])`|Range filter with property values greater than or equal to the given value| -|`lessThan('propertyName', value, [clauseType])` |Range filter with property values less than the given value| -|`lessThanOrEqual('propertyName', value, [clauseType])`|Range filter with property values less than or equal to the given value| -|`sortAsc('propertyName')` / `sortDesc('propertyName')`|Can also be used multiple times, e.g. `sortAsc('tag').sortDesc('date')` will first sort by tag ascending, and then by date descending.| -|`limit(5)` |Only return five results. If not specified, the default limit by Elasticsearch applies (which is at 10 by default)| -|`from(5)` |Return the results starting from the 6th one| -|`prefix('propertyName', 'prefix', [clauseType])` |Adds a prefix filter on the given field with the given prefix| -|`geoDistance(propertyName, geoPoint, distance, [clauseType])`. |Filters documents that include only hits that exists within a specific distance from a geo point.| -|`fulltext('searchWord', options)` |Does a query_string query on the Fulltext index using the searchword and additional [options](https://www.elastic.co/guide/en/elasticsearch/reference/7.6/query-dsl-query-string-query.html) to the query_string. Recommendation: **use simpleQueryStringFulltext instead, as it yields better results and is more tolerant to user input**.| -|`simpleQueryStringFulltext('searchWord', options)` |Does a simple_query_string query on the Fulltext index using the searchword and additional [options](https://www.elastic.co/guide/en/elasticsearch/reference/8.3/query-dsl-simple-query-string-query.html) to the simple_query_string. Supports phrase matching like `"firstname lastname"` and tolerates broken input without exceptions (in contrast to `fulltext()`)| -|`highlight(fragmentSize, fragmentCount, noMatchSize, field)` |Configure result highlighting for every fulltext field individually| +| Query Operator | Description | +|-----------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `nodeType('Your.Node:Type')` | Filters on the given NodeType | +| `nodeTypeFilter(['Your.Node:Type'],['Your.ExcludedNode:Type'])` | Filters multiple NodeTypes | +| `exactMatch('propertyName', value)` | Supports simple types: `exactMatch('tag', 'foo')`, or node references: `exactMatch('author', authorNode)` | +| `exclude('propertyName', value)` | Excludes results by property - the negation of exactMatch. | +| `greaterThan('propertyName', value, [clauseType])` | Range filter with property values greater than the given value | +| `greaterThanOrEqual('propertyName', value, [clauseType])` | Range filter with property values greater than or equal to the given value | +| `lessThan('propertyName', value, [clauseType])` | Range filter with property values less than the given value | +| `lessThanOrEqual('propertyName', value, [clauseType])` | Range filter with property values less than or equal to the given value | +| `sortAsc('propertyName')` / `sortDesc('propertyName')` | Can also be used multiple times, e.g. `sortAsc('tag').sortDesc('date')` will first sort by tag ascending, and then by date descending. | +| `limit(5)` | Only return five results. If not specified, the default limit by Elasticsearch applies (which is at 10 by default) | +| `from(5)` | Return the results starting from the 6th one | +| `prefix('propertyName', 'prefix', [clauseType])` | Adds a prefix filter on the given field with the given prefix | +| `geoDistance(propertyName, geoPoint, distance, [clauseType])`. | Filters documents that include only hits that exists within a specific distance from a geo point. | +| `fulltext('searchWord', options)` | Does a query_string query on the Fulltext index using the searchword and additional [options](https://www.elastic.co/guide/en/elasticsearch/reference/7.6/query-dsl-query-string-query.html) to the query_string. Recommendation: **use simpleQueryStringFulltext instead, as it yields better results and is more tolerant to user input**. | +| `simpleQueryStringFulltext('searchWord', options)` | Does a simple_query_string query on the Fulltext index using the searchword and additional [options](https://www.elastic.co/guide/en/elasticsearch/reference/8.3/query-dsl-simple-query-string-query.html) to the simple_query_string. Supports phrase matching like `"firstname lastname"` and tolerates broken input without exceptions (in contrast to `fulltext()`) | +| `highlight(fragmentSize, fragmentCount, noMatchSize, field)` | Configure result highlighting for every fulltext field individually | ## Search Result Highlighting diff --git a/Tests/Functional/Eel/ElasticSearchQueryTest.php b/Tests/Functional/Eel/ElasticSearchQueryTest.php index 021504b4..77db74cf 100644 --- a/Tests/Functional/Eel/ElasticSearchQueryTest.php +++ b/Tests/Functional/Eel/ElasticSearchQueryTest.php @@ -99,6 +99,18 @@ public function filterByNodeType(): void ->log($this->getLogMessagePrefix(__METHOD__)) ->nodeType('Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document') ->count(); + static::assertEquals(6, $resultCount); + } + + /** + * @test + */ + public function filterByNodeTypes(): void + { + $resultCount = $this->getQueryBuilder() + ->log($this->getLogMessagePrefix(__METHOD__)) + ->nodeTypeFilter(['Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document'], ['Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document2']) + ->count(); static::assertEquals(4, $resultCount); } @@ -142,7 +154,7 @@ public function limitDoesNotImpactCount(): void ->limit(1); $resultCount = $query->count(); - static::assertEquals(4, $resultCount, 'Asserting the count query returns the total count.'); + static::assertEquals(6, $resultCount, 'Asserting the count query returns the total count.'); } /** @@ -174,7 +186,7 @@ public function fieldBasedAggregations(): void ->getAggregations(); static::assertArrayHasKey($aggregationTitle, $result); - static::assertCount(3, $result[$aggregationTitle]['buckets']); + static::assertCount(5, $result[$aggregationTitle]['buckets']); $expectedChickenBucket = [ 'key' => 'chicken', @@ -247,8 +259,8 @@ public function nodesWillBeSortedDesc(): void /** @var QueryResultInterface $result $node */ static::assertInstanceOf(QueryResultInterface::class, $result); - static::assertCount(4, $result, 'The result should have 3 items'); - static::assertEquals(4, $result->count(), 'Count should be 3'); + static::assertCount(6, $result, 'The result should have 6 items'); + static::assertEquals(6, $result->count(), 'Count should be 6'); $node = $result->getFirst(); diff --git a/Tests/Functional/Traits/ContentRepositoryNodeCreationTrait.php b/Tests/Functional/Traits/ContentRepositoryNodeCreationTrait.php index f2bc888e..ab53da64 100644 --- a/Tests/Functional/Traits/ContentRepositoryNodeCreationTrait.php +++ b/Tests/Functional/Traits/ContentRepositoryNodeCreationTrait.php @@ -54,6 +54,14 @@ protected function createNodesForNodeSearchTest(): void $newDocumentNode3->setProperty('title', 'egg'); $newDocumentNode3->setProperty('title_analyzed', 'egg'); + $newDocumentNode4 = $this->siteNode->createNode('test-node-4', $this->nodeTypeManager->getNodeType('Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document2')); + $newDocumentNode4->setProperty('title', 'tiger'); + $newDocumentNode4->setProperty('title_analyzed', 'tiger'); + + $newDocumentNode5 = $this->siteNode->createNode('test-node-5', $this->nodeTypeManager->getNodeType('Flowpack.ElasticSearch.ContentRepositoryAdaptor:Document2')); + $newDocumentNode5->setProperty('title', 'elephant'); + $newDocumentNode5->setProperty('title_analyzed', 'elephant'); + $dimensionContext = $this->contextFactory->create([ 'workspaceName' => 'live', 'dimensions' => ['language' => ['de']]