Skip to content

Commit

Permalink
feat(csharp): add SearchFor Hits and Search for Facets (#2683)
Browse files Browse the repository at this point in the history
  • Loading branch information
morganleroi authored Feb 11, 2024
1 parent 3bb0d41 commit e31e15a
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,86 @@ public static IEnumerable<SynonymHit> BrowseSynonyms(this SearchClient client, s
AsyncHelper.RunSync(() => client.BrowseSynonymsAsync(indexName, synonymsParams, requestOptions));


/// <summary>
/// Executes a synchronous search for the provided search requests, with certainty that we will only request Algolia records (hits). Results will be received in the same order as the queries.
/// </summary>
/// <param name="client">Search client</param>
/// <param name="requests">A list of search requests to be executed.</param>
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
/// <returns>Task of List{SearchResponse{T}}</returns>
public static async Task<List<SearchResponse<T>>> SearchForHitsAsync<T>(this SearchClient client,
IEnumerable<SearchForHits> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
CancellationToken cancellationToken = default)
{
var queries = requests.Select(t => new SearchQuery(t)).ToList();
var searchMethod = new SearchMethodParams(queries) { Strategy = searchStrategy };
var searchResponses = await client.SearchAsync<T>(searchMethod, options, cancellationToken);
return searchResponses.Results.Where(x => x.IsSearchResponse()).Select(x => x.AsSearchResponse()).ToList();
}

/// <summary>
/// Executes a synchronous search for the provided search requests, with certainty that we will only request Algolia records (hits). Results will be received in the same order as the queries. (Synchronous version)
/// </summary>
/// <param name="client">Search client</param>
/// <param name="requests">A list of search requests to be executed.</param>
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
/// <returns>Task of List{SearchResponse{T}}</returns>
public static List<SearchResponse<T>> SearchForHits<T>(this SearchClient client,
IEnumerable<SearchForHits> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
CancellationToken cancellationToken = default) =>
AsyncHelper.RunSync(() =>
client.SearchForHitsAsync<T>(requests, searchStrategy, options, cancellationToken));


/// <summary>
/// Executes a synchronous search for the provided search requests, with certainty that we will only request Algolia facets. Results will be received in the same order as the queries.
/// </summary>
/// <param name="client">Search client</param>
/// <param name="requests">A list of search requests to be executed.</param>
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
/// <returns>Task of List{SearchResponse{T}}</returns>
public static async Task<List<SearchForFacetValuesResponse>> SearchForFacetsAsync(this SearchClient client,
IEnumerable<SearchForFacets> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
CancellationToken cancellationToken = default)
{
var queries = requests.Select(t => new SearchQuery(t)).ToList();
var searchMethod = new SearchMethodParams(queries) { Strategy = searchStrategy };
var searchResponses = await client.SearchAsync<object>(searchMethod, options, cancellationToken);
return searchResponses.Results.Where(x => x.IsSearchForFacetValuesResponse()).Select(x => x.AsSearchForFacetValuesResponse()).ToList();
}

/// <summary>
/// Executes a synchronous search for the provided search requests, with certainty that we will only request Algolia facets. Results will be received in the same order as the queries.
/// </summary>
/// <param name="client">Search client</param>
/// <param name="requests">A list of search requests to be executed.</param>
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
/// <returns>Task of List{SearchResponse{T}}</returns>
public static List<SearchForFacetValuesResponse> SearchForFacets(this SearchClient client,
IEnumerable<SearchForFacets> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
CancellationToken cancellationToken = default) =>
AsyncHelper.RunSync(() => client.SearchForFacetsAsync(requests, searchStrategy, options, cancellationToken));

private static async Task<T> RetryUntil<T>(Func<Task<T>> func, Func<T, bool> validate,
int maxRetries = DefaultMaxRetries, CancellationToken ct = default)
{
Expand All @@ -266,6 +346,7 @@ private static async Task<T> RetryUntil<T>(Func<Task<T>> func, Func<T, bool> val
throw new AlgoliaException(
"The maximum number of retries exceeded. (" + (retryCount + 1) + "/" + maxRetries + ")");
}

private static int NextDelay(int retryCount)
{
return Math.Min(retryCount * 200, 5000);
Expand Down
95 changes: 95 additions & 0 deletions tests/output/csharp/src/ClientExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Algolia.Search.Models.Search;
using Algolia.Search.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Moq;
using Newtonsoft.Json;
using Xunit;
Expand Down Expand Up @@ -469,4 +470,98 @@ public async Task ShouldBrowseRules()

Assert.Equal(6, browseSynonymsAsync.Count());
}

[Fact]
public async Task ShouldSearchForHits()
{
var httpMock = new Mock<IHttpRequester>();
httpMock
.Setup(c =>
c.SendRequestAsync(
It.Is<Request>(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/*/queries")),
It.IsAny<TimeSpan>(),
It.IsAny<TimeSpan>(),
It.IsAny<CancellationToken>()
)
).Returns(
Task.FromResult(
new AlgoliaHttpResponse
{
HttpStatusCode = 200,
Body = new MemoryStream(
Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(
new SearchResponses<object>([
new(new SearchForFacetValuesResponse(){ FacetHits = new List<FacetHits>()}),
new(new SearchResponse<object> { Hits = [new { ObjectID = "12345" }] }),
new(new SearchResponse<object> { Hits = [new { ObjectID = "678910" }] })
])
)
)
)
}
)
);

var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object);

var hits = await client.SearchForHitsAsync<Hit>(
new List<SearchForHits>
{
new()
{
IndexName = "my-test-index",
Query = " "
}
}, SearchStrategy.None);

Assert.Equal(2, hits.Count);
}

[Fact]
public async Task ShouldSearchForFacets()
{
var httpMock = new Mock<IHttpRequester>();
httpMock
.Setup(c =>
c.SendRequestAsync(
It.Is<Request>(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/*/queries")),
It.IsAny<TimeSpan>(),
It.IsAny<TimeSpan>(),
It.IsAny<CancellationToken>()
)
).Returns(
Task.FromResult(
new AlgoliaHttpResponse
{
HttpStatusCode = 200,
Body = new MemoryStream(
Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(
new SearchResponses<object>([
new(new SearchForFacetValuesResponse(){ FacetHits = [] }),
new(new SearchResponse<object> { Hits = [new { ObjectID = "12345" }] }),
new(new SearchResponse<object> { Hits = [new { ObjectID = "678910" }] })
])
)
)
)
}
)
);

var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object);

var hits = await client.SearchForFacetsAsync(
new List<SearchForFacets>
{
new()
{
IndexName = "my-test-index",
Query = " "
}
}, SearchStrategy.None);

Assert.Equal(1, hits.Count);
}
}

0 comments on commit e31e15a

Please sign in to comment.