Wiki-Quellcode von Solr Search Macros

Version 4.1 von Daniel Herrmann am 2025/12/16 11:14

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