Skip to content

Commit c4bed3a

Browse files
authored
[Search] Add tests for semantic hybrid search with vector and remove defaults for SearchOptions.QueryAnswerCount and SearchOptions.QueryAnswerThreshold (Azure#36393)
[Search] Add sample for semantic hybrid search with vector
1 parent 1824e7e commit c4bed3a

File tree

7 files changed

+504
-63
lines changed

7 files changed

+504
-63
lines changed

sdk/search/Azure.Search.Documents/src/Options/SearchOptions.cs

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics;
6+
using System.Text;
77
using Azure.Core;
88
using Azure.Search.Documents.Models;
99

@@ -18,9 +18,7 @@ namespace Azure.Search.Documents
1818
[CodeGenModel("SearchRequest")]
1919
public partial class SearchOptions
2020
{
21-
private const string QueryAnswerCountRawSplitter = "|count-";
2221
private const string QueryAnswerCountRaw = "count-";
23-
private const string QueryAnswerThresholdRawSplitter = "|threshold-";
2422
private const string QueryAnswerThresholdRaw = "threshold-";
2523
private const string QueryCaptionRawSplitter = "|highlight-";
2624

@@ -197,72 +195,71 @@ internal string OrderByRaw
197195
/// <summary> A value that specifies the threshold of <see cref="SearchResults{T}.Answers"/> that should be returned as part of the search response. </summary>
198196
public double? QueryAnswerThreshold { get; set; }
199197

200-
/// <summary> Constructed from <see cref="QueryAnswer"/>, <see cref="QueryAnswerCount"/> and <see cref="QueryAnswerThreshold"/>.</summary>
198+
/// <summary> Constructed from <see cref="QueryAnswer"/>, <see cref="QueryAnswerCount"/> and <see cref="QueryAnswerThreshold"/>. For example: "extractive|count-1,threshold-0.7"</summary>
201199
[CodeGenMember("Answers")]
202200
internal string QueryAnswerRaw
203201
{
204202
get
205203
{
206-
string queryAnswerStringValue = null;
207-
208204
if (QueryAnswer.HasValue)
209205
{
210-
queryAnswerStringValue = $"{QueryAnswer.Value}{QueryAnswerCountRawSplitter}{QueryAnswerCount.GetValueOrDefault(1)}{QueryAnswerThresholdRawSplitter}{QueryAnswerThreshold.GetValueOrDefault(0.7)}";
206+
StringBuilder queryAnswerStringValue = new(QueryAnswer.Value.ToString());
207+
208+
int tokens = 0;
209+
char NextToken() => tokens++ == 0 ? '|' : ',';
210+
211+
if (QueryAnswerCount.HasValue)
212+
{
213+
queryAnswerStringValue.Append(NextToken()).Append($"{QueryAnswerCountRaw}{QueryAnswerCount.Value}");
214+
tokens = 1;
215+
}
216+
217+
if (QueryAnswerThreshold.HasValue)
218+
{
219+
queryAnswerStringValue.Append(NextToken()).Append($"{QueryAnswerThresholdRaw}{QueryAnswerThreshold.Value}");
220+
}
221+
222+
return queryAnswerStringValue.ToString();
211223
}
212224

213-
return queryAnswerStringValue;
225+
return null;
214226
}
215-
216227
set
217228
{
218-
if (string.IsNullOrEmpty(value))
219-
{
220-
QueryAnswer = null;
221-
QueryAnswerCount = null;
222-
QueryAnswerThreshold = null;
223-
}
224-
else
229+
if (!string.IsNullOrEmpty(value)) // If the value is - "extractive" or "extractive|count-1" or "extractive|threshold-0.7" or "extractive|count-5,threshold-0.9" or "extractive|threshold-0.8,count-4"
225230
{
226-
if (value.Contains(QueryAnswerCountRawSplitter) || value.Contains(QueryAnswerThresholdRawSplitter))
231+
string[] queryAnswerValues = value.Split('|');
232+
if (!string.IsNullOrEmpty(queryAnswerValues[0]))
227233
{
228-
string[] queryAnswerValues = value.Split('|');
229-
230-
var queryAnswerPart = queryAnswerValues[0];
231-
if (string.IsNullOrEmpty(queryAnswerPart))
232-
{
233-
QueryAnswer = null;
234-
}
235-
else
236-
{
237-
QueryAnswer = new QueryAnswerType(queryAnswerPart);
238-
}
234+
QueryAnswer = new QueryAnswerType(queryAnswerValues[0]);
235+
}
239236

240-
foreach (var queryAnswerValue in queryAnswerValues)
237+
if (queryAnswerValues.Length == 2)
238+
{
239+
var queryAnswerParams = queryAnswerValues[1].Split(',');
240+
if (queryAnswerParams.Length <= 2)
241241
{
242-
if (queryAnswerValue.Contains(QueryAnswerCountRaw))
242+
foreach (var param in queryAnswerParams)
243243
{
244-
var countPart = queryAnswerValue.Substring(queryAnswerValue.IndexOf(QueryAnswerCountRaw, StringComparison.OrdinalIgnoreCase) + QueryAnswerCountRaw.Length);
245-
if (int.TryParse(countPart, out int countValue))
244+
if (param.Contains(QueryAnswerCountRaw))
246245
{
247-
QueryAnswerCount = countValue;
246+
var countPart = param.Substring(param.IndexOf(QueryAnswerCountRaw, StringComparison.OrdinalIgnoreCase) + QueryAnswerCountRaw.Length);
247+
if (int.TryParse(countPart, out int countValue))
248+
{
249+
QueryAnswerCount = countValue;
250+
}
248251
}
249-
}
250-
else if (queryAnswerValue.Contains(QueryAnswerThresholdRaw))
251-
{
252-
var thresholdPart = queryAnswerValue.Substring(queryAnswerValue.IndexOf(QueryAnswerThresholdRaw, StringComparison.OrdinalIgnoreCase) + QueryAnswerThresholdRaw.Length);
253-
if (double.TryParse(thresholdPart, out double thresholdValue))
252+
else if (param.Contains(QueryAnswerThresholdRaw))
254253
{
255-
QueryAnswerThreshold = thresholdValue;
254+
var thresholdPart = param.Substring(param.IndexOf(QueryAnswerThresholdRaw, StringComparison.OrdinalIgnoreCase) + QueryAnswerThresholdRaw.Length);
255+
if (double.TryParse(thresholdPart, out double thresholdValue))
256+
{
257+
QueryAnswerThreshold = thresholdValue;
258+
}
256259
}
257260
}
258261
}
259262
}
260-
else
261-
{
262-
QueryAnswer = new QueryAnswerType(value);
263-
QueryAnswerCount = null;
264-
QueryAnswerThreshold = null;
265-
}
266263
}
267264
}
268265
}

sdk/search/Azure.Search.Documents/tests/DocumentOperations/VectorSearchTests.cs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ public VectorSearch(bool async, SearchClientOptions.ServiceVersion serviceVersio
2020
}
2121

2222
private async Task AssertKeysEqual<T>(
23-
Response<SearchResults<T>> response,
23+
SearchResults<T> response,
2424
Func<SearchResult<T>, string> keyAccessor,
2525
params string[] expectedKeys)
2626
{
27-
List<SearchResult<T>> docs = await response.Value.GetResultsAsync().ToListAsync();
27+
List<SearchResult<T>> docs = await response.GetResultsAsync().ToListAsync();
2828
CollectionAssert.AreEquivalent(expectedKeys, docs.Select(keyAccessor));
2929
}
3030

@@ -36,7 +36,7 @@ public async Task SingleVectorSearch()
3636
var vectorizedResult = VectorSearchEmbeddings.SearchVectorizeDescription; // "Top hotels in town"
3737
await Task.Delay(TimeSpan.FromSeconds(1));
3838

39-
Response<SearchResults<Hotel>> response = await resources.GetSearchClient().SearchAsync<Hotel>(
39+
SearchResults<Hotel> response = await resources.GetSearchClient().SearchAsync<Hotel>(
4040
null,
4141
new SearchOptions
4242
{
@@ -57,7 +57,7 @@ public async Task SingleVectorSearchWithFilter()
5757

5858
var vectorizedResult = VectorSearchEmbeddings.SearchVectorizeDescription; // "Top hotels in town"
5959

60-
Response<SearchResults<Hotel>> response = await resources.GetSearchClient().SearchAsync<Hotel>(
60+
SearchResults<Hotel> response = await resources.GetSearchClient().SearchAsync<Hotel>(
6161
null,
6262
new SearchOptions
6363
{
@@ -79,7 +79,7 @@ public async Task SimpleHybridSearch()
7979

8080
var vectorizedResult = VectorSearchEmbeddings.SearchVectorizeDescription; // "Top hotels in town"
8181

82-
Response<SearchResults<Hotel>> response = await resources.GetSearchClient().SearchAsync<Hotel>(
82+
SearchResults<Hotel> response = await resources.GetSearchClient().SearchAsync<Hotel>(
8383
"Top hotels in town",
8484
new SearchOptions
8585
{
@@ -92,5 +92,48 @@ await AssertKeysEqual(
9292
h => h.Document.HotelId,
9393
"3", "1", "2", "10", "4", "5", "9");
9494
}
95+
96+
[Test]
97+
public async Task SemanticHybridSearch()
98+
{
99+
await using SearchResources resources = await SearchResources.CreateWithHotelsIndexAsync(this);
100+
101+
var vectorizedResult = VectorSearchEmbeddings.SearchVectorizeDescription; // "Top hotels in town"
102+
103+
SearchResults<Hotel> response = await resources.GetSearchClient().SearchAsync<Hotel>(
104+
"Is there any hotel located on the main commercial artery of the city in the heart of New York?",
105+
new SearchOptions
106+
{
107+
Vector = new SearchQueryVector { Value = vectorizedResult, K = 3, Fields = "descriptionVector" },
108+
Select = { "hotelId", "hotelName", "description", "category" },
109+
QueryType = SearchQueryType.Semantic,
110+
QueryLanguage = QueryLanguage.EnUs,
111+
SemanticConfigurationName = "my-semantic-config",
112+
QueryCaption = QueryCaptionType.Extractive,
113+
QueryAnswer = QueryAnswerType.Extractive,
114+
});
115+
116+
Assert.NotNull(response.Answers);
117+
Assert.AreEqual(1, response.Answers.Count);
118+
Assert.AreEqual("9", response.Answers[0].Key);
119+
Assert.NotNull(response.Answers[0].Highlights);
120+
Assert.NotNull(response.Answers[0].Text);
121+
122+
await foreach (SearchResult<Hotel> result in response.GetResultsAsync())
123+
{
124+
Hotel doc = result.Document;
125+
126+
Assert.NotNull(result.Captions);
127+
128+
var caption = result.Captions.FirstOrDefault();
129+
Assert.NotNull(caption.Highlights, "Caption highlight is null");
130+
Assert.NotNull(caption.Text, "Caption text is null");
131+
}
132+
133+
await AssertKeysEqual(
134+
response,
135+
h => h.Document.HotelId,
136+
"9", "3", "2", "5", "10", "1", "4");
137+
}
95138
}
96139
}

sdk/search/Azure.Search.Documents/tests/Models/SearchOptionsTests.cs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ public void QueryAnswerOptionWithNoCountAndThreshold()
5656
Assert.IsNull(searchOptions.QueryAnswerRaw);
5757

5858
searchOptions.QueryAnswer = QueryAnswerType.None;
59-
Assert.AreEqual($"{QueryAnswerType.None}|count-1|threshold-0.7", searchOptions.QueryAnswerRaw);
59+
Assert.AreEqual($"{QueryAnswerType.None}", searchOptions.QueryAnswerRaw);
6060
Assert.IsNull(searchOptions.QueryAnswerCount);
61+
Assert.IsNull(searchOptions.QueryAnswerThreshold);
6162

6263
searchOptions.QueryAnswer = QueryAnswerType.Extractive;
63-
Assert.AreEqual($"{QueryAnswerType.Extractive}|count-1|threshold-0.7", searchOptions.QueryAnswerRaw);
64+
Assert.AreEqual($"{QueryAnswerType.Extractive}", searchOptions.QueryAnswerRaw);
6465
Assert.IsNull(searchOptions.QueryAnswerCount);
6566

6667
searchOptions.QueryAnswerRaw = "none";
@@ -149,7 +150,7 @@ public void QueryAnswerOptionWithCountAndThreshold()
149150
Assert.IsNull(searchOptions.QueryAnswerRaw);
150151
Assert.IsNull(searchOptions.QueryAnswer);
151152

152-
searchOptions.QueryAnswerRaw = "|threshold-0.5|count-3";
153+
searchOptions.QueryAnswerRaw = "|threshold-0.5,count-3";
153154
Assert.AreEqual(3, searchOptions.QueryAnswerCount);
154155
Assert.AreEqual(0.5, searchOptions.QueryAnswerThreshold);
155156
Assert.IsNull(searchOptions.QueryAnswer);
@@ -164,25 +165,25 @@ public void QueryAnswerOption()
164165

165166
// We can set `QueryAnswer` to one of the known values, using either a string or a pre-defined value.
166167
searchOptions.QueryAnswer = "none";
167-
Assert.AreEqual($"{QueryAnswerType.None}|count-{searchOptions.QueryAnswerCount}|threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
168+
Assert.AreEqual($"{QueryAnswerType.None}|count-{searchOptions.QueryAnswerCount},threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
168169

169170
searchOptions.QueryAnswer = QueryAnswerType.None;
170-
Assert.AreEqual($"{QueryAnswerType.None}|count-{searchOptions.QueryAnswerCount}|threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
171+
Assert.AreEqual($"{QueryAnswerType.None}|count-{searchOptions.QueryAnswerCount},threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
171172

172173
searchOptions.QueryAnswer = "extractive";
173-
Assert.AreEqual($"{QueryAnswerType.Extractive}|count-{searchOptions.QueryAnswerCount}|threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
174+
Assert.AreEqual($"{QueryAnswerType.Extractive}|count-{searchOptions.QueryAnswerCount},threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
174175

175176
searchOptions.QueryAnswer = QueryAnswerType.Extractive;
176-
Assert.AreEqual($"{QueryAnswerType.Extractive}|count-{searchOptions.QueryAnswerCount}|threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
177+
Assert.AreEqual($"{QueryAnswerType.Extractive}|count-{searchOptions.QueryAnswerCount},threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
177178

178179
// We can also set `QueryAnswer` to a value unknown to the SDK.
179180
searchOptions.QueryAnswer = "unknown";
180-
Assert.AreEqual($"unknown|count-{searchOptions.QueryAnswerCount}|threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
181+
Assert.AreEqual($"unknown|count-{searchOptions.QueryAnswerCount},threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
181182

182183
searchOptions.QueryAnswer = new QueryAnswerType("unknown");
183-
Assert.AreEqual($"unknown|count-{searchOptions.QueryAnswerCount}|threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
184+
Assert.AreEqual($"unknown|count-{searchOptions.QueryAnswerCount},threshold-{searchOptions.QueryAnswerThreshold}", searchOptions.QueryAnswerRaw);
184185

185-
searchOptions.QueryAnswerRaw = "unknown|count-10|threshold-0.8";
186+
searchOptions.QueryAnswerRaw = "unknown|count-10,threshold-0.8";
186187
Assert.AreEqual("unknown", $"{searchOptions.QueryAnswer}");
187188
Assert.AreEqual(10, searchOptions.QueryAnswerCount);
188189
Assert.AreEqual(0.8, searchOptions.QueryAnswerThreshold);
@@ -279,7 +280,7 @@ public void SearchOptionsForSemanticSearch()
279280
QueryCaption = QueryCaptionType.Extractive,
280281
};
281282

282-
Assert.AreEqual("extractive|count-5|threshold-0.8", semanticSearchOptions.QueryAnswerRaw);
283+
Assert.AreEqual("extractive|count-5,threshold-0.8", semanticSearchOptions.QueryAnswerRaw);
283284
Assert.AreEqual("extractive|highlight-True", semanticSearchOptions.QueryCaptionRaw);
284285
}
285286
}

0 commit comments

Comments
 (0)