Skip to content

Commit 304b45e

Browse files
committed
[TASK] Add a generic sort operation
This changes adds a generic sort operation that can be used to create any kind of sorting that ElasticSearch supports.
1 parent ce5acea commit 304b45e

6 files changed

Lines changed: 317 additions & 28 deletions

File tree

Classes/Flowpack/ElasticSearch/ContentRepositoryAdaptor/Eel/ElasticSearchQueryBuilder.php

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,15 @@ public function nodeType($nodeType)
171171
*/
172172
public function sortDesc($propertyName)
173173
{
174-
if (!isset($this->request['sort'])) {
175-
$this->request['sort'] = array();
176-
}
174+
$configuration = [
175+
$propertyName => ['order' => 'desc']
176+
];
177177

178-
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-sort.html
179-
$this->request['sort'][] = array(
180-
$propertyName => array('order' => 'desc')
181-
);
178+
$this->sort($configuration);
182179

183180
return $this;
184181
}
185182

186-
187183
/**
188184
* Sort ascending by $propertyName
189185
*
@@ -192,20 +188,34 @@ public function sortDesc($propertyName)
192188
* @api
193189
*/
194190
public function sortAsc($propertyName)
191+
{
192+
$configuration = [
193+
$propertyName => ['order' => 'asc']
194+
];
195+
196+
$this->sort($configuration);
197+
198+
return $this;
199+
}
200+
201+
/**
202+
* Add a $configuration sort filter to the request
203+
*
204+
* @param array $configuration
205+
* @return ElasticSearchQueryBuilder
206+
* @api
207+
*/
208+
public function sort($configuration)
195209
{
196210
if (!isset($this->request['sort'])) {
197211
$this->request['sort'] = array();
198212
}
199213

200-
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-sort.html
201-
$this->request['sort'][] = array(
202-
$propertyName => array('order' => 'asc')
203-
);
214+
$this->request['sort'][] = $configuration;
204215

205216
return $this;
206217
}
207218

