Skip to content

Commit f2391fc

Browse files
committed
FEATURE: Add a method to calculate the cache lifetime of all nodes of a query
1 parent ad977d9 commit f2391fc

3 files changed

Lines changed: 137 additions & 10 deletions

File tree

Classes/Eel/ElasticSearchQueryBuilder.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Neos\Eel\ProtectedContextAwareInterface;
2323
use Neos\Flow\Annotations as Flow;
2424
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
25+
use Neos\Flow\Utility\Now;
26+
use Neos\Utility\Arrays;
2527

2628
/**
2729
* Query Builder for ElasticSearch Queries
@@ -73,6 +75,12 @@ class ElasticSearchQueryBuilder implements QueryBuilderInterface, ProtectedConte
7375
*/
7476
protected $from;
7577

78+
/**
79+
* @Flow\Inject(lazy=false)
80+
* @var Now
81+
*/
82+
protected $now;
83+
7684
/**
7785
* This (internal) array stores, for the last search request, a mapping from Node Identifiers
7886
* to the full Elasticsearch Hit which was returned.
@@ -181,7 +189,7 @@ public function sort($configuration)
181189
*/
182190
public function limit($limit)
183191
{
184-
if (!$limit) {
192+
if ($limit === null) {
185193
return $this;
186194
}
187195

@@ -419,7 +427,7 @@ public function fieldBasedAggregation($name, $field, $type = 'terms', $parentPat
419427
* @return $this
420428
* @throws QueryBuildingException
421429
*/
422-
public function aggregation($name, array $aggregationDefinition, $parentPath = '')
430+
public function aggregation(string $name, array $aggregationDefinition, $parentPath = ''): ElasticSearchQueryBuilder
423431
{
424432
$this->request->aggregation($name, $aggregationDefinition, $parentPath);
425433

@@ -769,6 +777,68 @@ protected function convertHitsToNodes(array $hits)
769777
return array_values($nodes);
770778
}
771779

780+
/**
781+
* This method will get the minimum of all allowed cache lifetimes for the
782+
* nodes that would result from the current defined query. This means it will evaluate to the nearest future value of the
783+
* hiddenBeforeDateTime or hiddenAfterDateTime properties of all nodes in the result.
784+
*
785+
* @return int
786+
* @throws QueryBuildingException
787+
* @throws \Flowpack\ElasticSearch\Exception
788+
*/
789+
public function cacheLifetime(): int
790+
{
791+
$this->request->aggregation('minHiddenBeforeDateTime', [
792+
'min' => [
793+
'field' => '_hiddenBeforeDateTime'
794+
]
795+
]);
796+
797+
$this->request->aggregation('minHiddenAfterDateTime', [
798+
'min' => [
799+
'field' => '_hiddenAfterDateTime'
800+
]
801+
]);
802+
803+
$this->request->size(0);
804+
805+
$requestArray = $this->request->toArray();
806+
807+
$mustNot = Arrays::getValueByPath($requestArray, 'query.bool.filter.bool.must_not');
808+
809+
/* Remove exclusion of not yet visible nodes
810+
- range:
811+
_hiddenBeforeDateTime:
812+
gt: now
813+
*/
814+
unset($mustNot[1]);
815+
816+
$requestArray = Arrays::setValueByPath($requestArray, 'query.bool.filter.bool.must_not', array_values($mustNot));
817+
$response = $this->elasticSearchClient->getIndex()->request('GET', '/_search', [], $requestArray);
818+
819+
$result = $response->getTreatedContent();
820+
821+
$convertDateResultToTimestamp = function (array $dateResult): int {
822+
if (!isset($dateResult['value_as_string'])) {
823+
return 0;
824+
}
825+
return (new \DateTime($dateResult['value_as_string']))->getTimestamp();
826+
};
827+
828+
$minTimestamps = array_filter([
829+
$convertDateResultToTimestamp(Arrays::getValueByPath($result, 'aggregations.minHiddenBeforeDateTime')),
830+
$convertDateResultToTimestamp(Arrays::getValueByPath($result, 'aggregations.minHiddenAfterDateTime'))
831+
]);
832+
833+
if (empty($minTimestamps)) {
834+
return 0;
835+
}
836+
837+
$minTimestamp = min($minTimestamps);
838+
839+
return $minTimestamp > $this->now->getTimestamp() ? $minTimestamp - $this->now->getTimestamp() : 0;
840+
}
841+
772842
/**
773843
* Proxy method to access the public method of the Request object
774844
*

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ of an array or a string. Check the documentation \Neos\Utility\Arrays::getValueB
402402
403403
**Important notice**
404404
405-
The ViewHelper GetHitArrayForNode will return the raw hit result array. The path poperty allows you to access some
405+
The ViewHelper GetHitArrayForNode will return the raw hit result array. The path property allows you to access some
406406
specific data like the the sort data. If there is only one value for your path the value will be returned.
407407
If there is more data the full array will be returned by GetHitArrayForNode-VH. So you might have to use the
408408
ForViewHelper to access your sort values.
@@ -475,6 +475,29 @@ suggestionsQueryDefinition = Neos.Fusion:RawArray {
475475
suggestions = ${Search.query(site)...suggestions('my_suggestions', this.suggestionsQueryDefinition)}
476476
```
477477
478+
## Calculate the maximum cache time
479+
480+
In order to set the maximum cache time of a fusion prototype that renders nodes fetched by `Search()`,
481+
the nearest future value of the hiddenBeforeDateTime or hiddenAfterDateTime properties of all nodes in the result needs to be calculated.
482+
483+
prototype(Acme.Blog:Listing) < prototype(Neos.Fusion:Collection) {
484+
@context.searchQuery = ${Search.query(site).nodeType('Acme.Blog:Post')}
485+
486+
collection = ${searchQuery.execute()}
487+
itemName = 'node'
488+
itemRenderer = Acme.Blog:Post
489+
490+
@cache {
491+
mode = 'cached'
492+
maximumLifetime = ${searchQuery.cacheLifetime()}
493+
494+
entryTags {
495+
map = ${'NodeType_Acme.Blog:Post'}
496+
}
497+
}
498+
}
499+
500+
478501
## Advanced: Configuration of Indexing
479502
480503
**The default configuration supports most usecases and often may not need to be touched, as this package comes

Tests/Functional/Eel/ElasticSearchQueryTest.php

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Command\NodeIndexCommandController;
1515
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Eel\ElasticSearchQueryBuilder;
1616
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Eel\ElasticSearchQueryResult;
17+
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Exception\QueryBuildingException;
18+
use Neos\ContentRepository\Exception\NodeExistsException;
19+
use Neos\ContentRepository\Exception\NodeTypeNotFoundException;
20+
use Neos\Flow\Mvc\Exception\StopActionException;
1721
use Neos\Flow\Persistence\QueryResultInterface;
1822
use Neos\Flow\Tests\FunctionalTestCase;
1923
use Neos\ContentRepository\Domain\Model\NodeInterface;
@@ -339,6 +343,22 @@ public function sortValuesAreReturned()
339343
}
340344
}
341345

346+
/**
347+
* @test
348+
* @throws QueryBuildingException
349+
* @throws \Flowpack\ElasticSearch\Exception
350+
*/
351+
public function cacheLifetimeIsCalculatedCorrectly()
352+
{
353+
$cacheLifetime = $this->getQueryBuilder()
354+
->log($this->getLogMessagePrefix(__METHOD__))
355+
->nodeType('Neos.NodeTypes:Text')
356+
->sortAsc('title')
357+
->cacheLifetime();
358+
359+
$this->assertEquals(3600, $cacheLifetime);
360+
}
361+
342362
/**
343363
* @return string
344364
*/
@@ -349,24 +369,36 @@ protected function getLogMessagePrefix($method)
349369

350370
/**
351371
* Creates some sample nodes to run tests against
372+
* @throws NodeExistsException
373+
* @throws NodeTypeNotFoundException
374+
* @throws StopActionException
352375
*/
353376
protected function createNodesForNodeSearchTest()
354377
{
355378
$newDocumentNode1 = $this->siteNode->createNode('test-node-1', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Page'));
356379
$newDocumentNode1->setProperty('title', 'chicken');
357380
$newDocumentNode1->setProperty('title_analyzed', 'chicken');
358381

359-
$newContentNode = $newDocumentNode1->getNode('main')->createNode('document-1-text-1', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Text'));
360-
$newContentNode->setProperty('text', 'A Scout smiles and whistles under all circumstances.');
382+
$newContentNode1 = $newDocumentNode1->getNode('main')->createNode('document-1-text-1', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Text'));
383+
$newContentNode1->setProperty('text', 'A Scout smiles and whistles under all circumstances.');
361384

362385
$newDocumentNode2 = $this->siteNode->createNode('test-node-2', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Page'));
363386
$newDocumentNode2->setProperty('title', 'chicken');
364387
$newDocumentNode2->setProperty('title_analyzed', 'chicken');
365388

389+
// Nodes for cacheLifetime test
390+
$newContentNode2 = $newDocumentNode2->getNode('main')->createNode('document-2-text-1', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Text'));
391+
$newContentNode2->setProperty('text', 'Hidden after 2030-01-01');
392+
$newContentNode2->setHiddenAfterDateTime(new \DateTime('@1532635200'));
393+
$newContentNode3 = $newDocumentNode2->getNode('main')->createNode('document-2-text-2', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Text'));
394+
$newContentNode3->setProperty('text', 'Hidden before 2018-07-18');
395+
$newContentNode3->setHiddenBeforeDateTime(new \DateTime('@1532631600'));
396+
366397
$newDocumentNode3 = $this->siteNode->createNode('test-node-3', $this->nodeTypeManager->getNodeType('Neos.NodeTypes:Page'));
367398
$newDocumentNode3->setProperty('title', 'egg');
368399
$newDocumentNode3->setProperty('title_analyzed', 'egg');
369400

401+
370402
$dimensionContext = $this->contextFactory->create([
371403
'workspaceName' => 'live',
372404
'dimensions' => ['language' => ['de']]
@@ -387,13 +419,15 @@ protected function createNodesForNodeSearchTest()
387419
}
388420

389421
/**
390-
* @return QueryBuilderInterface
422+
* @return ElasticSearchQueryBuilder
423+
* @throws QueryBuildingException
424+
* @throws \Exception
391425
*/
392-
protected function getQueryBuilder()
426+
protected function getQueryBuilder(): ElasticSearchQueryBuilder
393427
{
394-
/** @var ElasticSearchQueryBuilder $query */
395-
$query = $this->objectManager->get(ElasticSearchQueryBuilder::class);
428+
$elasticSearchQueryBuilder = $this->objectManager->get(ElasticSearchQueryBuilder::class);
429+
$this->inject($elasticSearchQueryBuilder, 'now', new \DateTimeImmutable('@1532628000'));
396430

397-
return $query->query($this->siteNode);
431+
return $elasticSearchQueryBuilder->query($this->siteNode);
398432
}
399433
}

0 commit comments

Comments
 (0)