Code source wiki de Solr Search Macros

Version 3.1 par Florent Charton le 2026/01/08 10:59

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