Wiki-Quellcode von Solr Search Macros

Zuletzt geändert von Daniel Herrmann am 2026/02/04 20:23

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