Skip to content

Commit 067cd6d

Browse files
author
Binon
committed
Azure AI search implementation, updated OpenAPI
1 parent 9cbc0c1 commit 067cd6d

File tree

21 files changed

+1675
-29
lines changed

21 files changed

+1675
-29
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
namespace LearningHub.Nhs.OpenApi.Models.Configuration
2+
{
3+
/// <summary>
4+
/// The Azure AI Search configuration settings.
5+
/// </summary>
6+
public class AzureSearchConfig
7+
{
8+
/// <summary>
9+
/// Gets or sets the Azure Search service endpoint URL.
10+
/// </summary>
11+
public string ServiceEndpoint { get; set; } = null!;
12+
13+
/// <summary>
14+
/// Gets or sets the Azure Search admin API key.
15+
/// </summary>
16+
public string AdminApiKey { get; set; } = null!;
17+
18+
/// <summary>
19+
/// Gets or sets the Azure Search query API key.
20+
/// </summary>
21+
public string QueryApiKey { get; set; } = null!;
22+
23+
/// <summary>
24+
/// Gets or sets the index name.
25+
/// </summary>
26+
public string IndexName { get; set; } = null!;
27+
28+
/// <summary>
29+
/// Gets or sets the default item limit for search results.
30+
/// </summary>
31+
public int DefaultItemLimitForSearch { get; set; } = 10;
32+
33+
/// <summary>
34+
/// Gets or sets the description length limit.
35+
/// </summary>
36+
public int DescriptionLengthLimit { get; set; } = 3000;
37+
38+
/// <summary>
39+
/// Gets or sets the maximum description length.
40+
/// </summary>
41+
public int MaximumDescriptionLength { get; set; } = 150;
42+
43+
/// <summary>
44+
/// Gets or sets the suggester name for auto-complete and suggestions.
45+
/// </summary>
46+
public string SuggesterName { get; set; } = "test-search-suggester";
47+
}
48+
}

OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</PropertyGroup>
1717

1818
<ItemGroup>
19+
<PackageReference Include="Azure.Search.Documents" Version="11.6.0" />
1920
<PackageReference Include="LearningHub.Nhs.Models" Version="4.0.3" />
2021
<PackageReference Include="NLog.Web.AspNetCore" Version="4.15.0" />
2122
</ItemGroup>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace LearningHub.Nhs.OpenApi.Models.ServiceModels.AzureSearch
2+
{
3+
/// <summary>
4+
/// A cacheable representation of Azure Search FacetResult.
5+
/// This DTO is used to cache facet data without serialization issues with Azure SDK types.
6+
/// </summary>
7+
public class CacheableFacetResult
8+
{
9+
/// <summary>
10+
/// Gets or sets the facet value.
11+
/// </summary>
12+
public object? Value { get; set; }
13+
14+
/// <summary>
15+
/// Gets or sets the count of documents matching this facet value.
16+
/// </summary>
17+
public long Count { get; set; }
18+
}
19+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
namespace LearningHub.Nhs.OpenApi.Models.ServiceModels.AzureSearch
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Text.Json.Serialization;
6+
using System.Text.RegularExpressions;
7+
using Azure.Search.Documents.Indexes;
8+
9+
/// <summary>
10+
/// Represents a search document for Azure AI Search integration.
11+
/// </summary>
12+
public class SearchDocument
13+
{
14+
private string _description = string.Empty;
15+
16+
/// <summary>
17+
/// Gets or sets the unique identifier.
18+
/// </summary>
19+
[SearchableField(IsKey = true)]
20+
[JsonPropertyName("id")]
21+
public string Id { get; set; } = string.Empty;
22+
23+
/// <summary>
24+
/// Gets or sets the title.
25+
/// </summary>
26+
[SearchableField(IsSortable = true)]
27+
[JsonPropertyName("title")]
28+
public string Title { get; set; } = string.Empty;
29+
30+
/// <summary>
31+
/// Gets or sets the description.
32+
/// </summary>
33+
[SearchableField]
34+
[JsonPropertyName("description")]
35+
public string Description
36+
{
37+
get => _description;
38+
set => _description = StripParagraphTags(value);
39+
}
40+
41+
/// <summary>
42+
/// Gets or sets the resource type.
43+
/// </summary>
44+
[SearchableField(IsFilterable = true, IsFacetable = true)]
45+
[JsonPropertyName("resource_collection")]
46+
public string ResourceCollection { get; set; } = string.Empty;
47+
48+
/// <summary>
49+
/// Gets or sets the manual tag JSON.
50+
/// </summary>
51+
[SearchableField(IsFilterable = true, IsFacetable = true)]
52+
[JsonPropertyName("manual_tag")]
53+
public string ManualTagJson { get; set; } = string.Empty;
54+
55+
/// <summary>
56+
/// Gets or sets the manual tags.
57+
/// </summary>
58+
[SearchableField(IsFilterable = true, IsFacetable = true)]
59+
[JsonPropertyName("manualTags")]
60+
public List<string> ManualTags { get; set; } = new List<string>();
61+
62+
/// <summary>
63+
/// Gets or sets the content type.
64+
/// </summary>
65+
[SearchableField(IsFilterable = true, IsFacetable = true)]
66+
[JsonPropertyName("resource_type")]
67+
public string? ResourceType { get; set; } = string.Empty;
68+
69+
/// <summary>
70+
/// Gets or sets the date authored.
71+
/// </summary>
72+
[SimpleField(IsFilterable = true, IsSortable = true)]
73+
[JsonPropertyName("date_authored")]
74+
public DateTime? DateAuthored { get; set; }
75+
76+
/// <summary>
77+
/// Gets or sets the rating.
78+
/// </summary>
79+
[SimpleField(IsFilterable = true, IsSortable = true)]
80+
[JsonPropertyName("rating")]
81+
public double? Rating { get; set; }
82+
83+
/// <summary>
84+
/// Gets or sets the provider IDs.
85+
/// </summary>
86+
[SearchableField]
87+
[JsonPropertyName("provider_ids")]
88+
public string ProviderIds { get; set; } = string.Empty;
89+
90+
/// <summary>
91+
/// Gets or sets a value indicating whether this is statutory mandatory.
92+
/// </summary>
93+
[SimpleField(IsFilterable = true, IsFacetable = true)]
94+
[JsonPropertyName("statutory_mandatory")]
95+
public bool? StatutoryMandatory { get; set; }
96+
97+
/// <summary>
98+
/// Gets or sets the author.
99+
/// </summary>
100+
[SearchableField]
101+
[JsonPropertyName("author")]
102+
public string Author { get; set; } = string.Empty;
103+
104+
/// <summary>
105+
/// Strips paragraph tags from input string.
106+
/// </summary>
107+
/// <param name="input">The input string.</param>
108+
/// <returns>The cleaned string.</returns>
109+
private static string StripParagraphTags(string input)
110+
{
111+
if (string.IsNullOrWhiteSpace(input))
112+
return string.Empty;
113+
114+
return Regex.Replace(input, @"&lt;\/?p[^&gt;]*&gt;", string.Empty, RegexOptions.IgnoreCase);
115+
}
116+
117+
/// <summary>
118+
/// Parses the ManualTagJson into ManualTags list.
119+
/// </summary>
120+
public void ParseManualTags()
121+
{
122+
if (!string.IsNullOrEmpty(ManualTagJson))
123+
{
124+
try
125+
{
126+
ManualTags = System.Text.Json.JsonSerializer.Deserialize<List<string>>(ManualTagJson) ?? new List<string>();
127+
}
128+
catch
129+
{
130+
ManualTags = new List<string>();
131+
}
132+
}
133+
}
134+
}
135+
}

OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace LearningHub.Nhs.OpenApi.Services.Interface.Services
22
{
3+
using System.Threading;
34
using System.Threading.Tasks;
45
using LearningHub.Nhs.Models.Search;
56
using LearningHub.Nhs.Models.Search.SearchClick;
@@ -17,8 +18,9 @@ public interface ISearchService
1718
/// </summary>
1819
/// <param name="searchRequestModel">The catalog search request model.</param>
1920
/// <param name="userId">The user id.</param>
21+
/// <param name="cancellationToken">Cancellation token.</param>
2022
/// <returns>The <see cref="Task"/>.</returns>
21-
Task<SearchResultModel> GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId);
23+
Task<SearchResultModel> GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId, CancellationToken cancellationToken = default);
2224

2325
/// <summary>
2426
/// The Get Catalogue Search Result Async method.
@@ -29,10 +31,11 @@ public interface ISearchService
2931
/// <param name="userId">
3032
/// The user id.
3133
/// </param>
34+
/// <param name="cancellationToken">Cancellation token.</param>
3235
/// <returns>
3336
/// The <see cref="Task"/>.
3437
/// </returns>
35-
Task<SearchCatalogueResultModel> GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId);
38+
Task<SearchCatalogueResultModel> GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default);
3639

3740
/// <summary>
3841
/// The create resource search action async.
@@ -152,8 +155,9 @@ public interface ISearchService
152155
/// The Get Auto suggestion Results Async method.
153156
/// </summary>
154157
/// <param name="term">The term.</param>
158+
/// <param name="cancellationToken">Cancellation token.</param>
155159
/// <returns>The <see cref="Task"/>.</returns>
156-
Task<AutoSuggestionModel> GetAutoSuggestionResultsAsync(string term);
160+
Task<AutoSuggestionModel> GetAutoSuggestionResultsAsync(string term, CancellationToken cancellationToken = default);
157161

158162
/// <summary>
159163
/// The Send AutoSuggestion Event Async.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace LearningHub.Nhs.OpenApi.Services.Helpers
2+
{
3+
using System;
4+
using Azure;
5+
using Azure.Search.Documents;
6+
using LearningHub.Nhs.OpenApi.Models.Configuration;
7+
8+
/// <summary>
9+
/// Factory for creating Azure Search clients.
10+
/// </summary>
11+
public static class AzureSearchClientFactory
12+
{
13+
/// <summary>
14+
/// Creates a SearchClient for querying the Azure Search index.
15+
/// </summary>
16+
/// <param name="config">The Azure Search configuration.</param>
17+
/// <returns>A configured SearchClient instance.</returns>
18+
public static SearchClient CreateQueryClient(AzureSearchConfig config)
19+
{
20+
if (config == null)
21+
throw new ArgumentNullException(nameof(config));
22+
23+
var credential = new AzureKeyCredential(config.QueryApiKey);
24+
return new SearchClient(
25+
new Uri(config.ServiceEndpoint),
26+
config.IndexName,
27+
credential);
28+
}
29+
30+
/// <summary>
31+
/// Creates a SearchClient with admin credentials for indexing operations.
32+
/// </summary>
33+
/// <param name="config">The Azure Search configuration.</param>
34+
/// <returns>A configured SearchClient instance with admin credentials.</returns>
35+
public static SearchClient CreateAdminClient(AzureSearchConfig config)
36+
{
37+
if (config == null)
38+
throw new ArgumentNullException(nameof(config));
39+
40+
var adminCredential = new AzureKeyCredential(config.AdminApiKey);
41+
return new SearchClient(
42+
new Uri(config.ServiceEndpoint),
43+
config.IndexName,
44+
adminCredential);
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)