Wiki source code of Solr Search Macros
Show last authors
1 | {{template name="hierarchy_macros.vm" /}} |
2 | |
3 | {{velocity output='false'}} |
4 | #set ($rangePattern = $regextool.compile('^[\[{](.+) TO (.+)[\]}]$')) |
5 | #set ($wildcardPattern = $regextool.compile('^\(.*\*.*\)$')) |
6 | |
7 | #macro (displaySearchForm) |
8 | #set($void = $services.progress.startStep('#displaySearchForm')) |
9 | {{html clean="false"}} |
10 | <form class="search-form row" action="$doc.getURL()" role="search"> |
11 | <div class="hidden"> |
12 | <input type="hidden" name="sort" value="$!escapetool.xml($sort)"/> |
13 | <input type="hidden" name="sortOrder" value="$!escapetool.xml($sortOrder)"/> |
14 | <input type="hidden" name="highlight" value="$highlightEnabled"/> |
15 | <input type="hidden" name="facet" value="$facetEnabled"/> |
16 | ## The parameter used to determine if the request has been redirected with default search filters. |
17 | <input type="hidden" name="r" value="$!escapetool.xml($request.r)"/> |
18 | #if ("$!request.debug" != '') |
19 | <input type="hidden" name="debug" value="$escapetool.xml($request.debug)"/> |
20 | #end |
21 | ## Preserve the current facet values when submitting a new search query. |
22 | #foreach ($entry in $request.parameterMap.entrySet()) |
23 | #if ($entry.key.startsWith('f_') || $entry.key.startsWith('l_')) |
24 | #foreach ($value in $entry.value) |
25 | <input type="hidden" name="$escapetool.xml($entry.key)" value="$escapetool.xml($value)"/> |
26 | #end |
27 | #end |
28 | #end |
29 | </div> |
30 | <div class="col-xs-12 col-sm-6"> |
31 | <div class="input-group"> |
32 | <input type="search" name="text" class="form-control withTip useTitleAsTip" |
33 | title="$services.localization.render('search.page.bar.query.title')" value="$escapetool.xml($text)"/> |
34 | <span class="input-group-btn"> |
35 | <button type="submit" class="btn btn-primary"> |
36 | $services.icon.renderHTML('search') |
37 | <span class="sr-only">$services.localization.render('search.page.bar.submit')</span> |
38 | </button> |
39 | </span> |
40 | </div> |
41 | </div> |
42 | </form> |
43 | {{/html}} |
44 | #set($void = $services.progress.endStep()) |
45 | #end |
46 | |
47 | #macro (displaySearchDebugInfo) |
48 | (% class="search-debug" %)((( |
49 | === Debug Information === |
50 | #set ($debugMap = $searchResponse.debugMap) |
51 | #if ($debugMap) |
52 | |
53 | {{html clean="false"}} |
54 | <dl> |
55 | <dt>Query Parser</dt> |
56 | <dd>$!escapetool.xml($debugMap.get('QParser'))</dd> |
57 | <dt>Parsed Query</dt> |
58 | <dd>$!escapetool.xml($debugMap.get('parsedquery_toString'))</dd> |
59 | <dt>Filter Queries</dt> |
60 | <dd> |
61 | <ul> |
62 | #foreach ($filterQuery in $debugMap.get('filter_queries')) |
63 | <li>$!escapetool.xml($filterQuery)</li> |
64 | #end |
65 | </ul> |
66 | </dd> |
67 | <dt>Processing Time</dt> |
68 | <dd> |
69 | #displayProcessingTime($debugMap.get('timing')) |
70 | </dd> |
71 | </dl> |
72 | {{/html}} |
73 | #end |
74 | ))) |
75 | #end |
76 | |
77 | #macro (displayProcessingTime $timing) |
78 | <ul> |
79 | ## The timing is not a Map but a NamedList. |
80 | #foreach ($entry in $timing) |
81 | <li> |
82 | $!escapetool.xml($entry.key): |
83 | #if ($entry.value.time && $entry.value.size() > 1) |
84 | #displayProcessingTime($entry.value) |
85 | #else |
86 | $!escapetool.xml($entry.value) |
87 | #end |
88 | </li> |
89 | #end |
90 | </ul> |
91 | #end |
92 | |
93 | #macro (displaySearchFacets $searchResponse) |
94 | #set($void = $services.progress.startStep('#displaySearchFacets')) |
95 | (% class="search-facets collapsed-xs xform" %)((( |
96 | (% class="search-facets-header" %)((( |
97 | **{{translation key="solr.facets.title"/}}** (% class="pull-right visible-xs" %)$services.icon.render('search-plus') |
98 | |
99 | (% class="xHint" %) |
100 | {{translation key="solr.facets.hint"/}} |
101 | ))) |
102 | (% class="search-facets-actions" %)((( |
103 | #set ($resetParameters = {}) |
104 | #foreach ($parameter in $request.parameterMap.entrySet()) |
105 | #if ($parameter.key.startsWith('f_') || $parameter.key.startsWith('l_')) |
106 | #set ($discard = $resetParameters.put($parameter.key, [])) |
107 | #end |
108 | #end |
109 | #extendQueryString($url $resetParameters) |
110 | [[{{translation key="solr.facets.resetAll"}}>>path:$url |
111 | ||class="search-facets-action-reset"]]## Continue in the same paragraph. |
112 | {{html clean="false"}} |
113 | <a href="#" class="search-facets-action-collapseAll hidden"> |
114 | $escapetool.xml($services.localization.render('solr.facets.collapseAll')) |
115 | </a> |
116 | <a href="#" class="search-facets-action-expandAll hidden"> |
117 | $escapetool.xml($services.localization.render('solr.facets.expandAll')) |
118 | </a> |
119 | <span class="clearfloats"></span> |
120 | {{/html}} |
121 | ))) |
122 | {{html clean="false"}} |
123 | #foreach ($facetField in $searchResponse.facetFields) |
124 | #displaySearchFacet($facetField) |
125 | #end |
126 | {{/html}} |
127 | ))) |
128 | #set($void = $services.progress.endStep()) |
129 | #end |
130 | |
131 | #macro (displaySearchFacet $facetField) |
132 | #set ($facetRequestParameter = "f_$facetField.name") |
133 | #set ($facetRequestValues = $request.getParameterValues($facetRequestParameter)) |
134 | #set ($facetValues = []) |
135 | #foreach ($facetValue in $facetField.values) |
136 | ## Keep only the values that have at least one match or that are specified on the request. |
137 | #if ($facetValue.count > 0 || ($facetRequestValues && $facetRequestValues.contains($facetValue.name))) |
138 | #set ($discard = $facetValues.add($facetValue)) |
139 | #end |
140 | #end |
141 | ## Facets that perform a 'facet.prefix'-based drill down (see https://wiki.apache.org/solr/HierarchicalFaceting) don't |
142 | ## have any values (not even with 0 count) when the prefix specified on the request doesn't have any "sub-values", but |
143 | ## we still want to display them to allow the user to reset the filter. |
144 | #if ($facetValues.size() > 0 || $facetRequestValues) |
145 | ## Show active facets (that have selected values or that have an explicit limit on the number of values, i.e. |
146 | ## pagination) as expanded. Collapse the rest, otherwise you have to scroll to see all the available facets. |
147 | #set ($facetValuesLimit = $request.getParameter("l_$facetField.name")) |
148 | <div class="search-facet#if ($facetRequestValues || $facetValuesLimit) expanded#end" data-name="$facetField.name"> |
149 | #displaySearchFacetHeader($facetField) |
150 | #displaySearchFacetBody($facetField) |
151 | </div> |
152 | #end |
153 | #end |
154 | |
155 | #macro (getXClassProperty $solrFieldName $property $classPropertyReference) |
156 | ## Remove the 'property.' prefix and the data type suffix. |
157 | #set ($stringReference = $stringtool.substringBeforeLast($solrFieldName.substring(9), '_')) |
158 | ## Note that the class property reference is resolved relative to the current wiki. This means the class must be |
159 | ## available on the wiki where the search is performed. |
160 | #set ($classPropertyReference = $NULL) |
161 | #setVariable("$classPropertyReference" $services.model.resolveClassProperty($stringReference, 'solr')) |
162 | #set ($classDocument = $xwiki.getDocument($classPropertyReference.parent)) |
163 | #set ($property = $NULL) |
164 | #setVariable("$property" $classDocument.xWikiClass.get($classPropertyReference.name)) |
165 | #end |
166 | |
167 | #macro (displaySearchFacetHeader $facetField) |
168 | #set ($facetPrettyNameKey = "solr.field.$facetField.name") |
169 | #if ($services.localization.get($facetPrettyNameKey)) |
170 | #set ($facetPrettyName = $services.localization.render($facetPrettyNameKey)) |
171 | #elseif ($facetField.name.startsWith('property.')) |
172 | ## Display the translated property pretty name. |
173 | #getXClassProperty($facetField.name $property $classPropertyReference) |
174 | #set ($facetPrettyName = $property.translatedPrettyName) |
175 | #if ("$!facetPrettyName" == '') |
176 | #set ($facetPrettyName = $classPropertyReference.name) |
177 | #end |
178 | #else |
179 | #set ($facetPrettyName = $facetField.name) |
180 | #end |
181 | <div class="search-facet-header">$escapetool.xml($facetPrettyName)</div> |
182 | #end |
183 | |
184 | #macro (displaySearchFacetBody $facetField) |
185 | <div class="search-facet-body"> |
186 | #set ($facetDisplayer = $solrConfig.facetDisplayers.get($facetField.name)) |
187 | #if (!$facetDisplayer && $facetField.name.startsWith('property.')) |
188 | ## Choose a facet displayer based on the property type. |
189 | #getXClassProperty($facetField.name $property) |
190 | ## We rely on configuration instead of using a naming convention like "Main.Solr${property.classType}Facet" |
191 | ## because most of the property types don't need a custom facet displayer. |
192 | #set ($facetDisplayer = $solrConfig.facetDisplayersByPropertyType.get($property.classType)) |
193 | #end |
194 | #if ($facetDisplayer) |
195 | #set ($facetDisplayer = $xwiki.getDocument($facetDisplayer)) |
196 | #if ("$!facetDisplayer.content" != '') |
197 | $!facetDisplayer.getRenderedContent(false) |
198 | #else |
199 | #displaySearchFacetValues($facetValues) |
200 | #end |
201 | #else |
202 | #displaySearchFacetValues($facetValues) |
203 | #end |
204 | </div> |
205 | #end |
206 | |
207 | #macro (displaySearchFacetValues $facetValues $customQueryStringParameters $customValueDisplayer) |
208 | #if ($facetValues.size() > 0) |
209 | <ul> |
210 | #displaySearchFacetValuesLimited($facetValues $customQueryStringParameters $customValueDisplayer) |
211 | </ul> |
212 | #end |
213 | #end |
214 | |
215 | #macro (displaySearchFacetValuesLimited $facetValues $customQueryStringParameters $customValueDisplayer) |
216 | #set ($limitRequestParameter = "l_$facetField.name") |
217 | #set ($limit = $mathtool.toInteger($request.getParameter($limitRequestParameter))) |
218 | #if ("$!limit" == '') |
219 | #set ($limit = $solrConfig.facetPaginationStep) |
220 | #end |
221 | #set ($limit = $mathtool.max($mathtool.min($limit, $facetValues.size()), 0)) |
222 | #foreach ($facetValue in $facetValues) |
223 | #if ($foreach.index < $limit) |
224 | <li>#displaySearchFacetValue($facetValue $customQueryStringParameters $customValueDisplayer)</li> |
225 | #else |
226 | #extendQueryString($url {$limitRequestParameter: [$mathtool.add($limit, $solrConfig.facetPaginationStep)]}) |
227 | <li><a href="$url" class="more">… $escapetool.xml($services.localization.render( |
228 | 'solr.facets.moreValues', [$mathtool.sub($facetValues.size(), $limit)]))</a></li> |
229 | #break |
230 | #end |
231 | #end |
232 | #end |
233 | |
234 | #macro (displaySearchFacetValue $facetValue $customQueryStringParameters $customValueDisplayer) |
235 | #set ($selectedValues = []) |
236 | #if ($facetRequestValues) |
237 | #set ($discard = $selectedValues.addAll($facetRequestValues.subList(0, $facetRequestValues.size()))) |
238 | #end |
239 | #set ($selected = $selectedValues.remove($facetValue.name)) |
240 | #if (!$selected) |
241 | #set ($discard = $selectedValues.add($facetValue.name)) |
242 | #end |
243 | ## Reset the pagination because the number of results can change when a facet is applied. |
244 | #set ($queryStringParameters = {$facetRequestParameter: $selectedValues, 'firstIndex': []}) |
245 | #if ($customQueryStringParameters) |
246 | #set ($discard = $queryStringParameters.putAll($customQueryStringParameters)) |
247 | #end |
248 | #extendQueryString($url $queryStringParameters) |
249 | <div class="itemCount">$facetValue.count</div> |
250 | <a href="$url" class="itemName#if ($selected) selected#end#if ($facetValue.name == '') empty#end"> |
251 | #if ($facetValue.name == '') |
252 | #set ($facetPrettyValueKey = "solr.field.${facetField.name}.emptyValue") |
253 | #if (!$services.localization.get($facetPrettyValueKey)) |
254 | #set ($facetPrettyValueKey = "solr.facets.emptyValue") |
255 | #end |
256 | #set ($facetPrettyValue = $services.localization.render($facetPrettyValueKey)) |
257 | #else |
258 | #set ($facetPrettyValue = $facetValue.name) |
259 | #end |
260 | #if ($customValueDisplayer) |
261 | #evaluate("${escapetool.h}${customValueDisplayer}(${escapetool.d}facetPrettyValue)") |
262 | #else |
263 | $escapetool.xml($facetPrettyValue) |
264 | #end |
265 | </a> |
266 | <div class="clearfloats"></div> |
267 | #end |
268 | |
269 | #** |
270 | * If the facet has values specified on the request then keep only those that are included in the list of matched facet |
271 | * values. Don't use this macro for date or range facets because in this case the values specified on the request are |
272 | * never found as is in the list of facet values (e.g. a range will match multiple facet values). This macro ensures |
273 | * that the URL to select/unselect a facet value doesn't keep unmatched values (otherwise the URL will have values that |
274 | * you cannot remove using the facet UI). |
275 | *# |
276 | #macro (retainMatchedRequestValues) |
277 | #if ($facetRequestValues) |
278 | #set ($matchedValues = []) |
279 | #foreach ($facetValue in $facetValues) |
280 | #set ($discard = $matchedValues.add($facetValue.name)) |
281 | #end |
282 | #set ($matchedRequestValues = []) |
283 | #set ($discard = $matchedRequestValues.addAll($facetRequestValues.subList(0, $facetRequestValues.size()))) |
284 | #set ($discard = $matchedRequestValues.retainAll($matchedValues)) |
285 | #set ($facetRequestValues = $matchedRequestValues) |
286 | #end |
287 | #end |
288 | |
289 | #macro (displaySearchResultsSort) |
290 | #set ($defaultSortOrder = $solrConfig.sortFields.get($type)) |
291 | #if (!$defaultSortOrder) |
292 | #set ($defaultSortOrder = {'score': 'desc'}) |
293 | #end |
294 | #set ($sortOrderSymbol = { |
295 | 'asc': $services.icon.render('caret-up'), |
296 | 'desc': $services.icon.render('caret-down') |
297 | }) |
298 | (% class="search-options" %) |
299 | * {{translation key="solr.options"/}} |
300 | #if($highlightEnabled)#extendQueryString($url {'highlight': [false]})#else#extendQueryString($url {'highlight': [true]})#end |
301 | * [[{{translation key="solr.options.highlight"/}}>>path:$url||class="options-item#if($highlightEnabled) active#end" title="$services.localization.render('solr.options.highlight.title')"]] |
302 | #if($facetEnabled)#extendQueryString($url {'facet': [false]})#else#extendQueryString($url {'facet': [true]})#end |
303 | * [[{{translation key="solr.options.facet"/}}>>path:$url||class="options-item#if($facetEnabled) active#end" title="$services.localization.render('solr.options.facet.title')"]] |
304 | |
305 | (% class="search-results-sort" %) |
306 | * {{translation key="solr.sortBy"/}} |
307 | #foreach ($entry in $defaultSortOrder.entrySet()) |
308 | #set ($class = 'sort-item') |
309 | #set ($sortOrderIndicator = $NULL) |
310 | #set ($targetSortOrder = $entry.value) |
311 | #if ($sort == $entry.key) |
312 | #set ($class = "$class active") |
313 | #set ($sortOrderHint = $services.localization.render("solr.sortOrder.$sortOrder")) |
314 | #set ($sortOrderIndicator = "(% class=""sort-item-order"" title=""$sortOrderHint"" %)$sortOrderSymbol.get($sortOrder)(%%)") |
315 | #set ($targetSortOrder = "#if ($sortOrder == 'asc')desc#{else}asc#end") |
316 | #end |
317 | #extendQueryString($url {'sort': [$entry.key], 'sortOrder': [$targetSortOrder]}) |
318 | * [[{{translation key="solr.sortBy.$entry.key"/}}$!sortOrderIndicator>>path:$url||class="$class"]] |
319 | #end |
320 | #end |
321 | |
322 | #macro (extendQueryString $url $extraParameters) |
323 | #set ($parameters = {}) |
324 | #set ($discard = $parameters.putAll($request.getParameterMap())) |
325 | #set ($discard = $parameters.putAll($extraParameters)) |
326 | #set ($queryString = $escapetool.url($parameters)) |
327 | #set ($url = $NULL) |
328 | #setVariable("$url" $doc.getURL('view', $queryString)) |
329 | #end |
330 | |
331 | #macro (displaySearchResults) |
332 | #set ($results = $searchResponse.results) |
333 | #set ($paginationParameters = { |
334 | 'url': $doc.getURL('view', "$!request.queryString.replaceAll('firstIndex=[0-9]*', '')"), |
335 | 'totalItems': $results.numFound, |
336 | 'defaultItemsPerPage': $rows, |
337 | 'position': 'top' |
338 | }) |
339 | {{html clean="false"}}#pagination($paginationParameters){{/html}} |
340 | (% class="search-results" %)((( |
341 | #foreach ($searchResult in $results) |
342 | #displaySearchResult($searchResult) |
343 | #end |
344 | ))) |
345 | #set ($discard = $paginationParameters.put('position', 'bottom')) |
346 | {{html clean="false"}}#pagination($paginationParameters){{/html}} |
347 | |
348 | #displayRSSLink() |
349 | #end |
350 | |
351 | #macro (displayRSSLink) |
352 | {{html clean="false"}} |
353 | #set ($parameters = {}) |
354 | ## We keep most of the current request parameters so that the RSS feed matches the current search query and filters. |
355 | #set ($discard = $parameters.putAll($request.getParameterMap())) |
356 | ## The feed will provide the most recent results that match the search query and filters. |
357 | #set ($discard = $parameters.put('sort', 'date')) |
358 | #set ($discard = $parameters.put('sortOrder', 'desc')) |
359 | ## Reset the pagination so that only the top results are included. |
360 | #set ($discard = $parameters.remove('firstIndex')) |
361 | ## Add the parameters required to output the RSS feed instead of the search UI. |
362 | #set ($discard = $parameters.put('outputSyntax', 'plain')) |
363 | #set ($discard = $parameters.put('media', 'rss')) |
364 | <a href="$doc.getURL('get', $escapetool.url($parameters))" class="hasIcon iconRSS"> |
365 | $services.localization.render('search.rss', ["[$escapetool.xml($text)]"]) |
366 | </a> |
367 | {{/html}} |
368 | #end |
369 | |
370 | #macro (displaySearchResult $searchResult) |
371 | #set ($searchResultReference = $services.solr.resolve($searchResult)) |
372 | (% class="search-result type-$searchResult.type.toLowerCase()" %)((( |
373 | ## We use the HTML macro here mainly because we don't have a way to escape the wiki syntax in the data provided by the user. |
374 | {{html clean="false"}} |
375 | #evaluate("${escapetool.h}displaySearchResult_$searchResult.type.toLowerCase()(${escapetool.d}searchResult)") |
376 | #displaySearchResultHighlighting($searchResult) |
377 | {{/html}} |
378 | #if ($debug) |
379 | |
380 | ## Scoring debug data. |
381 | ## The reason we used a separate HTML block with no cleaning is because the scoring debug data may contain some |
382 | ## characters that are considered invalid by JDOM library which is used for parsing the HTML when cleaning is on. |
383 | ## E.g. "0x0 is not a legal XML character" (org.jdom.IllegalDataException). |
384 | {{html clean="false"}} |
385 | <div class="search-result-debug">$!escapetool.xml($searchResponse.explainMap.get($searchResult.id))</div> |
386 | {{/html}} |
387 | #end |
388 | ))) |
389 | #end |
390 | |
391 | #macro (displaySearchResult_document $searchResult) |
392 | #displaySearchResultTitle() |
393 | #displaySearchResultLocation() |
394 | <div class="search-result-author"> |
395 | $services.localization.render('core.footer.modification', [ |
396 | "#displayUserProfileLink($searchResult.author $searchResult.author_display)", |
397 | $xwiki.formatDate($searchResult.date) |
398 | ]) |
399 | </div> |
400 | #end |
401 | |
402 | #macro (displaySearchResult_attachment $searchResult) |
403 | <h2 class="search-result-title"> |
404 | $services.icon.renderHTML('attach') |
405 | #set ($attachmentURL = $xwiki.getURL($searchResultReference)) |
406 | #set ($downloadHint = $services.localization.render('core.viewers.attachments.download')) |
407 | <a href="$attachmentURL" title="$escapetool.xml($downloadHint)"> |
408 | $escapetool.xml($searchResultReference.name) |
409 | </a> |
410 | #set ($attachmentHistoryURL = $xwiki.getURL($searchResultReference, 'viewattachrev', $NULL)) |
411 | #set ($historyHint = $services.localization.render('core.viewers.attachments.showHistory')) |
412 | <a href="$attachmentHistoryURL" title="$escapetool.xml($historyHint)" class="search-result-version"> |
413 | $escapetool.xml($searchResult.attversion) |
414 | </a> |
415 | </h2> |
416 | #displaySearchResultLocation($searchResult) |
417 | <div class="search-result-uploader"> |
418 | #set ($uploader = "#displayUserProfileLink($searchResult.attauthor.get(0) $searchResult.attauthor_display.get(0))") |
419 | #set ($uploadDate = $xwiki.formatDate($searchResult.attdate.get(0))) |
420 | #set ($fileSize = "#dynamicsize($searchResult.attsize.get(0))") |
421 | $services.localization.render('solr.result.uploadedBy', [$uploader, $uploadDate, $fileSize]) |
422 | </div> |
423 | <div class="search-result-mediaType">$services.localization.render('solr.result.mediaType', |
424 | [$escapetool.xml($searchResult.mimetype.get(0))])</div> |
425 | #end |
426 | |
427 | #macro (displaySearchResult_object $searchResult) |
428 | <h2 class="search-result-title"> |
429 | $services.icon.renderHTML('cubes') |
430 | $escapetool.xml("${searchResult.get('class').get(0)}[$searchResult.number]") |
431 | </h2> |
432 | #displaySearchResultLocation($searchResult) |
433 | #end |
434 | |
435 | #macro (displaySearchResult_object_property $searchResult) |
436 | <h2 class="search-result-title"> |
437 | $services.icon.renderHTML('cube') $escapetool.xml($searchResult.propertyname) |
438 | </h2> |
439 | #displaySearchResultLocation($searchResult) |
440 | #end |
441 | |
442 | #macro (displaySearchResultTitle) |
443 | #set ($showLocale = $searchResult.locale != '' && $searchResult.locale != "$xcontext.locale") |
444 | #set ($titleURL = $xwiki.getURL($searchResultReference)) |
445 | #if ($showLocale) |
446 | #set ($titleURL = $xwiki.getURL($searchResultReference, 'view', "language=$searchResult.locale")) |
447 | #end |
448 | <h2 class="search-result-title"> |
449 | $services.icon.renderHTML('file-white') |
450 | <a href="$titleURL">$escapetool.xml($searchResult.title_)</a> |
451 | #if ($showLocale) |
452 | <span title="$escapetool.xml($services.localization.render('solr.result.language'))" |
453 | class="search-result-language" >($escapetool.xml($searchResult.locale))</span> |
454 | #end |
455 | </h2> |
456 | #end |
457 | |
458 | #macro (displaySearchResultLocation $searchResult) |
459 | <div class="search-result-location"> |
460 | $services.localization.render('solr.result.locatedIn') |
461 | #set ($locationOptions = { |
462 | 'excludeSelf': true, |
463 | 'limit': 6 |
464 | }) |
465 | #hierarchy($searchResultReference $locationOptions) |
466 | </div> |
467 | #end |
468 | |
469 | #macro (displayUserProfileLink $userReference $userName) |
470 | #if ($userReference) |
471 | ## We could test if the specified user exists but we want to speed up the search. |
472 | <a href="$xwiki.getURL($userReference)">$escapetool.xml($userName)</a>## |
473 | #else |
474 | $services.localization.render('core.users.unknownUser')## |
475 | #end |
476 | #end |
477 | |
478 | #macro (displaySearchResultHighlighting $searchResult) |
479 | #getSearchResultHighlighting($searchResult $highlighting) |
480 | #if ($highlighting.size() > 0) |
481 | <dl class="search-result-highlights"> |
482 | #foreach ($entry in $highlighting) |
483 | <dt> |
484 | #if ($services.localization.get("solr.field.$entry.field")) |
485 | $services.localization.render("solr.field.$entry.field") |
486 | #elseif ($entry.field.startsWith('property.')) |
487 | #getXClassProperty($entry.field $property $classPropertyReference) |
488 | #set ($propertyPrettyName = $property.translatedPrettyName) |
489 | #if ("$!propertyPrettyName" == '') |
490 | #set ($propertyPrettyName = $classPropertyReference.name) |
491 | #end |
492 | $propertyPrettyName |
493 | #else |
494 | $entry.field |
495 | #end |
496 | </dt> |
497 | <dd>#displaySearchResultMatches($entry.matches)</dd> |
498 | #end |
499 | </dl> |
500 | #if ($highlighting.size() > 1) |
501 | ## We wrap the link in a DIV because otherwise the HTML cleaning generates a paragraph. |
502 | <div> |
503 | <a href="#" class="search-result-highlightAll hidden"> |
504 | $escapetool.xml($services.localization.render('solr.result.highlightAll')) |
505 | </a> |
506 | </div> |
507 | #end |
508 | #end |
509 | #end |
510 | |
511 | #macro (displaySearchResultMatches $matches) |
512 | #foreach ($match in $matches) |
513 | #if ($foreach.count > 1) |
514 | <span class="separator">…</span> |
515 | #end |
516 | <blockquote class="search-result-highlight">$match</blockquote> |
517 | #end |
518 | #end |
519 | |
520 | #macro (getSearchResultHighlighting $searchResult $return) |
521 | #set ($highlighting = $searchResponse.highlighting.get($searchResult.id)) |
522 | #set ($highlightingByLanguage = {}) |
523 | #foreach ($entry in $highlighting.entrySet()) |
524 | ## Remove the language suffix (e.g. __, _en, _fr, _de) from the field name. |
525 | #set ($field = $stringtool.removeEnd($entry.key, '__')) |
526 | #set ($language = $stringtool.substringAfterLast($field, '_')) |
527 | #if ($services.localization.toLocale($language)) |
528 | #set ($field = $stringtool.substringBeforeLast($field, '_')) |
529 | #else |
530 | #set ($language = '') |
531 | #end |
532 | #set ($matchesByLanguage = $highlightingByLanguage.get($field)) |
533 | #if (!$matchesByLanguage) |
534 | #set ($matchesByLanguage = {}) |
535 | #set ($discard = $highlightingByLanguage.put($field, $matchesByLanguage)) |
536 | #end |
537 | #set ($discard = $matchesByLanguage.put($language, $entry.value)) |
538 | #end |
539 | ## Keep only the matches correspoding to the search result locale. |
540 | #set ($highlighting = []) |
541 | ## Fields with a higher index will be displayed first. Fields that are not included will be displayed at the end. |
542 | #set ($fieldPriority = ['filename', 'attcontent', 'objcontent', 'comment', 'propertyname', 'propertyvalue', 'title', 'doccontent']) |
543 | #foreach ($entry in $highlightingByLanguage.entrySet()) |
544 | #set ($matches = $entry.value.get($searchResult.locale)) |
545 | #if (!$matches) |
546 | ## This should not happen but let's play safe. |
547 | #set ($matches = $entry.value.entrySet().iterator().next().value) |
548 | #end |
549 | ## Sanitize the matches. |
550 | #foreach ($match in $matches) |
551 | #set ($match = $match.replace('<span class="search-text-highlight">', "\u0011")) |
552 | #set ($match = $match.replace('<span class="search-text-highlight-stop"></span></span>', "\u0013")) |
553 | #set ($match = $escapetool.xml($match)) |
554 | #set ($match = $match.replace("\u0011", '<span class="search-text-highlight">')) |
555 | #set ($match = $match.replace("\u0013", '</span>')) |
556 | #set ($discard = $matches.set($mathtool.sub($foreach.count, 1), $match)) |
557 | #end |
558 | #set ($discard = $highlighting.add({ |
559 | 'field': $entry.key, |
560 | 'priority': $fieldPriority.indexOf($entry.key), |
561 | 'matches': $matches |
562 | })) |
563 | #end |
564 | #set ($highlighting = $sorttool.sort($highlighting, 'priority:desc')) |
565 | #set ($return = $NULL) |
566 | #setVariable("$return" $highlighting) |
567 | #end |
568 | |
569 | #macro (getSearchResults) |
570 | #set ($queryString = "$!{text}") |
571 | ## |
572 | ## Create the query and set the query string. |
573 | #set ($query = $services.query.createQuery($queryString, 'solr')) |
574 | ## |
575 | ## Set query parameters. |
576 | #set ($discard = $query.setLimit($rows)) |
577 | #set ($discard = $query.setOffset($start)) |
578 | #set ($discard = $query.bindValue('sort', "${sort} ${sortOrder}")) |
579 | #set ($discard = $query.bindValue('tie', $solrConfig.tieBreaker)) |
580 | #set ($discard = $query.bindValue('mm', $solrConfig.minShouldMatch)) |
581 | #setQueryFields($query) |
582 | #setPhraseFields($query) |
583 | #setFacetFields($query) |
584 | #setFilterQuery($query) |
585 | #setHighlightQuery($query) |
586 | #if ($debug) |
587 | #set ($discard = $query.bindValue('debugQuery', 'on')) |
588 | #end |
589 | ## |
590 | ## Execute the query. |
591 | #set ($searchResponse = $query.execute()[0]) |
592 | #end |
593 | |
594 | #macro (setQueryFields $query) |
595 | ## Specify which index fields are matched when a free text search is performed. |
596 | #if ($boost == '') |
597 | #if ($solrConfig.queryFields.substring(0, 0) == '') |
598 | ## If the value of the 'queryFields' parameter is a string then it means that the same query fields are used for |
599 | ## all result types. |
600 | #set ($boost = $solrConfig.queryFields) |
601 | #else |
602 | ## There are different query fields for each result type. |
603 | #set ($boost = $solrConfig.queryFields.get($type)) |
604 | #end |
605 | #end |
606 | #if ("$!boost" != '') |
607 | #set ($discard = $query.bindValue('qf', $boost)) |
608 | #end |
609 | #end |
610 | |
611 | #macro (setPhraseFields $query) |
612 | ## Set the main phrase field parameter boosts so that queries with all search terms |
613 | ## in close proximity have high relevance |
614 | #if ($solrConfig.phraseFields.substring(0, 0) == '') |
615 | ## If the value of the 'phraseFields' parameter is a string then it means that the |
616 | ## same query fields are used for all result types. |
617 | #set ($phraseFieldsBoost = $solrConfig.phraseFields) |
618 | #else |
619 | ## There are different phrase fields for each result type. |
620 | ## Including type = null, which will result from all facets being deselected |
621 | #set ($phraseFieldsBoost = $solrConfig.phraseFields.get("$!type")) |
622 | #end |
623 | #if ("$!phraseFieldsBoost" != '') |
624 | #set ($discard = $query.bindValue('pf', $phraseFieldsBoost)) |
625 | #set ($discard = $query.bindValue('ps', $solrConfig.phraseFieldSlop)) |
626 | #end |
627 | ## Set the bigram phrase field parameter boosts so that queries with groups of two |
628 | ## search terms in close proximity have high relevance |
629 | #if ($solrConfig.bigramPhraseFields.substring(0, 0) == '') |
630 | ## If the value of the 'bigramPhraseFields' parameter is a string then it means that the |
631 | ## same query fields are used for all result types. |
632 | #set ($bigramPhraseFieldsBoost = $solrConfig.bigramPhraseFields) |
633 | #else |
634 | ## There are different phrase fields for each result type. |
635 | ## Including type = null, which will result from all facets being deselected |
636 | #set ($bigramPhraseFieldsBoost = $solrConfig.bigramPhraseFields.get("$!type")) |
637 | #end |
638 | #if ("$!bigramPhraseFieldsBoost" != '') |
639 | #set ($discard = $query.bindValue('pf2', $bigramPhraseFieldsBoost)) |
640 | #set ($discard = $query.bindValue('ps2', $solrConfig.bigramPhraseFieldSlop)) |
641 | #end |
642 | ## Set the trigram phrase field parameter boosts so that queries with groups of three |
643 | ## search terms in close proximity have high relevance. |
644 | ## Generally (pf boost) > (pf3 boost) > (pf2 boost) |
645 | #if ($solrConfig.trigramPhraseFields.substring(0, 0) == '') |
646 | ## If the value of the 'trigramPhraseFields' parameter is a string then it means that the |
647 | ## same query fields are used for all result types. |
648 | #set ($trigramPhraseFieldsBoost = $solrConfig.trigramPhraseFields) |
649 | #else |
650 | ## There are different phrase fields for each result type. |
651 | ## including type = null, which will result from all facets being deselected |
652 | #set ($trigramPhraseFieldsBoost = $solrConfig.trigramPhraseFields.get("$!type")) |
653 | #end |
654 | #if ("$!trigramPhraseFieldsBoost" != '') |
655 | #set ($discard = $query.bindValue('pf3', $trigramPhraseFieldsBoost)) |
656 | #set ($discard = $query.bindValue('ps3', $solrConfig.trigramPhraseFieldSlop)) |
657 | #end |
658 | #end |
659 | |
660 | #macro (setFacetFields $query) |
661 | #set ($discard = $query.bindValue('facet', $facetEnabled)) |
662 | #if ($facetEnabled) |
663 | ## The facets are displayed in this order so keep the most important facets first. |
664 | #set ($facetFields = $solrConfig.facetFields) |
665 | ## In order to support multi-select faceting we need to exclude the corresponding filters when faceting. |
666 | ## See http://wiki.apache.org/solr/SimpleFacetParameters#Multi-Select_Faceting_and_LocalParams |
667 | #set ($facetFieldsWithFilterExcludes = []) |
668 | ## The type facet doesn't support multiple selection because we use different query fields for different result |
669 | ## types so the number of matches for the type facet changes when a result type is selected/unselected. |
670 | ## We don't allow multiple selection on the space facet because we perform a 'facet.prefix'-based drill down. |
671 | #set ($singleSelectionFacets = ['type', 'space_facet']) |
672 | #foreach ($facet in $facetFields) |
673 | #set ($excludeTaggedFilter = '') |
674 | #if (!$singleSelectionFacets.contains($facet)) |
675 | #set ($excludeTaggedFilter = "{!ex=$facet}") |
676 | #end |
677 | #set ($discard = $facetFieldsWithFilterExcludes.add("$excludeTaggedFilter$facet")) |
678 | #end |
679 | #set ($discard = $query.bindValue('facet.field', $facetFieldsWithFilterExcludes)) |
680 | #end |
681 | #end |
682 | |
683 | #macro (setFilterQuery $query) |
684 | ## |
685 | ## Collect the query filters. |
686 | #set ($filters = {}) |
687 | ## Add the default filters if not specified in the configuration. |
688 | #if (!$solrConfig.filterQuery || $solrConfig.filterQuery.isEmpty()) |
689 | ## Uncomment the following line of code if you want to search by default also in: |
690 | ## * the default translation of documents that are not translated in the current locale |
691 | ## * the "xx" translation if the current locale "xx_YY" doesn't have a translation available |
692 | ## (e.g. "pt" when "pt_BR" is not available) |
693 | ## See the discussion on XWIKI-9977. |
694 | ##set ($discard = $filters.put('locales', ["$xcontext.locale"])) |
695 | #if (!$xcontext.isMainWiki()) |
696 | ## Subwikis search by default in their content only. |
697 | #set ($discard = $filters.put('wiki', [$xcontext.database])) |
698 | #elseif ($solrConfig.wikisSearchableFromMainWiki) |
699 | ## The list of wikis that are searched by default can be configured. |
700 | #set ($discard = $filters.put('wiki', $solrConfig.wikisSearchableFromMainWiki)) |
701 | #end |
702 | #if ($xwiki.getUserPreference('displayHiddenDocuments') != 1) |
703 | #set ($discard = $filters.put('hidden', [false])) |
704 | #end |
705 | #end |
706 | ## Add the facets. |
707 | #set ($prefixFacets = ['space_facet']) |
708 | #foreach ($parameter in $request.parameterMap.entrySet()) |
709 | #if ($parameter.key.startsWith('f_')) |
710 | #set ($fieldName = $parameter.key.substring(2)) |
711 | #set ($escapedValues = []) |
712 | #foreach ($value in $parameter.value) |
713 | #set ($discard = $escapedValues.add("#escapeFilterValue($value)")) |
714 | #end |
715 | #set ($discard = $filters.put($fieldName, $escapedValues)) |
716 | #if ($prefixFacets.contains($fieldName)) |
717 | #set ($parts = $parameter.value.get(0).split('/', 2)) |
718 | #set ($length = $mathtool.toInteger($parts.get(0)) + 1) |
719 | #set ($prefix = "$length/$parts.get(1)") |
720 | #set ($discard = $query.bindValue("f.${fieldName}.facet.prefix", $prefix)) |
721 | #set ($discard = $prefixFacets.remove($fieldName)) |
722 | #end |
723 | #end |
724 | #end |
725 | ## Specify the initial prefix for the remaining prefix facets. |
726 | #foreach ($facet in $prefixFacets) |
727 | #set ($discard = $query.bindValue("f.${facet}.facet.prefix", '0/')) |
728 | #end |
729 | ## |
730 | ## Build the filter query. |
731 | #set ($filterQuery = []) |
732 | #if ($solrConfig.filterQuery) |
733 | #set ($discard = $filterQuery.addAll($solrConfig.filterQuery)) |
734 | #end |
735 | #foreach ($filter in $filters.entrySet()) |
736 | ## Use OR between different values of the same filter/facet. |
737 | ## Tag the filter so that we can exclude it when faceting in order to support multi-select faceting. |
738 | #set ($discard = $filterQuery.add("{!tag=$filter.key}$filter.key:($!stringtool.join($filter.value, ' OR '))")) |
739 | #end |
740 | #set ($discard = $query.bindValue('fq', $filterQuery)) |
741 | #end |
742 | |
743 | #macro(setHighlightQuery $query) |
744 | #set ($discard = $query.bindValue('hl', $highlightEnabled)) |
745 | #end |
746 | |
747 | #macro (escapeFilterValue $value) |
748 | ## Check if the given value is a range. |
749 | #if ($rangePattern.matcher($value).matches() || $wildcardPattern.matcher($value).matches())## |
750 | $value## |
751 | #else## |
752 | "$stringtool.replaceEach($value, ['\', '"'], ['\\', '\"'])"## |
753 | #end## |
754 | #end |
755 | |
756 | #macro (processRequestParameters) |
757 | #set ($text = "$!request.text") |
758 | #set ($boost = "$!request.boost") |
759 | #set ($debug = "$!request.debug" != '') |
760 | ## |
761 | ## Highlight enabled |
762 | ## First check the request, then the configuration and enable it by default |
763 | #if ($request.highlight) |
764 | #set ($highlightEnabled = $request.highlight != 'false') |
765 | #elseif ($solrConfig.containsKey('highlightEnabled')) |
766 | #set ($highlightEnabled = $solrConfig.highlightEnabled) |
767 | #else |
768 | #set ($highlightEnabled = true) |
769 | #end |
770 | ## |
771 | ## Facet enabled |
772 | ## First check the request, then the configuration and enable it by default |
773 | #if ($request.facet) |
774 | #set ($facetEnabled = $request.facet != 'false') |
775 | #elseif ($solrConfig.containsKey('facetEnabled')) |
776 | #set ($facetEnabled = $solrConfig.facetEnabled) |
777 | #else |
778 | #set ($facetEnabled = true) |
779 | #end |
780 | ## |
781 | ## Pagination |
782 | #set ($rows = $numbertool.integer($request.rows)) |
783 | #if ("$!rows" == '') |
784 | #set ($rows = 10) |
785 | #end |
786 | #set ($start = $numbertool.integer($request.firstIndex)) |
787 | #if ("$!start" == '') |
788 | #set ($start = 0) |
789 | #end |
790 | ## |
791 | ## Sort |
792 | #set ($sort = $request.sort) |
793 | #if ("$!sort" == '') |
794 | #set ($sort = 'score') |
795 | #end |
796 | #set ($sortOrder = $request.sortOrder) |
797 | #if ("$!sortOrder" == '') |
798 | #set ($sortOrder = 'desc') |
799 | #elseif ($sortOrder != 'desc') |
800 | #set ($sortOrder = 'asc') |
801 | #end |
802 | ## |
803 | ## Result type |
804 | ## We store the selected result type because we need it to decide what search and sort fields to use. |
805 | #set ($type = $request.getParameterValues('f_type')) |
806 | #if ($type && $type.size() == 1) |
807 | #set ($type = $type.get(0)) |
808 | #else |
809 | ## Extract the result type from the filter query, if specified. |
810 | #foreach ($item in $solrConfig.filterQuery) |
811 | #if ($item.startsWith('type:')) |
812 | #set ($type = $item.substring(5)) |
813 | #break |
814 | #end |
815 | #end |
816 | #end |
817 | #end |
818 | |
819 | #macro (displaySearchUI) |
820 | #set($void = $services.progress.startStep('#displaySearchUI')) |
821 | #set($void = $services.progress.pushLevel()) |
822 | #set ($discard = $xwiki.ssx.use('Main.SolrSearch')) |
823 | #set ($discard = $xwiki.jsx.use('Main.SolrSearch')) |
824 | ## Disable the document extra data: comments, attachments, history... |
825 | #set ($displayDocExtra = false) |
826 | #processRequestParameters() |
827 | (% class="search-ui" %)((( |
828 | #if ($xcontext.action == 'get') |
829 | {{html clean="false"}} |
830 | ## The search UI is updated dynamically through AJAX and we need to pull the skin extensions. |
831 | ## We put the skin extension imports inside a <noscript> element to prevent jQuery from fetching the JavaScript |
832 | ## files automatically (we want to fetch only the new JavaScript files). |
833 | <noscript class="hidden skin-extension-imports">#skinExtensionHooks</noscript> |
834 | {{/html}} |
835 | |
836 | #end |
837 | #displaySearchForm() |
838 | #if ($text != '') |
839 | #getSearchResults() |
840 | #if ($debug) |
841 | #displaySearchDebugInfo() |
842 | #end |
843 | (% class="search-results-container row" %)((( |
844 | #if ($facetEnabled) |
845 | (% class="col-xs-12 col-sm-4 col-sm-push-8 col-md-3 col-md-push-9" %)((( |
846 | #displaySearchFacets($searchResponse) |
847 | ))) |
848 | #end |
849 | (% class="search-results-left col-xs-12#if ($facetEnabled) col-sm-8 col-sm-pull-4 col-md-9 col-md-pull-3#end" %) |
850 | ((( |
851 | #displaySearchResultsSort() |
852 | |
853 | #displaySearchResults() |
854 | ))) |
855 | ))) |
856 | #end |
857 | ))) |
858 | #set($void = $services.progress.popLevel()) |
859 | #set($void = $services.progress.endStep()) |
860 | #end |
861 | |
862 | #macro (outputRSSFeed) |
863 | ## |
864 | ## Get the search results. |
865 | ## |
866 | #processRequestParameters() |
867 | #getSearchResults() |
868 | #set ($list = []) |
869 | #set ($results = $searchResponse.results) |
870 | #foreach ($searchResult in $results) |
871 | #set ($searchResultDocumentReference = $services.solr.resolveDocument($searchResult)) |
872 | #set ($discard = $list.add("$searchResultDocumentReference")) |
873 | #end |
874 | ## |
875 | ## Compute the feed URI. |
876 | ## |
877 | #set ($parameters = {}) |
878 | #set ($discard = $parameters.putAll($request.getParameterMap())) |
879 | #set ($discard = $parameters.remove('outputSyntax')) |
880 | #set ($discard = $parameters.remove('media')) |
881 | #set ($feedURI = $doc.getExternalURL('view', $escapetool.url($parameters))) |
882 | ## |
883 | ## Configure the feed. |
884 | ## |
885 | #set ($feed = $xwiki.feed.getDocumentFeed($list, {})) |
886 | #set ($discard = $feed.setLink($feedURI)) |
887 | #set ($discard = $feed.setUri($feedURI)) |
888 | #set ($discard = $feed.setAuthor('XWiki')) |
889 | #set ($title = $services.localization.render('search.rss', ["[$text]"])) |
890 | #set ($discard = $feed.setTitle($title)) |
891 | #set ($discard = $feed.setDescription($title)) |
892 | #set ($discard = $feed.setLanguage("$xcontext.locale")) |
893 | #set ($discard = $feed.setCopyright($xwiki.getXWikiPreference('copyright'))) |
894 | ## |
895 | ## Output the feed. |
896 | ## |
897 | #set ($discard = $response.setContentType('application/rss+xml')) |
898 | $xwiki.feed.getFeedOutput($feed, 'rss_2.0') |
899 | #end |
900 | |
901 | #macro (handleSolrSearchRequest) |
902 | ## Preselect facet values only for the facets that are enabled. |
903 | #set ($discard = $solrConfig.facetQuery.keySet().retainAll($solrConfig.facetFields)) |
904 | #if ($request.media == 'rss') |
905 | #outputRSSFeed() |
906 | #elseif ("$!request.r" == '1' || $solrConfig.facetQuery.isEmpty()) |
907 | #displaySearchUI() |
908 | #else |
909 | ## Redirect using preselected facet values. |
910 | #set ($extraParams = {}) |
911 | #foreach ($entry in $solrConfig.facetQuery.entrySet()) |
912 | #set ($discard = $extraParams.put("f_$entry.key", $entry.value)) |
913 | #end |
914 | ## Prevent redirect loop. |
915 | #set ($extraParams.r = 1) |
916 | #extendQueryString($url $extraParams) |
917 | $response.sendRedirect($url) |
918 | #end |
919 | #end |
920 | {{/velocity}} |