Skip to content

Commit e31e15a

Browse files
authored
feat(csharp): add SearchFor Hits and Search for Facets (#2683)
1 parent 3bb0d41 commit e31e15a

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

clients/algoliasearch-client-csharp/algoliasearch/Utils/ClientExtensions.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,86 @@ public static IEnumerable<SynonymHit> BrowseSynonyms(this SearchClient client, s
247247
AsyncHelper.RunSync(() => client.BrowseSynonymsAsync(indexName, synonymsParams, requestOptions));
248248

249249

250+
/// <summary>
251+
/// 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.
252+
/// </summary>
253+
/// <param name="client">Search client</param>
254+
/// <param name="requests">A list of search requests to be executed.</param>
255+
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
256+
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
257+
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
258+
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
259+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
260+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
261+
/// <returns>Task of List{SearchResponse{T}}</returns>
262+
public static async Task<List<SearchResponse<T>>> SearchForHitsAsync<T>(this SearchClient client,
263+
IEnumerable<SearchForHits> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
264+
CancellationToken cancellationToken = default)
265+
{
266+
var queries = requests.Select(t => new SearchQuery(t)).ToList();
267+
var searchMethod = new SearchMethodParams(queries) { Strategy = searchStrategy };
268+
var searchResponses = await client.SearchAsync<T>(searchMethod, options, cancellationToken);
269+
return searchResponses.Results.Where(x => x.IsSearchResponse()).Select(x => x.AsSearchResponse()).ToList();
270+
}
271+
272+
/// <summary>
273+
/// 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)
274+
/// </summary>
275+
/// <param name="client">Search client</param>
276+
/// <param name="requests">A list of search requests to be executed.</param>
277+
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
278+
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
279+
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
280+
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
281+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
282+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
283+
/// <returns>Task of List{SearchResponse{T}}</returns>
284+
public static List<SearchResponse<T>> SearchForHits<T>(this SearchClient client,
285+
IEnumerable<SearchForHits> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
286+
CancellationToken cancellationToken = default) =>
287+
AsyncHelper.RunSync(() =>
288+
client.SearchForHitsAsync<T>(requests, searchStrategy, options, cancellationToken));
289+
290+
291+
/// <summary>
292+
/// 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.
293+
/// </summary>
294+
/// <param name="client">Search client</param>
295+
/// <param name="requests">A list of search requests to be executed.</param>
296+
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
297+
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
298+
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
299+
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
300+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
301+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
302+
/// <returns>Task of List{SearchResponse{T}}</returns>
303+
public static async Task<List<SearchForFacetValuesResponse>> SearchForFacetsAsync(this SearchClient client,
304+
IEnumerable<SearchForFacets> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
305+
CancellationToken cancellationToken = default)
306+
{
307+
var queries = requests.Select(t => new SearchQuery(t)).ToList();
308+
var searchMethod = new SearchMethodParams(queries) { Strategy = searchStrategy };
309+
var searchResponses = await client.SearchAsync<object>(searchMethod, options, cancellationToken);
310+
return searchResponses.Results.Where(x => x.IsSearchForFacetValuesResponse()).Select(x => x.AsSearchForFacetValuesResponse()).ToList();
311+
}
312+
313+
/// <summary>
314+
/// 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.
315+
/// </summary>
316+
/// <param name="client">Search client</param>
317+
/// <param name="requests">A list of search requests to be executed.</param>
318+
/// <param name="searchStrategy">The search strategy to be employed during the search. (optional)</param>
319+
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
320+
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
321+
/// <exception cref="ArgumentException">Thrown when arguments are not correct</exception>
322+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaApiException">Thrown when the API call was rejected by Algolia</exception>
323+
/// <exception cref="Algolia.Search.Exceptions.AlgoliaUnreachableHostException">Thrown when the client failed to call the endpoint</exception>
324+
/// <returns>Task of List{SearchResponse{T}}</returns>
325+
public static List<SearchForFacetValuesResponse> SearchForFacets(this SearchClient client,
326+
IEnumerable<SearchForFacets> requests, SearchStrategy? searchStrategy, RequestOptions options = null,
327+
CancellationToken cancellationToken = default) =>
328+
AsyncHelper.RunSync(() => client.SearchForFacetsAsync(requests, searchStrategy, options, cancellationToken));
329+
250330
private static async Task<T> RetryUntil<T>(Func<Task<T>> func, Func<T, bool> validate,
251331
int maxRetries = DefaultMaxRetries, CancellationToken ct = default)
252332
{
@@ -266,6 +346,7 @@ private static async Task<T> RetryUntil<T>(Func<Task<T>> func, Func<T, bool> val
266346
throw new AlgoliaException(
267347
"The maximum number of retries exceeded. (" + (retryCount + 1) + "/" + maxRetries + ")");
268348
}
349+
269350
private static int NextDelay(int retryCount)
270351
{
271352
return Math.Min(retryCount * 200, 5000);

tests/output/csharp/src/ClientExtensionsTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Algolia.Search.Models.Search;
66
using Algolia.Search.Utils;
77
using Microsoft.Extensions.Logging;
8+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
89
using Moq;
910
using Newtonsoft.Json;
1011
using Xunit;
@@ -469,4 +470,98 @@ public async Task ShouldBrowseRules()
469470

470471
Assert.Equal(6, browseSynonymsAsync.Count());
471472
}
473+
474+
[Fact]
475+
public async Task ShouldSearchForHits()
476+
{
477+
var httpMock = new Mock<IHttpRequester>();
478+
httpMock
479+
.Setup(c =>
480+
c.SendRequestAsync(
481+
It.Is<Request>(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/*/queries")),
482+
It.IsAny<TimeSpan>(),
483+
It.IsAny<TimeSpan>(),
484+
It.IsAny<CancellationToken>()
485+
)
486+
).Returns(
487+
Task.FromResult(
488+
new AlgoliaHttpResponse
489+
{
490+
HttpStatusCode = 200,
491+
Body = new MemoryStream(
492+
Encoding.UTF8.GetBytes(
493+
JsonConvert.SerializeObject(
494+
new SearchResponses<object>([
495+
new(new SearchForFacetValuesResponse(){ FacetHits = new List<FacetHits>()}),
496+
new(new SearchResponse<object> { Hits = [new { ObjectID = "12345" }] }),
497+
new(new SearchResponse<object> { Hits = [new { ObjectID = "678910" }] })
498+
])
499+
)
500+
)
501+
)
502+
}
503+
)
504+
);
505+
506+
var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object);
507+
508+
var hits = await client.SearchForHitsAsync<Hit>(
509+
new List<SearchForHits>
510+
{
511+
new()
512+
{
513+
IndexName = "my-test-index",
514+
Query = " "
515+
}
516+
}, SearchStrategy.None);
517+
518+
Assert.Equal(2, hits.Count);
519+
}
520+
521+
[Fact]
522+
public async Task ShouldSearchForFacets()
523+
{
524+
var httpMock = new Mock<IHttpRequester>();
525+
httpMock
526+
.Setup(c =>
527+
c.SendRequestAsync(
528+
It.Is<Request>(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/*/queries")),
529+
It.IsAny<TimeSpan>(),
530+
It.IsAny<TimeSpan>(),
531+
It.IsAny<CancellationToken>()
532+
)
533+
).Returns(
534+
Task.FromResult(
535+
new AlgoliaHttpResponse
536+
{
537+
HttpStatusCode = 200,
538+
Body = new MemoryStream(
539+
Encoding.UTF8.GetBytes(
540+
JsonConvert.SerializeObject(
541+
new SearchResponses<object>([
542+
new(new SearchForFacetValuesResponse(){ FacetHits = [] }),
543+
new(new SearchResponse<object> { Hits = [new { ObjectID = "12345" }] }),
544+
new(new SearchResponse<object> { Hits = [new { ObjectID = "678910" }] })
545+
])
546+
)
547+
)
548+
)
549+
}
550+
)
551+
);
552+
553+
var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object);
554+
555+
var hits = await client.SearchForFacetsAsync(
556+
new List<SearchForFacets>
557+
{
558+
new()
559+
{
560+
IndexName = "my-test-index",
561+
Query = " "
562+
}
563+
}, SearchStrategy.None);
564+
565+
Assert.Equal(1, hits.Count);
566+
}
472567
}

0 commit comments

Comments
 (0)