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