Skip to content

Commit 11e52ee

Browse files
committed
Merge pull request #100 from johannessteu/feature/geo-distance-sort
[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.
2 parents 6c0e948 + 304b45e commit 11e52ee

6 files changed

Lines changed: 316 additions & 27 deletions

File tree

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

Lines changed: 23 additions & 13 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
*

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();
@@ -229,6 +229,23 @@ public function searchHitForNode(NodeInterface $node)
229229
return $this->elasticSearchQuery->getQueryBuilder()->getFullElasticSearchHitForNode($node);
230230
}
231231

232+
/**
233+
* Returns the array with all sort values for a given node. The values are fetched from the raw content
234+
* ElasticSearch returns within the hit data
235+
*
236+
* @param NodeInterface $node
237+
* @return array
238+
*/
239+
public function getSortValuesForNode(NodeInterface $node)
240+
{
241+
$hit = $this->searchHitForNode($node);
242+
if (is_array($hit) && array_key_exists('sort', $hit)) {
243+
return $hit['sort'];
244+
}
245+
246+
return array();
247+
}
248+
232249
/**
233250
* @param string $methodName
234251
* @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
@@ -264,7 +264,7 @@ your node data. Aggregations also allows you to build a complex filter for e.g.
264264

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

270270
* `aggregation($name, array $aggregationDefinition, $parentPath = NULL)` -- generic method to add a $aggregationDefinition under a path $parentPath with the name $name
@@ -278,7 +278,7 @@ a property price:
278278
```
279279
nodes = ${Search.query(site)...fieldBasedAggregation("avgprice", "price", "avg").execute()}
280280
```
281-
Now you can access your aggregations inside your fluid template with
281+
Now you can access your aggregations inside your fluid template with
282282
```
283283
{nodes.aggregations}
284284
```
@@ -290,7 +290,7 @@ to know the average price for all your colors you just nest an aggregation in yo
290290
nodes = ${Search.query(site)...fieldBasedAggregation("colors", "color").fieldBasedAggregation("avgprice", "price", "avg", "colors").execute()}
291291
```
292292
The first `fieldBasedAggregation` will add a simple terms aggregation (https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html)
293-
with the name colors. So all different colors of your nodetype will be listed here.
293+
with the name colors. So all different colors of your nodetype will be listed here.
294294
The second `fieldBasedAggregation` will add another sub-aggregation named avgprice below your colors-aggregation.
295295

296296
You can nest even more aggregations like this:
@@ -323,7 +323,7 @@ prototype(Vendor.Name:FilteredProductList) {
323323
searchFilter = TYPO3.TypoScript:RawArray {
324324
sku = ${String.split(q(node).property("products"), ",")}
325325
}
326-
326+
327327
# Search for all products that matches your queryFilter and add aggregations
328328
filter = ${Search.query(site).nodeType("Vendor.Name:Product").queryFilterMultiple(this.searchFilter, "must").fieldBasedAggregation("color", "color").fieldBasedAggregation("size", "size").execute()}
329329
@@ -347,7 +347,7 @@ prototype(Vendor.Name:FilteredProductList) {
347347
```
348348

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

374+
## Sorting
375+
376+
This package adapts ElasticSearchs sorting capabilities. You can add multiple sort operations to your query.
377+
Right now there are three methods you can use:
378+
379+
* `sortAsc('propertyName')`
380+
* `sortDesc('propertyName')`
381+
* `sort('configuration')`
382+
383+
Just append those method to your query like this:
384+
```
385+
# sort ascending by property title
386+
nodes = ${q(Search.query(site).....sortAsc("title").execute())}
387+
388+
# sort for multiple properties
389+
nodes = ${q(Search.query(site).....sortAsc("title").sortDesc("name").execute())}
390+
391+
# custom sort opertation
392+
geoSorting = TYPO3.TypoScript:RawArray {
393+
_geo_distance = TYPO3.TypoScript:RawArray {
394+
latlng = TYPO3.TypoScript:RawArray {
395+
lat = 51.512711
396+
lon = 7.453084
397+
}
398+
order = "plane"
399+
unit = "km"
400+
distance_type = "sloppy_arc"
401+
}
402+
}
403+
nodes = ${Search.query(site).....sort(this.geoSorting).execute()}
404+
405+
```
406+
Check https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html for more configuration
407+
options.
408+
409+
### Example with pagination and sort by distance
410+
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
411+
node you want to display the distance to a specific point.
412+
413+
First of all you have to define a property in your NodeTypes.yaml for your node where to store lat/lon information's:
414+
```
415+
'Vendor.Name:Retailer':
416+
properties:
417+
'latlng':
418+
type: string
419+
search:
420+
elasticSearchMapping:
421+
type: "geo_point"
422+
```
423+
424+
Query your nodes in your TypoScript:
425+
```
426+
geoSorting = TYPO3.TypoScript:RawArray {
427+
_geo_distance = TYPO3.TypoScript:RawArray {
428+
latlng = TYPO3.TypoScript:RawArray {
429+
lat = 51.512711
430+
lon = 7.453084
431+
}
432+
order = "plane"
433+
unit = "km"
434+
distance_type = "sloppy_arc"
435+
}
436+
}
437+
nodes = ${Search.query(site).nodeType('Vendor.Name:Retailer').sort(this.geoSorting)}
438+
```
439+
440+
Now you can paginate that nodes in your template. To get your actually distance for each node use
441+
the `GetHitArrayForNodeViewHelper`:
442+
```
443+
{namespace es=Flowpack\ElasticSearch\ContentRepositoryAdaptor\ViewHelpers}
444+
445+
<typo3cr:widget.paginate query="{nodes}" as="paginatedNodes">
446+
<f:for each="{paginatedNodes}" as="singleNode">
447+
{singleNode.name} - <es:getHitArrayForNode queryResultObject="{nodes}" node="{singleNode}" path="sort.0" />
448+
</f:for>
449+
</typo3cr:widget.paginate>
450+
451+
```
452+
453+
The ViewHelper will use \TYPO3\Flow\Utility\Arrays::getValueByPath() to return a specified path. So you can make use
454+
of an array or a string. Check the documentation \TYPO3\Flow\Utility\Arrays::getValueByPath() for more informations.
455+
456+
**Important notice**
457+
The ViewHelper GetHitArrayForNode will return the raw hit result array. The path poperty allows you to access some
458+
specific data like the the sort data. If there is only one value for your path the value will be returned.
459+
If there is more data the full array will be returned by GetHitArrayForNode-VH. So you might have to use the
460+
ForViewHelper to access your sort values.
461+
462+
374463
## Fulltext Search / Indexing
375464

376465
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

@@ -170,19 +170,31 @@ public function termSuggestion()
170170
*/
171171
public function nodesWillBeSortedDesc()
172172
{
173-
$descendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortDesc("title")->execute();
173+
$descendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortDesc('title')->execute();
174174
$node = $descendingResult->getFirst();
175-
$this->assertEquals("egg", $node->getProperty("title"), "Asserting a desc sort order by property title");
175+
$this->assertEquals('egg', $node->getProperty('title'), 'Asserting a desc sort order by property title');
176176
}
177177

