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}