67
67
import org .hibernate .search .engine .search .projection .dsl .CompositeProjectionOptionsStep ;
68
68
import org .hibernate .search .engine .search .projection .dsl .SearchProjectionFactory ;
69
69
import org .hibernate .search .engine .search .query .dsl .SearchQueryOptionsStep ;
70
- import org .hibernate .search .engine .search .sort .dsl .SearchSortFactory ;
71
70
import org .hibernate .search .mapper .orm .Search ;
72
71
import org .hibernate .search .mapper .orm .common .EntityReference ;
73
72
import org .hibernate .search .mapper .orm .search .loading .dsl .SearchLoadingOptionsStep ;
76
75
import org .hibernate .search .util .common .SearchException ;
77
76
import org .hl7 .fhir .instance .model .api .IBaseResource ;
78
77
import org .springframework .beans .factory .annotation .Autowired ;
79
- import org .springframework .stereotype .Service ;
80
78
import org .springframework .transaction .PlatformTransactionManager ;
81
79
import org .springframework .transaction .annotation .Transactional ;
82
80
import org .springframework .transaction .support .TransactionTemplate ;
93
91
import static ca .uhn .fhir .rest .server .BasePagingProvider .DEFAULT_MAX_PAGE_SIZE ;
94
92
import static org .apache .commons .lang3 .StringUtils .isNotBlank ;
95
93
96
- @ Service
97
94
public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
98
95
private static final org .slf4j .Logger ourLog = org .slf4j .LoggerFactory .getLogger (FulltextSearchSvcImpl .class );
99
96
private static final int DEFAULT_MAX_NON_PAGED_SIZE = 500 ;
@@ -154,19 +151,28 @@ public boolean canUseHibernateSearch(String theResourceType, SearchParameterMap
154
151
boolean requiresHibernateSearchAccess = myParams .containsKey (Constants .PARAM_CONTENT )
155
152
|| myParams .containsKey (Constants .PARAM_TEXT )
156
153
|| myParams .isLastN ();
154
+ // we have to use it - _text and _content searches only use hibernate
157
155
if (requiresHibernateSearchAccess ) {
158
156
return true ;
159
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)
160
164
if (!mySearchParamRegistry .isInitialized ()) {
161
165
return false ;
162
166
}
167
+
163
168
return myStorageSettings .isHibernateSearchIndexSearchParams ()
164
169
&& myAdvancedIndexQueryBuilder .canUseHibernateSearch (theResourceType , myParams , mySearchParamRegistry );
165
170
}
166
171
167
172
@ Override
168
173
public void reindex (ResourceTable theEntity ) {
169
174
validateHibernateSearchIsEnabled ();
175
+
170
176
SearchIndexingPlan plan = getSearchSession ().indexingPlan ();
171
177
plan .addOrUpdate (theEntity );
172
178
}
@@ -178,6 +184,7 @@ public ISearchQueryExecutor searchNotScrolled(
178
184
Integer theMaxResultsToFetch ,
179
185
RequestDetails theRequestDetails ) {
180
186
validateHibernateSearchIsEnabled ();
187
+
181
188
return doSearch (theResourceName , theParams , null , theMaxResultsToFetch , theRequestDetails );
182
189
}
183
190
@@ -187,13 +194,14 @@ public ISearchQueryExecutor searchScrolled(
187
194
String theResourceType , SearchParameterMap theParams , RequestDetails theRequestDetails ) {
188
195
validateHibernateSearchIsEnabled ();
189
196
190
- SearchQueryOptionsStep <JpaPid , ?, ? , SearchLoadingOptionsStep , ?, ?> searchQueryOptionsStep =
197
+ SearchQueryOptionsStep <? , ?, JpaPid , SearchLoadingOptionsStep , ?, ?> searchQueryOptionsStep =
191
198
getSearchQueryOptionsStep (theResourceType , theParams , null );
192
199
logQuery (searchQueryOptionsStep , theRequestDetails );
193
200
194
201
return new SearchScrollQueryExecutorAdaptor (searchQueryOptionsStep .scroll (SearchBuilder .getMaximumPageSize ()));
195
202
}
196
203
204
+ // keep this in sync with supportsSomeOf();
197
205
@ SuppressWarnings ("rawtypes" )
198
206
private ISearchQueryExecutor doSearch (
199
207
String theResourceType ,
@@ -205,11 +213,13 @@ private ISearchQueryExecutor doSearch(
205
213
int offset = theParams .getOffset () == null ? 0 : theParams .getOffset ();
206
214
int count = getMaxFetchSize (theParams , theMaxResultsToFetch );
207
215
208
- SearchQueryOptionsStep <JpaPid , ?, ?, SearchLoadingOptionsStep , SearchSortFactory , ?> searchQueryOptionsStep =
216
+ // perform an offset search instead of a scroll one, which doesn't allow for offset
217
+ SearchQueryOptionsStep <?, ?, JpaPid , SearchLoadingOptionsStep , ?, ?> searchQueryOptionsStep =
209
218
getSearchQueryOptionsStep (theResourceType , theParams , theReferencingPid );
210
219
logQuery (searchQueryOptionsStep , theRequestDetails );
211
220
List <JpaPid > longs = searchQueryOptionsStep .fetchHits (offset , count );
212
221
222
+ // indicate param was already processed, otherwise queries DB to process it
213
223
theParams .setOffset (null );
214
224
return SearchQueryExecutors .from (longs );
215
225
}
@@ -218,26 +228,32 @@ private int getMaxFetchSize(SearchParameterMap theParams, Integer theMax) {
218
228
if (theMax != null ) {
219
229
return theMax ;
220
230
}
231
+
232
+ // todo mb we should really pass this in.
221
233
if (theParams .getCount () != null ) {
222
234
return theParams .getCount ();
223
235
}
236
+
224
237
return DEFAULT_MAX_NON_PAGED_SIZE ;
225
238
}
226
239
227
240
@ SuppressWarnings ("rawtypes" )
228
- private SearchQueryOptionsStep <JpaPid , ?, ?, SearchLoadingOptionsStep , SearchSortFactory , ?>
229
- getSearchQueryOptionsStep (
230
- String theResourceType , SearchParameterMap theParams , IResourcePersistentId theReferencingPid ) {
241
+ private SearchQueryOptionsStep <?, ?, JpaPid , SearchLoadingOptionsStep , ?, ?> getSearchQueryOptionsStep (
242
+ String theResourceType , SearchParameterMap theParams , IResourcePersistentId theReferencingPid ) {
231
243
232
244
dispatchEvent (IHSearchEventListener .HSearchEventType .SEARCH );
233
- SearchQueryOptionsStep <JpaPid , ?, ? , SearchLoadingOptionsStep , SearchSortFactory , ?> query = getSearchSession ()
245
+ SearchQueryOptionsStep <? , ?, JpaPid , SearchLoadingOptionsStep , ? , ?> query = getSearchSession ()
234
246
.search (ResourceTable .class )
235
- // The document id is the PK which is pid. We use this instead of _myId to avoid fetching the doc body.
236
- .select (f -> f .composite (docRef -> JpaPid .fromId (Long .valueOf (docRef .id ())), f .documentReference ()))
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 ()))
237
251
.where (f -> buildWhereClause (f , theResourceType , theParams , theReferencingPid ));
238
252
239
253
if (theParams .getSort () != null ) {
240
254
query .sort (f -> myExtendedFulltextSortHelper .getSortClauses (f , theParams .getSort (), theResourceType ));
255
+
256
+ // indicate parameter was processed
241
257
theParams .setSort (null );
242
258
}
243
259
@@ -257,9 +273,23 @@ private PredicateFinalStep buildWhereClause(
257
273
ExtendedHSearchClauseBuilder builder =
258
274
new ExtendedHSearchClauseBuilder (myFhirContext , myStorageSettings , b , f );
259
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
+ */
260
283
List <List <IQueryParameterType >> contentAndTerms = theParams .remove (Constants .PARAM_CONTENT );
261
284
builder .addStringTextSearch (Constants .PARAM_CONTENT , contentAndTerms );
262
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
+ */
263
293
List <List <IQueryParameterType >> textAndTerms = theParams .remove (Constants .PARAM_TEXT );
264
294
builder .addStringTextSearch (Constants .PARAM_TEXT , textAndTerms );
265
295
@@ -271,6 +301,9 @@ private PredicateFinalStep buildWhereClause(
271
301
builder .addResourceTypeClause (theResourceType );
272
302
}
273
303
304
+ /*
305
+ * Handle other supported parameters
306
+ */
274
307
if (myStorageSettings .isHibernateSearchIndexSearchParams () && theParams .getEverythingMode () == null ) {
275
308
ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams params =
276
309
new ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams ();
@@ -279,15 +312,23 @@ private PredicateFinalStep buildWhereClause(
279
312
.setSearchParameterMap (theParams );
280
313
myAdvancedIndexQueryBuilder .addAndConsumeAdvancedQueryClauses (builder , params );
281
314
}
315
+ // DROP EARLY HERE IF BOOL IS EMPTY?
282
316
});
283
317
}
284
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
+ */
285
324
private void verifyContentAndTextParamsAreSupportedIfUsed (String theResourceType , SearchParameterMap theParams ) {
286
325
boolean haveParamText = theParams .containsKey (Constants .PARAM_TEXT );
287
326
boolean haveParamContent = theParams .containsKey (Constants .PARAM_CONTENT );
288
327
if (!haveParamText && !haveParamContent ) {
289
328
return ;
290
329
}
330
+
331
+ // Can't use _content or _text if FullText indexing is disabled
291
332
if (!myStorageSettings .isHibernateSearchIndexFullText ()) {
292
333
String failingParams = theParams .keySet ().stream ()
293
334
.filter (t -> t .equals (Constants .PARAM_TEXT ) || t .equals (Constants .PARAM_CONTENT ))
@@ -300,6 +341,8 @@ private void verifyContentAndTextParamsAreSupportedIfUsed(String theResourceType
300
341
}
301
342
302
343
List <String > failingParams = null ;
344
+
345
+ // theResourceType is null for $everything queries
303
346
if (theResourceType != null ) {
304
347
if (haveParamContent
305
348
&& !mySearchParamRegistry .hasActiveSearchParam (
@@ -351,6 +394,7 @@ public List<IResourcePersistentId> everything(
351
394
RequestDetails theRequestDetails ) {
352
395
validateHibernateSearchIsEnabled ();
353
396
397
+ // todo mb what about max results here?
354
398
List <IResourcePersistentId > retVal =
355
399
toList (doSearch (null , theParams , theReferencingPid , 10_000 , theRequestDetails ), 10_000 );
356
400
if (theReferencingPid != null ) {
@@ -400,6 +444,9 @@ public List<IResourcePersistentId> search(
400
444
DEFAULT_MAX_NON_PAGED_SIZE );
401
445
}
402
446
447
+ /**
448
+ * Adapt our async interface to the legacy concrete List
449
+ */
403
450
@ SuppressWarnings ("rawtypes" )
404
451
private List <IResourcePersistentId > toList (ISearchQueryExecutor theSearchResultStream , long theMaxSize ) {
405
452
return StreamSupport .stream (Spliterators .spliteratorUnknownSize (theSearchResultStream , 0 ), false )
@@ -420,10 +467,17 @@ public IBaseResource tokenAutocompleteValueSetSearch(ValueSetAutocompleteOptions
420
467
return autocomplete .search (theOptions );
421
468
}
422
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
+ */
423
476
private void ensureElastic () {
424
477
try {
425
478
getSearchSession ().scope (ResourceTable .class ).aggregation ().extension (ElasticsearchExtension .get ());
426
479
} catch (SearchException e ) {
480
+ // unsupported. we are probably running Lucene.
427
481
throw new IllegalStateException (
428
482
Msg .code (2070 ) + "This operation requires Elasticsearch. Lucene is not supported." );
429
483
}
@@ -450,9 +504,12 @@ public List<IBaseResource> getResources(Collection<Long> thePids) {
450
504
dispatchEvent (IHSearchEventListener .HSearchEventType .SEARCH );
451
505
List <ExtendedHSearchResourceProjection > rawResourceDataList = session .search (ResourceTable .class )
452
506
.select (this ::buildResourceSelectClause )
453
- .where (f -> f .id ().matchingAny (JpaPid .fromLongList (thePids )))
507
+ .where (
508
+ f -> f .id ().matchingAny (JpaPid .fromLongList (thePids )) // matches '_id' from resource index
509
+ )
454
510
.fetchAllHits ();
455
511
512
+ // order resource projections as per thePids
456
513
ArrayList <Long > pidList = new ArrayList <>(thePids );
457
514
List <ExtendedHSearchResourceProjection > orderedAsPidsResourceDataList = rawResourceDataList .stream ()
458
515
.sorted (Ordering .explicit (pidList ).onResultOf (ExtendedHSearchResourceProjection ::getPid ))
@@ -479,7 +536,7 @@ private CompositeProjectionOptionsStep<?, ExtendedHSearchResourceProjection> bui
479
536
480
537
@ Override
481
538
public long count (String theResourceName , SearchParameterMap theParams ) {
482
- SearchQueryOptionsStep <JpaPid , ?, ? , SearchLoadingOptionsStep , SearchSortFactory , ?> queryOptionsStep =
539
+ SearchQueryOptionsStep <? , ?, JpaPid , SearchLoadingOptionsStep , ? , ?> queryOptionsStep =
483
540
getSearchQueryOptionsStep (theResourceName , theParams , null );
484
541
485
542
return queryOptionsStep .fetchTotalHitCount ();
@@ -494,16 +551,16 @@ public List<IBaseResource> searchForResources(
494
551
495
552
if (theParams .getOffset () != null && theParams .getOffset () != 0 ) {
496
553
offset = theParams .getOffset ();
554
+ // indicate param was already processed, otherwise queries DB to process it
497
555
theParams .setOffset (null );
498
556
}
499
557
500
558
dispatchEvent (IHSearchEventListener .HSearchEventType .SEARCH );
501
559
502
- SearchQueryOptionsStep <ExtendedHSearchResourceProjection , ?, ?, SearchLoadingOptionsStep , SearchSortFactory , ?>
503
- query = getSearchSession ()
504
- .search (ResourceTable .class )
505
- .select (this ::buildResourceSelectClause )
506
- .where (f -> buildWhereClause (f , theResourceType , theParams , null ));
560
+ var query = getSearchSession ()
561
+ .search (ResourceTable .class )
562
+ .select (this ::buildResourceSelectClause )
563
+ .where (f -> buildWhereClause (f , theResourceType , theParams , null ));
507
564
508
565
if (theParams .getSort () != null ) {
509
566
query .sort (f -> myExtendedFulltextSortHelper .getSortClauses (f , theParams .getSort (), theResourceType ));
@@ -515,6 +572,11 @@ public List<IBaseResource> searchForResources(
515
572
return resourceProjectionsToResources (extendedLuceneResourceProjections );
516
573
}
517
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
+ */
518
580
@ SuppressWarnings ("rawtypes" )
519
581
private void logQuery (SearchQueryOptionsStep theQuery , RequestDetails theRequestDetails ) {
520
582
IInterceptorBroadcaster compositeBroadcaster =
0 commit comments