Skip to content

Commit 43dac6b

Browse files
committed
FEATURE: Improve flowQuery find and children operation
The flowQuery operations find and children are fixed optimized for the new cr to use a combined query for nodetype and property criteria. The number of performed db-queries is the number of contextNodes times the number of filterGroups (comma separated parts) In addition the `find` operation now can also handle single property criteria and does not rely on having an instanceof filter first.
1 parent 67f343e commit 43dac6b

File tree

7 files changed

+349
-130
lines changed

7 files changed

+349
-130
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Neos\ContentRepository\NodeAccess\Filter;
6+
7+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
8+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface;
9+
10+
readonly class NodeFilterCriteria
11+
{
12+
public function __construct(
13+
public ?NodeTypeCriteria $nodeTypeCriteria = null,
14+
public ?PropertyValueCriteriaInterface $propertyValueCriteria = null) {
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Neos\ContentRepository\NodeAccess\Filter;
6+
7+
use Traversable;
8+
9+
/**
10+
* @implements \IteratorAggregate<int, NodeFilterCriteria>
11+
*/
12+
readonly class NodeFilterCriteriaGroup implements \IteratorAggregate
13+
{
14+
/**
15+
* @var array<int, NodeFilterCriteria>
16+
*/
17+
protected array $criteria;
18+
19+
public function __construct(NodeFilterCriteria ...$criteria)
20+
{
21+
$this->criteria = array_values($criteria);
22+
}
23+
24+
/**
25+
* @return Traversable<int, NodeFilterCriteria>
26+
*/
27+
public function getIterator(): Traversable
28+
{
29+
return new \ArrayIterator($this->criteria);
30+
}
31+
32+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Neos\ContentRepository\NodeAccess\Filter;
6+
7+
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
8+
use Neos\ContentRepository\Core\NodeType\NodeTypeNames;
9+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
10+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\AndCriteria;
11+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\NegateCriteria;
12+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueContains;
13+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface;
14+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEndsWith;
15+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEquals;
16+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThan;
17+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThanOrEqual;
18+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThan;
19+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThanOrEqual;
20+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueStartsWith;
21+
use Neos\ContentRepository\Core\SharedModel\Node\PropertyName;
22+
use Neos\Eel\FlowQuery\FizzleParser;
23+
24+
readonly class NodeFilterCriteriaGroupFactory
25+
{
26+
public static function createFromFizzleExpressionString (string $fizzleExpression): ?NodeFilterCriteriaGroup
27+
{
28+
$parsedFilter = FizzleParser::parseFilterGroup($fizzleExpression);
29+
return self::createFromParsedFizzleExpression($parsedFilter);
30+
}
31+
32+
/**
33+
* @param mixed[] $parsedFizzleExpression
34+
*/
35+
public static function createFromParsedFizzleExpression (array $parsedFizzleExpression): ?NodeFilterCriteriaGroup
36+
{
37+
$filterCriteria = [];
38+
if (is_array($parsedFizzleExpression)
39+
&& array_key_exists('name', $parsedFizzleExpression) && $parsedFizzleExpression['name'] === 'FilterGroup'
40+
&& array_key_exists('Filters', $parsedFizzleExpression) && is_array($parsedFizzleExpression['Filters'])
41+
) {
42+
foreach ($parsedFizzleExpression['Filters'] as $filter) {
43+
// anything but AttributeFilters yield a null result
44+
if (array_key_exists('PathFilter', $filter)
45+
|| array_key_exists('PropertyNameFilter', $filter)
46+
|| array_key_exists('IdentifierFilter', $filter)
47+
) {
48+
return null;
49+
}
50+
if (array_key_exists('AttributeFilters', $filter) && is_array ($filter['AttributeFilters'])) {
51+
52+
$allowedNodeTypeNames = NodeTypeNames::createEmpty();
53+
$disallowedNodeTypeNames = NodeTypeNames::createEmpty();
54+
55+
/**
56+
* @var PropertyValueCriteriaInterface[]
57+
*/
58+
$propertyCriteria = [];
59+
foreach ($filter['AttributeFilters'] as $attributeFilter) {
60+
$propertyPath = $attributeFilter['PropertyPath'] ?? null;
61+
$operator = $attributeFilter['Operator'] ?? null;
62+
$operand = $attributeFilter['Operand'] ?? null;
63+
switch ($operator) {
64+
case 'instanceof':
65+
$allowedNodeTypeNames = $allowedNodeTypeNames->withAdditionalNodeTypeName(NodeTypeName::fromString($operand));
66+
break;
67+
case '!instanceof':
68+
$disallowedNodeTypeNames = $disallowedNodeTypeNames->withAdditionalNodeTypeName(NodeTypeName::fromString($operand));
69+
break;
70+
case '=':
71+
$propertyCriteria[] = PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand);
72+
break;
73+
case '!=':
74+
$propertyCriteria[] = NegateCriteria::create(PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand));
75+
break;
76+
case '^=':
77+
$propertyCriteria[] = PropertyValueStartsWith::create(PropertyName::fromString($propertyPath), $operand);
78+
break;
79+
case '$=':
80+
$propertyCriteria[] = PropertyValueEndsWith::create(PropertyName::fromString($propertyPath), $operand);
81+
break;
82+
case '*=':
83+
$propertyCriteria[] = PropertyValueContains::create(PropertyName::fromString($propertyPath), $operand);
84+
break;
85+
case '>':
86+
$propertyCriteria[] = PropertyValueGreaterThan::create(PropertyName::fromString($propertyPath), $operand);
87+
break;
88+
case '>=':
89+
$propertyCriteria[] = PropertyValueGreaterThanOrEqual::create(PropertyName::fromString($propertyPath), $operand);
90+
break;
91+
case '<':
92+
$propertyCriteria[] = PropertyValueLessThan::create(PropertyName::fromString($propertyPath), $operand);
93+
break;
94+
case '<=':
95+
$propertyCriteria[] = PropertyValueLessThanOrEqual::create(PropertyName::fromString($propertyPath), $operand);
96+
break;
97+
default:
98+
return null;
99+
}
100+
}
101+
102+
if (count($propertyCriteria) > 1) {
103+
$propertyCriteriaCombined = array_shift($propertyCriteria);
104+
while ($other = array_shift($propertyCriteria)) {
105+
$propertyCriteriaCombined = AndCriteria::create($propertyCriteriaCombined, $other);
106+
}
107+
} elseif (count($propertyCriteria) == 1) {
108+
$propertyCriteriaCombined = $propertyCriteria[0];
109+
} else {
110+
$propertyCriteriaCombined = null;
111+
}
112+
113+
$filterCriteria[] = new NodeFilterCriteria(
114+
($allowedNodeTypeNames->isEmpty() && $disallowedNodeTypeNames->isEmpty()) ? null : NodeTypeCriteria::create($allowedNodeTypeNames, $disallowedNodeTypeNames),
115+
$propertyCriteriaCombined
116+
);
117+
} else {
118+
return null;
119+
}
120+
}
121+
return new NodeFilterCriteriaGroup(...$filterCriteria);
122+
}
123+
return null;
124+
}
125+
}

Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php

Lines changed: 20 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
*/
1313

1414
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
15+
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
16+
use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes;
1517
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
1618
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
1719
use Neos\ContentRepository\Core\NodeType\NodeTypeNames;
20+
use Neos\ContentRepository\NodeAccess\Filter\NodeFilterCriteriaGroup;
21+
use Neos\ContentRepository\NodeAccess\Filter\NodeFilterCriteriaGroupFactory;
1822
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
1923
use Neos\Eel\FlowQuery\FizzleParser;
2024
use Neos\Eel\FlowQuery\FlowQuery;
@@ -73,15 +77,28 @@ public function evaluate(FlowQuery $flowQuery, array $arguments)
7377
{
7478
$output = [];
7579
$outputNodeAggregateIds = [];
80+
$contextNodes = $flowQuery->getContext();
81+
7682
if (isset($arguments[0]) && !empty($arguments[0])) {
77-
$parsedFilter = FizzleParser::parseFilterGroup($arguments[0]);
78-
if ($this->earlyOptimizationOfFilters($flowQuery, $parsedFilter)) {
83+
// optimized cr query for instanceof and attribute filters
84+
$nodeFilterCriteriaGroup = NodeFilterCriteriaGroupFactory::createFromFizzleExpressionString($arguments[0]);
85+
if ($nodeFilterCriteriaGroup instanceof NodeFilterCriteriaGroup) {
86+
$result = Nodes::createEmpty();
87+
foreach ($nodeFilterCriteriaGroup as $nodeFilterCriteria) {
88+
$findChildNodesFilter = FindChildNodesFilter::create(nodeTypes: $nodeFilterCriteria->nodeTypeCriteria, propertyValue: $nodeFilterCriteria->propertyValueCriteria);
89+
foreach ($contextNodes as $contextNode) {
90+
$subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode);
91+
$descendantNodes = $subgraph->findChildNodes($contextNode->nodeAggregateId, $findChildNodesFilter);
92+
$result = $result->merge($descendantNodes);
93+
}
94+
}
95+
$flowQuery->setContext(iterator_to_array($result->getIterator()));
7996
return;
8097
}
8198
}
8299

83100
/** @var Node $contextNode */
84-
foreach ($flowQuery->getContext() as $contextNode) {
101+
foreach ($contextNodes as $contextNode) {
85102
$childNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode)
86103
->findChildNodes($contextNode->nodeAggregateId, FindChildNodesFilter::create());
87104
foreach ($childNodes as $childNode) {
@@ -97,122 +114,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments)
97114
$flowQuery->pushOperation('filter', $arguments);
98115
}
99116
}
100-
101-
/**
102-
* Optimize for typical use cases, filter by node name and filter
103-
* by NodeType (instanceof). These cases are now optimized and will
104-
* only load the nodes that match the filters.
105-
*
106-
* @param FlowQuery<int,mixed> $flowQuery
107-
* @param array<string,mixed> $parsedFilter
108-
* @return boolean
109-
* @throws \Neos\Eel\Exception
110-
*/
111-
protected function earlyOptimizationOfFilters(FlowQuery $flowQuery, array $parsedFilter)
112-
{
113-
$optimized = false;
114-
$output = [];
115-
$outputNodeAggregateIds = [];
116-
foreach ($parsedFilter['Filters'] as $filter) {
117-
$instanceOfFilters = [];
118-
$attributeFilters = [];
119-
if (isset($filter['AttributeFilters'])) {
120-
foreach ($filter['AttributeFilters'] as $attributeFilter) {
121-
if ($attributeFilter['Operator'] === 'instanceof' && $attributeFilter['Identifier'] === null) {
122-
$instanceOfFilters[] = $attributeFilter;
123-
} else {
124-
$attributeFilters[] = $attributeFilter;
125-
}
126-
}
127-
}
128-
129-
// Only apply optimization if there's a property name filter or an instanceof filter
130-
// or another filter already did optimization
131-
if ((isset($filter['PropertyNameFilter']) || isset($filter['PathFilter']))
132-
|| count($instanceOfFilters) > 0 || $optimized === true) {
133-
$optimized = true;
134-
$filteredOutput = [];
135-
$filteredOutputNodeIdentifiers = [];
136-
// Optimize property name filter if present
137-
if (isset($filter['PropertyNameFilter']) || isset($filter['PathFilter'])) {
138-
$nodePath = $filter['PropertyNameFilter'] ?? $filter['PathFilter'];
139-
$nodePathSegments = explode('/', $nodePath);
140-
/** @var Node $contextNode */
141-
foreach ($flowQuery->getContext() as $contextNode) {
142-
$currentPathSegments = $nodePathSegments;
143-
$resolvedNode = $contextNode;
144-
while (($nodePathSegment = array_shift($currentPathSegments)) && !is_null($resolvedNode)) {
145-
$resolvedNode = $this->contentRepositoryRegistry->subgraphForNode($resolvedNode)
146-
->findChildNodeConnectedThroughEdgeName(
147-
$resolvedNode->nodeAggregateId,
148-
NodeName::fromString($nodePathSegment)
149-
);
150-
}
151-
152-
if (!is_null($resolvedNode) && !isset($filteredOutputNodeIdentifiers[
153-
$resolvedNode->nodeAggregateId->value
154-
])) {
155-
$filteredOutput[] = $resolvedNode;
156-
$filteredOutputNodeIdentifiers[$resolvedNode->nodeAggregateId->value] = true;
157-
}
158-
}
159-
} elseif (count($instanceOfFilters) > 0) {
160-
// Optimize node type filter if present
161-
$allowedNodeTypes = array_map(function ($instanceOfFilter) {
162-
return $instanceOfFilter['Operand'];
163-
}, $instanceOfFilters);
164-
/** @var Node $contextNode */
165-
foreach ($flowQuery->getContext() as $contextNode) {
166-
$childNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode)
167-
->findChildNodes(
168-
$contextNode->nodeAggregateId,
169-
FindChildNodesFilter::create(
170-
nodeTypes: NodeTypeCriteria::create(
171-
NodeTypeNames::fromStringArray($allowedNodeTypes),
172-
NodeTypeNames::createEmpty()
173-
)
174-
)
175-
);
176-
177-
foreach ($childNodes as $childNode) {
178-
if (!isset($filteredOutputNodeIdentifiers[
179-
$childNode->nodeAggregateId->value
180-
])) {
181-
$filteredOutput[] = $childNode;
182-
$filteredOutputNodeIdentifiers[$childNode->nodeAggregateId->value] = true;
183-
}
184-
}
185-
}
186-
}
187-
188-
// Apply attribute filters if present
189-
if (isset($filter['AttributeFilters'])) {
190-
$attributeFilters = array_reduce($filter['AttributeFilters'], function (
191-
$filters,
192-
$attributeFilter
193-
) {
194-
return $filters . $attributeFilter['text'];
195-
});
196-
$filteredFlowQuery = new FlowQuery($filteredOutput);
197-
$filteredFlowQuery->pushOperation('filter', [$attributeFilters]);
198-
$filteredOutput = $filteredFlowQuery->getContext();
199-
}
200-
201-
// Add filtered nodes to output
202-
/** @var Node $filteredNode */
203-
foreach ($filteredOutput as $filteredNode) {
204-
/** @phpstan-ignore-next-line undefined behaviour https://github.com/neos/neos-development-collection/issues/4507#issuecomment-1784123143 */
205-
if (!isset($outputNodeAggregateIds[$filteredNode->nodeAggregateId->value])) {
206-
$output[] = $filteredNode;
207-
}
208-
}
209-
}
210-
}
211-
212-
if ($optimized === true) {
213-
$flowQuery->setContext($output);
214-
}
215-
216-
return $optimized;
217-
}
218117
}

0 commit comments

Comments
 (0)