208-
209219
/**
210220
* output only $limit records
211221
*
@@ -451,7 +461,7 @@ public function fieldBasedAggregation($name, $field, $type = "terms", $parentPat
451461
* nodes = ${Search....aggregation("color", this.aggregationDefinition).execute()}
452462
*
453463
* Access all aggregation data with {nodes.aggregations} in your fluid template
454-
*
464+
*
455465
* @param string $name
456466
* @param array $aggregationDefinition
457467
* @param null $parentPath

Classes/Flowpack/ElasticSearch/ContentRepositoryAdaptor/Eel/ElasticSearchQueryResult.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public function getAccessibleCount()
192192
public function getAggregations()
193193
{
194194
$this->initialize();
195-
if (array_key_exists("aggregations", $this->result)) {
195+
if (array_key_exists('aggregations', $this->result)) {
196196
return $this->result['aggregations'];
197197
}
198198
return array();
@@ -213,6 +213,23 @@ public function searchHitForNode(NodeInterface $node)
213213
return $this->elasticSearchQuery->getQueryBuilder()->getFullElasticSearchHitForNode($node);
214214
}
215215

216+
/**
217+
* Returns the array with all sort values for a given node. The values are fetched from the raw content
218+
* ElasticSearch returns within the hit data
219+
*
220+
* @param NodeInterface $node
221+
* @return array
222+
*/
223+
public function getSortValuesForNode(NodeInterface $node)
224+
{
225+
$hit = $this->searchHitForNode($node);
226+
if (is_array($hit) && array_key_exists('sort', $hit)) {
227+
return $hit['sort'];
228+
}
229+
230+
return array();
231+
}
232+
216233
/**
217234
* @param string $methodName
218235
* @return boolean
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
namespace Flowpack\ElasticSearch\ContentRepositoryAdaptor\ViewHelpers;
3+
4+
/* *
5+
* This script belongs to the TYPO3 Flow package "Flowpack.ElasticSearch.ContentRepositoryAdaptor". *
6+
* *
7+
* It is free software; you can redistribute it and/or modify it under *
8+
* the terms of the GNU Lesser General Public License, either version 3 *
9+
* of the License, or (at your option) any later version. *
10+
* *
11+
* The TYPO3 project - inspiring people to share! *
12+
* */
13+
14+
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Eel\ElasticSearchQueryResult;
15+
use TYPO3\TYPO3CR\Domain\Model\NodeInterface;
16+
use TYPO3\Flow\Annotations as Flow;
17+
use TYPO3\Fluid\Core\ViewHelper\AbstractViewHelper;
18+
19+
/**
20+
* View helper to get the raw "hits" array of an ElasticSearchQueryResult for a
21+
* specific node.
22+
*
23+
* = Examples =
24+
*
25+
* <code title="Basic usage">
26+
* {esCrAdapter:geHitArrayForNode(queryResultObject: result, node: node)}
27+
* </code>
28+
* <output>
29+
* array
30+
* </output>
31+
*
32+
* You can also return specific data
33+
* <code title="Fetch specific data">
34+
* {esCrAdapter:geHitArrayForNode(queryResultObject: result, node: node, path: 'sort')}
35+
* </code>
36+
* <output>
37+
* array or single value
38+
* </output>
39+
*/
40+
class GetHitArrayForNodeViewHelper extends AbstractViewHelper
41+
{
42+
/**
43+
* @param ElasticSearchQueryResult $queryResultObject
44+
* @param NodeInterface $node
45+
* @param array|string $path
46+
* @return array
47+
*/
48+
public function render(ElasticSearchQueryResult $queryResultObject, NodeInterface $node, $path = NULL)
49+
{
50+
$hitArray = $queryResultObject->searchHitForNode($node);
51+
52+
if (!empty($path)) {
53+
return \TYPO3\Flow\Utility\Arrays::getValueByPath($hitArray, $path);
54+
}
55+
56+
return $hitArray;
57+
}
58+
}

README.md

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ your node data. Aggregations also allows you to build a complex filter for e.g.
240240

241241
**Aggregation methods**
242242
Right now there are two methods implemented. One generic `aggregation` function that allows you to add any kind of
243-
aggregation definition and a pre-configured `fieldBasedAggregation`. Both methods can be added to your TS search query.
243+
aggregation definition and a pre-configured `fieldBasedAggregation`. Both methods can be added to your TS search query.
244244
You can nest aggregations by providing a parent name.
245245

246246
* `aggregation($name, array $aggregationDefinition, $parentPath = NULL)` -- generic method to add a $aggregationDefinition under a path $parentPath with the name $name
@@ -254,7 +254,7 @@ a property price:
254254
```
255255
nodes = ${Search.query(site)...fieldBasedAggregation("avgprice", "price", "avg").execute()}
256256
```
257-
Now you can access your aggregations inside your fluid template with
257+
Now you can access your aggregations inside your fluid template with
258258
```
259259
{nodes.aggregations}
260260
```
@@ -266,7 +266,7 @@ to know the average price for all your colors you just nest an aggregation in yo
266266
nodes = ${Search.query(site)...fieldBasedAggregation("colors", "color").fieldBasedAggregation("avgprice", "price", "avg", "colors").execute()}
267267
```
268268
The first `fieldBasedAggregation` will add a simple terms aggregation (https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html)
269-
with the name colors. So all different colors of your nodetype will be listed here.
269+
with the name colors. So all different colors of your nodetype will be listed here.
270270
The second `fieldBasedAggregation` will add another sub-aggregation named avgprice below your colors-aggregation.
271271

272272
You can nest even more aggregations like this:
@@ -299,7 +299,7 @@ prototype(Vendor.Name:FilteredProductList) {
299299
searchFilter = TYPO3.TypoScript:RawArray {
300300
sku = ${String.split(q(node).property("products"), ",")}
301301
}
302-
302+
303303
# Search for all products that matches your queryFilter and add aggregations
304304
filter = ${Search.query(site).nodeType("Vendor.Name:Product").queryFilterMultiple(this.searchFilter, "must").fieldBasedAggregation("color", "color").fieldBasedAggregation("size", "size").execute()}
305305
@@ -323,7 +323,7 @@ prototype(Vendor.Name:FilteredProductList) {
323323
```
324324