178178
/**
179179
* @test
180180
*/
181181
public function nodesWillBeSortedAsc()
182182
{
183-
$ascendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortAsc("title")->execute();
183+
$ascendingResult = $this->queryBuilder->query($this->context->getRootNode())->sortAsc('title')->execute();
184184
$node = $ascendingResult->getFirst();
185-
$this->assertEquals("chicken", $node->getProperty("title"), "Asserting a asc sort order by property title");
185+
$this->assertEquals('chicken', $node->getProperty('title'), 'Asserting a asc sort order by property title');
186+
}
187+
188+
/**
189+
* @test
190+
*/
191+
public function sortValuesAreReturned()
192+
{
193+
$result = $this->queryBuilder->query($this->context->getRootNode())->sortAsc('title')->execute();
194+
195+
foreach ($result as $node) {
196+
$this->assertEquals(array($node->getProperty('title')), $result->getSortValuesForNode($node));
197+
}
186198
}
187199

188200
/**
@@ -205,7 +217,6 @@ protected function createNodesForNodeSearchTest()
205217
$translatedNode3 = $dimensionContext->adoptNode($newNode3, TRUE);
206218
$translatedNode3->setProperty('title', 'Ei');
207219

208-
209220
$this->persistenceManager->persistAll();
210221
$this->nodeIndexCommandController->buildCommand();
211222
}

0 commit comments

Comments
 (0)