
001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 028import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 029import ca.uhn.fhir.jpa.dao.search.ExtendedHSearchClauseBuilder; 030import ca.uhn.fhir.jpa.dao.search.ExtendedHSearchIndexExtractor; 031import ca.uhn.fhir.jpa.dao.search.ExtendedHSearchResourceProjection; 032import ca.uhn.fhir.jpa.dao.search.ExtendedHSearchSearchBuilder; 033import ca.uhn.fhir.jpa.dao.search.IHSearchSortHelper; 034import ca.uhn.fhir.jpa.dao.search.LastNOperation; 035import ca.uhn.fhir.jpa.dao.search.SearchScrollQueryExecutorAdaptor; 036import ca.uhn.fhir.jpa.model.dao.JpaPid; 037import ca.uhn.fhir.jpa.model.entity.ResourceTable; 038import ca.uhn.fhir.jpa.model.search.ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams; 039import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData; 040import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 041import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions; 042import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteSearch; 043import ca.uhn.fhir.jpa.search.builder.ISearchQueryExecutor; 044import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 045import ca.uhn.fhir.jpa.search.builder.SearchQueryExecutors; 046import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 047import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 048import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 049import ca.uhn.fhir.model.api.IQueryParameterType; 050import ca.uhn.fhir.parser.IParser; 051import ca.uhn.fhir.rest.api.Constants; 052import ca.uhn.fhir.rest.api.server.RequestDetails; 053import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 054import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 055import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 056import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 057import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 058import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 059import com.google.common.collect.Ordering; 060import jakarta.annotation.Nonnull; 061import jakarta.persistence.EntityManager; 062import jakarta.persistence.PersistenceContext; 063import jakarta.persistence.PersistenceContextType; 064import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension; 065import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep; 066import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; 067import org.hibernate.search.engine.search.projection.dsl.CompositeProjectionOptionsStep; 068import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory; 069import org.hibernate.search.engine.search.query.dsl.SearchQueryOptionsStep; 070import org.hibernate.search.mapper.orm.Search; 071import org.hibernate.search.mapper.orm.common.EntityReference; 072import org.hibernate.search.mapper.orm.search.loading.dsl.SearchLoadingOptionsStep; 073import org.hibernate.search.mapper.orm.session.SearchSession; 074import org.hibernate.search.mapper.orm.work.SearchIndexingPlan; 075import org.hibernate.search.util.common.SearchException; 076import org.hl7.fhir.instance.model.api.IBaseResource; 077import org.springframework.beans.factory.annotation.Autowired; 078import org.springframework.transaction.PlatformTransactionManager; 079import org.springframework.transaction.annotation.Transactional; 080import org.springframework.transaction.support.TransactionTemplate; 081 082import java.util.ArrayList; 083import java.util.Collection; 084import java.util.Collections; 085import java.util.List; 086import java.util.Objects; 087import java.util.Spliterators; 088import java.util.stream.Collectors; 089import java.util.stream.StreamSupport; 090 091import static ca.uhn.fhir.rest.server.BasePagingProvider.DEFAULT_MAX_PAGE_SIZE; 092import static org.apache.commons.lang3.StringUtils.isNotBlank; 093 094public class FulltextSearchSvcImpl implements IFulltextSearchSvc { 095 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FulltextSearchSvcImpl.class); 096 private static final int DEFAULT_MAX_NON_PAGED_SIZE = 500; 097 private final ExtendedHSearchSearchBuilder myAdvancedIndexQueryBuilder = new ExtendedHSearchSearchBuilder(); 098 099 @Autowired 100 ISearchParamExtractor mySearchParamExtractor; 101 102 @Autowired 103 IIdHelperService myIdHelperService; 104 105 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 106 private EntityManager myEntityManager; 107 108 @Autowired 109 private PlatformTransactionManager myTxManager; 110 111 @Autowired 112 private FhirContext myFhirContext; 113 114 @Autowired 115 private ISearchParamRegistry mySearchParamRegistry; 116 117 @Autowired 118 private JpaStorageSettings myStorageSettings; 119 120 @Autowired 121 private IHSearchSortHelper myExtendedFulltextSortHelper; 122 123 @Autowired(required = false) 124 private IHSearchEventListener myHSearchEventListener; 125 126 @Autowired 127 private IInterceptorBroadcaster myInterceptorBroadcaster; 128 129 private Boolean ourDisabled; 130 131 /** 132 * Constructor 133 */ 134 public FulltextSearchSvcImpl() { 135 super(); 136 } 137 138 @Override 139 public ExtendedHSearchIndexData extractLuceneIndexData( 140 IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) { 141 String resourceType = myFhirContext.getResourceType(theResource); 142 ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams( 143 resourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 144 ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor( 145 myStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor); 146 return extractor.extract(theResource, theEntity, theNewParams); 147 } 148 149 @Override 150 public boolean canUseHibernateSearch(String theResourceType, SearchParameterMap myParams) { 151 boolean requiresHibernateSearchAccess = myParams.containsKey(Constants.PARAM_CONTENT) 152 || myParams.containsKey(Constants.PARAM_TEXT) 153 || myParams.isLastN(); 154 // we have to use it - _text and _content searches only use hibernate 155 if (requiresHibernateSearchAccess) { 156 return true; 157 } 158 159 // if the registry has not been initialized 160 // we cannot use HibernateSearch because it 161 // will, internally, trigger a new search 162 // when it refreshes the search parameters 163 // (which will cause an infinite loop) 164 if (!mySearchParamRegistry.isInitialized()) { 165 return false; 166 } 167 168 return myStorageSettings.isHibernateSearchIndexSearchParams() 169 && myAdvancedIndexQueryBuilder.canUseHibernateSearch(theResourceType, myParams, mySearchParamRegistry); 170 } 171 172 @Override 173 public void reindex(ResourceTable theEntity) { 174 validateHibernateSearchIsEnabled(); 175 176 SearchIndexingPlan plan = getSearchSession().indexingPlan(); 177 plan.addOrUpdate(theEntity); 178 } 179 180 @Override 181 public ISearchQueryExecutor searchNotScrolled( 182 String theResourceName, 183 SearchParameterMap theParams, 184 Integer theMaxResultsToFetch, 185 RequestDetails theRequestDetails) { 186 validateHibernateSearchIsEnabled(); 187 188 return doSearch(theResourceName, theParams, null, theMaxResultsToFetch, theRequestDetails); 189 } 190 191 @Transactional 192 @Override 193 public ISearchQueryExecutor searchScrolled( 194 String theResourceType, SearchParameterMap theParams, RequestDetails theRequestDetails) { 195 validateHibernateSearchIsEnabled(); 196 197 SearchQueryOptionsStep<?, JpaPid, SearchLoadingOptionsStep, ?, ?> searchQueryOptionsStep = 198 getSearchQueryOptionsStep(theResourceType, theParams, null); 199 logQuery(searchQueryOptionsStep, theRequestDetails); 200 201 return new SearchScrollQueryExecutorAdaptor(searchQueryOptionsStep.scroll(SearchBuilder.getMaximumPageSize())); 202 } 203 204 // keep this in sync with supportsSomeOf(); 205 @SuppressWarnings("rawtypes") 206 private ISearchQueryExecutor doSearch( 207 String theResourceType, 208 SearchParameterMap theParams, 209 IResourcePersistentId theReferencingPid, 210 Integer theMaxResultsToFetch, 211 RequestDetails theRequestDetails) { 212 213 int offset = theParams.getOffset() == null ? 0 : theParams.getOffset(); 214 int count = getMaxFetchSize(theParams, theMaxResultsToFetch); 215 216 // perform an offset search instead of a scroll one, which doesn't allow for offset 217 SearchQueryOptionsStep<?, JpaPid, SearchLoadingOptionsStep, ?, ?> searchQueryOptionsStep = 218 getSearchQueryOptionsStep(theResourceType, theParams, theReferencingPid); 219 logQuery(searchQueryOptionsStep, theRequestDetails); 220 List<JpaPid> longs = searchQueryOptionsStep.fetchHits(offset, count); 221 222 // indicate param was already processed, otherwise queries DB to process it 223 theParams.setOffset(null); 224 return SearchQueryExecutors.from(longs); 225 } 226 227 private int getMaxFetchSize(SearchParameterMap theParams, Integer theMax) { 228 if (theMax != null) { 229 return theMax; 230 } 231 232 // todo mb we should really pass this in. 233 if (theParams.getCount() != null) { 234 return theParams.getCount(); 235 } 236 237 return DEFAULT_MAX_NON_PAGED_SIZE; 238 } 239 240 @SuppressWarnings("rawtypes") 241 private SearchQueryOptionsStep<?, JpaPid, SearchLoadingOptionsStep, ?, ?> getSearchQueryOptionsStep( 242 String theResourceType, SearchParameterMap theParams, IResourcePersistentId theReferencingPid) { 243 244 dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH); 245 SearchQueryOptionsStep<?, JpaPid, SearchLoadingOptionsStep, ?, ?> query = getSearchSession() 246 .search(ResourceTable.class) 247 // The document id is the PK which is pid. We use this instead of _myId to avoid fetching the doc body. 248 .select( 249 // adapt the String docRef.id() to the Long that it really is. 250 f -> f.composite(docRef -> JpaPid.fromId(Long.valueOf(docRef.id())), f.documentReference())) 251 .where(f -> buildWhereClause(f, theResourceType, theParams, theReferencingPid)); 252 253 if (theParams.getSort() != null) { 254 query.sort(f -> myExtendedFulltextSortHelper.getSortClauses(f, theParams.getSort(), theResourceType)); 255 256 // indicate parameter was processed 257 theParams.setSort(null); 258 } 259 260 return query; 261 } 262 263 @SuppressWarnings("rawtypes") 264 private PredicateFinalStep buildWhereClause( 265 SearchPredicateFactory f, 266 String theResourceType, 267 SearchParameterMap theParams, 268 IResourcePersistentId theReferencingPid) { 269 270 verifyContentAndTextParamsAreSupportedIfUsed(theResourceType, theParams); 271 272 return f.bool(b -> { 273 ExtendedHSearchClauseBuilder builder = 274 new ExtendedHSearchClauseBuilder(myFhirContext, myStorageSettings, b, f); 275 276 /* 277 * Handle _content parameter (resource body content) 278 * 279 * Posterity: 280 * We do not want the HAPI-FHIR dao's to process the 281 * _content parameter, so we remove it from the map here 282 */ 283 List<List<IQueryParameterType>> contentAndTerms = theParams.remove(Constants.PARAM_CONTENT); 284 builder.addStringTextSearch(Constants.PARAM_CONTENT, contentAndTerms); 285 286 /* 287 * Handle _text parameter (resource narrative content) 288 * 289 * Posterity: 290 * We do not want the HAPI-FHIR dao's to process the 291 * _text parameter, so we remove it from the map here 292 */ 293 List<List<IQueryParameterType>> textAndTerms = theParams.remove(Constants.PARAM_TEXT); 294 builder.addStringTextSearch(Constants.PARAM_TEXT, textAndTerms); 295 296 if (theReferencingPid != null) { 297 b.must(f.match().field("myResourceLinksField").matching(theReferencingPid.toString())); 298 } 299 300 if (isNotBlank(theResourceType)) { 301 builder.addResourceTypeClause(theResourceType); 302 } 303 304 /* 305 * Handle other supported parameters 306 */ 307 if (myStorageSettings.isHibernateSearchIndexSearchParams() && theParams.getEverythingMode() == null) { 308 ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams params = 309 new ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams(); 310 params.setSearchParamRegistry(mySearchParamRegistry) 311 .setResourceType(theResourceType) 312 .setSearchParameterMap(theParams); 313 myAdvancedIndexQueryBuilder.addAndConsumeAdvancedQueryClauses(builder, params); 314 } 315 // DROP EARLY HERE IF BOOL IS EMPTY? 316 }); 317 } 318 319 /** 320 * If either (or both) of the <code>_content</code> or <code>_text</code> Search Parameters 321 * are present in the request, verify the given parameter(s) are actually supported by the 322 * current configuration and throw a {@link InvalidRequestException} if not. 323 */ 324 private void verifyContentAndTextParamsAreSupportedIfUsed(String theResourceType, SearchParameterMap theParams) { 325 boolean haveParamText = theParams.containsKey(Constants.PARAM_TEXT); 326 boolean haveParamContent = theParams.containsKey(Constants.PARAM_CONTENT); 327 if (!haveParamText && !haveParamContent) { 328 return; 329 } 330 331 // Can't use _content or _text if FullText indexing is disabled 332 if (!myStorageSettings.isHibernateSearchIndexFullText()) { 333 String failingParams = theParams.keySet().stream() 334 .filter(t -> t.equals(Constants.PARAM_TEXT) || t.equals(Constants.PARAM_CONTENT)) 335 .sorted() 336 .collect(Collectors.joining(", ")); 337 String msg = myFhirContext 338 .getLocalizer() 339 .getMessage(FulltextSearchSvcImpl.class, "fullTextSearchingNotPossible", failingParams); 340 throw new InvalidRequestException(Msg.code(2566) + msg); 341 } 342 343 List<String> failingParams = null; 344 345 // theResourceType is null for $everything queries 346 if (theResourceType != null) { 347 if (haveParamContent 348 && !mySearchParamRegistry.hasActiveSearchParam( 349 theResourceType, 350 Constants.PARAM_CONTENT, 351 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)) { 352 failingParams = new ArrayList<>(2); 353 failingParams.add(Constants.PARAM_CONTENT); 354 } 355 356 if (haveParamText 357 && !mySearchParamRegistry.hasActiveSearchParam( 358 theResourceType, 359 Constants.PARAM_TEXT, 360 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)) { 361 failingParams = Objects.requireNonNullElseGet(failingParams, () -> new ArrayList<>(2)); 362 failingParams.add(Constants.PARAM_TEXT); 363 } 364 } 365 366 if (failingParams != null) { 367 String msg = myFhirContext 368 .getLocalizer() 369 .getMessage( 370 FulltextSearchSvcImpl.class, 371 "fullTextSearchingNotPossibleForResourceType", 372 theResourceType, 373 String.join(",", failingParams)); 374 throw new InvalidRequestException(Msg.code(2727) + msg); 375 } 376 } 377 378 @Nonnull 379 private SearchSession getSearchSession() { 380 return Search.session(myEntityManager); 381 } 382 383 @SuppressWarnings("rawtypes") 384 private List<IResourcePersistentId> convertLongsToResourcePersistentIds(List<Long> theLongPids) { 385 return theLongPids.stream().map(JpaPid::fromId).collect(Collectors.toList()); 386 } 387 388 @Override 389 @SuppressWarnings({"rawtypes", "unchecked"}) 390 public List<IResourcePersistentId> everything( 391 String theResourceName, 392 SearchParameterMap theParams, 393 IResourcePersistentId theReferencingPid, 394 RequestDetails theRequestDetails) { 395 validateHibernateSearchIsEnabled(); 396 397 // todo mb what about max results here? 398 List<IResourcePersistentId> retVal = 399 toList(doSearch(null, theParams, theReferencingPid, 10_000, theRequestDetails), 10_000); 400 if (theReferencingPid != null) { 401 retVal.add(theReferencingPid); 402 } 403 return retVal; 404 } 405 406 private void validateHibernateSearchIsEnabled() { 407 if (isDisabled()) { 408 throw new UnsupportedOperationException(Msg.code(2137) + "Hibernate search is not enabled!"); 409 } 410 } 411 412 @Override 413 public boolean isDisabled() { 414 Boolean retVal = ourDisabled; 415 416 if (retVal == null) { 417 retVal = new TransactionTemplate(myTxManager).execute(t -> { 418 try { 419 SearchSession searchSession = getSearchSession(); 420 searchSession.search(ResourceTable.class); 421 return Boolean.FALSE; 422 } catch (Exception e) { 423 ourLog.trace("FullText test failed", e); 424 ourLog.debug( 425 "Hibernate Search (Lucene) appears to be disabled on this server, fulltext will be disabled"); 426 return Boolean.TRUE; 427 } 428 }); 429 ourDisabled = retVal; 430 } 431 432 assert retVal != null; 433 return retVal; 434 } 435 436 @Transactional() 437 @Override 438 @SuppressWarnings("unchecked") 439 public List<IResourcePersistentId> search( 440 String theResourceName, SearchParameterMap theParams, RequestDetails theRequestDetails) { 441 validateHibernateSearchIsEnabled(); 442 return toList( 443 doSearch(theResourceName, theParams, null, DEFAULT_MAX_NON_PAGED_SIZE, theRequestDetails), 444 DEFAULT_MAX_NON_PAGED_SIZE); 445 } 446 447 /** 448 * Adapt our async interface to the legacy concrete List 449 */ 450 @SuppressWarnings("rawtypes") 451 private List<IResourcePersistentId> toList(ISearchQueryExecutor theSearchResultStream, long theMaxSize) { 452 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(theSearchResultStream, 0), false) 453 .limit(theMaxSize) 454 .collect(Collectors.toList()); 455 } 456 457 @Transactional() 458 @Override 459 public IBaseResource tokenAutocompleteValueSetSearch(ValueSetAutocompleteOptions theOptions) { 460 validateHibernateSearchIsEnabled(); 461 ensureElastic(); 462 463 ValueSetAutocompleteSearch autocomplete = 464 new ValueSetAutocompleteSearch(myFhirContext, myStorageSettings, getSearchSession()); 465 466 dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH); 467 return autocomplete.search(theOptions); 468 } 469 470 /** 471 * Throws an error if configured with Lucene. 472 * <p> 473 * Some features only work with Elasticsearch. 474 * Lastn and the autocomplete search use nested aggregations which are Elasticsearch-only 475 */ 476 private void ensureElastic() { 477 try { 478 getSearchSession().scope(ResourceTable.class).aggregation().extension(ElasticsearchExtension.get()); 479 } catch (SearchException e) { 480 // unsupported. we are probably running Lucene. 481 throw new IllegalStateException( 482 Msg.code(2070) + "This operation requires Elasticsearch. Lucene is not supported."); 483 } 484 } 485 486 @Override 487 @SuppressWarnings("rawtypes") 488 public List<IResourcePersistentId> lastN(SearchParameterMap theParams, Integer theMaximumResults) { 489 ensureElastic(); 490 dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH); 491 List<Long> pidList = new LastNOperation( 492 getSearchSession(), myFhirContext, myStorageSettings, mySearchParamRegistry) 493 .executeLastN(theParams, theMaximumResults); 494 return convertLongsToResourcePersistentIds(pidList); 495 } 496 497 @Override 498 public List<IBaseResource> getResources(Collection<Long> thePids) { 499 if (thePids.isEmpty()) { 500 return Collections.emptyList(); 501 } 502 503 SearchSession session = getSearchSession(); 504 dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH); 505 List<ExtendedHSearchResourceProjection> rawResourceDataList = session.search(ResourceTable.class) 506 .select(this::buildResourceSelectClause) 507 .where( 508 f -> f.id().matchingAny(JpaPid.fromLongList(thePids)) // matches '_id' from resource index 509 ) 510 .fetchAllHits(); 511 512 // order resource projections as per thePids 513 ArrayList<Long> pidList = new ArrayList<>(thePids); 514 List<ExtendedHSearchResourceProjection> orderedAsPidsResourceDataList = rawResourceDataList.stream() 515 .sorted(Ordering.explicit(pidList).onResultOf(ExtendedHSearchResourceProjection::getPid)) 516 .collect(Collectors.toList()); 517 518 return resourceProjectionsToResources(orderedAsPidsResourceDataList); 519 } 520 521 @Nonnull 522 private List<IBaseResource> resourceProjectionsToResources( 523 List<ExtendedHSearchResourceProjection> theResourceDataList) { 524 IParser parser = myFhirContext.newJsonParser(); 525 return theResourceDataList.stream().map(p -> p.toResource(parser)).collect(Collectors.toList()); 526 } 527 528 private CompositeProjectionOptionsStep<?, ExtendedHSearchResourceProjection> buildResourceSelectClause( 529 SearchProjectionFactory<EntityReference, ResourceTable> f) { 530 return f.composite( 531 ExtendedHSearchResourceProjection::new, 532 f.field("myId", JpaPid.class), 533 f.field("myForcedId", String.class), 534 f.field("myRawResource", String.class)); 535 } 536 537 @Override 538 public long count(String theResourceName, SearchParameterMap theParams) { 539 SearchQueryOptionsStep<?, JpaPid, SearchLoadingOptionsStep, ?, ?> queryOptionsStep = 540 getSearchQueryOptionsStep(theResourceName, theParams, null); 541 542 return queryOptionsStep.fetchTotalHitCount(); 543 } 544 545 @Override 546 @Transactional(readOnly = true) 547 public List<IBaseResource> searchForResources( 548 String theResourceType, SearchParameterMap theParams, RequestDetails theRequestDetails) { 549 int offset = 0; 550 int limit = theParams.getCount() == null ? DEFAULT_MAX_PAGE_SIZE : theParams.getCount(); 551 552 if (theParams.getOffset() != null && theParams.getOffset() != 0) { 553 offset = theParams.getOffset(); 554 // indicate param was already processed, otherwise queries DB to process it 555 theParams.setOffset(null); 556 } 557 558 dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH); 559 560 var query = getSearchSession() 561 .search(ResourceTable.class) 562 .select(this::buildResourceSelectClause) 563 .where(f -> buildWhereClause(f, theResourceType, theParams, null)); 564 565 if (theParams.getSort() != null) { 566 query.sort(f -> myExtendedFulltextSortHelper.getSortClauses(f, theParams.getSort(), theResourceType)); 567 } 568 569 logQuery(query, theRequestDetails); 570 List<ExtendedHSearchResourceProjection> extendedLuceneResourceProjections = query.fetchHits(offset, limit); 571 572 return resourceProjectionsToResources(extendedLuceneResourceProjections); 573 } 574 575 /** 576 * Fire the JPA_PERFTRACE_INFO hook if it is enabled 577 * @param theQuery the query to log 578 * @param theRequestDetails the request details 579 */ 580 @SuppressWarnings("rawtypes") 581 private void logQuery(SearchQueryOptionsStep theQuery, RequestDetails theRequestDetails) { 582 IInterceptorBroadcaster compositeBroadcaster = 583 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 584 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 585 StorageProcessingMessage storageProcessingMessage = new StorageProcessingMessage(); 586 String queryString = theQuery.toQuery().queryString(); 587 storageProcessingMessage.setMessage(queryString); 588 HookParams params = new HookParams() 589 .add(RequestDetails.class, theRequestDetails) 590 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 591 .add(StorageProcessingMessage.class, storageProcessingMessage); 592 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 593 } 594 } 595 596 @Override 597 public boolean supportsAllOf(SearchParameterMap theParams) { 598 return myAdvancedIndexQueryBuilder.isSupportsAllOf(theParams); 599 } 600 601 @Override 602 public boolean supportsAllSortTerms(String theResourceType, SearchParameterMap theParams) { 603 return myExtendedFulltextSortHelper.supportsAllSortTerms(theResourceType, theParams); 604 } 605 606 private void dispatchEvent(IHSearchEventListener.HSearchEventType theEventType) { 607 if (myHSearchEventListener != null) { 608 myHSearchEventListener.hsearchEvent(theEventType); 609 } 610 } 611 612 @Override 613 public void deleteIndexedDocumentsByTypeAndId(Class theClazz, List<Object> theGivenIds) { 614 SearchSession session = Search.session(myEntityManager); 615 SearchIndexingPlan indexingPlan = session.indexingPlan(); 616 for (Object givenId : theGivenIds) { 617 indexingPlan.purge(theClazz, givenId, null); 618 } 619 indexingPlan.process(); 620 indexingPlan.execute(); 621 } 622}