325325
In the first lines we will add a new searchFilter variable and add your selected sku's as a filter. Based on this selection
326-
we will add two aggregations of type terms. You can access the filter in your template with `{filter.aggregation}`. With
326+
we will add two aggregations of type terms. You can access the filter in your template with `{filter.aggregation}`. With
327327
this information it is easy to create a form with some select fields with all available options. If you submit the form
328328
just call the same page and add the get parameter color and/or size.
329329
The next lines will parse those parameters and add them to the searchFilter. Based on your selection all products will
@@ -347,6 +347,95 @@ your filterable properties like this:
347347
index: 'not_analyzed'
348348
```
349349

350+
## Sorting
351+
352+
This package adapts ElasticSearchs sorting capabilities. You can add multiple sort operations to your query.
353+
Right now there are three methods you can use:
354+
355+
* `sortAsc('propertyName')`
356+
* `sortDesc('propertyName')`
357+
* `sort('configuration')`
358+
359+
Just append those method to your query like this:
360+
```
361+
# sort ascending by property title
362+
nodes = ${q(Search.query(site).....sortAsc("title").execute())}
363+
364+
# sort for multiple properties
365+
nodes = ${q(Search.query(site).....sortAsc("title").sortDesc("name").execute())}
366+
367+
# custom sort opertation
368+
geoSorting = TYPO3.TypoScript:RawArray {
369+
_geo_distance = TYPO3.TypoScript:RawArray {
370+
latlng = TYPO3.TypoScript:RawArray {
371+
lat = 51.512711
372+
lon = 7.453084
373+
}
374+
order = "plane"
375+
unit = "km"
376+
distance_type = "sloppy_arc"
377+
}
378+
}
379+
nodes = ${Search.query(site).....sort(this.geoSorting).execute()}
380+
381+
```
382+
Check https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html for more configuration
383+
options.
384+
385+
### Example with pagination and sort by distance
386+
This is how a more complex example could look like. Imagine you a want to render a list of nodes and in addition to each
387+
node you want to display the distance to a specific point.
388+
389+
First of all you have to define a property in your NodeTypes.yaml for your node where to store lat/lon information's:
390+
```
391+
'Vendor.Name:Retailer':
392+
properties:
393+
'latlng':
394+
type: string
395+
search:
396+
elasticSearchMapping:
397+
type: "geo_point"
398+
```
399+
400+
Query your nodes in your TypoScript:
401+
```
402+
geoSorting = TYPO3.TypoScript:RawArray {
403+
_geo_distance = TYPO3.TypoScript:RawArray {
404+
latlng = TYPO3.TypoScript:RawArray {
405+
lat = 51.512711
406+
lon = 7.453084
407+
}
408+
order = "plane"
409+
unit = "km"
410+
distance_type = "sloppy_arc"
411+
}
412+
}
413+
nodes = ${Search.query(site).nodeType('Vendor.Name:Retailer').sort(this.geoSorting)}
414+
```
415+
416+
Now you can paginate that nodes in your template. To get your actually distance for each node use
417+
the `GetHitArrayForNodeViewHelper`:
418+
```
419+
{namespace es=Flowpack\ElasticSearch\ContentRepositoryAdaptor\ViewHelpers}
420+
421+
<typo3cr:widget.paginate query="{nodes}" as="paginatedNodes">
422+
<f:for each="{paginatedNodes}" as="singleNode">
423+
{singleNode.name} - <es:getHitArrayForNode queryResultObject="{nodes}" node="{singleNode}" path="sort.0" />
424+
</f:for>
425+
</typo3cr:widget.paginate>
426+
427+
```
428+
429+
The ViewHelper will use \TYPO3\Flow\Utility\Arrays::getValueByPath() to return a specified path. So you can make use
430+
of an array or a string. Check the documentation \TYPO3\Flow\Utility\Arrays::getValueByPath() for more informations.
431+
432+
**Important notice**
433+
The ViewHelper GetHitArrayForNode will return the raw hit result array. The path poperty allows you to access some
434+
specific data like the the sort data. If there is only one value for your path the value will be returned.
435+
If there is more data the full array will be returned by GetHitArrayForNode-VH. So you might have to use the
436+
ForViewHelper to access your sort values.
437+
438+
350439
## Fulltext Search / Indexing
351440

352441
When searching in a fulltext index, we want to show Pages, or, generally speaking, everything

Tests/Functional/Eel/ElasticSearchQueryTest.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function setUp()
6767
{
6868
parent::setUp();
6969
$this->workspaceRepository = $this->objectManager->get('TYPO3\TYPO3CR\Domain\Repository\WorkspaceRepository');
70-
$liveWorkspace = new Workspace("live");
70+
$liveWorkspace = new Workspace('live');
7171
$this->workspaceRepository->add($liveWorkspace);
7272

7373
$this->nodeTypeManager = $this->objectManager->get('TYPO3\TYPO3CR\Domain\Service\NodeTypeManager');
@@ -128,8 +128,8 @@ public function filterLimitQuery()
128128
*/
129129
public function fieldBasedAggregations()
130130
{
131-
$aggregationTitle = "titleagg";
132-
$result = $this->queryBuilder->query($this->context->getRootNode())->fieldBasedAggregation($aggregationTitle, "title")->execute()->getAggregations();
131+
$aggregationTitle = 'titleagg';
132+
$result = $this->queryBuilder->query($this->context->getRootNode())->fieldBasedAggregation($aggregationTitle, 'title')->execute()->getAggregations();
133133

134134
$this->assertArrayHasKey($aggregationTitle, $result);
135135

@@ -148,19 +148,31 @@ public function fieldBasedAggregations()
148148
*/
149149
public function nodesWillBeSortedDesc()
150150
{
151-
$descendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortDesc("title")->execute();
151+
$descendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortDesc('title')->execute();
152152
$node = $descendingResult->getFirst();
153-
$this->assertEquals("egg", $node->getProperty("title"), "Asserting a desc sort order by property title");
153+
$this->assertEquals('egg', $node->getProperty('title'), 'Asserting a desc sort order by property title');
154154
}
155155

156156
/**
157157
* @test
158158
*/
159159
public function nodesWillBeSortedAsc()
160160
{
161-
$ascendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortAsc("title")->execute();
161+
$ascendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortAsc('title')->execute();
162162
$node = $ascendingResult->getFirst();
163-
$this->assertEquals("chicken", $node->getProperty("title"), "Asserting a asc sort order by property title");
163+
$this->assertEquals('chicken', $node->getProperty('title'), 'Asserting a asc sort order by property title');
164+
}
165+
166+
/**
167+
* @test
168+
*/
169+
public function sortValuesAreReturned()
170+
{
171+
$result = $this->queryBuilder->query($this->context->getRootNode())->sortAsc('title')->execute();
172+
173+
foreach ($result as $node) {
174+
$this->assertEquals(array($node->getProperty('title')), $result->getSortValuesForNode($node));
175+
}
164176
}
165177

166178
/**
@@ -183,7 +195,6 @@ protected function createNodesForNodeSearchTest()
183195
$translatedNode3 = $dimensionContext->adoptNode($newNode3, TRUE);
184196
$translatedNode3->setProperty('title', 'Ei');
185197

186-
187198
$this->persistenceManager->persistAll();
188199
$this->nodeIndexCommandController->buildCommand();
189200
}

0 commit comments

Comments
 (0)