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 authored and mhsdesign committed Feb 3, 2024
1 parent d182715 commit a4d7230
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 118 deletions.
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,125 @@
<?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\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
{
$parsedFilter = FizzleParser::parseFilterGroup($fizzleExpression);
return self::createFromParsedFizzleExpression($parsedFilter);
}

/**
* @param mixed[] $parsedFizzleExpression
*/
public 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);
break;
case '!=':
$propertyCriteria[] = NegateCriteria::create(PropertyValueEquals::create(PropertyName::fromString($propertyPath), $operand));
break;
case '^=':
$propertyCriteria[] = PropertyValueStartsWith::create(PropertyName::fromString($propertyPath), $operand);
break;
case '$=':
$propertyCriteria[] = PropertyValueEndsWith::create(PropertyName::fromString($propertyPath), $operand);
break;
case '*=':
$propertyCriteria[] = PropertyValueContains::create(PropertyName::fromString($propertyPath), $operand);
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 @@ -12,10 +12,13 @@
*/

use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes;
use Neos\ContentRepository\Core\SharedModel\Node\NodeName;
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 @@ -74,15 +77,28 @@ public function evaluate(FlowQuery $flowQuery, array $arguments)
{
$output = [];
$outputNodeAggregateIds = [];
$contextNodes = $flowQuery->getContext();

if (isset($arguments[0]) && !empty($arguments[0])) {
$parsedFilter = FizzleParser::parseFilterGroup($arguments[0]);
if ($this->earlyOptimizationOfFilters($flowQuery, $parsedFilter)) {
// 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 ($contextNodes 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;
}
}

/** @var Node $contextNode */
foreach ($flowQuery->getContext() as $contextNode) {
foreach ($contextNodes as $contextNode) {
$childNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode)
->findChildNodes($contextNode->nodeAggregateId, FindChildNodesFilter::create());
foreach ($childNodes as $childNode) {
Expand All @@ -98,117 +114,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments)
$flowQuery->pushOperation('filter', $arguments);
}
}

/**
* Optimize for typical use cases, filter by node name and filter
* by NodeType (instanceof). These cases are now optimized and will
* only load the nodes that match the filters.
*
* @param FlowQuery<int,mixed> $flowQuery
* @param array<string,mixed> $parsedFilter
* @return boolean
* @throws \Neos\Eel\Exception
*/
protected function earlyOptimizationOfFilters(FlowQuery $flowQuery, array $parsedFilter)
{
$optimized = false;
$output = [];
$outputNodeAggregateIds = [];
foreach ($parsedFilter['Filters'] as $filter) {
$instanceOfFilters = [];
$attributeFilters = [];
if (isset($filter['AttributeFilters'])) {
foreach ($filter['AttributeFilters'] as $attributeFilter) {
if ($attributeFilter['Operator'] === 'instanceof' && $attributeFilter['Identifier'] === null) {
$instanceOfFilters[] = $attributeFilter;
} else {
$attributeFilters[] = $attributeFilter;
}
}
}

// Only apply optimization if there's a property name filter or an instanceof filter
// or another filter already did optimization
if ((isset($filter['PropertyNameFilter']) || isset($filter['PathFilter']))
|| count($instanceOfFilters) > 0 || $optimized === true) {
$optimized = true;
$filteredOutput = [];
$filteredOutputNodeIdentifiers = [];
// Optimize property name filter if present
if (isset($filter['PropertyNameFilter']) || isset($filter['PathFilter'])) {
$nodePath = $filter['PropertyNameFilter'] ?? $filter['PathFilter'];
/** @var Node $contextNode */
foreach ($flowQuery->getContext() as $contextNode) {
$resolvedNode = $this->contentRepositoryRegistry->subgraphForNode($contextNode)
->findNodeByPath(
NodePath::fromString($nodePath),
$contextNode->nodeAggregateId,
);

if (!is_null($resolvedNode) && !isset($filteredOutputNodeIdentifiers[
$resolvedNode->nodeAggregateId->value
])) {
$filteredOutput[] = $resolvedNode;
$filteredOutputNodeIdentifiers[$resolvedNode->nodeAggregateId->value] = true;
}
}
} elseif (count($instanceOfFilters) > 0) {
// Optimize node type filter if present
$allowedNodeTypes = array_map(function ($instanceOfFilter) {
return $instanceOfFilter['Operand'];
}, $instanceOfFilters);
/** @var Node $contextNode */
foreach ($flowQuery->getContext() as $contextNode) {
$childNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode)
->findChildNodes(
$contextNode->nodeAggregateId,
FindChildNodesFilter::create(
nodeTypes: NodeTypeCriteria::create(
NodeTypeNames::fromStringArray($allowedNodeTypes),
NodeTypeNames::createEmpty()
)
)
);

foreach ($childNodes as $childNode) {
if (!isset($filteredOutputNodeIdentifiers[
$childNode->nodeAggregateId->value
])) {
$filteredOutput[] = $childNode;
$filteredOutputNodeIdentifiers[$childNode->nodeAggregateId->value] = true;
}
}
}
}

// Apply attribute filters if present
if (isset($filter['AttributeFilters'])) {
$attributeFilters = array_reduce($filter['AttributeFilters'], function (
$filters,
$attributeFilter
) {
return $filters . $attributeFilter['text'];
});
$filteredFlowQuery = new FlowQuery($filteredOutput);
$filteredFlowQuery->pushOperation('filter', [$attributeFilters]);
$filteredOutput = iterator_to_array($filteredFlowQuery);
}

// Add filtered nodes to output
/** @var Node $filteredNode */
foreach ($filteredOutput as $filteredNode) {
/** @phpstan-ignore-next-line undefined behaviour https://github.com/neos/neos-development-collection/issues/4507#issuecomment-1784123143 */
if (!isset($outputNodeAggregateIds[$filteredNode->nodeAggregateId->value])) {
$output[] = $filteredNode;
}
}
}
}

if ($optimized === true) {
$flowQuery->setContext($output);
}

return $optimized;
}
}
Loading

0 comments on commit a4d7230

Please sign in to comment.