001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.ametys.cms.search.solr; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.nio.charset.StandardCharsets; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Optional; 030import java.util.Set; 031import java.util.stream.Collectors; 032 033import org.apache.avalon.framework.activity.Initializable; 034import org.apache.avalon.framework.component.Component; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.commons.collections.CollectionUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.solr.client.solrj.SolrClient; 041import org.apache.solr.client.solrj.request.RequestWriter.ContentWriter; 042import org.apache.solr.client.solrj.request.json.DomainMap; 043import org.apache.solr.client.solrj.request.json.JsonQueryRequest; 044import org.apache.solr.client.solrj.request.json.TermsFacetMap; 045import org.apache.solr.client.solrj.response.FacetField; 046import org.apache.solr.client.solrj.response.FacetField.Count; 047import org.apache.solr.client.solrj.response.QueryResponse; 048import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet; 049import org.apache.solr.client.solrj.response.json.BucketJsonFacet; 050import org.apache.solr.common.params.CommonParams; 051import org.apache.solr.common.params.HighlightParams; 052import org.slf4j.Logger; 053 054import org.ametys.cms.search.SearchResults; 055import org.ametys.cms.search.SortOrder; 056import org.ametys.cms.search.query.JoinQuery; 057import org.ametys.cms.search.query.MatchAllQuery; 058import org.ametys.cms.search.query.OrQuery; 059import org.ametys.cms.search.query.Query; 060import org.ametys.cms.search.query.QuerySyntaxException; 061import org.ametys.core.group.GroupIdentity; 062import org.ametys.core.group.GroupManager; 063import org.ametys.core.right.AllowedUsers; 064import org.ametys.core.user.CurrentUserProvider; 065import org.ametys.core.user.UserIdentity; 066import org.ametys.plugins.repository.AmetysObject; 067import org.ametys.plugins.repository.AmetysObjectIterable; 068import org.ametys.plugins.repository.AmetysObjectResolver; 069import org.ametys.runtime.plugin.component.AbstractLogEnabled; 070 071/** 072 * Component searching objects corresponding to a {@link Query}. 073 */ 074public class SearcherFactory extends AbstractLogEnabled implements Component, Serviceable, Initializable 075{ 076 077 /** The component role. */ 078 public static final String ROLE = SearcherFactory.class.getName(); 079 080 /** The default size for highlight snippet */ 081 public static final int DEFAULT_HIGHLIGHT_SNIPPET_SIZE = 400; 082 083 /** The default count for highlight snippet */ 084 public static final int DEFAULT_HIGHLIGHT_SNIPPET_COUNT = 5; 085 086 /** The {@link AmetysObjectResolver} */ 087 protected AmetysObjectResolver _resolver; 088 089 /** The solr client provider */ 090 protected SolrClientProvider _solrClientProvider; 091 092 /** The current user provider. */ 093 protected CurrentUserProvider _currentUserProvider; 094 095 /** The group manager */ 096 protected GroupManager _groupManager; 097 098 /** The solr client */ 099 protected SolrClient _solrClient; 100 101 @Override 102 public void service(ServiceManager serviceManager) throws ServiceException 103 { 104 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 105 _solrClientProvider = (SolrClientProvider) serviceManager.lookup(SolrClientProvider.ROLE); 106 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 107 _groupManager = (GroupManager) serviceManager.lookup(GroupManager.ROLE); 108 } 109 110 @Override 111 public void initialize() throws Exception 112 { 113 _solrClient = _solrClientProvider.getReadClient(); 114 } 115 116 /** 117 * Create a Searcher. 118 * @return a Searcher object. 119 */ 120 public Searcher create() 121 { 122 return new Searcher(getLogger()); 123 } 124 125 /** 126 * Class searching objects corresponding to a query, with optional sort, facets, and so on. 127 */ 128 public class Searcher 129 { 130 private Logger _logger; 131 132 private String _queryString; 133 private Query _query; 134 private List<String> _filterQueryStrings; 135 private List<Query> _filterQueries; 136 private List<SortDefinition> _sortClauses; 137 private List<FacetDefinition> _facets; 138 private Map<String, List<String>> _facetValues; 139 private int _start; 140 private int _maxResults; 141 private Map<String, Object> _searchContext; 142 private boolean _checkRights; 143 private AllowedUsers _checkRightsComparingTo; 144 private boolean _debug; 145 private HighlightDefinition _highlightDefinition; 146 147 /** 148 * Build a Searcher with default values. 149 * @param logger The logger. 150 */ 151 protected Searcher(Logger logger) 152 { 153 _logger = logger; 154 155 _filterQueryStrings = new ArrayList<>(); 156 _filterQueries = new ArrayList<>(); 157 _sortClauses = new ArrayList<>(); 158 _facets = new ArrayList<>(); 159 _facetValues = new HashMap<>(); 160 _start = 0; 161 _maxResults = Integer.MAX_VALUE; 162 _searchContext = new HashMap<>(); 163 _checkRights = true; 164 } 165 166 /** 167 * Set the query (as a String). 168 * @param query the query (as a String). 169 * @return The Searcher object itself. 170 */ 171 public Searcher withQueryString(String query) 172 { 173 if (this._query != null) 174 { 175 throw new IllegalArgumentException("Query and query string can't be used at the same time."); 176 } 177 this._queryString = query; 178 return this; 179 } 180 181 /** 182 * Set the query (as a {@link Query} object). 183 * @param query the query (as a {@link Query} object). 184 * @return The Searcher object itself. 185 */ 186 public Searcher withQuery(Query query) 187 { 188 if (this._queryString != null) 189 { 190 throw new IllegalArgumentException("Query and query string can't be used at the same time."); 191 } 192 this._query = query; 193 return this; 194 } 195 196 /** 197 * Set the filter queries (as Strings). 198 * @param queries the filter queries (as Strings). 199 * @return The Searcher object itself. The Searcher object itself. 200 */ 201 public Searcher withFilterQueryStrings(String... queries) 202 { 203 _filterQueryStrings = new ArrayList<>(queries.length); 204 CollectionUtils.addAll(_filterQueryStrings, queries); 205 return this; 206 } 207 208 /** 209 * Set the filter queries (as Strings). 210 * @param queries the filter queries (as Strings). 211 * @return The Searcher object itself. The Searcher object itself. 212 */ 213 public Searcher withFilterQueryStrings(Collection<String> queries) 214 { 215 _filterQueryStrings = new ArrayList<>(queries); 216 return this; 217 } 218 219 /** 220 * Add a filter query to the existing ones (as a String). 221 * @param query the filter query to add (as a String). 222 * @return The Searcher object itself. The Searcher object itself. 223 */ 224 public Searcher addFilterQueryString(String query) 225 { 226 _filterQueryStrings.add(query); 227 return this; 228 } 229 230 /** 231 * Set the filter queries (as {@link Query} objects). 232 * @param queries the filter queries (as {@link Query} objects). 233 * @return The Searcher object itself. The Searcher object itself. 234 */ 235 public Searcher withFilterQueries(Query... queries) 236 { 237 _filterQueries = new ArrayList<>(queries.length); 238 CollectionUtils.addAll(_filterQueries, queries); 239 return this; 240 } 241 242 /** 243 * Set the filter queries (as {@link Query} objects). 244 * @param queries the filter queries (as {@link Query} objects). 245 * @return The Searcher object itself. The Searcher object itself. 246 */ 247 public Searcher withFilterQueries(Collection<Query> queries) 248 { 249 _filterQueries = new ArrayList<>(queries); 250 return this; 251 } 252 253 /** 254 * Add a filter query to the existing ones (as a {@link Query} object). 255 * @param query the filter query to add (as a {@link Query} object). 256 * @return The Searcher object itself. The Searcher object itself. 257 */ 258 public Searcher addFilterQuery(Query query) 259 { 260 _filterQueries.add(query); 261 return this; 262 } 263 264 /** 265 * Set the sort clauses. 266 * @param sortClauses the sort clauses. 267 * @return The Searcher object itself. 268 */ 269 public Searcher withSort(SortDefinition... sortClauses) 270 { 271 _sortClauses = new ArrayList<>(sortClauses.length); 272 CollectionUtils.addAll(_sortClauses, sortClauses); 273 return this; 274 } 275 276 /** 277 * Set the sort clauses. 278 * @param sortClauses the sort clauses. 279 * @return The Searcher object itself. 280 */ 281 public Searcher withSort(List<SortDefinition> sortClauses) 282 { 283 _sortClauses = new ArrayList<>(sortClauses); 284 return this; 285 } 286 287 /** 288 * Add a sort clause to the existing ones. 289 * @param sortClause The sort clause to add. 290 * @return The Searcher object itself. 291 */ 292 public Searcher addSort(SortDefinition sortClause) 293 { 294 _sortClauses.add(sortClause); 295 return this; 296 } 297 298 /** 299 * Set the faceted fields. 300 * @param facets the faceted fields. 301 * @return The Searcher object itself. 302 */ 303 public Searcher withFacets(FacetDefinition... facets) 304 { 305 _facets = new ArrayList<>(facets.length); 306 CollectionUtils.addAll(_facets, facets); 307 return this; 308 } 309 310 /** 311 * Set the faceted fields. 312 * @param facets the faceted fields. 313 * @return The Searcher object itself. 314 */ 315 public Searcher withFacets(Collection<FacetDefinition> facets) 316 { 317 _facets = new ArrayList<>(facets); 318 return this; 319 } 320 321 /** 322 * Add a faceted field. 323 * @param facet The faceted field to add. 324 * @return The Searcher object itself. 325 */ 326 public Searcher addFacet(FacetDefinition facet) 327 { 328 _facets.add(facet); 329 return this; 330 } 331 332 /** 333 * Set the facet values. 334 * @param facetValues The facet values. 335 * @return The Searcher object itself. 336 */ 337 public Searcher withFacetValues(Map<String, List<String>> facetValues) 338 { 339 _facetValues = new HashMap<>(facetValues); 340 return this; 341 } 342 343 /** 344 * Set the search offset and limit. 345 * @param start The start index (offset). 346 * @param maxResults The maximum number of results. 347 * @return The Searcher object itself. 348 */ 349 public Searcher withLimits(int start, int maxResults) 350 { 351 this._start = start; 352 this._maxResults = maxResults; 353 return this; 354 } 355 356 /** 357 * Set the search context. 358 * @param searchContext The search context. 359 * @return The Searcher object itself. 360 */ 361 public Searcher withContext(Map<String, Object> searchContext) 362 { 363 _searchContext = new HashMap<>(searchContext); 364 return this; 365 } 366 367 /** 368 * Set the highlight. 369 * @param query the highlighting query 370 * @return the searcher object itself. 371 */ 372 public Searcher withHighlightingQuery(Query query) 373 { 374 this._highlightDefinition = new HighlightDefinition(query, DEFAULT_HIGHLIGHT_SNIPPET_COUNT, DEFAULT_HIGHLIGHT_SNIPPET_SIZE); 375 return this; 376 } 377 378 /** 379 * Set the highlight. 380 * @param highlightDefinition the record with the parameters 381 * @return the searcher object itself. 382 */ 383 public Searcher withHighlight(HighlightDefinition highlightDefinition) 384 { 385 this._highlightDefinition = highlightDefinition; 386 return this; 387 } 388 389 /** 390 * Add a value to the search context. 391 * @param key The context key. 392 * @param value The value. 393 * @return The Searcher object itself. 394 */ 395 public Searcher addContextElement(String key, Object value) 396 { 397 _searchContext.put(key, value); 398 return this; 399 } 400 401 /** 402 * Whether to check rights when searching, false otherwise. 403 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 404 * @return The Searcher object itself. 405 */ 406 public Searcher setCheckRights(boolean checkRights) 407 { 408 _checkRights = checkRights; 409 return this; 410 } 411 412 /** 413 * Check rights when searching, <b>not</b> according to the current user, 414 * but according to the given {@link AllowedUsers visibilty} to compare each 415 * result with. 416 * @param compareTo the {@link AllowedUsers visibilty} to compare each result with. 417 * @return The Searcher object itself. 418 */ 419 public Searcher checkRightsComparingTo(AllowedUsers compareTo) 420 { 421 _checkRights = false; 422 _checkRightsComparingTo = compareTo; 423 return this; 424 } 425 426 /** 427 * Sets the debug on the Solr query 428 * @return The Searcher object itself. 429 */ 430 public Searcher setDebugOn() 431 { 432 _debug = true; 433 return this; 434 } 435 436 /** 437 * Execute the search with the current parameters. 438 * @param <A> The type of search results 439 * @return An iterable on the result ametys objects. 440 * @throws Exception If an error occurs. 441 */ 442 public <A extends AmetysObject> AmetysObjectIterable<A> search() throws Exception 443 { 444 SearchResults<A> searchResults = searchWithFacets(); 445 return searchResults.getObjects(); 446 } 447 448 /** 449 * Execute the search with the current parameters. 450 * @param <A> The type of search results 451 * @return An iterable on the search result objects. 452 * @throws Exception If an error occurs. 453 */ 454 public <A extends AmetysObject> SearchResults<A> searchWithFacets() throws Exception 455 { 456 QueryResponse response = _querySolrServer(); 457 return _buildResults(response, _facets); 458 } 459 460 /** 461 * From the Solr server response, builds the {@link SearchResults} object. 462 * @param <A> The type of search results 463 * @param response The response from the Solr server 464 * @param facets The facet fields to return 465 * @return An iterable on the search result objects. 466 * @throws Exception If an error occurs. 467 */ 468 protected <A extends AmetysObject> SearchResults<A> _buildResults(QueryResponse response, List<FacetDefinition> facets) throws Exception 469 { 470 _handleDebug(response); 471 Map<String, Map<String, Integer>> facetResults = getFacetResults(response, facets); 472 return new SolrSearchResults<>(response, _resolver, facetResults); 473 } 474 475 private void _handleDebug(QueryResponse response) 476 { 477 if (_debug && _logger.isDebugEnabled()) 478 { 479 Map<String, Object> debugMap = response.getDebugMap(); 480 _logger.debug("Debug response: \n{}", debugMap); 481 } 482 } 483 484 private QueryResponse _querySolrServer() throws Exception 485 { 486 _logSearchQueries(); 487 488 Object query = getQuery(); 489 List<Object> filterQueries = getFilterQueries(); 490 491 AmetysQueryRequest solrQuery = getSolrQuery(query, filterQueries, _start, _maxResults, _searchContext, _checkRights, _checkRightsComparingTo); 492 493 // Set the sort specification and facets in the solr query object. 494 setSort(solrQuery, _sortClauses); 495 setFacets(solrQuery, _facets, _facetValues); 496 497 modifySolrQuery(solrQuery); 498 499 QueryResponse response = solrQuery.process(_solrClient, _solrClientProvider.getCollectionName()); 500 501 if (_logger.isInfoEnabled()) 502 { 503 _logger.info("Solr request executed in {} ms", response.getQTime()); 504 } 505 506 return response; 507 } 508 509 private void _logSearchQueries() 510 { 511 if (!_logger.isDebugEnabled()) 512 { 513 return; 514 } 515 516 if (_queryString == null && _query != null) 517 { 518 _logger.debug("Query before building: \n{}", _query.toString(0)); 519 } 520 521 if (!_filterQueries.isEmpty()) 522 { 523 _logger.debug("Filter Queries before building: \n{}", _filterQueries 524 .stream() 525 .map(fq -> fq.toString(0)) 526 .collect(Collectors.joining("\n###\n"))); 527 } 528 } 529 530 /** 531 * Get the query string from the parameters. 532 * @return The query string. 533 * @throws QuerySyntaxException If the query is invalid. 534 */ 535 protected Object getQuery() throws QuerySyntaxException 536 { 537 Object query = "*:*"; 538 539 if (_queryString != null) 540 { 541 query = _queryString; 542 } 543 else if (_query != null) 544 { 545 query = _query.rewrite().orElse(new MatchAllQuery()) 546 .buildAsJson().orElse(new MatchAllQuery().buildAsJson()); 547 } 548 549 return query; 550 } 551 552 /** 553 * Get the filter queries from the parameters. 554 * @return The list of filter queries. 555 * @throws QuerySyntaxException If one of the queries is invalid. 556 */ 557 protected List<Object> getFilterQueries() throws QuerySyntaxException 558 { 559 List<Object> filterQueries = new ArrayList<>(); 560 561 filterQueries.addAll(_filterQueryStrings); 562 563 for (Query fq : _filterQueries) 564 { 565 // discard useless empty or "*:*" filter queries 566 Optional<Query> query = fq.rewrite(); 567 if (query.isPresent() && !(query.get() instanceof MatchAllQuery)) 568 { 569 Optional<Object> fqAsJson = query.get().buildAsJson(); 570 if (fqAsJson.isPresent()) 571 { 572 filterQueries.add(fqAsJson.get()); 573 } 574 } 575 } 576 577 return filterQueries; 578 } 579 580 /** 581 * Get the solr query object. 582 * @param query The solr query string. 583 * @param filterQueries The filter queries (as Strings). 584 * @param start The start index. 585 * @param maxResults The maximum number of results. 586 * @param searchContext The search context. 587 * @param checkRights Whether to check rights when searching or not. 588 * @param allowedUsersToCompare The {@link AllowedUsers} object to compare with for checking rights 589 * @return The solr query object. 590 * @throws Exception If an error occurs. 591 */ 592 @SuppressWarnings("unchecked") 593 protected AmetysQueryRequest getSolrQuery(Object query, Collection<Object> filterQueries, int start, int maxResults, Map<String, Object> searchContext, boolean checkRights, AllowedUsers allowedUsersToCompare) throws Exception 594 { 595 AmetysQueryRequest solrQuery = new AmetysQueryRequest(_logger); 596 597 if (query instanceof String q) 598 { 599 solrQuery.setQuery(StringUtils.isNotBlank(q) ? q : "*:*"); 600 } 601 else if (query instanceof Map) 602 { 603 solrQuery.setQuery((Map<String, Object>) query); 604 } 605 606 // Set the query string, pagination spec and fields to be returned. 607 if (start > 0) 608 { 609 solrQuery.setOffset(start); 610 } 611 612 solrQuery.setLimit(maxResults); 613 614 solrQuery.returnFields("id", "score"); 615 616 // Add filter queries. 617 for (Object fq : filterQueries) 618 { 619 if (fq instanceof String) 620 { 621 solrQuery.withFilter((String) fq); 622 } 623 else if (fq instanceof Map) 624 { 625 solrQuery.withFilter((Map<String, Object>) fq); 626 } 627 } 628 629 if (checkRights) 630 { 631 _checkRightsQuery(solrQuery); 632 } 633 else if (allowedUsersToCompare != null) 634 { 635 _checkAllowedUsers(solrQuery, allowedUsersToCompare); 636 } 637 if (_highlightDefinition != null && _highlightDefinition.query() != null) 638 { 639 // hl.q="query"&hl.fl=*&hl.requireMatchField=true 640 solrQuery.withParam(HighlightParams.HIGHLIGHT, true); 641 solrQuery.withParam(HighlightParams.Q, _highlightDefinition.query().build()); 642 // returns fields in query that match 643 solrQuery.withParam(HighlightParams.FIELDS, "*"); 644 solrQuery.withParam(HighlightParams.FIELD_MATCH, true); 645 if (_highlightDefinition.snippets() > 0) 646 { 647 solrQuery.withParam(HighlightParams.SNIPPETS, _highlightDefinition.snippets()); 648 } 649 if (_highlightDefinition.fragSize() > 0) 650 { 651 solrQuery.withParam(HighlightParams.FRAGSIZE, _highlightDefinition.fragSize()); 652 } 653 } 654 if (_debug) 655 { 656 solrQuery.withParam(CommonParams.DEBUG, "true"); 657 } 658 659 return solrQuery; 660 } 661 662 private void _checkRightsQuery(JsonQueryRequest solrQuery) 663 { 664 Map<String, Object> acl; 665 666 UserIdentity user = _currentUserProvider.getUser(); 667 if (user == null) 668 { 669 acl = Map.of("anonymous", ""); 670 } 671 else 672 { 673 acl = new HashMap<>(); 674 acl.put("populationId", user.getPopulationId()); 675 acl.put("login", user.getLogin()); 676 677 Set<GroupIdentity> groups = _groupManager.getUserGroups(user); 678 if (!groups.isEmpty()) 679 { 680 List<String> groupIds = groups.stream() 681 .map(GroupIdentity::groupIdentityToString) 682 .toList(); 683 684 acl.put("groups", groupIds); 685 } 686 } 687 688 // {!acl anonymous=} or {!acl populationId=users login=user1 groups="group1#groupDirectory,group2#groupDirectory"} to check acl 689 solrQuery.withFilter(Map.of("acl", acl)); 690 } 691 692 private void _checkAllowedUsers(JsonQueryRequest solrQuery, AllowedUsers allowedUsersToCompare) 693 { 694 Map<String, Object> acl; 695 696 if (allowedUsersToCompare.isAnonymousAllowed()) 697 { 698 acl = Map.of("anonymous", "true"); 699 } 700 else 701 { 702 acl = new HashMap<>(); 703 704 if (allowedUsersToCompare.isAnyConnectedUserAllowed()) 705 { 706 acl.put("anyConnected", "true"); 707 } 708 709 Set<String> allowedUsers = allowedUsersToCompare.getAllowedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toSet()); 710 Set<String> deniedUsers = allowedUsersToCompare.getDeniedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toSet()); 711 Set<String> allowedGroups = allowedUsersToCompare.getAllowedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toSet()); 712 Set<String> deniedGroups = allowedUsersToCompare.getDeniedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toSet()); 713 714 if (!allowedUsers.isEmpty()) 715 { 716 acl.put("allowedUsers", allowedUsers); 717 } 718 719 if (!deniedUsers.isEmpty()) 720 { 721 acl.put("deniedUsers", deniedUsers); 722 } 723 724 if (!allowedGroups.isEmpty()) 725 { 726 acl.put("allowedGroups", allowedGroups); 727 } 728 729 if (!deniedGroups.isEmpty()) 730 { 731 acl.put("deniedGroups", deniedGroups); 732 } 733 } 734 735 solrQuery.withFilter(Map.of("aclCompare", acl)); 736 } 737 738 /** 739 * Set the sort definition in the solr query object. 740 * @param solrQuery The solr query object. 741 * @param sortCriteria The sort criteria. 742 */ 743 protected void setSort(JsonQueryRequest solrQuery, List<SortDefinition> sortCriteria) 744 { 745 if (sortCriteria.isEmpty()) 746 { 747 solrQuery.setSort("score desc"); 748 } 749 750 String sort = sortCriteria.stream() 751 .map(sortCriterion -> _getSortFinalFieldName(sortCriterion) + " " + (sortCriterion.order() == SortOrder.ASC ? "asc" : "desc")) 752 .collect(Collectors.joining(",")); 753 754 if (StringUtils.isNotBlank(sort)) 755 { 756 solrQuery.setSort(sort); 757 } 758 } 759 760 private String _getSortFinalFieldName(SortDefinition sortCriterion) 761 { 762 return sortCriterion.joinedPaths().isEmpty() 763 ? sortCriterion.solrSortFieldName() 764 : "ametys(" + _getJoinedFunction(sortCriterion.joinedPaths(), sortCriterion.solrSortFieldName()) + ")"; 765 } 766 767 /** 768 * Set the facet definition in the solr query object and return a mapping from solr field name to criterion ID. 769 * @param solrQuery the solr query object to fill. 770 * @param facetDefinitions The facet definitions to use. 771 * @param facetValues the facet values. 772 * @throws QuerySyntaxException if there's a syntax error in queries 773 */ 774 protected void setFacets(JsonQueryRequest solrQuery, Collection<FacetDefinition> facetDefinitions, Map<String, List<String>> facetValues) throws QuerySyntaxException 775 { 776 List<String> joinedFacets = new ArrayList<>(); 777 for (FacetDefinition facetDefinition : facetDefinitions) 778 { 779 String fieldName = facetDefinition.id(); 780 String facetFieldName = facetDefinition.solrFacetFieldName(); 781 782 if (StringUtils.isNotBlank(fieldName)) 783 { 784 List<String> joinedPaths = facetDefinition.joinedPaths(); 785 if (joinedPaths.isEmpty()) 786 { 787 _setNonJoinedFacet(solrQuery, fieldName, facetFieldName, facetValues); 788 } 789 else 790 { 791 String joinedFacet = _setJoinedFacet(solrQuery, fieldName, joinedPaths, facetFieldName, facetValues); 792 joinedFacets.add(joinedFacet); 793 } 794 } 795 } 796 797 if (!joinedFacets.isEmpty()) 798 { 799 solrQuery.withParam("facet", "true"); 800 solrQuery.withParam("facet.ametys", joinedFacets); 801 } 802 } 803 804 private String _setJoinedFacet(JsonQueryRequest solrQuery, String fieldName, List<String> joinedPaths, String solrFieldName, Map<String, List<String>> facetValues) throws QuerySyntaxException 805 { 806 List<String> fieldFacetValues = facetValues.get(fieldName); 807 if (fieldFacetValues != null && !fieldFacetValues.isEmpty()) 808 { 809 List<Query> facetQueries = new ArrayList<>(); 810 for (String facetValue : fieldFacetValues) 811 { 812 facetQueries.add(() -> solrFieldName + ":\"" + facetValue + '"'); 813 } 814 815 JoinQuery joinQuery = new JoinQuery(new OrQuery(facetQueries), joinedPaths); 816 817 solrQuery.withFilter(Map.of("#" + fieldName, joinQuery.buildAsJson().orElse(new MatchAllQuery().buildAsJson()))); 818 } 819 820 return "{!ex=" + fieldName + " key=" + fieldName + "}" + _getJoinedFunction(joinedPaths, solrFieldName); 821 } 822 823 private String _getJoinedFunction(List<String> joinedPaths, String solrFieldName) 824 { 825 return StringUtils.join(joinedPaths, "->") + "," + solrFieldName; 826 } 827 828 private void _setNonJoinedFacet(JsonQueryRequest solrQuery, String fieldName, String solrFieldName, Map<String, List<String>> facetValues) throws QuerySyntaxException 829 { 830 List<String> fieldFacetValues = facetValues.get(fieldName); 831 if (fieldFacetValues != null && !fieldFacetValues.isEmpty()) 832 { 833 List<Query> facetQueries = new ArrayList<>(); 834 for (String facetValue : fieldFacetValues) 835 { 836 facetQueries.add(() -> solrFieldName + ":\"" + facetValue + '"'); 837 } 838 839 solrQuery.withFilter(Map.of("#" + fieldName, new OrQuery(facetQueries).buildAsJson().orElse(new MatchAllQuery().buildAsJson()))); 840 } 841 842 TermsFacetMap facetMap = new TermsFacetMap(solrFieldName).setLimit(-1) 843 .withDomain(new DomainMap().withTagsToExclude(fieldName)); 844 845 solrQuery.withFacet(fieldName, facetMap); 846 } 847 848 /** 849 * Retrieve the facet results from the solr response. 850 * @param response the solr response. 851 * @param facetDefinitions The facet fields to return. 852 * @return the facet results. 853 */ 854 protected Map<String, Map<String, Integer>> getFacetResults(QueryResponse response, Collection<FacetDefinition> facetDefinitions) 855 { 856 Map<String, Map<String, Integer>> facetResults = new LinkedHashMap<>(); 857 858 for (FacetDefinition facetDefinition : facetDefinitions) 859 { 860 String fieldName = facetDefinition.id(); 861 862 if (facetDefinition.joinedPaths().isEmpty()) 863 { 864 BucketBasedJsonFacet facet = response.getJsonFacetingResponse().getBucketBasedFacets(fieldName); 865 866 List<BucketJsonFacet> values = facet.getBuckets(); 867 868 Map<String, Integer> solrFacetValues = new HashMap<>(); 869 facetResults.put(fieldName, solrFacetValues); 870 871 for (BucketJsonFacet value : values) 872 { 873 solrFacetValues.put(value.getVal().toString(), (int) value.getCount()); 874 } 875 } 876 else 877 { 878 FacetField solrFacetField = response.getFacetField(fieldName); 879 880 List<Count> values = solrFacetField.getValues(); 881 882 Map<String, Integer> solrFacetValues = new HashMap<>(); 883 facetResults.put(fieldName, solrFacetValues); 884 885 for (Count count : values) 886 { 887 solrFacetValues.put(count.getName(), (int) count.getCount()); 888 } 889 } 890 } 891 892 return facetResults; 893 } 894 895 /** 896 * Template method to do additional operations on the Solr query before passing it to the Solr client 897 * @param query the Solr query 898 */ 899 protected void modifySolrQuery(JsonQueryRequest query) 900 { 901 // do nothing by default 902 } 903 } 904 905 static class AmetysQueryRequest extends JsonQueryRequest 906 { 907 private Logger _logger; 908 909 public AmetysQueryRequest(Logger logger) 910 { 911 super(); 912 _logger = logger; 913 } 914 915 @Override 916 public ContentWriter getContentWriter(String expectedType) 917 { 918 ContentWriter writer = super.getContentWriter(expectedType); 919 920 if (!_logger.isInfoEnabled()) 921 { 922 return writer; 923 } 924 925 return new ContentWriter() 926 { 927 public void write(OutputStream os) throws IOException 928 { 929 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 930 writer.write(baos); 931 932 _logger.info("Solr query:\n" + baos.toString(StandardCharsets.UTF_8)); 933 934 os.write(baos.toByteArray(), 0, baos.size()); 935 } 936 937 public String getContentType() 938 { 939 return writer.getContentType(); 940 } 941 }; 942 } 943 } 944 945 /** 946 * Record representing a sort criterion. 947 * @param solrSortFieldName the name of the solr sort field 948 * @param joinedPaths the joined paths 949 * @param order The sort order 950 */ 951 public record SortDefinition (String solrSortFieldName, List<String> joinedPaths, SortOrder order) { 952 953 /** 954 * Creates a {@link SortDefinition} record 955 * @param facetFieldName the name of the solr sort field 956 * @param sortOrder The sort order 957 */ 958 public SortDefinition(String facetFieldName, SortOrder sortOrder) 959 { 960 this(facetFieldName, new ArrayList<>(), sortOrder); 961 } 962 } 963 964 /** 965 * Record representing a facet definition. 966 * @param id the facet identifier 967 * @param solrFacetFieldName the name of the solr facet field 968 * @param joinedPaths the joined paths 969 */ 970 public record FacetDefinition (String id, String solrFacetFieldName, List<String> joinedPaths) { 971 972 /** 973 * Creates a {@link FacetDefinition} record 974 * @param facetId the facet identifier 975 * @param facetFieldName the name of the solr facet field 976 */ 977 public FacetDefinition(String facetId, String facetFieldName) 978 { 979 this(facetId, facetFieldName, new ArrayList<>()); 980 } 981 } 982 983 /** 984 * Record representing the highlighting parameters. 985 * @param query The highlighting query 986 * @param snippets The maximum number of fragments to return 987 * @param fragSize The size of fragments in characters 988 */ 989 public record HighlightDefinition(Query query, int snippets, int fragSize) { } 990}