Wiki source code of Solr Search Macros

Last modified by Bimit Administrator on 05.12.2023, 13:23

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