Skip to content

Commit

Permalink
FEATURE: Improve flowQuery find and children operation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mficzel committed Feb 16, 2024
1 parent 669ba6d commit 4167f9b
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\Filter;

use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface;

readonly class NodeFilterCriteria
{
public function __construct(
public ?NodeTypeCriteria $nodeTypeCriteria = null,
public ?PropertyValueCriteriaInterface $propertyValueCriteria = null) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\Filter;

use Traversable;

/**
* @implements \IteratorAggregate<int, NodeFilterCriteria>
*/
readonly class NodeFilterCriteriaGroup implements \IteratorAggregate
{
/**
* @var array<int, NodeFilterCriteria>
*/
protected array $criteria;

public function __construct(NodeFilterCriteria ...$criteria)
{
$this->criteria = array_values($criteria);
}

/**
* @return Traversable<int, NodeFilterCriteria>
*/
public function getIterator(): Traversable
{
return new \ArrayIterator($this->criteria);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\Filter;

use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\NodeType\NodeTypeNames;
use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\AndCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\NegateCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueContains;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEndsWith;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueEquals;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThan;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueGreaterThanOrEqual;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThan;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThanOrEqual;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueStartsWith;
use Neos\ContentRepository\Core\SharedModel\Node\PropertyName;
use Neos\Eel\FlowQuery\FizzleParser;

readonly class NodeFilterCriteriaGroupFactory
{
public static function createFromFizzleExpressionString (string $fizzleExpression): ?NodeFilterCriteriaGroup
{
// ensure absolute node pathes are ignored
// @todo remove once the parser can handle those
$parts = explode(',', $fizzleExpression);
foreach ($parts as $part) {
if (AbsoluteNodePath::tryFromString($part)) {
return null;
}
}

$parsedFilter = FizzleParser::parseFilterGroup($fizzleExpression);
return self::createFromParsedFizzleExpression($parsedFilter);
}

/**
* @param mixed[] $parsedFizzleExpression
*/
private static function createFromParsedFizzleExpression (array $parsedFizzleExpression): ?NodeFilterCriteriaGroup
{
$filterCriteria = [];
if (is_array($parsedFizzleExpression)
&& array_key_exists('name', $parsedFizzleExpression) && $parsedFizzleExpression['name'] === 'FilterGroup'
&& array_key_exists('Filters', $parsedFizzleExpression) && is_array($parsedFizzleExpression['Filters'])
) {
foreach ($parsedFizzleExpression['Filters'] as $filter) {
// anything but AttributeFilters yield a null result
if (array_key_exists('PathFilter', $filter)
|| array_key_exists('PropertyNameFilter', $filter)
|| array_key_exists('IdentifierFilter', $filter)
) {
return null;
}
if (array_key_exists('AttributeFilters', $filter) && is_array ($filter['AttributeFilters'])) {

$allowedNodeTypeNames = NodeTypeNames::createEmpty();
$disallowedNodeTypeNames = NodeTypeNames::createEmpty();

/**
* @var PropertyValueCriteriaInterface[]
*/
$propertyCriteria = [];
foreach ($filter['AttributeFilters'] as $attributeFilter) {
$propertyPath = $attributeFilter['PropertyPath'] ?? null;
$operator = $attributeFilter['Operator'] ?? null;
$operand = $attributeFilter['Operand'] ?? null;
switch ($operator) {
case 'instanceof':
$allowedNodeTypeNames = $allowedNodeTypeNames->withAdditionalNodeTypeName(NodeTypeName::fromString($operand));
break;
case '!instanceof':
$disallowedNodeTypeNames = $disallowedNodeTypeNames->withAdditionalNodeTypeName(NodeTypeName::fromString($operand));
break;
case '=':
$propertyCriteria[] = PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand, true);
break;
case '!=':
$propertyCriteria[] = NegateCriteria::create(PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand, true));
break;
case '^=':
$propertyCriteria[] = PropertyValueStartsWith::create(PropertyName::fromString($propertyPath), $operand, true);
break;
case '$=':
$propertyCriteria[] = PropertyValueEndsWith::create(PropertyName::fromString($propertyPath), $operand, true);
break;
case '*=':
$propertyCriteria[] = PropertyValueContains::create(PropertyName::fromString($propertyPath), $operand, true);
break;
case '=~':
$propertyCriteria[] = PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand, false);
break;
case '!=~':
$propertyCriteria[] = NegateCriteria::create(PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand, false));
break;
case '^=~':
$propertyCriteria[] = PropertyValueStartsWith::create(PropertyName::fromString($propertyPath), $operand, false);
break;
case '$=~':
$propertyCriteria[] = PropertyValueEndsWith::create(PropertyName::fromString($propertyPath), $operand, false);
break;
case '*=~':
$propertyCriteria[] = PropertyValueContains::create(PropertyName::fromString($propertyPath), $operand, false);
break;
case '>':
$propertyCriteria[] = PropertyValueGreaterThan::create(PropertyName::fromString($propertyPath), $operand);
break;
case '>=':
$propertyCriteria[] = PropertyValueGreaterThanOrEqual::create(PropertyName::fromString($propertyPath), $operand);
break;
case '<':
$propertyCriteria[] = PropertyValueLessThan::create(PropertyName::fromString($propertyPath), $operand);
break;
case '<=':
$propertyCriteria[] = PropertyValueLessThanOrEqual::create(PropertyName::fromString($propertyPath), $operand);
break;
default:
return null;
}
}

if (count($propertyCriteria) > 1) {
$propertyCriteriaCombined = array_shift($propertyCriteria);
while ($other = array_shift($propertyCriteria)) {
$propertyCriteriaCombined = AndCriteria::create($propertyCriteriaCombined, $other);
}
} elseif (count($propertyCriteria) == 1) {
$propertyCriteriaCombined = $propertyCriteria[0];
} else {
$propertyCriteriaCombined = null;
}

$filterCriteria[] = new NodeFilterCriteria(
($allowedNodeTypeNames->isEmpty() && $disallowedNodeTypeNames->isEmpty()) ? null : NodeTypeCriteria::create($allowedNodeTypeNames, $disallowedNodeTypeNames),
$propertyCriteriaCombined
);
} else {
return null;
}
}
return new NodeFilterCriteriaGroup(...$filterCriteria);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@

use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\NodeType\NodeTypeNames;
use Neos\ContentRepository\NodeAccess\Filter\NodeFilterCriteriaGroup;
use Neos\ContentRepository\NodeAccess\Filter\NodeFilterCriteriaGroupFactory;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Eel\FlowQuery\FizzleParser;
use Neos\Eel\FlowQuery\FlowQuery;
Expand Down Expand Up @@ -75,6 +77,22 @@ public function evaluate(FlowQuery $flowQuery, array $arguments)
$output = [];
$outputNodeAggregateIds = [];
if (isset($arguments[0]) && !empty($arguments[0])) {
// optimized cr query for instanceof and attribute filters
$nodeFilterCriteriaGroup = NodeFilterCriteriaGroupFactory::createFromFizzleExpressionString($arguments[0]);
if ($nodeFilterCriteriaGroup instanceof NodeFilterCriteriaGroup) {
$result = Nodes::createEmpty();
foreach ($nodeFilterCriteriaGroup as $nodeFilterCriteria) {
$findChildNodesFilter = FindChildNodesFilter::create(nodeTypes: $nodeFilterCriteria->nodeTypeCriteria, propertyValue: $nodeFilterCriteria->propertyValueCriteria);
foreach ($flowQuery->getContext() as $contextNode) {
$subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode);
$descendantNodes = $subgraph->findChildNodes($contextNode->nodeAggregateId, $findChildNodesFilter);
$result = $result->merge($descendantNodes);
}
}
$flowQuery->setContext(iterator_to_array($result->getIterator()));
return;
}

$parsedFilter = FizzleParser::parseFilterGroup($arguments[0]);
if ($this->earlyOptimizationOfFilters($flowQuery, $parsedFilter)) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\NodeAccess\Filter\NodeFilterCriteriaGroup;
use Neos\ContentRepository\NodeAccess\Filter\NodeFilterCriteriaGroupFactory;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Eel\FlowQuery\FizzleParser;
use Neos\Eel\FlowQuery\FlowQuery;
Expand Down Expand Up @@ -63,6 +66,8 @@
*/
class FindOperation extends AbstractOperation
{
use CreateNodeHashTrait;

/**
* {@inheritdoc}
*
Expand Down Expand Up @@ -120,6 +125,31 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void

$selectorAndFilter = $arguments[0];

// optimized cr query for instanceof and attribute filters
$nodeFilterCriteriaGroup = NodeFilterCriteriaGroupFactory::createFromFizzleExpressionString($selectorAndFilter);
if ($nodeFilterCriteriaGroup instanceof NodeFilterCriteriaGroup) {
$result = Nodes::createEmpty();
foreach ($nodeFilterCriteriaGroup as $nodeFilterCriteria) {
$findDescendantNodesFilter = FindDescendantNodesFilter::create(nodeTypes: $nodeFilterCriteria->nodeTypeCriteria, propertyValue: $nodeFilterCriteria->propertyValueCriteria);
foreach ($contextNodes as $contextNode) {
$subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode);
$descendantNodes = $subgraph->findDescendantNodes($contextNode->nodeAggregateId, $findDescendantNodesFilter);
$result = $result->merge($descendantNodes);
}
}

$nodesByHash = [];
foreach ($result as $node) {
$hash = $this->createNodeHash($node);
if (!array_key_exists($hash, $nodesByHash)) {
$nodesByHash[$hash] = $node;
}
}
$flowQuery->setContext(array_values($nodesByHash));
return;
}


$firstContextNode = reset($contextNodes);
assert($firstContextNode instanceof Node);

Expand Down
Loading

0 comments on commit 4167f9b

Please sign in to comment.