Skip to content

Commit d6ffff1

Browse files
author
Binon
committed
Minor fix
1 parent c877dbd commit d6ffff1

File tree

2 files changed

+81
-69
lines changed

2 files changed

+81
-69
lines changed

OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search
1+
namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search
22
{
33
using System;
44
using System.Collections.Generic;
@@ -15,7 +15,7 @@ public static Dictionary<string, List<string>> CombineAndNormaliseFilters(string
1515
{
1616
var filters = new Dictionary<string, List<string>>
1717
{
18-
// { "resource_collection", new List<string> { "Resource" } }
18+
// { "resource_collection", new List<string> { "Resource" } }
1919
};
2020

2121
// Parse and merge additional filters from query string
@@ -26,7 +26,7 @@ public static Dictionary<string, List<string>> CombineAndNormaliseFilters(string
2626
MergeFilterDictionary(filters, requestTypeFilters);
2727
// MergeFilterDictionary(filters, providerFilters);
2828

29-
NormaliseFilters(filters);
29+
//NormaliseFilters(filters);
3030

3131
return filters;
3232
}
@@ -47,25 +47,93 @@ private static void MergeFilterDictionary(Dictionary<string, List<string>> targe
4747
/// </summary>
4848
/// <param name="filters">The filters to apply.</param>
4949
/// <returns>The filter expression string.</returns>
50-
public static string BuildFilterExpression(Dictionary<string, List<string>>? filters)
50+
/// <summary>
51+
/// Build an OData filter that supports multi-select values.
52+
/// Pass a dictionary where key = field name, value = list of selected values.
53+
/// If `collectionFields` contains a field name, that field will be treated as a collection and use any(...).
54+
/// </summary>
55+
public static string BuildFilterExpression(
56+
Dictionary<string, List<string>>? filters,
57+
ISet<string>? collectionFields = null)
5158
{
5259
if (filters == null || !filters.Any())
5360
return string.Empty;
5461

55-
var filterExpressions = new List<string>();
62+
collectionFields ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
5663

57-
foreach (var filter in filters)
64+
// Handle spacing, NBSP, escaping quotes
65+
string Normalize(string v)
5866
{
59-
if (filter.Value?.Any() == true)
67+
if (v == null) return string.Empty;
68+
69+
// Replace NBSP
70+
v = v.Replace('\u00A0', ' ').Trim();
71+
72+
// Collapse multiple spaces
73+
v = System.Text.RegularExpressions.Regex.Replace(v, @"\s+", " ");
74+
75+
// Escape single quotes for OData
76+
v = v.Replace("'", "''");
77+
78+
return v;
79+
}
80+
81+
var expressions = new List<string>();
82+
83+
foreach (var kvp in filters)
84+
{
85+
var field = kvp.Key;
86+
var values = kvp.Value?.Where(v => !string.IsNullOrWhiteSpace(v)).ToList();
87+
88+
if (values == null || values.Count == 0)
89+
continue;
90+
91+
// Normalize all values
92+
var normalizedValues = values.Select(Normalize).Distinct().ToList();
93+
94+
// Single value → use eq
95+
if (normalizedValues.Count == 1)
96+
{
97+
var v = normalizedValues[0];
98+
99+
if (collectionFields.Contains(field))
100+
{
101+
expressions.Add($"{field}/any(t: t eq '{v}')");
102+
}
103+
else
104+
{
105+
expressions.Add($"{field} eq '{v}'");
106+
}
107+
108+
continue;
109+
}
110+
111+
// Multiple values → use OR conditions (ALWAYS works)
112+
if (collectionFields.Contains(field))
60113
{
61-
var values = string.Join(",", filter.Value);
62-
filterExpressions.Add($"search.in({filter.Key}, '{values}')");
114+
// collection field (array) → OR any(...) conditions
115+
var ors = normalizedValues
116+
.Select(v => $"{field}/any(t: t eq '{v}')");
117+
118+
expressions.Add("(" + string.Join(" or ", ors) + ")");
119+
}
120+
else
121+
{
122+
// single string field → OR eq conditions
123+
var ors = normalizedValues
124+
.Select(v => $"{field} eq '{v}'");
125+
126+
expressions.Add("(" + string.Join(" or ", ors) + ")");
63127
}
64128
}
65129

66-
return filterExpressions.Any() ? string.Join(" and ", filterExpressions) : string.Empty;
130+
return expressions.Count > 0
131+
? string.Join(" and ", expressions)
132+
: string.Empty;
67133
}
68134

135+
136+
69137
/// <summary>
70138
/// Parses filter parameters from a query string.
71139
/// </summary>

OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs

Lines changed: 3 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,15 @@ public async Task<SearchResultModel> GetSearchResultAsync(SearchRequestModel sea
142142
ProviderIds = doc.ProviderIds?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(),
143143
CatalogueIds =
144144
doc.ResourceType == "catalogue"
145-
? new List<int> { Convert.ToInt32(doc.Id) } // convert single id → List<int>
145+
? new List<int> { Convert.ToInt32(doc.Id) }
146146
: (
147147
doc.CatalogueId?
148148
.Split(',', StringSplitOptions.RemoveEmptyEntries)
149149
.Select(id => int.TryParse(id, out var val) ? val : 0)
150150
.ToList()
151151
?? new List<int>()
152152
),
153-
// CatalogueIds = doc.CatalogueId?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(id => int.TryParse(id, out var val) ? val : 0).ToList() ?? new List<int>(),
154-
//CatalogueIds = doc.CatalogueId?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(),
155-
//CatalogueIds = new List<int>(1),
156-
Rating = Convert.ToDecimal(doc.Rating),
153+
Rating = Convert.ToDecimal(doc.Rating),
157154
Author = doc.Author,
158155
Authors = doc.Author?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(a => a.Trim()).ToList(),
159156
AuthoredDate = doc.DateAuthored?.ToString(),
@@ -213,60 +210,7 @@ private string MapToResourceType(string resourceType)
213210

214211
return cleanedResourceType;
215212
}
216-
217-
/// <summary>
218-
/// Executes a search query with the specified parameters.
219-
/// </summary>
220-
/// <param name="query">The search query text.</param>
221-
/// <param name="searchQueryType">The type of search query.</param>
222-
/// <param name="offset">The number of results to skip.</param>
223-
/// <param name="pageSize">The number of results to return.</param>
224-
/// <param name="filters">The filters to apply.</param>
225-
/// <param name="searchBy">The sort to apply.</param>
226-
/// <param name="includeFacets">Whether to include facets in the results.</param>
227-
/// <param name="cancellationToken">Cancellation token.</param>
228-
/// <returns>The search results.</returns>
229-
//private async Task<SearchResults<Models.ServiceModels.AzureSearch.SearchDocument>> ExecuteSearchAsync(
230-
// string query,
231-
// SearchQueryType searchQueryType,
232-
// int offset,
233-
// int pageSize,
234-
// Dictionary<string, List<string>> filters,
235-
// Dictionary<string, string> searchBy,
236-
// bool includeFacets,
237-
// CancellationToken cancellationToken)
238-
//{
239-
// var searchOptions = BuildSearchOptions(searchQueryType, offset, pageSize, filters, searchBy, includeFacets);
240-
// return await this.searchClient.SearchAsync<Models.ServiceModels.AzureSearch.SearchDocument>(query, searchOptions, cancellationToken);
241-
//}
242-
243-
/// <summary>
244-
/// Builds search options for Azure Search queries.
245-
/// </summary>
246-
/// <param name="searchQueryType">The type of search query.</param>
247-
/// <param name="offset">The number of results to skip.</param>
248-
/// <param name="pageSize">The number of results to return.</param>
249-
/// <param name="filters">The filters to apply.</param>
250-
/// <param name="sortBy">The sort to apply.</param>
251-
/// <param name="includeFacets">Whether to include facets.</param>
252-
/// <returns>The configured search options.</returns>
253-
private SearchOptions BuildSearchOptions(
254-
SearchQueryType searchQueryType,
255-
int offset,
256-
int pageSize,
257-
Dictionary<string, List<string>> filters,
258-
Dictionary<string, string> sortBy,
259-
bool includeFacets)
260-
{
261-
return SearchOptionsBuilder.BuildSearchOptions(
262-
searchQueryType,
263-
offset,
264-
pageSize,
265-
filters,
266-
sortBy,
267-
includeFacets);
268-
}
269-
213+
270214
/// <summary>
271215
/// Gets unfiltered facets for a search term, using caching.
272216
/// </summary>

0 commit comments

Comments
 (0)