Skip to content

Commit 209df71

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 45d3bd4 commit 209df71

File tree

7 files changed

+333
-178
lines changed

7 files changed

+333
-178
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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
$filterCriteria = [];
30+
if (is_array($parsedFilter)
31+
&& array_key_exists('name', $parsedFilter) && $parsedFilter['name'] === 'FilterGroup'
32+
&& array_key_exists('Filters', $parsedFilter) && is_array($parsedFilter['Filters'])
33+
) {
34+
foreach ($parsedFilter['Filters'] as $filter) {
35+
// anything but AttributeFilters yield a null result
36+
if (array_key_exists('PathFilter', $filter)
37+
|| array_key_exists('PropertyNameFilter', $filter)
38+
|| array_key_exists('IdentifierFilter', $filter)
39+
) {
40+
return null;
41+
}
42+
if (array_key_exists('AttributeFilters', $filter) && is_array ($filter['AttributeFilters'])) {
43+
44+
$nodeTypeNames = NodeTypeNames::createEmpty();
45+
/**
46+
* @var PropertyValueCriteriaInterface[]
47+
*/
48+
$propertyCriteria = [];
49+
foreach ($filter['AttributeFilters'] as $attributeFilter) {
50+
$propertyPath = $attributeFilter['PropertyPath'] ?? null;
51+
$operator = $attributeFilter['Operator'] ?? null;
52+
$operand = $attributeFilter['Operand'] ?? null;
53+
switch ($operator) {
54+
case 'instanceof':
55+
$nodeTypeNames = $nodeTypeNames->withAdditionalNodeTypeName(NodeTypeName::fromString($operand));
56+
break;
57+
case '=':
58+
$propertyCriteria[] = PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand);
59+
break;
60+
case '!=':
61+
$propertyCriteria[] = NegateCriteria::create(PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand));
62+
break;
63+
case '^=':
64+
$propertyCriteria[] = PropertyValueStartsWith::create(PropertyName::fromString($propertyPath), $operand);
65+
break;
66+
case '$=':
67+
$propertyCriteria[] = PropertyValueEndsWith::create(PropertyName::fromString($propertyPath), $operand);
68+
break;
69+
case '*=':
70+
$propertyCriteria[] = PropertyValueContains::create(PropertyName::fromString($propertyPath), $operand);
71+
break;
72+
case '>':
73+
$propertyCriteria[] = PropertyValueGreaterThan::create(PropertyName::fromString($propertyPath), $operand);
74+
break;
75+
case '>=':
76+
$propertyCriteria[] = PropertyValueGreaterThanOrEqual::create(PropertyName::fromString($propertyPath), $operand);
77+
break;
78+
case '<':
79+
$propertyCriteria[] = PropertyValueLessThan::create(PropertyName::fromString($propertyPath), $operand);
80+
break;
81+
case '<=':
82+
$propertyCriteria[] = PropertyValueLessThanOrEqual::create(PropertyName::fromString($propertyPath), $operand);
83+
break;
84+
default:
85+
return null;
86+
}
87+
}
88+
89+
if (count($propertyCriteria) > 1) {
90+
$propertyCriteriaCombined = array_shift($propertyCriteria);
91+
while ($other = array_shift($propertyCriteria)) {
92+
$propertyCriteriaCombined = AndCriteria::create($propertyCriteriaCombined, $other);
93+
}
94+
} elseif (count($propertyCriteria) == 1) {
95+
$propertyCriteriaCombined = $propertyCriteria[0];
96+
} else {
97+
$propertyCriteriaCombined = null;
98+
}
99+
100+
$filterCriteria[] = new NodeFilterCriteria(
101+
NodeTypeCriteria::createWithAllowedNodeTypeNames($nodeTypeNames),
102+
$propertyCriteriaCombined
103+
);
104+
} else {
105+
return null;
106+
}
107+
}
108+
return new NodeFilterCriteriaGroup(...$filterCriteria);
109+
}
110+
return null;
111+
}
112+
}

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)