Skip to content

Commit d3a31e6

Browse files
authored
fix: display an accurate number of total hits for search (#1716)
* fix: track total hits when doing an elastic search query to get an accurate number of total hits, cleanup * add null check due to mocks in unit tests * move maxResultWindow initialization to application event listener
1 parent e09b6d0 commit d3a31e6

1 file changed

Lines changed: 41 additions & 35 deletions

File tree

server/src/main/java/org/eclipse/openvsx/search/ElasticSearchService.java

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
4242
import org.springframework.retry.annotation.Retryable;
4343
import org.springframework.scheduling.annotation.Async;
44-
import org.springframework.stereotype.Component;
44+
import org.springframework.stereotype.Service;
4545
import org.springframework.util.StopWatch;
4646

4747
import java.time.ZoneId;
@@ -52,7 +52,7 @@
5252

5353
import static org.eclipse.openvsx.cache.CacheService.CACHE_AVERAGE_REVIEW_RATING;
5454

55-
@Component
55+
@Service
5656
public class ElasticSearchService implements ISearchService {
5757

5858
protected final ReadWriteLock rwLock = new ReentrantReadWriteLock();
@@ -68,7 +68,7 @@ public class ElasticSearchService implements ISearchService {
6868
@Value("${ovsx.elasticsearch.clear-on-start:false}")
6969
boolean clearOnStart;
7070

71-
private Long maxResultWindow;
71+
private long maxResultWindow;
7272

7373
public ElasticSearchService(
7474
RepositoryService repositories,
@@ -81,7 +81,7 @@ public ElasticSearchService(
8181
this.relevanceService = relevanceService;
8282
this.scheduler = scheduler;
8383
}
84-
84+
8585
public boolean isEnabled() {
8686
return enableSearch;
8787
}
@@ -90,29 +90,43 @@ public boolean isEnabled() {
9090
* Application start listener that initializes the search index. If the application property
9191
* {@code ovsx.elasticsearch.clear-on-start} is set to {@code true}, the index is cleared
9292
* and rebuilt from scratch. If the property is {@code false} and the search index does
93-
* not exist yet, it is created and initialized. Otherwise nothing happens.
93+
* not exist yet, it is created and initialized. Otherwise, nothing happens.
9494
*/
9595
@EventListener
96-
@Retryable(DataAccessResourceFailureException.class)
96+
@Retryable(retryFor = DataAccessResourceFailureException.class)
9797
@CacheEvict(value = CACHE_AVERAGE_REVIEW_RATING, allEntries = true)
9898
public void initSearchIndex(ApplicationStartedEvent event) {
99-
scheduler.scheduleRecurrently("ElasticSearchUpdateIndex", Cron.daily(4), ZoneId.of("UTC"), new HandlerJobRequest<>(ElasticSearchUpdateIndexJobRequestHandler.class));
100-
if (!isEnabled() || !clearOnStart && searchOperations.indexOps(ExtensionSearch.class).exists()) {
99+
if (!isEnabled()) {
100+
scheduler.deleteRecurringJob("ElasticSearchUpdateIndex");
101101
return;
102102
}
103-
var stopWatch = new StopWatch();
104-
stopWatch.start();
105-
updateSearchIndex(clearOnStart);
106-
stopWatch.stop();
107-
logger.info("Initialized search index in {} ms", stopWatch.getTotalTimeMillis());
103+
104+
// schedule recurring job to update the search index
105+
scheduler.scheduleRecurrently(
106+
"ElasticSearchUpdateIndex",
107+
Cron.daily(4),
108+
ZoneId.of("UTC"),
109+
new HandlerJobRequest<>(ElasticSearchUpdateIndexJobRequestHandler.class)
110+
);
111+
112+
if (clearOnStart || !searchOperations.indexOps(ExtensionSearch.class).exists()) {
113+
var stopWatch = new StopWatch();
114+
stopWatch.start();
115+
updateSearchIndex(clearOnStart);
116+
stopWatch.stop();
117+
logger.info("Initialized search index in {} ms", stopWatch.getTotalTimeMillis());
118+
}
119+
120+
var settings = searchOperations.indexOps(ExtensionSearch.class).getSettings(true);
121+
maxResultWindow = Long.parseLong(settings.getOrDefault("index.max_result_window", "10000").toString());
108122
}
109123

110124
/**
111125
* Soft-update the search index, because the relevance of index entries
112126
* consider the extension publishing timestamps in relation to the current
113127
* time or the extension rating.
114128
*/
115-
@Retryable(DataAccessResourceFailureException.class)
129+
@Retryable(retryFor = DataAccessResourceFailureException.class)
116130
@CacheEvict(value = CACHE_AVERAGE_REVIEW_RATING, allEntries = true)
117131
public void updateSearchIndex() {
118132
if (!isEnabled()) {
@@ -134,7 +148,7 @@ public void updateSearchIndex() {
134148
* In any case, this method scans all extensions in the database and indexes their
135149
* relevant metadata.
136150
*/
137-
@Retryable(DataAccessResourceFailureException.class)
151+
@Retryable(retryFor = DataAccessResourceFailureException.class)
138152
public void updateSearchIndex(boolean clear) {
139153
var locked = false;
140154
try {
@@ -180,12 +194,12 @@ public void updateSearchIndex(boolean clear) {
180194
}
181195

182196
@Async
183-
@Retryable(DataAccessResourceFailureException.class)
197+
@Retryable(retryFor = DataAccessResourceFailureException.class)
184198
public void updateSearchEntriesAsync(List<Extension> extensions) {
185199
updateSearchEntries(extensions);
186200
}
187201

188-
@Retryable(DataAccessResourceFailureException.class)
202+
@Retryable(retryFor = DataAccessResourceFailureException.class)
189203
public void updateSearchEntries(List<Extension> extensions) {
190204
if (!isEnabled() || extensions.isEmpty()) {
191205
return;
@@ -205,7 +219,7 @@ public void updateSearchEntries(List<Extension> extensions) {
205219
}
206220
}
207221

208-
@Retryable(DataAccessResourceFailureException.class)
222+
@Retryable(retryFor = DataAccessResourceFailureException.class)
209223
public void updateSearchEntry(Extension extension) {
210224
if (!isEnabled()) {
211225
return;
@@ -223,7 +237,7 @@ public void updateSearchEntry(Extension extension) {
223237
}
224238
}
225239

226-
@Retryable(DataAccessResourceFailureException.class)
240+
@Retryable(retryFor = DataAccessResourceFailureException.class)
227241
public void removeSearchEntries(Collection<Long> ids) {
228242
if (!isEnabled()) {
229243
return;
@@ -235,7 +249,7 @@ public void removeSearchEntries(Collection<Long> ids) {
235249
}
236250

237251

238-
@Retryable(DataAccessResourceFailureException.class)
252+
@Retryable(retryFor = DataAccessResourceFailureException.class)
239253
public void removeSearchEntry(Extension extension) {
240254
if (!isEnabled()) {
241255
return;
@@ -251,7 +265,7 @@ public void removeSearchEntry(Extension extension) {
251265

252266
public SearchResult search(Options options) {
253267
var resultWindow = options.requestedOffset() + options.requestedSize();
254-
if(resultWindow > getMaxResultWindow()) {
268+
if (resultWindow > maxResultWindow) {
255269
return new SearchResult(0L, Collections.emptyList());
256270
}
257271

@@ -263,15 +277,16 @@ public SearchResult search(Options options) {
263277

264278
var pages = new ArrayList<Pageable>();
265279
pages.add(PageRequest.of(options.requestedOffset() / options.requestedSize(), options.requestedSize()));
266-
if(options.requestedOffset() % options.requestedSize() > 0) {
280+
if (options.requestedOffset() % options.requestedSize() > 0) {
267281
// size is not exact multiple of offset; this means we need to get two pages
268282
// e.g. when offset is 20 and size is 50, you want results 20 to 70 which span pages 0 and 1 of a 50 item page
269-
pages.add(pages.get(0).next());
283+
pages.add(pages.getFirst().next());
270284
}
271285

272286
var searchHitsList = new ArrayList<SearchHits<ExtensionSearch>>(pages.size());
273-
for(var page : pages) {
287+
for (var page : pages) {
274288
queryBuilder.withPageable(page);
289+
queryBuilder.withTrackTotalHits(true);
275290
try {
276291
rwLock.readLock().lock();
277292
var searchHits = searchOperations.search(queryBuilder.build(), ExtensionSearch.class, searchOperations.indexOps(ExtensionSearch.class).getIndexCoordinates());
@@ -283,7 +298,7 @@ public SearchResult search(Options options) {
283298

284299
var firstSearchHitsPage = searchHitsList.get(0);
285300
List<SearchHit<ExtensionSearch>> searchHits = new ArrayList<>(firstSearchHitsPage.getSearchHits());
286-
if(searchHitsList.size() == 2) {
301+
if (searchHitsList.size() == 2) {
287302
var secondSearchHitsPage = searchHitsList.get(1);
288303

289304
searchHits.addAll(secondSearchHitsPage.getSearchHits());
@@ -383,7 +398,7 @@ private void sortResults(NativeQueryBuilder queryBuilder, String sortOrder, Stri
383398
);
384399

385400
var type = types.get(sortBy);
386-
if(type == null) {
401+
if (type == null) {
387402
throw new ErrorResultException("sortBy parameter must be " + SortBy.OPTIONS + ".");
388403
}
389404

@@ -392,13 +407,4 @@ private void sortResults(NativeQueryBuilder queryBuilder, String sortOrder, Stri
392407
var sortOptions = sortBy.equals(SortBy.RELEVANCE) ? List.of(scoreSort, fieldSort) : List.of(fieldSort, scoreSort);
393408
queryBuilder.withSort(sortOptions);
394409
}
395-
396-
private long getMaxResultWindow() {
397-
if(maxResultWindow == null) {
398-
var settings = searchOperations.indexOps(ExtensionSearch.class).getSettings(true);
399-
maxResultWindow = Long.parseLong(settings.getOrDefault("index.max_result_window", "10000").toString());
400-
}
401-
402-
return maxResultWindow;
403-
}
404410
}

0 commit comments

Comments
 (0)