Skip to content

Expose search shard statistics in search hits #2806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* {@link org.springframework.data.elasticsearch.core.document.Document}
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4
*/
final class DocumentAdapters {
Expand All @@ -73,7 +74,7 @@ public static SearchDocument from(Hit<?> hit, JsonpMapper jsonpMapper) {
Map<String, SearchDocumentResponse> innerHits = new LinkedHashMap<>();
hit.innerHits().forEach((name, innerHitsResult) -> {
// noinspection ReturnOfNull
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, null,
innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, null, null,
searchDocument -> null, jsonpMapper));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.client.elc;

import co.elastic.clients.elasticsearch._types.ShardStatistics;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.SearchTemplateResponse;
Expand All @@ -36,6 +37,7 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.elasticsearch.core.SearchShardStatistics;
import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
Expand All @@ -52,6 +54,7 @@
* Factory class to create {@link SearchDocumentResponse} instances.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.4
*/
class SearchDocumentResponseBuilder {
Expand All @@ -78,8 +81,9 @@ public static <T> SearchDocumentResponse from(ResponseBody<EntityAsMap> response
Map<String, Aggregate> aggregations = responseBody.aggregations();
Map<String, List<Suggestion<EntityAsMap>>> suggest = responseBody.suggest();
var pointInTimeId = responseBody.pitId();
var shards = responseBody.shards();

return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
}

/**
Expand All @@ -98,13 +102,14 @@ public static <T> SearchDocumentResponse from(SearchTemplateResponse<EntityAsMap
Assert.notNull(entityCreator, "entityCreator must not be null");
Assert.notNull(jsonpMapper, "jsonpMapper must not be null");

var shards = response.shards();
var hitsMetadata = response.hits();
var scrollId = response.scrollId();
var aggregations = response.aggregations();
var suggest = response.suggest();
var pointInTimeId = response.pitId();

return from(hitsMetadata, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
return from(hitsMetadata, shards, scrollId, pointInTimeId, aggregations, suggest, entityCreator, jsonpMapper);
}

/**
Expand All @@ -120,8 +125,8 @@ public static <T> SearchDocumentResponse from(SearchTemplateResponse<EntityAsMap
* @param jsonpMapper to map JsonData objects
* @return the {@link SearchDocumentResponse}
*/
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable String scrollId,
@Nullable String pointInTimeId, @Nullable Map<String, Aggregate> aggregations,
public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nullable ShardStatistics shards,
@Nullable String scrollId, @Nullable String pointInTimeId, @Nullable Map<String, Aggregate> aggregations,
Map<String, List<Suggestion<EntityAsMap>>> suggestES, SearchDocumentResponse.EntityCreator<T> entityCreator,
JsonpMapper jsonpMapper) {

Expand Down Expand Up @@ -155,8 +160,14 @@ public static <T> SearchDocumentResponse from(HitsMetadata<?> hitsMetadata, @Nul

Suggest suggest = suggestFrom(suggestES, entityCreator);

SearchShardStatistics shardStatistics = shards != null ? shardsFrom(shards) : null;

return new SearchDocumentResponse(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchDocuments,
aggregationsContainer, suggest);
aggregationsContainer, suggest, shardStatistics);
}

private static SearchShardStatistics shardsFrom(ShardStatistics shards) {
return new SearchShardStatistics(shards.failed(), shards.successful(), shards.total(), shards.skipped());
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
* @author Matt Gilene
* @author Sascha Woo
* @author Jakob Hoeper
* @author Haibo Liu
* @since 4.0
*/
public class SearchHitMapping<T> {
Expand Down Expand Up @@ -84,6 +85,7 @@ private SearchHitsImpl<T> mapHitsFromResponse(SearchDocumentResponse searchDocum
"Count of documents must match the count of entities");

long totalHits = searchDocumentResponse.getTotalHits();
SearchShardStatistics shardStatistics = searchDocumentResponse.getSearchShardStatistics();
float maxScore = searchDocumentResponse.getMaxScore();
String scrollId = searchDocumentResponse.getScrollId();
String pointInTimeId = searchDocumentResponse.getPointInTimeId();
Expand All @@ -103,7 +105,7 @@ private SearchHitsImpl<T> mapHitsFromResponse(SearchDocumentResponse searchDocum
mapHitsInCompletionSuggestion(suggest);

return new SearchHitsImpl<>(totalHits, totalHitsRelation, maxScore, scrollId, pointInTimeId, searchHits,
aggregations, suggest);
aggregations, suggest, shardStatistics);
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -240,7 +242,8 @@ private SearchHits<?> mapInnerDocuments(SearchHits<SearchDocument> searchHits, C
searchHits.getPointInTimeId(), //
convertedSearchHits, //
searchHits.getAggregations(), //
searchHits.getSuggest());
searchHits.getSuggest(),
searchHits.getSearchShardStatistics());
}
} catch (Exception e) {
throw new UncategorizedElasticsearchException("Unable to convert inner hits.", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
*
* @param <T> the result data class.
* @author Sascha Woo
* @author Haibo Liu
* @since 4.0
*/
public interface SearchHits<T> extends Streamable<SearchHit<T>> {
Expand Down Expand Up @@ -108,4 +109,10 @@ default Iterator<SearchHit<T>> iterator() {
*/
@Nullable
String getPointInTimeId();

/**
* @return a count of shards used for the request.
*/
@Nullable
SearchShardStatistics getSearchShardStatistics();
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* @param <T> the result data class.
* @author Peter-Josef Meisch
* @author Sascha Woo
* @author Haibo Liu
* @since 4.0
*/
public class SearchHitsImpl<T> implements SearchScrollHits<T> {
Expand All @@ -42,6 +43,7 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
@Nullable private final AggregationsContainer<?> aggregations;
@Nullable private final Suggest suggest;
@Nullable private String pointInTimeId;
@Nullable private final SearchShardStatistics searchShardStatistics;

/**
* @param totalHits the number of total hits for the search
Expand All @@ -53,7 +55,8 @@ public class SearchHitsImpl<T> implements SearchScrollHits<T> {
*/
public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float maxScore, @Nullable String scrollId,
@Nullable String pointInTimeId, List<? extends SearchHit<T>> searchHits,
@Nullable AggregationsContainer<?> aggregations, @Nullable Suggest suggest) {
@Nullable AggregationsContainer<?> aggregations, @Nullable Suggest suggest,
@Nullable SearchShardStatistics searchShardStatistics) {

Assert.notNull(searchHits, "searchHits must not be null");

Expand All @@ -66,6 +69,7 @@ public SearchHitsImpl(long totalHits, TotalHitsRelation totalHitsRelation, float
this.aggregations = aggregations;
this.suggest = suggest;
this.unmodifiableSearchHits = Lazy.of(() -> Collections.unmodifiableList(searchHits));
this.searchShardStatistics = searchShardStatistics;
}

// region getter
Expand Down Expand Up @@ -118,6 +122,11 @@ public String getPointInTimeId() {
return pointInTimeId;
}

@Override
public SearchShardStatistics getSearchShardStatistics() {
return searchShardStatistics;
}

@Override
public String toString() {
return "SearchHits{" + //
Expand All @@ -128,6 +137,7 @@ public String toString() {
", pointInTimeId='" + pointInTimeId + '\'' + //
", searchHits={" + searchHits.size() + " elements}" + //
", aggregations=" + aggregations + //
", shardStatistics=" + searchShardStatistics + //
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2013-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;

import org.springframework.lang.Nullable;

/**
* @author Haibo Liu
*/
public record SearchShardStatistics(Number failed, Number successful, Number total, @Nullable Number skipped) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
import java.util.function.Function;

import org.springframework.data.elasticsearch.core.AggregationsContainer;
import org.springframework.data.elasticsearch.core.SearchShardStatistics;
import org.springframework.data.elasticsearch.core.suggest.response.Suggest;
import org.springframework.lang.Nullable;

/**
* This represents the complete search response from Elasticsearch, including the returned documents.
*
* @author Peter-Josef Meisch
* @author Haibo Liu
* @since 4.0
*/
public class SearchDocumentResponse {
Expand All @@ -40,10 +42,12 @@ public class SearchDocumentResponse {
@Nullable private final Suggest suggest;

@Nullable String pointInTimeId;
@Nullable private final SearchShardStatistics searchShardStatistics;

public SearchDocumentResponse(long totalHits, String totalHitsRelation, float maxScore, @Nullable String scrollId,
@Nullable String pointInTimeId, List<SearchDocument> searchDocuments,
@Nullable AggregationsContainer<?> aggregationsContainer, @Nullable Suggest suggest) {
@Nullable AggregationsContainer<?> aggregationsContainer, @Nullable Suggest suggest,
@Nullable SearchShardStatistics searchShardStatistics) {
this.totalHits = totalHits;
this.totalHitsRelation = totalHitsRelation;
this.maxScore = maxScore;
Expand All @@ -52,6 +56,7 @@ public SearchDocumentResponse(long totalHits, String totalHitsRelation, float ma
this.searchDocuments = searchDocuments;
this.aggregations = aggregationsContainer;
this.suggest = suggest;
this.searchShardStatistics = searchShardStatistics;
}

public long getTotalHits() {
Expand Down Expand Up @@ -93,6 +98,11 @@ public String getPointInTimeId() {
return pointInTimeId;
}

@Nullable
public SearchShardStatistics getSearchShardStatistics() {
return searchShardStatistics;
}

/**
* A function to convert a {@link SearchDocument} async into an entity. Asynchronous so that it can be used from the
* imperative and the reactive code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@
*/
package org.springframework.data.elasticsearch.repository.query;

import java.util.Collections;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.SearchHitsImpl;
import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.BaseQuery;
Expand All @@ -42,6 +38,7 @@
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Peter-Josef Meisch
* @author Haibo Liu
*/

public abstract class AbstractElasticsearchRepositoryQuery implements RepositoryQuery {
Expand Down Expand Up @@ -110,13 +107,7 @@ public Object execute(Object[] parameters) {

if (parameterAccessor.getPageable().isUnpaged()) {
int itemCount = (int) elasticsearchOperations.count(query, clazz, index);

if (itemCount == 0) {
result = new SearchHitsImpl<>(0, TotalHitsRelation.EQUAL_TO, Float.NaN, null,
query.getPointInTime() != null ? query.getPointInTime().id() : null, Collections.emptyList(), null, null);
} else {
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
}
query.setPageable(PageRequest.of(0, Math.max(1, itemCount)));
} else {
query.setPageable(parameterAccessor.getPageable());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.client.elc;

import co.elastic.clients.elasticsearch._types.ShardStatistics;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.elasticsearch.core.search.TotalHitsRelation;
Expand All @@ -31,10 +32,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import static org.assertj.core.api.Assertions.*;

/**
* Tests for the factory class to create {@link SearchDocumentResponse} instances.
*
* @author Sébastien Comeau
* @author Haibo Liu
* @since 5.2
*/
class SearchDocumentResponseBuilderUnitTests {
Expand Down Expand Up @@ -73,7 +77,7 @@ void shouldGetPhraseSuggestion() throws JSONException {
.build();

// act
final var actual = SearchDocumentResponseBuilder.from(hitsMetadata, null, null, null, sortProperties, null,
final var actual = SearchDocumentResponseBuilder.from(hitsMetadata, null, null, null, null, sortProperties, null,
jsonpMapper);

// assert
Expand Down Expand Up @@ -108,4 +112,32 @@ void shouldGetPhraseSuggestion() throws JSONException {

softly.assertAll();
}

@Test // #2605
void shouldGetShardStatisticsInfo() {
// arrange
HitsMetadata<EntityAsMap> hitsMetadata = new HitsMetadata.Builder<EntityAsMap>()
.total(t -> t
.value(0)
.relation(TotalHitsRelation.Eq))
.hits(new ArrayList<>())
.build();

ShardStatistics shards = new ShardStatistics.Builder()
.total(15)
.successful(15)
.skipped(0)
.failed(0)
.build();

// act
SearchDocumentResponse response = SearchDocumentResponseBuilder.from(hitsMetadata, shards, null, null,
null, null, null, jsonpMapper);

// assert
assertThat(response.getSearchShardStatistics().total()).isEqualTo(15);
assertThat(response.getSearchShardStatistics().successful()).isEqualTo(15);
assertThat(response.getSearchShardStatistics().skipped()).isEqualTo(0);
assertThat(response.getSearchShardStatistics().failed()).isEqualTo(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
/**
* @author Roman Puchkovskiy
* @author Peter-Josef Meisch
* @author Haibo Liu
*/
class SearchHitSupportTest {

Expand Down Expand Up @@ -65,7 +66,7 @@ void shouldReturnTheSameListInstanceInSearchHitsAndGetContent() {
hits.add(new SearchHit<>(null, null, null, 0, null, null, null, null, null, null, "five"));

SearchHits<String> originalSearchHits = new SearchHitsImpl<>(hits.size(), TotalHitsRelation.EQUAL_TO, 0, "scroll",
null, hits, null, null);
null, hits, null, null, null);

SearchPage<String> searchPage = SearchHitSupport.searchPageFor(originalSearchHits, PageRequest.of(0, 3));
SearchHits<String> searchHits = searchPage.getSearchHits();
Expand Down
Loading