Skip to content

Commit 7f1fc9a

Browse files
SeboboSebastian Helzle
authored andcommitted
!!! TASK: Improve suggestion results
With this change the SuggestController can now better be used for autocompletion. It respects the context nodes dimensions, workspace and path. More context variables could be added for security. If no completions are found fuzzy suggestions are returned.
1 parent df4d87c commit 7f1fc9a

6 files changed

Lines changed: 220 additions & 18 deletions

File tree

Classes/Controller/SuggestController.php

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,23 @@
1111
* source code.
1212
*/
1313

14-
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\ElasticSearchClient;
14+
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Eel\ElasticSearchQueryBuilder;
15+
use Flowpack\ElasticSearch\ContentRepositoryAdaptor\Eel\ElasticSearchQueryResult;
16+
use Neos\ContentRepository\Domain\Model\NodeInterface;
1517
use Neos\Flow\Annotations as Flow;
1618
use Neos\Flow\Mvc\Controller\ActionController;
1719
use Neos\Flow\Mvc\View\JsonView;
1820

21+
/**
22+
* Class SuggestController
23+
*/
1924
class SuggestController extends ActionController
2025
{
2126
/**
2227
* @Flow\Inject
23-
* @var ElasticSearchClient
28+
* @var ElasticSearchQueryBuilder
2429
*/
25-
protected $elasticSearchClient;
30+
protected $elasticSearchQueryBuilder;
2631

2732
/**
2833
* @var array
@@ -32,26 +37,81 @@ class SuggestController extends ActionController
3237
];
3338

3439
/**
40+
* @param NodeInterface $contextNode
3541
* @param string $term
36-
*
3742
* @return void
3843
*/
39-
public function indexAction($term)
44+
public function indexAction(NodeInterface $contextNode, $term)
4045
{
41-
$request = [
42-
'suggests' => [
46+
$result = [
47+
'completions' => [],
48+
'suggestions' => []
49+
];
50+
51+
if (!is_string($term)) {
52+
$result['errors'] = ['term has to be a string'];
53+
$this->view->assign('value', $result);
54+
return;
55+
}
56+
57+
$term = strtolower($term);
58+
59+
// TODO: cache query by node identifier
60+
61+
/** @var ElasticSearchQueryBuilder $query */
62+
$query = $this->elasticSearchQueryBuilder->query($contextNode);
63+
$query
64+
->queryFilter('prefix', [
65+
'__completion' => $term
66+
])
67+
->limit(0)
68+
->aggregation('autocomplete', [
69+
'terms' => [
70+
'field' => '__completion',
71+
'order' => [
72+
'_count' => 'desc'
73+
],
74+
'include' => [
75+
'pattern' => $term . '.*'
76+
]
77+
]
78+
])
79+
->suggestions('suggestions', [
4380
'text' => $term,
44-
'term' => [
45-
'field' => '_all'
81+
'completion' => [
82+
'field' => '__suggestions',
83+
'fuzzy' => true,
84+
'context' => [
85+
'parentPath' => $contextNode->getPath(),
86+
'workspace' => 'live',
87+
'dimensionCombinationHash' => md5(json_encode($contextNode->getContext()->getDimensions())),
88+
]
4689
]
47-
]
48-
];
90+
]);
91+
92+
try {
93+
/** @var ElasticSearchQueryResult $queryResult */
94+
$queryResult = $query->execute();
95+
} catch (\Exception $e) {
96+
$result['errors'] = ['Could not execute query'];
97+
$this->view->assign('value', $result);
98+
return;
99+
}
100+
101+
$aggregations = $queryResult->getAggregations();
102+
103+
// Extract autocomplete options
104+
$autoCompletionOptions = array_map(function ($option) {
105+
return $option['key'];
106+
}, $aggregations['autocomplete']['buckets']);
107+
$result['completions'] = $autoCompletionOptions;
49108

50-
$response = $this->elasticSearchClient->getIndex()->request('POST', '/_suggest', [], json_encode($request))->getTreatedContent();
51-
$suggestions = array_map(function ($option) {
52-
return $option['text'];
53-
}, $response['suggests'][0]['options']);
109+
// Extract suggestion options
110+
$suggestionOptions = $queryResult->getSuggestions();
111+
if (count($suggestionOptions['suggestions'][0]['options']) > 0) {
112+
$result['suggestions'] = $suggestionOptions['suggestions'][0]['options'];
113+
}
54114

55-
$this->view->assign('value', $suggestions);
115+
$this->view->assign('value', $result);
56116
}
57117
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
namespace Flowpack\SearchPlugin\EelHelper;
3+
4+
/*
5+
* This file is part of the Flowpack.SearchPlugin package.
6+
*
7+
* (c) Contributors of the Flowpack Team - flowpack.org
8+
*
9+
* This package is Open Source Software. For the full copyright and license
10+
* information, please view the LICENSE file which was distributed with this
11+
* source code.
12+
*/
13+
14+
use Neos\Eel\ProtectedContextAwareInterface;
15+
use Neos\Flow\Annotations as Flow;
16+
17+
/**
18+
* Helper for building suggestion configurations
19+
*
20+
* @Flow\Proxy(false)
21+
*/
22+
class SuggestionIndexHelper implements ProtectedContextAwareInterface
23+
{
24+
25+
/**
26+
* @param string $input
27+
* @param array $payload
28+
* @param int $weight
29+
* @return array
30+
*/
31+
public function buildConfig($input, array $payload = [], $weight = 1)
32+
{
33+
return [
34+
'input' => $this->prepareInput($input),
35+
'output' => $this->prepareOutput($input),
36+
'payload' => json_encode($payload),
37+
'weight' => $weight
38+
];
39+
}
40+
41+
/**
42+
* @param string $input
43+
* @return array
44+
*/
45+
protected function prepareInput($input)
46+
{
47+
return array_filter(explode(' ', preg_replace("/[^[:alnum:][:space:]]/u", ' ', strip_tags($input))));
48+
}
49+
50+
/**
51+
* @param string $input
52+
* @return array
53+
*/
54+
protected function prepareOutput($input)
55+
{
56+
return strip_tags($input);
57+
}
58+
59+
/**
60+
* All methods are considered safe
61+
*
62+
* @param string $methodName
63+
* @return boolean
64+
*/
65+
public function allowsCallOfMethod($methodName)
66+
{
67+
return true;
68+
}
69+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'Flowpack.SearchPlugin:SuggestableMixin':
2+
abstract: true
3+
properties:
4+
'__suggestions':
5+
search:
6+
elasticSearchMapping:
7+
type: completion
8+
payloads: true
9+
context:
10+
workspace:
11+
type: category
12+
path: '__workspace'
13+
parentPath:
14+
type: category
15+
path: '__parentPath'
16+
dimensionCombinationHash:
17+
type: category
18+
path: '__dimensionCombinationHash'
19+
indexing: "${Flowpack.SearchPlugin.Suggestion.buildConfig(q(node).property('title'), {nodeIdentifier: node.identifier}, 20)}"
20+
21+
'Flowpack.SearchPlugin:AutocompletableMixin':
22+
abstract: true
23+
properties:
24+
'__completion':
25+
search:
26+
elasticSearchMapping:
27+
type: string
28+
analyzer: autocomplete
29+
indexing: "${q(node).property('title')}"

Configuration/NodeTypes.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,25 @@
44
ui:
55
label: 'Search'
66
icon: 'icon-search'
7+
8+
'Neos.Neos:Document':
9+
superTypes:
10+
'Flowpack.SearchPlugin:SuggestableMixin': true
11+
'Flowpack.SearchPlugin:AutocompletableMixin': true
12+
13+
'Neos.NodeTypes:TitleMixin':
14+
superTypes:
15+
'Flowpack.SearchPlugin:SuggestableMixin': true
16+
'Flowpack.SearchPlugin:AutocompletableMixin': true
17+
18+
'Neos.NodeTypes:TextMixin':
19+
superTypes:
20+
'Flowpack.SearchPlugin:SuggestableMixin': true
21+
'Flowpack.SearchPlugin:AutocompletableMixin': true
22+
properties:
23+
'__completion':
24+
search:
25+
indexing: "${Flowpack.SearchPlugin.Suggestion.buildConfig(q(node).property('text'), {nodeIdentifier: node.identifier}, 10)}"
26+
'__completion':
27+
search:
28+
indexing: "${q(node).property('text')}"

Configuration/Settings.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,25 @@ Neos:
1111
fusion:
1212
autoInclude:
1313
Flowpack.SearchPlugin: true
14+
ContentRepository:
15+
Search:
16+
defaultContext:
17+
Flowpack.SearchPlugin.Suggestion: Flowpack\SearchPlugin\EelHelper\SuggestionIndexHelper
18+
19+
Flowpack:
20+
ElasticSearch:
21+
indexes:
22+
default:
23+
typo3cr:
24+
analysis:
25+
filter:
26+
autocompleteFilter:
27+
max_shingle_size: 5
28+
min_shingle_size: 2
29+
type: 'shingle'
30+
analyzer:
31+
autocomplete:
32+
filter: [ 'lowercase', 'autocompleteFilter' ]
33+
char_filter: [ 'html_strip' ]
34+
type: 'custom'
35+
tokenizer: 'standard'
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<form method="GET">
2-
<input type="search" name="search" value="{searchWord}" placeholder="{f:translate(id: 'search', package: 'Flowpack.SearchPlugin')}" data-autocomplete-source="{f:uri.action(action: 'index', controller: 'Suggest', package: 'Flowpack.SearchPlugin', format: 'json', absolute: 1)}"/>
2+
<input type="search" name="search" value="{searchWord}" placeholder="{f:translate(id: 'search', package: 'Flowpack.SearchPlugin')}" data-autocomplete-source="{f:uri.action(action: 'index', controller: 'Suggest', package: 'Flowpack.SearchPlugin', format: 'json', absolute: 1, arguments: {contextNode: node})}"/>
33
<button type="submit">{f:translate(id: 'search', package: 'Flowpack.SearchPlugin')}</button>
4-
</form>
4+
</form>

0 commit comments

Comments
 (0)