• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 14752386992

30 Apr 2025 10:25AM UTC coverage: 64.96% (-0.1%) from 65.095%
14752386992

push

github

web-flow
TRUNK-6316 Upgrade Hibernate Search to 6.2.4 (#5005)

501 of 591 new or added lines in 24 files covered. (84.77%)

21 existing lines in 6 files now uncovered.

23344 of 35936 relevant lines covered (64.96%)

0.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.3
/api/src/main/java/org/openmrs/api/db/hibernate/search/SearchQueryUnique.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.db.hibernate.search;
11

12
import java.util.ArrayList;
13
import java.util.Collection;
14
import java.util.LinkedHashSet;
15
import java.util.List;
16
import java.util.function.Function;
17
import java.util.stream.Collectors;
18

19
import org.apache.lucene.search.BooleanQuery;
20
import org.hibernate.search.engine.search.predicate.SearchPredicate;
21
import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory;
22
import org.hibernate.search.engine.search.query.SearchQuery;
23
import org.hibernate.search.engine.search.query.SearchScroll;
24
import org.hibernate.search.engine.search.query.SearchScrollResult;
25
import org.hibernate.search.mapper.orm.scope.SearchScope;
26
import org.hibernate.search.mapper.orm.session.SearchSession;
27
import org.openmrs.api.db.hibernate.search.session.SearchSessionFactory;
28

29
/**
30
 * Provides methods for removing duplicate search results based on the given uniqueKey and combining
31
 * search results from multiple queries.
32
 * <p>
33
 * Due to max clause count limit dictated by performance reasons only first <code>max = {@link BooleanQuery#getMaxClauseCount()} / 2.5</code>
34
 * duplicate keys are removed from results. 
35
 * <p>
36
 * The same <code>max</code> limit applies when combining results of multiple queries. 
37
 * Up to <code>max</code> unique keys from the previous queries are removed from results in the following queries.
38
 * 
39
 * @param <T> query scope
40
 * @param <R> query return type
41
 * @since 2.8.0
42
 */
43
public class SearchQueryUnique<T, R> {
44
        Class<? extends T> scope;
45
        Function<SearchPredicateFactory, SearchPredicate> search;
46
        Function<T, R> mapper;
47
        String uniqueKey;
48
        SearchQueryUnique<?, R> joinedQuery;
49

50
        public SearchQueryUnique(Class<? extends T> scope, Function<SearchPredicateFactory, SearchPredicate> search,
51
                                                         String uniqueKey, Function<T, R> mapper,
52
                                                         SearchQueryUnique<?, R> joinedQuery) {
1✔
53
                this.scope = scope;
1✔
54
                this.search = search;
1✔
55
                this.mapper = mapper;
1✔
56
                this.uniqueKey = uniqueKey;
1✔
57
                this.joinedQuery = joinedQuery;
1✔
58
        }
1✔
59

60
        public Class<? extends T> getScope() {
61
                return scope;
1✔
62
        }
63

64
        public Function<SearchPredicateFactory, SearchPredicate> getSearch() {
65
                return search;
1✔
66
        }
67

68
        public Function<T, R> getMapper() {
69
                return mapper;
1✔
70
        }
71

72
        public String getUniqueKey() {
73
                return uniqueKey;
1✔
74
        }
75

76
        public SearchQueryUnique<?, R> getJoinedQuery() {
77
                return joinedQuery;
1✔
78
        }
79

80
        /**
81
         * When joining a query, the algorithm will use the unique key values from the previous query (if any) to
82
         * filter out items using its unique key.
83
         * 
84
         * @param query the query to join
85
         * @return joined queries
86
         */
87
        public SearchQueryUnique<?, R> join(SearchQueryUnique<?, R> query) {
88
                joinedQuery = query;
1✔
89
                return this;
1✔
90
        }
91

92
        /**
93
         * Creates a new query for {@link #search(SearchSessionFactory, SearchQueryUnique)}
94
         * @param scope the index type to be searched
95
         * @param search the search predicate
96
         * @param uniqueKey the field to use as unique key, or <code>null</code> if none
97
         * @param mapper the mapper from index type to result type, or <code>null</code> if none
98
         * @return the search
99
         * @param <T> the index type
100
         * @param <R> the result type
101
         */
102
        public static <T,R> SearchQueryUnique<T,R> newQuery(Class<? extends T> scope, 
103
                                                                                                                Function<SearchPredicateFactory, SearchPredicate> search, 
104
                                                                                                                String uniqueKey, Function<T, R> mapper) {
105
                return new SearchQueryUnique<>(scope, search, uniqueKey, mapper, null);
1✔
106
        }
107

108
        /**
109
         * See {@link #newQuery(Class, Function, String, Function)}.
110
         * 
111
         * @param scope thh index type to be searched
112
         * @param search the search predicate
113
         * @param uniqueKey the field to use as unique key, or <code>null</code> if none
114
         * @return the search
115
         * @param <T> the index type
116
         * @param <R> the result type
117
         */
118
        public static <T,R> SearchQueryUnique<T,R> newQuery(Class<? extends T> scope,
119
                                                                                                                Function<SearchPredicateFactory, SearchPredicate> search,
120
                                                                                                                String uniqueKey) {
121
                return new SearchQueryUnique<>(scope, search, uniqueKey, null,null);
1✔
122
        }
123

124
        public static class SearchUniqueResults<T> {
125
                List<T> results;
126
                Integer offset;
127
                Integer limit;
128
                Long totalHitCount;
129

130
                public SearchUniqueResults(List<T> results, Integer offset, Integer limit, Long totalHitCount) {
1✔
131
                        this.results = results;
1✔
132
                        this.offset = offset;
1✔
133
                        this.limit = limit;
1✔
134
                        this.totalHitCount = totalHitCount;
1✔
135
                }
1✔
136

137
                public List<T> getResults() {
138
                        return results;
1✔
139
                }
140

141
                public Integer getOffset() {
NEW
142
                        return offset;
×
143
                }
144

145
                public Integer getLimit() {
NEW
146
                        return limit;
×
147
                }
148

149
                public Long getTotalHitCount() {
150
                        return totalHitCount;
1✔
151
                }
152
        }
153

154
        /**
155
         * Runs the search calculating the total hit count only.
156
         * See {@link #search(SearchSessionFactory, SearchQueryUnique, Integer, Integer, Boolean)}.
157
         *
158
         * @param searchSessionFactory SearchSessionFactory
159
         * @param uniqueQuery  unique query {@link #newQuery(Class, Function, String, Function)}
160
         * @return the total hit count
161
         */
162
        public static Long searchCount(SearchSessionFactory searchSessionFactory,
163
                                                                   SearchQueryUnique<?, ?> uniqueQuery) {
164
                return search(searchSessionFactory, uniqueQuery,0, 0, true).getTotalHitCount();
1✔
165
        }
166

167
        /**
168
         * Runs the search without calculating the total hit count. 
169
         * See {@link #search(SearchSessionFactory, SearchQueryUnique, Integer, Integer, Boolean)}.
170
         *
171
         * @param searchSessionFactory SearchSessionFactory
172
         * @param uniqueQuery  unique query {@link #newQuery(Class, Function, String, Function)}
173
         * @return the results
174
         * @param <T> the type of results
175
         */
176
        public static <T> List<T> search(SearchSessionFactory searchSessionFactory,
177
                                                                         SearchQueryUnique<?, T> uniqueQuery) {
178
                return search(searchSessionFactory, uniqueQuery, null, null, false).getResults();
1✔
179
        }
180

181
        /**
182
         * Runs the search without calculating the total hit count. 
183
         * See {@link #search(SearchSessionFactory, SearchQueryUnique, Integer, Integer, Boolean)}.
184
         * 
185
         * @param searchSessionFactory SearchSessionFactory
186
         * @param uniqueQuery  unique query {@link #newQuery(Class, Function, String, Function)}
187
         * @param offset offset of results
188
         * @param limit limit of results
189
         * @return the results
190
         * @param <T> the type of results
191
         */
192
        public static <T> List<T> search(SearchSessionFactory searchSessionFactory,
193
                                                                         SearchQueryUnique<?, T> uniqueQuery, Integer offset, Integer limit) {
194
                return search(searchSessionFactory, uniqueQuery, offset, limit, false).getResults();
1✔
195
        }
196

197
        /**
198
         * Executes unique queries applying joins and mapping to the result type.
199
         * 
200
         * @param searchSessionFactory search session factory
201
         * @param uniqueQuery unique query {@link #newQuery(Class, Function, String, Function)}
202
         * @param offset offset of results
203
         * @param limit limit of results
204
         * @param includeTotalHitCount calculate total hit count (it is more expensive query)
205
         * @return the results
206
         * @param <T> the type of results
207
         */
208
        public static <T> SearchUniqueResults<T> search(SearchSessionFactory searchSessionFactory,
209
                                                                                                        SearchQueryUnique<?, T> uniqueQuery,
210
                                                                                                        final Integer offset, final Integer limit, Boolean includeTotalHitCount) {
211
                if (includeTotalHitCount == null) {
1✔
NEW
212
                        includeTotalHitCount = false;
×
213
                }
214

215
                SearchSession searchSession = searchSessionFactory.getSearchSession();
1✔
216
                List<T> results = new ArrayList<>();
1✔
217
                Collection<Object> uniqueKeys = new LinkedHashSet<>(); // Preserve the order
1✔
218
                final int maxClauseCount = Math.round(BooleanQuery.getMaxClauseCount() / 2.5f);
1✔
219
                SearchQueryUnique<?, T> nextQuery = uniqueQuery;
1✔
220
                long totalHitCount = 0;
1✔
221
                Integer currentOffset = offset;
1✔
222
                Integer currentLimit = limit;
1✔
223
                while (nextQuery != null) {
1✔
224
                        SearchScope<?> scope = searchSession.scope(nextQuery.getScope());
1✔
225
                        SearchPredicateFactory predicateFactory = scope.predicate();
1✔
226
                        SearchPredicate searchPredicate = nextQuery.getSearch().apply(predicateFactory);
1✔
227
                        SearchQuery<?> query;
228

229
                        final Collection<Object> previousQueryUniqueKeys = new ArrayList<>(uniqueKeys);
1✔
230
                        
231
                        if (nextQuery.getUniqueKey() != null) {
1✔
232
                                final String uniqueKey = nextQuery.getUniqueKey();
1✔
233
                                // Find unique keys and duplicate ids
234
                                SearchQuery<List<?>> uniqueKeyQuery = searchSession.search(scope).select(f ->
1✔
235
                                        f.composite(
1✔
236
                                                f.field(uniqueKey),
1✔
237
                                                f.id()
1✔
238
                                        )).where(searchPredicate).toQuery();
1✔
239
                                
240
                                final List<Object> duplicateIds = new ArrayList<>();
1✔
241
                                try (SearchScroll<List<?>> scroll = uniqueKeyQuery.scroll(500)){
1✔
242
                                        SearchScrollResult<List<?>> chunk = scroll.next();
1✔
243
                                        while (chunk.hasHits()) {
1✔
244
                                                boolean limitReached = false;
1✔
245
                                                for (List<?> match : chunk.hits()) {
1✔
246
                                                        if (!uniqueKeys.add(match.get(0))) {
1✔
247
                                                                duplicateIds.add(match.get(1));
1✔
248
                                                                if (duplicateIds.size() > maxClauseCount) {
1✔
249
                                                                        // stop at max clause count
NEW
250
                                                                        limitReached = true;
×
NEW
251
                                                                        break;
×
252
                                                                }
253
                                                        }
254
                                                }
1✔
255
                                                if (limitReached) {
1✔
NEW
256
                                                        break;
×
257
                                                }
258
                                                chunk = scroll.next();
1✔
259
                                        }
1✔
260
                                }
261

262
                                if (uniqueKeys.size() > maxClauseCount) {
1✔
263
                                        uniqueKeys = new ArrayList<>(uniqueKeys).subList(0, maxClauseCount);
1✔
264
                                }
265
                                
266
                                query = searchSession.search(scope).where(f -> f.bool().with(b -> {
1✔
267
                                        b.must(searchPredicate);
1✔
268
                                        if (!duplicateIds.isEmpty()) {
1✔
269
                                                b.filter(f.not(f.id().matchingAny(duplicateIds)));
1✔
270
                                        }
271
                                        // Get rid of unique keys that were added to results in a previous query
272
                                        if (!previousQueryUniqueKeys.isEmpty()) {
1✔
273
                                                b.filter(f.not(f.terms().field(uniqueKey).matchingAny(previousQueryUniqueKeys)));
1✔
274
                                        }
275
                                })).toQuery();
1✔
276
                        } else {
1✔
NEW
277
                                query = searchSession.search(scope).where(searchPredicate).toQuery();
×
278
                        }
279

280
                        List<?> partialResults;
281
                        if (currentOffset != null) {
1✔
282
                                partialResults = query.fetchHits(currentOffset, currentLimit);
1✔
283
                        } else if (currentLimit != null) {
1✔
284
                                partialResults = query.fetchHits(currentLimit);
1✔
285
                        } else {
286
                                partialResults = query.fetchAllHits();
1✔
287
                        }
288

289
                        if (!partialResults.isEmpty()) {
1✔
290
                                if (nextQuery.getMapper() != null) {
1✔
291
                                        //noinspection unchecked
292
                                        results.addAll(partialResults.stream().map((Function<Object, T>) nextQuery.getMapper()).collect(Collectors.toList()));
1✔
293
                                } else {
294
                                        //noinspection unchecked
NEW
295
                                        results.addAll((Collection<? extends T>) partialResults);
×
296
                                }
297
                        }
298

299
                        if (limit != null && results.size() == limit && !includeTotalHitCount) {
1✔
300
                                // End early as we don't need to calculate total hit count
301
                                return new SearchUniqueResults<>(results, offset, limit, null);
1✔
302
                        }
303

304
                        if (includeTotalHitCount || (nextQuery.getJoinedQuery() != null && partialResults.isEmpty() && 
1✔
305
                                currentOffset != null && currentOffset != 0)) {
1✔
306
                                // Fetch total hit count only if explicitly requested or if offset caused the query to return 0 results,
307
                                // and we will be trying to fetch more results in the next query
308
                                totalHitCount += query.fetchTotalHitCount();
1✔
309
                        } else {
310
                                totalHitCount += partialResults.size();
1✔
311
                        }
312

313
                        if (currentLimit != null) {
1✔
314
                                currentLimit = limit - results.size();
1✔
315
                        }
316
                        if (currentOffset != null) {
1✔
317
                                currentOffset = currentOffset - (int) totalHitCount;
1✔
318
                                currentOffset = currentOffset < 0 ? 0 : currentOffset;
1✔
319
                        }
320
                        
321
                        nextQuery = nextQuery.getJoinedQuery();
1✔
322
                }
1✔
323
                
324
                return new SearchUniqueResults<>(results, offset, limit, includeTotalHitCount ? totalHitCount : null);
1✔
325
        }
326

327
        /**
328
         * Finds unique keys for the specified search.
329
         * <p>
330
         * It returns up to <code>max = {@link BooleanQuery#getMaxClauseCount()} / 2</code> unique keys.
331
         * 
332
         * @param searchSession searchSession
333
         * @param scope scope
334
         * @param searchPredicate searchPredicate
335
         * @param uniqueKey uniqueKey
336
         * 
337
         * @return unique keys
338
         */
339
        public static List<Object> findUniqueKeys(SearchSession searchSession, SearchScope<?> scope,
340
                                                                                           SearchPredicate searchPredicate, String uniqueKey) {
341
                // The default Lucene clauses limit is 1024. We arbitrarily choose to use half here as it does 
342
                // not make sense to return more hits by concept name anyway.
343
                int maxClauseCount = BooleanQuery.getMaxClauseCount() / 2;
1✔
344
                LinkedHashSet<Object> uniqueKeys = new LinkedHashSet<>(); // Preserve the order
1✔
345

346
                try (SearchScroll<?> scroll = searchSession.search(scope)
1✔
347
                        .select(f -> f.field(uniqueKey))
1✔
348
                        .where(searchPredicate).scroll(500)) {
1✔
349

350
                        SearchScrollResult<?> chunk = scroll.next();
1✔
351
                        while (chunk.hasHits()) {
1✔
352
                                uniqueKeys.addAll(chunk.hits());
1✔
353
                                if (uniqueKeys.size() > maxClauseCount) {
1✔
NEW
354
                                        break;
×
355
                                }
356
                                chunk = scroll.next();
1✔
357
                        }
358
                }
359

360

361
                if (uniqueKeys.size() > maxClauseCount) {
1✔
NEW
362
                        return new ArrayList<>(uniqueKeys).subList(0, maxClauseCount);
×
363
                } else {
364
                        return new ArrayList<>(uniqueKeys);
1✔
365
                }
366
        }
367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc