diff --git a/sdk/search/Azure.Search.Documents/CHANGELOG.md b/sdk/search/Azure.Search.Documents/CHANGELOG.md index 852e275d1a7fe..08ed5d5ec7e71 100644 --- a/sdk/search/Azure.Search.Documents/CHANGELOG.md +++ b/sdk/search/Azure.Search.Documents/CHANGELOG.md @@ -2,6 +2,11 @@ ## 11.1.0-preview.1 (Unreleased) +### Added + +- Added `SearchClientOptions.Serializer` to set which `ObjectSerializer` to use for serialization. +- Added `FieldBuilder` to easily create `SearchIndex` fields from a model type. + ### Removed - Removed `$select` from the query parameters logged by default. You can add it back via `SearchClientOptions.Diagnostics.LoggedQueryParameters("$select");` if desired. diff --git a/sdk/search/Azure.Search.Documents/Directory.Build.props b/sdk/search/Azure.Search.Documents/Directory.Build.props index 6428c499177e1..81e574947ae70 100644 --- a/sdk/search/Azure.Search.Documents/Directory.Build.props +++ b/sdk/search/Azure.Search.Documents/Directory.Build.props @@ -2,7 +2,7 @@ - + false @@ -10,16 +10,19 @@ $(UseAzureCoreExperimental) EXPERIMENTAL_SPATIAL;$(DefineConstants) - - $(UseAzureCoreExperimental) - EXPERIMENTAL_SERIALIZER;$(DefineConstants) + + true + EXPERIMENTAL_SERIALIZER;$(DefineConstants) $(UseAzureCoreExperimental) EXPERIMENTAL_DYNAMIC;$(DefineConstants) + true + EXPERIMENTAL_FIELDBUILDER;$(DefineConstants) + - true + true $(UseProjectReferenceToAzureClients) diff --git a/sdk/search/Azure.Search.Documents/README.md b/sdk/search/Azure.Search.Documents/README.md index 2a4568e3d401d..34696f6e64e57 100644 --- a/sdk/search/Azure.Search.Documents/README.md +++ b/sdk/search/Azure.Search.Documents/README.md @@ -214,14 +214,20 @@ Let's explore them with a search for a "luxury" hotel. We can decorate our own C# types with [attributes from `System.Text.Json`](https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to): ```C# Snippet:Azure_Search_Tests_Samples_Readme_StaticType -public class Hotel -{ - [JsonPropertyName("hotelId")] - public string Id { get; set; } - - [JsonPropertyName("hotelName")] - public string Name { get; set; } -} + public class Hotel + { + [JsonPropertyName("hotelId")] +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsKey = true, IsFilterable = true, IsSortable = true)] +#endif + public string Id { get; set; } + + [JsonPropertyName("hotelName")] +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(IsFilterable = true, IsSortable = true)] +#endif + public string Name { get; set; } + } ``` Then we use them as the type parameter when querying to return strongly-typed search results: @@ -276,8 +282,8 @@ SearchResults response = client.Search("luxury", options); ### Creating an index You can use the `SearchIndexClient` to create a search index. Fields can be -defined using convenient `SimpleField`, `SearchableField`, or `ComplexField` -classes. Indexes can also define suggesters, lexical analyzers, and more. +defined from a model class using `FieldBuilder`. Indexes can also define +suggesters, lexical analyzers, and more: ```C# Snippet:Azure_Search_Tests_Samples_Readme_CreateIndex Uri endpoint = new Uri(Environment.GetEnvironmentVariable("SEARCH_ENDPOINT")); @@ -287,7 +293,26 @@ string key = Environment.GetEnvironmentVariable("SEARCH_API_KEY"); AzureKeyCredential credential = new AzureKeyCredential(key); SearchIndexClient client = new SearchIndexClient(endpoint, credential); -// Create the index +// Create the index using FieldBuilder. +SearchIndex index = new SearchIndex("hotels") +{ + Fields = new FieldBuilder().Build(typeof(Hotel)), + Suggesters = + { + // Suggest query terms from the hotelName field. + new SearchSuggester("sg", "hotelName") + } +}; + +client.CreateIndex(index); +``` + +In scenarios when the model is not known or cannot be modified, you can +also create fields explicitly using convenient `SimpleField`, +`SearchableField`, or `ComplexField` classes: + +```C# Snippet:Azure_Search_Tests_Samples_Readme_CreateManualIndex +// Create the index using field definitions. SearchIndex index = new SearchIndex("hotels") { Fields = @@ -310,8 +335,8 @@ SearchIndex index = new SearchIndex("hotels") }, Suggesters = { - // Suggest query terms from both the hotelName and description fields. - new SearchSuggester("sg", "hotelName", "description") + // Suggest query terms from the hotelName field. + new SearchSuggester("sg", "hotelName") } }; diff --git a/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs b/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs index 58723e9b6e437..6eedb87980720 100644 --- a/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs +++ b/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs @@ -56,6 +56,7 @@ public SearchClient(System.Uri endpoint, string indexName, Azure.AzureKeyCredent public partial class SearchClientOptions : Azure.Core.ClientOptions { public SearchClientOptions(Azure.Search.Documents.SearchClientOptions.ServiceVersion version = Azure.Search.Documents.SearchClientOptions.ServiceVersion.V2020_06_30) { } + public Azure.Core.Serialization.ObjectSerializer Serializer { get { throw null; } set { } } public Azure.Search.Documents.SearchClientOptions.ServiceVersion Version { get { throw null; } } public enum ServiceVersion { @@ -103,6 +104,26 @@ public SuggestOptions() { } } namespace Azure.Search.Documents.Indexes { + public partial class FieldBuilder + { + public FieldBuilder() { } + public Azure.Core.Serialization.ObjectSerializer Serializer { get { throw null; } set { } } + public System.Collections.Generic.IList Build(System.Type modelType) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] + public partial class FieldBuilderIgnoreAttribute : System.Attribute + { + public FieldBuilderIgnoreAttribute() { } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] + public partial class SearchableFieldAttribute : Azure.Search.Documents.Indexes.SimpleFieldAttribute + { + public SearchableFieldAttribute() { } + public string AnalyzerName { get { throw null; } set { } } + public string IndexAnalyzerName { get { throw null; } set { } } + public string SearchAnalyzerName { get { throw null; } set { } } + public string[] SynonymMapNames { get { throw null; } set { } } + } public partial class SearchIndexClient { protected SearchIndexClient() { } @@ -202,6 +223,16 @@ public SearchIndexerClient(System.Uri endpoint, Azure.AzureKeyCredential credent public virtual Azure.Response RunIndexer(string indexerName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual System.Threading.Tasks.Task RunIndexerAsync(string indexerName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] + public partial class SimpleFieldAttribute : System.Attribute + { + public SimpleFieldAttribute() { } + public bool IsFacetable { get { throw null; } set { } } + public bool IsFilterable { get { throw null; } set { } } + public bool IsHidden { get { throw null; } set { } } + public bool IsKey { get { throw null; } set { } } + public bool IsSortable { get { throw null; } set { } } + } } namespace Azure.Search.Documents.Indexes.Models { @@ -729,6 +760,102 @@ internal LexicalAnalyzer() { } public static implicit operator Azure.Search.Documents.Indexes.Models.LexicalAnalyzerName (string value) { throw null; } public static bool operator !=(Azure.Search.Documents.Indexes.Models.LexicalAnalyzerName left, Azure.Search.Documents.Indexes.Models.LexicalAnalyzerName right) { throw null; } public override string ToString() { throw null; } + public static partial class Values + { + public const string ArLucene = "ar.lucene"; + public const string ArMicrosoft = "ar.microsoft"; + public const string BgLucene = "bg.lucene"; + public const string BgMicrosoft = "bg.microsoft"; + public const string BnMicrosoft = "bn.microsoft"; + public const string CaLucene = "ca.lucene"; + public const string CaMicrosoft = "ca.microsoft"; + public const string CsLucene = "cs.lucene"; + public const string CsMicrosoft = "cs.microsoft"; + public const string DaLucene = "da.lucene"; + public const string DaMicrosoft = "da.microsoft"; + public const string DeLucene = "de.lucene"; + public const string DeMicrosoft = "de.microsoft"; + public const string ElLucene = "el.lucene"; + public const string ElMicrosoft = "el.microsoft"; + public const string EnLucene = "en.lucene"; + public const string EnMicrosoft = "en.microsoft"; + public const string EsLucene = "es.lucene"; + public const string EsMicrosoft = "es.microsoft"; + public const string EtMicrosoft = "et.microsoft"; + public const string EuLucene = "eu.lucene"; + public const string FaLucene = "fa.lucene"; + public const string FiLucene = "fi.lucene"; + public const string FiMicrosoft = "fi.microsoft"; + public const string FrLucene = "fr.lucene"; + public const string FrMicrosoft = "fr.microsoft"; + public const string GaLucene = "ga.lucene"; + public const string GlLucene = "gl.lucene"; + public const string GuMicrosoft = "gu.microsoft"; + public const string HeMicrosoft = "he.microsoft"; + public const string HiLucene = "hi.lucene"; + public const string HiMicrosoft = "hi.microsoft"; + public const string HrMicrosoft = "hr.microsoft"; + public const string HuLucene = "hu.lucene"; + public const string HuMicrosoft = "hu.microsoft"; + public const string HyLucene = "hy.lucene"; + public const string IdLucene = "id.lucene"; + public const string IdMicrosoft = "id.microsoft"; + public const string IsMicrosoft = "is.microsoft"; + public const string ItLucene = "it.lucene"; + public const string ItMicrosoft = "it.microsoft"; + public const string JaLucene = "ja.lucene"; + public const string JaMicrosoft = "ja.microsoft"; + public const string Keyword = "keyword"; + public const string KnMicrosoft = "kn.microsoft"; + public const string KoLucene = "ko.lucene"; + public const string KoMicrosoft = "ko.microsoft"; + public const string LtMicrosoft = "lt.microsoft"; + public const string LvLucene = "lv.lucene"; + public const string LvMicrosoft = "lv.microsoft"; + public const string MlMicrosoft = "ml.microsoft"; + public const string MrMicrosoft = "mr.microsoft"; + public const string MsMicrosoft = "ms.microsoft"; + public const string NbMicrosoft = "nb.microsoft"; + public const string NlLucene = "nl.lucene"; + public const string NlMicrosoft = "nl.microsoft"; + public const string NoLucene = "no.lucene"; + public const string PaMicrosoft = "pa.microsoft"; + public const string Pattern = "pattern"; + public const string PlLucene = "pl.lucene"; + public const string PlMicrosoft = "pl.microsoft"; + public const string PtBrLucene = "pt-BR.lucene"; + public const string PtBrMicrosoft = "pt-BR.microsoft"; + public const string PtPtLucene = "pt-PT.lucene"; + public const string PtPtMicrosoft = "pt-PT.microsoft"; + public const string RoLucene = "ro.lucene"; + public const string RoMicrosoft = "ro.microsoft"; + public const string RuLucene = "ru.lucene"; + public const string RuMicrosoft = "ru.microsoft"; + public const string Simple = "simple"; + public const string SkMicrosoft = "sk.microsoft"; + public const string SlMicrosoft = "sl.microsoft"; + public const string SrCyrillicMicrosoft = "sr-cyrillic.microsoft"; + public const string SrLatinMicrosoft = "sr-latin.microsoft"; + public const string StandardAsciiFoldingLucene = "standardasciifolding.lucene"; + public const string StandardLucene = "standard.lucene"; + public const string Stop = "stop"; + public const string SvLucene = "sv.lucene"; + public const string SvMicrosoft = "sv.microsoft"; + public const string TaMicrosoft = "ta.microsoft"; + public const string TeMicrosoft = "te.microsoft"; + public const string ThLucene = "th.lucene"; + public const string ThMicrosoft = "th.microsoft"; + public const string TrLucene = "tr.lucene"; + public const string TrMicrosoft = "tr.microsoft"; + public const string UkMicrosoft = "uk.microsoft"; + public const string UrMicrosoft = "ur.microsoft"; + public const string ViMicrosoft = "vi.microsoft"; + public const string Whitespace = "whitespace"; + public const string ZhHansLucene = "zh-Hans.lucene"; + public const string ZhHansMicrosoft = "zh-Hans.microsoft"; + public const string ZhHantLucene = "zh-Hant.lucene"; + public const string ZhHantMicrosoft = "zh-Hant.microsoft"; + } } public partial class LexicalTokenizer { @@ -1163,7 +1290,7 @@ public SearchIndex(string name, System.Collections.Generic.IEnumerable Fields { get { throw null; } } + public System.Collections.Generic.IList Fields { get { throw null; } set { } } public string Name { get { throw null; } } public System.Collections.Generic.IList ScoringProfiles { get { throw null; } } public Azure.Search.Documents.Indexes.Models.SimilarityAlgorithm Similarity { get { throw null; } set { } } diff --git a/sdk/search/Azure.Search.Documents/samples/README.md b/sdk/search/Azure.Search.Documents/samples/README.md index 78e2d75aa4ab1..4e5d0558f0df4 100644 --- a/sdk/search/Azure.Search.Documents/samples/README.md +++ b/sdk/search/Azure.Search.Documents/samples/README.md @@ -14,6 +14,7 @@ description: Samples for the Azure.Search.Documents client library - Get started either [synchronously](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/search/Azure.Search.Documents/samples/Sample01a_HelloWorld.md) or [asynchronously](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/search/Azure.Search.Documents/samples/Sample01b_HelloWorldAsync.md). - Perform [service level operations](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/search/Azure.Search.Documents/samples/Sample02_Service.md). - Perform [index level operations](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/search/Azure.Search.Documents/samples/Sample03_Index.md). +- Use [`[FieldBuilderIgnore]`](Sample04_FieldBuilderIgnore.md) to add fields for unsupported properties using `FieldBuilder`. ## Sample Code to Include in your Projects diff --git a/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilderIgnore.md b/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilderIgnore.md new file mode 100644 index 0000000000000..258a78ca273af --- /dev/null +++ b/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilderIgnore.md @@ -0,0 +1,134 @@ +# Azure.Search.Documents Samples - Using FieldBuilderIgnoreAttribute + +The `FieldBuilder` class allows you to define a Search index from a model type, +though not all property types are supported by a Search index without workarounds. + +If when using `FieldBuilder` you see the following exception, use the following workaround: + +> Property 'Genre' is of type 'MovieGenre', which does not map to an +> Azure Search data type. Please use a supported data type or mark the property with \[FieldBuilderIgnore\] +> and define the field by creating a SearchField object. See https://aka.ms/azsdk/net/search/fieldbuilder for more information."; + +## Model + +Consider the following model, which declares a property of an `enum` type, +which are not currently supported. + +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_Types +public class Movie +{ + [SimpleField(IsKey = true)] + public string Id { get; set; } + + [SearchableField(IsSortable = true, AnalyzerName = LexicalAnalyzerName.Values.EnLucene)] + public string Name { get; set; } + + [FieldBuilderIgnore] + [JsonConverter(typeof(JsonStringEnumConverter))] + public MovieGenre Genre { get; set; } + + [SimpleField(IsFacetable = true, IsFilterable = true, IsSortable = true)] + public int Year { get; set; } + + [SimpleField(IsFilterable = true, IsSortable = true)] + public double Rating { get; set; } +} + +public enum MovieGenre +{ + Unknown, + Action, + Comedy, + Drama, + Fantasy, + Horror, + Romance, + SciFi, +} +``` + +Declaring enum property types will throw an exception if not ignored using either the +`[FieldBuilderIgnore]` attribute, or another attribute like `[JsonIgnore]` of +`System.Text.Json`, `Newtonsoft.Json`, or any other method of ignoring a property entirely +for the serializer you've specified in `SearchClientOptions.Serializer`. Enum property types +are not supported by default because you may instead want to serialize them as their +integer values to save space in an index. + +The `Genre` property is attributed with `[FieldBuilderIgnore]` so that `FieldBuilder` will ignore it +when generating fields for a Search index. You can then define a field manually as shown below. + +## Create an index + +After creating the index using `FieldBuilder`, you can add fields manually. +For the `Genre` property we declare that the serializer should convert the `MovieGenre` enum +to a string, and define the `genre` index field ourselves. + +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_CreateIndex +Uri endpoint = new Uri(Environment.GetEnvironmentVariable("SEARCH_ENDPOINT")); +string key = Environment.GetEnvironmentVariable("SEARCH_API_KEY"); + +// Define client options to use camelCase when serializing property names. +SearchClientOptions options = new SearchClientOptions +{ + Serializer = new JsonObjectSerializer( + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }) +}; + +// Create a service client. +AzureKeyCredential credential = new AzureKeyCredential(key); +SearchIndexClient indexClient = new SearchIndexClient(endpoint, credential, options); + +// Create the FieldBuilder using the same serializer. +FieldBuilder fieldBuilder = new FieldBuilder +{ + Serializer = options.Serializer +}; + +// Create the index using FieldBuilder. +SearchIndex index = new SearchIndex("movies") +{ + Fields = fieldBuilder.Build(typeof(Movie)), + Suggesters = + { + // Suggest query terms from the "name" field. + new SearchSuggester("n", "name") + } +}; + +// Define the "genre" field as a string. +SearchableField genreField = new SearchableField("genre") +{ + AnalyzerName = LexicalAnalyzerName.Values.EnLucene, + IsFacetable = true, + IsFilterable = true +}; +index.Fields.Add(genreField); + +// Create the index. +indexClient.CreateIndex(index); +``` + +## Upload a document + +When a `Movie` is serialized, the `Genre` property is converted to a string and +populates the index's `genre` field. + +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_UploadDocument +Movie movie = new Movie +{ + Id = Guid.NewGuid().ToString("n"), + Name = "The Lord of the Rings: The Return of the King", + Genre = MovieGenre.Fantasy, + Year = 2003, + Rating = 9.1 +}; + +// Add a movie to the index. +SearchClient searchClient = indexClient.GetSearchClient(index.Name); +searchClient.UploadDocuments(new[] { movie }); +``` + +Similarly, deserializing converts it from a string back into a `MovieGenre` enum value. diff --git a/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj b/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj index d363d53f954ea..bc4b45d8c8786 100644 --- a/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj +++ b/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj @@ -1,4 +1,4 @@ - + Microsoft Azure.Search.Documents client library 11.1.0-preview.1 @@ -16,6 +16,15 @@ $(NoWarn);AZC0007;AZC0004;AZC0001 + + + + + + + + + diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs new file mode 100644 index 0000000000000..42d695aa80ce1 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -0,0 +1,452 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif +using Azure.Search.Documents.Indexes.Models; +#if EXPERIMENTAL_SPATIAL +using Azure.Core.Spatial; +#endif + +namespace Azure.Search.Documents.Indexes +{ + /// + /// Builds field definitions for a search index by reflecting over a user-defined model type. + /// + public class FieldBuilder + { + private const string HelpLink = "https://aka.ms/azsdk/net/search/fieldbuilder"; + + private static readonly IReadOnlyDictionary s_primitiveTypeMap = + new ReadOnlyDictionary( + new Dictionary() + { + [typeof(string)] = SearchFieldDataType.String, + [typeof(int)] = SearchFieldDataType.Int32, + [typeof(long)] = SearchFieldDataType.Int64, + [typeof(double)] = SearchFieldDataType.Double, + [typeof(bool)] = SearchFieldDataType.Boolean, + [typeof(DateTime)] = SearchFieldDataType.DateTimeOffset, + [typeof(DateTimeOffset)] = SearchFieldDataType.DateTimeOffset, +#if EXPERIMENTAL_SPATIAL + [typeof(PointGeometry)] = SearchFieldDataType.GeographyPoint, +#endif + }); + + private static readonly ISet s_unsupportedTypes = + new HashSet + { + typeof(decimal), + }; + + /// + /// Initializes a new instance of the class. + /// + public FieldBuilder() + { + } + + /// + /// Gets or sets the to use to generate field names that match JSON property names. + /// You should use hte same value as . + /// will be used if no value is provided. + /// + public ObjectSerializer Serializer { get; set; } + + /// + /// Creates a list of objects corresponding to + /// the properties of the type supplied. + /// + /// + /// The type for which fields will be created, based on its properties. + /// + /// A collection of fields. + /// . + public IList Build(Type modelType) + { + Argument.AssertNotNull(modelType, nameof(modelType)); + + ArgumentException FailOnNonObjectDataType() + { + string errorMessage = + $"Type '{modelType}' does not have properties which map to fields of an Azure Search index. Please use a " + + "class or struct with public properties."; + + throw new ArgumentException(errorMessage, nameof(modelType)); + } + + Serializer ??= new JsonObjectSerializer(); + IMemberNameConverter nameProvider = Serializer as IMemberNameConverter ?? DefaultSerializedNameProvider.Shared; + + if (ObjectInfo.TryGet(modelType, nameProvider, out ObjectInfo info)) + { + if (info.Properties.Length == 0) + { + throw FailOnNonObjectDataType(); + } + + // Use Stack to avoid a dependency on ImmutableStack for now. + return Build(modelType, info, nameProvider, new Stack(new[] { modelType })); + } + + throw FailOnNonObjectDataType(); + } + + private static IList Build( + Type modelType, + ObjectInfo info, + IMemberNameConverter nameProvider, + Stack processedTypes) + { + SearchField BuildField(ObjectPropertyInfo prop) + { + // The IMemberNameConverter will return null for implementation-specific ways of ignoring members. + static bool ShouldIgnore(Attribute attribute) => + attribute is FieldBuilderIgnoreAttribute; + + IList attributes = prop.GetCustomAttributes(true).Cast().ToArray(); + if (attributes.Any(ShouldIgnore)) + { + return null; + } + + SearchField CreateComplexField(SearchFieldDataType dataType, Type underlyingClrType, ObjectInfo info) + { + if (processedTypes.Contains(underlyingClrType)) + { + // Skip recursive types. + return null; + } + + processedTypes.Push(underlyingClrType); + try + { + IList subFields = + Build(underlyingClrType, info, nameProvider, processedTypes); + + if (prop.SerializedName is null) + { + // Member is unsupported or ignored. + return null; + } + + // Start with a ComplexField to make sure all properties default to language defaults. + SearchField field = new ComplexField(prop.SerializedName, dataType); + foreach (SearchField subField in subFields) + { + field.Fields.Add(subField); + } + + return field; + } + finally + { + processedTypes.Pop(); + } + } + + SearchField CreateSimpleField(SearchFieldDataType SearchFieldDataType) + { + if (prop.SerializedName is null) + { + // Member is unsupported or ignored. + return null; + } + + // Start with a SimpleField to make sure all properties default to language defaults. + SearchField field = new SimpleField(prop.SerializedName, SearchFieldDataType); + foreach (Attribute attribute in attributes) + { + switch (attribute) + { + case SearchableFieldAttribute searchableFieldAttribute: + ((ISearchFieldAttribute)searchableFieldAttribute).SetField(field); + break; + + case SimpleFieldAttribute simpleFieldAttribute: + ((ISearchFieldAttribute)simpleFieldAttribute).SetField(field); + break; + + default: + Type attributeType = attribute.GetType(); + + // Match on name to avoid dependency - don't want to force people not using + // this feature to bring in the annotations component. + // + // Also, ignore key attributes on sub-fields. + if (attributeType.FullName == "System.ComponentModel.DataAnnotations.KeyAttribute" && + processedTypes.Count <= 1) + { + field.IsKey = true; + } + break; + } + } + + return field; + } + + ArgumentException FailOnUnknownDataType() + { + string errorMessage = + $"Property '{prop.Name}' is of type '{prop.PropertyType}', which does not map to an " + + "Azure Search data type. Please use a supported data type or mark the property with [FieldBuilderIgnore] " + + $"and define the field by creating a SearchField object. See {HelpLink} for more information."; + + return new ArgumentException(errorMessage, nameof(modelType)) + { + HelpLink = HelpLink, + }; + } + + IDataTypeInfo dataTypeInfo = GetDataTypeInfo(prop.PropertyType, nameProvider); + + return dataTypeInfo.Match( + onUnknownDataType: () => throw FailOnUnknownDataType(), + onSimpleDataType: CreateSimpleField, + onComplexDataType: CreateComplexField); + } + + return info.Properties.Select(BuildField).Where(field => field != null).ToList(); + } + + private static IDataTypeInfo GetDataTypeInfo(Type propertyType, IMemberNameConverter nameProvider) + { + static bool IsNullableType(Type type) => + type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + + if (s_primitiveTypeMap.TryGetValue(propertyType, out SearchFieldDataType SearchFieldDataType)) + { + return DataTypeInfo.Simple(SearchFieldDataType); + } + else if (IsNullableType(propertyType)) + { + return GetDataTypeInfo(propertyType.GenericTypeArguments[0], nameProvider); + } + else if (TryGetEnumerableElementType(propertyType, out Type elementType)) + { + IDataTypeInfo elementTypeInfo = GetDataTypeInfo(elementType, nameProvider); + return DataTypeInfo.AsCollection(elementTypeInfo); + } + else if (ObjectInfo.TryGet(propertyType, nameProvider, out ObjectInfo info)) + { + return DataTypeInfo.Complex(SearchFieldDataType.Complex, propertyType, info); + } + else + { + return DataTypeInfo.Unknown; + } + } + + private static bool TryGetEnumerableElementType(Type candidateType, out Type elementType) + { + static Type GetElementTypeIfIEnumerable(Type t) => + t.IsConstructedGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>) + ? t.GenericTypeArguments[0] + : null; + + elementType = GetElementTypeIfIEnumerable(candidateType); + if (elementType != null) + { + return true; + } + else + { + TypeInfo ti = candidateType.GetTypeInfo(); + var listElementTypes = ti + .ImplementedInterfaces + .Select(GetElementTypeIfIEnumerable) + .Where(p => p != null) + .ToList(); + + if (listElementTypes.Count == 1) + { + elementType = listElementTypes[0]; + return true; + } + else + { + return false; + } + } + } + + private interface IDataTypeInfo + { + T Match( + Func onUnknownDataType, + Func onSimpleDataType, + Func onComplexDataType); + } + + private static class DataTypeInfo + { + public static IDataTypeInfo Unknown { get; } = new UnknownDataTypeInfo(); + + public static IDataTypeInfo Simple(SearchFieldDataType SearchFieldDataType) => new SimpleDataTypeInfo(SearchFieldDataType); + + public static IDataTypeInfo Complex(SearchFieldDataType SearchFieldDataType, Type underlyingClrType, ObjectInfo info) => + new ComplexDataTypeInfo(SearchFieldDataType, underlyingClrType, info); + + public static IDataTypeInfo AsCollection(IDataTypeInfo dataTypeInfo) => + dataTypeInfo.Match( + onUnknownDataType: () => Unknown, + onSimpleDataType: SearchFieldDataType => Simple(SearchFieldDataType.Collection(SearchFieldDataType)), + onComplexDataType: (SearchFieldDataType, underlyingClrType, info) => + Complex(SearchFieldDataType.Collection(SearchFieldDataType), underlyingClrType, info)); + + private sealed class UnknownDataTypeInfo : IDataTypeInfo + { + public UnknownDataTypeInfo() + { + } + + public T Match( + Func onUnknownDataType, + Func onSimpleDataType, + Func onComplexDataType) + => onUnknownDataType(); + } + + private sealed class SimpleDataTypeInfo : IDataTypeInfo + { + private readonly SearchFieldDataType _dataType; + + public SimpleDataTypeInfo(SearchFieldDataType SearchFieldDataType) + { + _dataType = SearchFieldDataType; + } + + public T Match( + Func onUnknownDataType, + Func onSimpleDataType, + Func onComplexDataType) + => onSimpleDataType(_dataType); + } + + private sealed class ComplexDataTypeInfo : IDataTypeInfo + { + private readonly SearchFieldDataType _dataType; + private readonly Type _underlyingClrType; + private readonly ObjectInfo _info; + + public ComplexDataTypeInfo(SearchFieldDataType SearchFieldDataType, Type underlyingClrType, ObjectInfo info) + { + _dataType = SearchFieldDataType; + _underlyingClrType = underlyingClrType; + _info = info; + } + + public T Match( + Func onUnknownDataType, + Func onSimpleDataType, + Func onComplexDataType) + => onComplexDataType(_dataType, _underlyingClrType, _info); + } + } + + private class ObjectInfo + { + private ObjectInfo(ObjectPropertyInfo[] properties) + { + Properties = properties; + } + + public static bool TryGet(Type type, IMemberNameConverter nameProvider, out ObjectInfo info) + { + // Close approximation to Newtonsoft.Json.Serialization.DefaultContractResolver that was used in Microsoft.Azure.Search. + if (!type.IsPrimitive && !type.IsEnum && !s_unsupportedTypes.Contains(type) && !s_primitiveTypeMap.ContainsKey(type) && !typeof(IEnumerable).IsAssignableFrom(type)) + { + const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + List properties = new List(); + foreach (PropertyInfo property in type.GetProperties(bindingFlags)) + { + string serializedName = nameProvider.ConvertMemberName(property); + if (serializedName != null) + { + properties.Add(new ObjectPropertyInfo(property, serializedName)); + } + } + + foreach (FieldInfo field in type.GetFields(bindingFlags)) + { + string serializedName = nameProvider.ConvertMemberName(field); + if (serializedName != null) + { + properties.Add(new ObjectPropertyInfo(field, serializedName)); + } + } + + if (properties.Count != 0) + { + info = new ObjectInfo(properties.ToArray()); + return true; + } + } + + info = null; + return false; + } + + public ObjectPropertyInfo[] Properties { get; } + } + + private struct ObjectPropertyInfo + { + private readonly MemberInfo _memberInfo; + + public ObjectPropertyInfo(PropertyInfo property, string serializedName) + { + Debug.Assert(serializedName != null, $"{nameof(serializedName)} cannot be null"); + + _memberInfo = property; + + SerializedName = serializedName; + PropertyType = property.PropertyType; + } + + public ObjectPropertyInfo(FieldInfo field, string serializedName) + { + Debug.Assert(serializedName != null, $"{nameof(serializedName)} cannot be null"); + + _memberInfo = field; + + SerializedName = serializedName; + PropertyType = field.FieldType; + } + + public string Name => _memberInfo.Name; + + public string SerializedName { get; } + + public Type PropertyType { get; } + + public static implicit operator MemberInfo(ObjectPropertyInfo property) => + property._memberInfo; + + public object[] GetCustomAttributes(bool inherit) => + _memberInfo.GetCustomAttributes(inherit); + } + + private class DefaultSerializedNameProvider : IMemberNameConverter + { + public static IMemberNameConverter Shared { get; } = new DefaultSerializedNameProvider(); + + private DefaultSerializedNameProvider() + { + } + + public string ConvertMemberName(MemberInfo member) => member?.Name; + } + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs new file mode 100644 index 0000000000000..ea337a0a25a63 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Indexes +{ + /// + /// Indicates that the target property should be ignored by . + /// + /// + /// This attribute is useful in situations where a property definition doesn't cleanly map to a + /// object, but its values still need to be converted to and from JSON. In that case, + /// can't be used since it would disable JSON conversion. + /// An example of a scenario where this is useful is when mapping between a string field in Azure Cognitive Search and an enum + /// property. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class FieldBuilderIgnoreAttribute : Attribute + { + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/ISearchFieldAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/ISearchFieldAttribute.cs new file mode 100644 index 0000000000000..5bab61211739f --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/ISearchFieldAttribute.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Indexes +{ + /// + /// Represents an attribute that creates a . + /// + internal interface ISearchFieldAttribute + { + /// + /// Sets properties on the given based on attributes' properties that are set. + /// + /// The to update. + void SetField(SearchField field); + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/Models/ComplexField.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/ComplexField.cs index 13536e53811f6..fdaadfd3ec5f7 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/ComplexField.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/ComplexField.cs @@ -23,6 +23,21 @@ public ComplexField(string name, bool collection = false) : base(name, collectio { } + /// + /// Initializes a new instance of the class. + /// + /// The name of the field, which must be unique within the index or parent field. + /// The data type of the field. + /// is an empty string. + /// is null. + /// + /// This is used internally by FieldBuilder to avoid detecting if the type is a collection just + /// to return the same type we started with. + /// + internal ComplexField(string name, SearchFieldDataType type) : base(name, type) + { + } + /// /// Gets a collection of or child fields. /// diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs new file mode 100644 index 0000000000000..6e455d1125252 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Search.Documents.Indexes.Models +{ + public readonly partial struct LexicalAnalyzerName + { +#if EXPERIMENTAL_FIELDBUILDER +#pragma warning disable CA1034 // Nested types should not be visible + /// + /// The values of all declared properties as string constants. + /// These can be used in and anywhere else constants are required. + /// + public static class Values + { + /// Microsoft analyzer for Arabic. + public const string ArMicrosoft = LexicalAnalyzerName.ArMicrosoftValue; + /// Lucene analyzer for Arabic. + public const string ArLucene = LexicalAnalyzerName.ArLuceneValue; + /// Lucene analyzer for Armenian. + public const string HyLucene = LexicalAnalyzerName.HyLuceneValue; + /// Microsoft analyzer for Bangla. + public const string BnMicrosoft = LexicalAnalyzerName.BnMicrosoftValue; + /// Lucene analyzer for Basque. + public const string EuLucene = LexicalAnalyzerName.EuLuceneValue; + /// Microsoft analyzer for Bulgarian. + public const string BgMicrosoft = LexicalAnalyzerName.BgMicrosoftValue; + /// Lucene analyzer for Bulgarian. + public const string BgLucene = LexicalAnalyzerName.BgLuceneValue; + /// Microsoft analyzer for Catalan. + public const string CaMicrosoft = LexicalAnalyzerName.CaMicrosoftValue; + /// Lucene analyzer for Catalan. + public const string CaLucene = LexicalAnalyzerName.CaLuceneValue; + /// Microsoft analyzer for Chinese (Simplified). + public const string ZhHansMicrosoft = LexicalAnalyzerName.ZhHansMicrosoftValue; + /// Lucene analyzer for Chinese (Simplified). + public const string ZhHansLucene = LexicalAnalyzerName.ZhHansLuceneValue; + /// Microsoft analyzer for Chinese (Traditional). + public const string ZhHantMicrosoft = LexicalAnalyzerName.ZhHantMicrosoftValue; + /// Lucene analyzer for Chinese (Traditional). + public const string ZhHantLucene = LexicalAnalyzerName.ZhHantLuceneValue; + /// Microsoft analyzer for Croatian. + public const string HrMicrosoft = LexicalAnalyzerName.HrMicrosoftValue; + /// Microsoft analyzer for Czech. + public const string CsMicrosoft = LexicalAnalyzerName.CsMicrosoftValue; + /// Lucene analyzer for Czech. + public const string CsLucene = LexicalAnalyzerName.CsLuceneValue; + /// Microsoft analyzer for Danish. + public const string DaMicrosoft = LexicalAnalyzerName.DaMicrosoftValue; + /// Lucene analyzer for Danish. + public const string DaLucene = LexicalAnalyzerName.DaLuceneValue; + /// Microsoft analyzer for Dutch. + public const string NlMicrosoft = LexicalAnalyzerName.NlMicrosoftValue; + /// Lucene analyzer for Dutch. + public const string NlLucene = LexicalAnalyzerName.NlLuceneValue; + /// Microsoft analyzer for English. + public const string EnMicrosoft = LexicalAnalyzerName.EnMicrosoftValue; + /// Lucene analyzer for English. + public const string EnLucene = LexicalAnalyzerName.EnLuceneValue; + /// Microsoft analyzer for Estonian. + public const string EtMicrosoft = LexicalAnalyzerName.EtMicrosoftValue; + /// Microsoft analyzer for Finnish. + public const string FiMicrosoft = LexicalAnalyzerName.FiMicrosoftValue; + /// Lucene analyzer for Finnish. + public const string FiLucene = LexicalAnalyzerName.FiLuceneValue; + /// Microsoft analyzer for French. + public const string FrMicrosoft = LexicalAnalyzerName.FrMicrosoftValue; + /// Lucene analyzer for French. + public const string FrLucene = LexicalAnalyzerName.FrLuceneValue; + /// Lucene analyzer for Galician. + public const string GlLucene = LexicalAnalyzerName.GlLuceneValue; + /// Microsoft analyzer for German. + public const string DeMicrosoft = LexicalAnalyzerName.DeMicrosoftValue; + /// Lucene analyzer for German. + public const string DeLucene = LexicalAnalyzerName.DeLuceneValue; + /// Microsoft analyzer for Greek. + public const string ElMicrosoft = LexicalAnalyzerName.ElMicrosoftValue; + /// Lucene analyzer for Greek. + public const string ElLucene = LexicalAnalyzerName.ElLuceneValue; + /// Microsoft analyzer for Gujarati. + public const string GuMicrosoft = LexicalAnalyzerName.GuMicrosoftValue; + /// Microsoft analyzer for Hebrew. + public const string HeMicrosoft = LexicalAnalyzerName.HeMicrosoftValue; + /// Microsoft analyzer for Hindi. + public const string HiMicrosoft = LexicalAnalyzerName.HiMicrosoftValue; + /// Lucene analyzer for Hindi. + public const string HiLucene = LexicalAnalyzerName.HiLuceneValue; + /// Microsoft analyzer for Hungarian. + public const string HuMicrosoft = LexicalAnalyzerName.HuMicrosoftValue; + /// Lucene analyzer for Hungarian. + public const string HuLucene = LexicalAnalyzerName.HuLuceneValue; + /// Microsoft analyzer for Icelandic. + public const string IsMicrosoft = LexicalAnalyzerName.IsMicrosoftValue; + /// Microsoft analyzer for Indonesian (Bahasa). + public const string IdMicrosoft = LexicalAnalyzerName.IdMicrosoftValue; + /// Lucene analyzer for Indonesian. + public const string IdLucene = LexicalAnalyzerName.IdLuceneValue; + /// Lucene analyzer for Irish. + public const string GaLucene = LexicalAnalyzerName.GaLuceneValue; + /// Microsoft analyzer for Italian. + public const string ItMicrosoft = LexicalAnalyzerName.ItMicrosoftValue; + /// Lucene analyzer for Italian. + public const string ItLucene = LexicalAnalyzerName.ItLuceneValue; + /// Microsoft analyzer for Japanese. + public const string JaMicrosoft = LexicalAnalyzerName.JaMicrosoftValue; + /// Lucene analyzer for Japanese. + public const string JaLucene = LexicalAnalyzerName.JaLuceneValue; + /// Microsoft analyzer for Kannada. + public const string KnMicrosoft = LexicalAnalyzerName.KnMicrosoftValue; + /// Microsoft analyzer for Korean. + public const string KoMicrosoft = LexicalAnalyzerName.KoMicrosoftValue; + /// Lucene analyzer for Korean. + public const string KoLucene = LexicalAnalyzerName.KoLuceneValue; + /// Microsoft analyzer for Latvian. + public const string LvMicrosoft = LexicalAnalyzerName.LvMicrosoftValue; + /// Lucene analyzer for Latvian. + public const string LvLucene = LexicalAnalyzerName.LvLuceneValue; + /// Microsoft analyzer for Lithuanian. + public const string LtMicrosoft = LexicalAnalyzerName.LtMicrosoftValue; + /// Microsoft analyzer for Malayalam. + public const string MlMicrosoft = LexicalAnalyzerName.MlMicrosoftValue; + /// Microsoft analyzer for Malay (Latin). + public const string MsMicrosoft = LexicalAnalyzerName.MsMicrosoftValue; + /// Microsoft analyzer for Marathi. + public const string MrMicrosoft = LexicalAnalyzerName.MrMicrosoftValue; + /// Microsoft analyzer for Norwegian (Bokmål). + public const string NbMicrosoft = LexicalAnalyzerName.NbMicrosoftValue; + /// Lucene analyzer for Norwegian. + public const string NoLucene = LexicalAnalyzerName.NoLuceneValue; + /// Lucene analyzer for Persian. + public const string FaLucene = LexicalAnalyzerName.FaLuceneValue; + /// Microsoft analyzer for Polish. + public const string PlMicrosoft = LexicalAnalyzerName.PlMicrosoftValue; + /// Lucene analyzer for Polish. + public const string PlLucene = LexicalAnalyzerName.PlLuceneValue; + /// Microsoft analyzer for Portuguese (Brazil). + public const string PtBrMicrosoft = LexicalAnalyzerName.PtBrMicrosoftValue; + /// Lucene analyzer for Portuguese (Brazil). + public const string PtBrLucene = LexicalAnalyzerName.PtBrLuceneValue; + /// Microsoft analyzer for Portuguese (Portugal). + public const string PtPtMicrosoft = LexicalAnalyzerName.PtPtMicrosoftValue; + /// Lucene analyzer for Portuguese (Portugal). + public const string PtPtLucene = LexicalAnalyzerName.PtPtLuceneValue; + /// Microsoft analyzer for Punjabi. + public const string PaMicrosoft = LexicalAnalyzerName.PaMicrosoftValue; + /// Microsoft analyzer for Romanian. + public const string RoMicrosoft = LexicalAnalyzerName.RoMicrosoftValue; + /// Lucene analyzer for Romanian. + public const string RoLucene = LexicalAnalyzerName.RoLuceneValue; + /// Microsoft analyzer for Russian. + public const string RuMicrosoft = LexicalAnalyzerName.RuMicrosoftValue; + /// Lucene analyzer for Russian. + public const string RuLucene = LexicalAnalyzerName.RuLuceneValue; + /// Microsoft analyzer for Serbian (Cyrillic). + public const string SrCyrillicMicrosoft = LexicalAnalyzerName.SrCyrillicMicrosoftValue; + /// Microsoft analyzer for Serbian (Latin). + public const string SrLatinMicrosoft = LexicalAnalyzerName.SrLatinMicrosoftValue; + /// Microsoft analyzer for Slovak. + public const string SkMicrosoft = LexicalAnalyzerName.SkMicrosoftValue; + /// Microsoft analyzer for Slovenian. + public const string SlMicrosoft = LexicalAnalyzerName.SlMicrosoftValue; + /// Microsoft analyzer for Spanish. + public const string EsMicrosoft = LexicalAnalyzerName.EsMicrosoftValue; + /// Lucene analyzer for Spanish. + public const string EsLucene = LexicalAnalyzerName.EsLuceneValue; + /// Microsoft analyzer for Swedish. + public const string SvMicrosoft = LexicalAnalyzerName.SvMicrosoftValue; + /// Lucene analyzer for Swedish. + public const string SvLucene = LexicalAnalyzerName.SvLuceneValue; + /// Microsoft analyzer for Tamil. + public const string TaMicrosoft = LexicalAnalyzerName.TaMicrosoftValue; + /// Microsoft analyzer for Telugu. + public const string TeMicrosoft = LexicalAnalyzerName.TeMicrosoftValue; + /// Microsoft analyzer for Thai. + public const string ThMicrosoft = LexicalAnalyzerName.ThMicrosoftValue; + /// Lucene analyzer for Thai. + public const string ThLucene = LexicalAnalyzerName.ThLuceneValue; + /// Microsoft analyzer for Turkish. + public const string TrMicrosoft = LexicalAnalyzerName.TrMicrosoftValue; + /// Lucene analyzer for Turkish. + public const string TrLucene = LexicalAnalyzerName.TrLuceneValue; + /// Microsoft analyzer for Ukrainian. + public const string UkMicrosoft = LexicalAnalyzerName.UkMicrosoftValue; + /// Microsoft analyzer for Urdu. + public const string UrMicrosoft = LexicalAnalyzerName.UrMicrosoftValue; + /// Microsoft analyzer for Vietnamese. + public const string ViMicrosoft = LexicalAnalyzerName.ViMicrosoftValue; + /// Standard Lucene analyzer. + public const string StandardLucene = LexicalAnalyzerName.StandardLuceneValue; + /// Standard ASCII Folding Lucene analyzer. See https://docs.microsoft.com/rest/api/searchservice/Custom-analyzers-in-Azure-Search#Analyzers. + public const string StandardAsciiFoldingLucene = LexicalAnalyzerName.StandardAsciiFoldingLuceneValue; + /// Treats the entire content of a field as a single token. This is useful for data like zip codes, ids, and some product names. See http://lucene.apache.org/core/4_10_3/analyzers-common/org/apache/lucene/analysis/core/KeywordAnalyzer.html. + public const string Keyword = LexicalAnalyzerName.KeywordValue; + /// Flexibly separates text into terms via a regular expression pattern. See http://lucene.apache.org/core/4_10_3/analyzers-common/org/apache/lucene/analysis/miscellaneous/PatternAnalyzer.html. + public const string Pattern = LexicalAnalyzerName.PatternValue; + /// Divides text at non-letters and converts them to lower case. See http://lucene.apache.org/core/4_10_3/analyzers-common/org/apache/lucene/analysis/core/SimpleAnalyzer.html. + public const string Simple = LexicalAnalyzerName.SimpleValue; + /// Divides text at non-letters; Applies the lowercase and stopword token filters. See http://lucene.apache.org/core/4_10_3/analyzers-common/org/apache/lucene/analysis/core/StopAnalyzer.html. + public const string Stop = LexicalAnalyzerName.StopValue; + /// An analyzer that uses the whitespace tokenizer. See http://lucene.apache.org/core/4_10_3/analyzers-common/org/apache/lucene/analysis/core/WhitespaceAnalyzer.html. + public const string Whitespace = LexicalAnalyzerName.WhitespaceValue; + } +#pragma warning restore CA1034 // Nested types should not be visible +#endif + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs index 7a2aa45db8eb7..899db7fd762f3 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs @@ -10,6 +10,8 @@ namespace Azure.Search.Documents.Indexes.Models { public partial class SearchIndex { + private IList _fields; + [CodeGenMember("etag")] private string _etag; @@ -27,7 +29,7 @@ public SearchIndex(string name) Analyzers = new ChangeTrackingList(); CharFilters = new ChangeTrackingList(); - Fields = new ChangeTrackingList(); + Fields = new List(); ScoringProfiles = new ChangeTrackingList(); Suggesters = new ChangeTrackingList(); TokenFilters = new ChangeTrackingList(); @@ -50,7 +52,7 @@ public SearchIndex(string name, IEnumerable fields) Analyzers = new ChangeTrackingList(); CharFilters = new ChangeTrackingList(); - Fields = new ChangeTrackingList((Optional>)fields.ToArray()); + Fields = fields.ToList(); ScoringProfiles = new ChangeTrackingList(); Suggesters = new ChangeTrackingList(); TokenFilters = new ChangeTrackingList(); @@ -73,12 +75,103 @@ public SearchIndex(string name, IEnumerable fields) /// public IList CharFilters { get; } +#if EXPERIMENTAL_FIELDBUILDER + /// + /// Gets or sets the fields in the index. + /// Use to define fields based on a model class, + /// or , , and to manually define fields. + /// Index fields have many constraints that are not validated with until the index is created on the server. + /// + /// + /// You can create fields from a model class using : + /// + /// SearchIndex index = new SearchIndex("hotels") + /// { + /// Fields = new FieldBuilder().Build(typeof(Hotel)), + /// Suggesters = + /// { + /// // Suggest query terms from the hotelName field. + /// new SearchSuggester("sg", "hotelName") + /// } + /// }; + /// + /// For this reason, is settable. In scenarios when the model is not known or cannot be modified, you can + /// also create fields manually using helper classes: + /// + /// SearchIndex index = new SearchIndex("hotels") + /// { + /// Fields = + /// { + /// new SimpleField("hotelId", SearchFieldDataType.String) { IsKey = true, IsFilterable = true, IsSortable = true }, + /// new SearchableField("hotelName") { IsFilterable = true, IsSortable = true }, + /// new SearchableField("description") { AnalyzerName = LexicalAnalyzerName.EnLucene }, + /// new SearchableField("tags", collection: true) { IsFilterable = true, IsFacetable = true }, + /// new ComplexField("address") + /// { + /// Fields = + /// { + /// new SearchableField("streetAddress"), + /// new SearchableField("city") { IsFilterable = true, IsSortable = true, IsFacetable = true }, + /// new SearchableField("stateProvince") { IsFilterable = true, IsSortable = true, IsFacetable = true }, + /// new SearchableField("country") { IsFilterable = true, IsSortable = true, IsFacetable = true }, + /// new SearchableField("postalCode") { IsFilterable = true, IsSortable = true, IsFacetable = true } + /// } + /// } + /// }, + /// Suggesters = + /// { + /// // Suggest query terms from the hotelName field. + /// new SearchSuggester("sg", "hotelName") + /// } + /// }; + /// + /// +#else /// - /// Gets the fields in the index. - /// Use , , and for help defining valid indexes. + /// Gets or sets the fields in the index. + /// Use , , and to manually define fields. /// Index fields have many constraints that are not validated with until the index is created on the server. /// - public IList Fields { get; } + /// + /// You can create fields manually using helper classes: + /// + /// SearchIndex index = new SearchIndex("hotels") + /// { + /// Fields = + /// { + /// new SimpleField("hotelId", SearchFieldDataType.String) { IsKey = true, IsFilterable = true, IsSortable = true }, + /// new SearchableField("hotelName") { IsFilterable = true, IsSortable = true }, + /// new SearchableField("description") { AnalyzerName = LexicalAnalyzerName.EnLucene }, + /// new SearchableField("tags", collection: true) { IsFilterable = true, IsFacetable = true }, + /// new ComplexField("address") + /// { + /// Fields = + /// { + /// new SearchableField("streetAddress"), + /// new SearchableField("city") { IsFilterable = true, IsSortable = true, IsFacetable = true }, + /// new SearchableField("stateProvince") { IsFilterable = true, IsSortable = true, IsFacetable = true }, + /// new SearchableField("country") { IsFilterable = true, IsSortable = true, IsFacetable = true }, + /// new SearchableField("postalCode") { IsFilterable = true, IsSortable = true, IsFacetable = true } + /// } + /// } + /// }, + /// Suggesters = + /// { + /// // Suggest query terms from the hotelName field. + /// new SearchSuggester("sg", "hotelName") + /// } + /// }; + /// + /// +#endif + public IList Fields + { + get => _fields; + set + { + _fields = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Fields)} cannot be null. To clear values, call {nameof(Fields.Clear)}."); + } + } /// /// Gets the scoring profiles for the index. diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/SearchIndexClient.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SearchIndexClient.cs index c4850a77cb1b9..b260bd6b6058c 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/SearchIndexClient.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SearchIndexClient.cs @@ -8,6 +8,9 @@ using System.Threading.Tasks; using Azure.Core; using Azure.Core.Pipeline; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif using Azure.Search.Documents.Indexes.Models; namespace Azure.Search.Documents.Indexes diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs new file mode 100644 index 0000000000000..9f5e4fb860b09 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Indexes +{ + /// + /// Attributes a simple field using a primitive type or a collection of a primitive type. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class SearchableFieldAttribute : SimpleFieldAttribute, ISearchFieldAttribute + { + /// + /// Gets or sets the name of the language analyzer. This property cannot be set when either or are set. + /// Once the analyzer is chosen, it cannot be changed for the field in the index. + /// + /// String values from LexicalAnalyzerName, or the name of a custom analyzer previously uploaded. + public string AnalyzerName { get; set; } + + /// + /// Gets or sets the name of the language analyzer for searching. This property must be set together with , and cannot be set when is set. + /// This property cannot be set to the name of a language analyzer; use the property instead if you need a language analyzer. + /// Once the analyzer is chosen, it cannot be changed for the field in the index. + /// + /// String values from LexicalAnalyzerName, or the name of a custom analyzer previously uploaded. + public string SearchAnalyzerName { get; set; } + + /// + /// Gets or sets the name of the language analyzer for indexing. This property must be set together with , and cannot be set when is set. + /// This property cannot be set to the name of a language analyzer; use the property instead if you need a language analyzer. + /// Once the analyzer is chosen, it cannot be changed for the field in the index. + /// + /// String values from LexicalAnalyzerName, or the name of a custom analyzer previously uploaded. + public string IndexAnalyzerName { get; set; } + + /// + /// Gets or sets a list of names of synonym maps to associate with this field. + /// Currently, only one synonym map per field is supported. + /// + /// + /// Assigning a synonym map to a field ensures that query terms targeting that field are expanded at query-time using the rules in the synonym map. + /// This attribute can be changed on existing fields. + /// + public string[] SynonymMapNames { get; set; } + + /// + void ISearchFieldAttribute.SetField(SearchField field) + { + SetField(field); + + field.IsSearchable = true; + + if (SynonymMapNames != null) + { + field.SynonymMapNames.Clear(); + for (int i = 0; i < SynonymMapNames.Length; ++i) + { + field.SynonymMapNames.Add(SynonymMapNames[i]); + } + } + + if (AnalyzerName != null) + { + field.AnalyzerName = AnalyzerName; + } + + if (SearchAnalyzerName != null) + { + field.SearchAnalyzerName = SearchAnalyzerName; + } + + if (IndexAnalyzerName != null) + { + field.IndexAnalyzerName = IndexAnalyzerName; + } + } + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs new file mode 100644 index 0000000000000..d48c027de2b49 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Indexes +{ + /// + /// Attributes a simple field using a primitive type or a collection of a primitive type. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class SimpleFieldAttribute : Attribute, ISearchFieldAttribute + { + /// + /// Gets or sets whether the field is the key field. The default is false. + /// A must have exactly one key field of type . + /// + public bool IsKey { get; set; } + + /// + /// Gets or sets whether the field is returned in search results. The default is false. + /// A key field where is true must have this property set to false. + /// + public bool IsHidden { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be referenced in $filter queries. The default is false. + /// + /// + /// Filterable differs from searchable in how strings are handled. Fields of type or "Collection(DataType.String)" that are filterable do not undergo word-breaking, so comparisons are for exact matches only. + /// For example, if you set such a field f to "sunny day", $filter=f eq 'sunny' will find no matches, but $filter=f eq 'sunny day' will. + /// + public bool IsFilterable { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be retrieved in facet queries. The default is false. + /// + /// + /// Facets are used in presentation of search results that include hit counts by categories. + /// For example, in a search for digital cameras, facets might include branch, megapixels, price, etc. + /// + public bool IsFacetable { get; set; } + + /// + /// Gets or sets a value indicating whether to enable the field can be referenced in $orderby expressions. The default is false. + /// + /// + /// By default Azure Cognitive Search sorts results by score, but in many experiences users may want to sort by fields in the documents. + /// + public bool IsSortable { get; set; } + + /// + void ISearchFieldAttribute.SetField(SearchField field) => SetField(field); + + private protected void SetField(SearchField field) + { + field.IsKey = IsKey; + field.IsHidden = IsHidden; + field.IsFilterable = IsFilterable; + field.IsFacetable = IsFacetable; + field.IsSortable = IsSortable; + + // Only set the field if not already set (e.g. both SimpleFieldAttribute and SearchableFieldAttribute on same property). + if (!field.IsSearchable.HasValue) + { + // Use a SearchableFieldAttribute instead, which will override this property. + // The service will return Searchable == false for all non-searchable simple types. + field.IsSearchable = false; + } + } + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs index e2e9e564a12ee..6eaee9c46128c 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs @@ -8,6 +8,9 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type @@ -100,11 +103,11 @@ internal async Task SerializeAsync( using MemoryStream stream = new MemoryStream(); if (async) { - await serializer.SerializeAsync(stream, Document, typeof(T)).ConfigureAwait(false); + await serializer.SerializeAsync(stream, Document, typeof(T), cancellationToken).ConfigureAwait(false); } else { - serializer.Serialize(stream, Document, typeof(T)); + serializer.Serialize(stream, Document, typeof(T), cancellationToken); } json = stream.ToArray(); } diff --git a/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsBatch{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsBatch{T}.cs index c17b0f50dbe1f..4bf46ff31cbae 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsBatch{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsBatch{T}.cs @@ -8,6 +8,9 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type diff --git a/sdk/search/Azure.Search.Documents/src/Models/SearchResults{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/SearchResults{T}.cs index e1c9d515e9690..282f866651670 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/SearchResults{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/SearchResults{T}.cs @@ -6,13 +6,14 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; -using System.Runtime.Serialization; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Core.Pipeline; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type diff --git a/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs index d336f14ba449c..e34663d6ed324 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs @@ -1,15 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type @@ -108,8 +109,8 @@ internal static async Task> DeserializeAsync( { using Stream stream = element.ToStream(); T document = async ? - (T)await serializer.DeserializeAsync(stream, typeof(T)).ConfigureAwait(false) : - (T)serializer.Deserialize(stream, typeof(T)); + (T)await serializer.DeserializeAsync(stream, typeof(T), cancellationToken).ConfigureAwait(false) : + (T)serializer.Deserialize(stream, typeof(T), cancellationToken); result.Document = document; } else diff --git a/sdk/search/Azure.Search.Documents/src/Models/SearchSuggestion{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/SearchSuggestion{T}.cs index 0e0736c4d5d87..ecbd9bde44464 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/SearchSuggestion{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/SearchSuggestion{T}.cs @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Diagnostics; using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type @@ -87,8 +88,8 @@ internal static async Task> DeserializeAsync( { using Stream stream = element.ToStream(); T document = async ? - (T)await serializer.DeserializeAsync(stream, typeof(T)).ConfigureAwait(false) : - (T)serializer.Deserialize(stream, typeof(T)); + (T)await serializer.DeserializeAsync(stream, typeof(T), cancellationToken).ConfigureAwait(false) : + (T)serializer.Deserialize(stream, typeof(T), cancellationToken); suggestion.Document = document; } else diff --git a/sdk/search/Azure.Search.Documents/src/Models/SuggestResults{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/SuggestResults{T}.cs index 6acf2ac769109..d318fc54d9a32 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/SuggestResults{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/SuggestResults{T}.cs @@ -1,17 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type diff --git a/sdk/search/Azure.Search.Documents/src/SearchClient.cs b/sdk/search/Azure.Search.Documents/src/SearchClient.cs index 6b735f4620a07..d767825b1d310 100644 --- a/sdk/search/Azure.Search.Documents/src/SearchClient.cs +++ b/sdk/search/Azure.Search.Documents/src/SearchClient.cs @@ -9,6 +9,9 @@ using System.Threading.Tasks; using Azure.Core; using Azure.Core.Pipeline; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif using Azure.Search.Documents.Indexes; using Azure.Search.Documents.Models; @@ -183,7 +186,7 @@ public SearchClient( Version.ToVersionString()); } - #pragma warning disable CS1572 // Not all parameters will be used depending on feature flags +#pragma warning disable CS1573 // Not all parameters will be used depending on feature flags /// /// Initializes a new instance of the SearchClient class from a /// . @@ -217,7 +220,7 @@ internal SearchClient( HttpPipeline pipeline, ClientDiagnostics diagnostics, SearchClientOptions.ServiceVersion version) - #pragma warning restore CS1572 + #pragma warning restore CS1573 { Debug.Assert(endpoint != null); Debug.Assert(string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)); diff --git a/sdk/search/Azure.Search.Documents/src/SearchClientOptions.cs b/sdk/search/Azure.Search.Documents/src/SearchClientOptions.cs index 722071265a59f..6fe535fb94c71 100644 --- a/sdk/search/Azure.Search.Documents/src/SearchClientOptions.cs +++ b/sdk/search/Azure.Search.Documents/src/SearchClientOptions.cs @@ -5,6 +5,9 @@ using System.Diagnostics; using Azure.Core; using Azure.Core.Pipeline; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #pragma warning disable SA1402 // File may only contain a single type diff --git a/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs b/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs index 1257128375fb2..b786d0bc53d67 100644 --- a/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs +++ b/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs @@ -9,8 +9,10 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Azure.Core; using Azure.Core.Pipeline; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #endif @@ -362,8 +364,8 @@ public static async Task DeserializeAsync( else if (serializer != null) { return async ? - (T)await serializer.DeserializeAsync(json, typeof(T)).ConfigureAwait(false) : - (T)serializer.Deserialize(json, typeof(T)); + (T)await serializer.DeserializeAsync(json, typeof(T), cancellationToken).ConfigureAwait(false) : + (T)serializer.Deserialize(json, typeof(T), cancellationToken); } #endif else if (async) diff --git a/sdk/search/Azure.Search.Documents/tests/Azure.Search.Documents.Tests.csproj b/sdk/search/Azure.Search.Documents/tests/Azure.Search.Documents.Tests.csproj index a0a09a595296c..859ff58e05636 100644 --- a/sdk/search/Azure.Search.Documents/tests/Azure.Search.Documents.Tests.csproj +++ b/sdk/search/Azure.Search.Documents/tests/Azure.Search.Documents.Tests.csproj @@ -32,12 +32,22 @@ - + - + + + + + + + + + + + diff --git a/sdk/search/Azure.Search.Documents/tests/DocumentOperations/IndexingTests.cs b/sdk/search/Azure.Search.Documents/tests/DocumentOperations/IndexingTests.cs index adec6c88b9d07..90fa45b81efd4 100644 --- a/sdk/search/Azure.Search.Documents/tests/DocumentOperations/IndexingTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/DocumentOperations/IndexingTests.cs @@ -7,7 +7,9 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #endif diff --git a/sdk/search/Azure.Search.Documents/tests/DocumentOperations/SearchTests.cs b/sdk/search/Azure.Search.Documents/tests/DocumentOperations/SearchTests.cs index f807d18c162f8..0be5ee9280fc5 100644 --- a/sdk/search/Azure.Search.Documents/tests/DocumentOperations/SearchTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/DocumentOperations/SearchTests.cs @@ -6,7 +6,9 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Azure.Core; +#if EXPERIMENTAL_SERIALIZER +using Azure.Core.Serialization; +#endif #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #endif diff --git a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index 94d557b510ea0..b58dd4469f368 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -5,12 +5,15 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes; using Azure.Search.Documents.Indexes.Models; -using Azure.Search.Documents.Tests; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif using NUnit.Framework; using KeyFieldAttribute = System.ComponentModel.DataAnnotations.KeyAttribute; -namespace Azure.Search.Documents.Samples.Tests +namespace Azure.Search.Documents.Tests { public class FieldBuilderTests { @@ -65,7 +68,9 @@ from typeAndField in primitiveSubFieldTestData (SearchFieldDataType.Boolean, nameof(ReflectableModel.Flag)), (SearchFieldDataType.DateTimeOffset, nameof(ReflectableModel.Time)), (SearchFieldDataType.DateTimeOffset, nameof(ReflectableModel.TimeWithoutOffset)), +#if EXPERIMENTAL_SPATIAL (SearchFieldDataType.GeographyPoint, nameof(ReflectableModel.GeographyPoint)) +#endif }; (SearchFieldDataType, string)[] primitivePropertyTestData = @@ -119,11 +124,13 @@ public static IEnumerable CollectionTypeTestData (SearchFieldDataType.DateTimeOffset, nameof(ReflectableModel.DateTimeOffsetIEnumerable)), (SearchFieldDataType.DateTimeOffset, nameof(ReflectableModel.DateTimeOffsetList)), (SearchFieldDataType.DateTimeOffset, nameof(ReflectableModel.DateTimeOffsetICollection)), +#if EXPERIMENTAL_SPATIAL (SearchFieldDataType.GeographyPoint, nameof(ReflectableModel.GeographyPointArray)), (SearchFieldDataType.GeographyPoint, nameof(ReflectableModel.GeographyPointIList)), (SearchFieldDataType.GeographyPoint, nameof(ReflectableModel.GeographyPointIEnumerable)), (SearchFieldDataType.GeographyPoint, nameof(ReflectableModel.GeographyPointList)), (SearchFieldDataType.GeographyPoint, nameof(ReflectableModel.GeographyPointICollection)), +#endif (SearchFieldDataType.Complex, nameof(ReflectableModel.ComplexArray)), (SearchFieldDataType.Complex, nameof(ReflectableModel.ComplexIList)), (SearchFieldDataType.Complex, nameof(ReflectableModel.ComplexIEnumerable)), @@ -219,7 +226,16 @@ public void ReportsIsSearchableOnlyOnPropertiesWithIsSearchableAttribute(Type mo nameof(ReflectableModel.ComplexIEnumerable) + "/" + nameof(ReflectableComplexObject.Name), nameof(ReflectableModel.ComplexIEnumerable) + "/" + nameof(ReflectableComplexObject.Address) + "/" + nameof(ReflectableAddress.City), nameof(ReflectableModel.ComplexICollection) + "/" + nameof(ReflectableComplexObject.Name), - nameof(ReflectableModel.ComplexICollection) + "/" + nameof(ReflectableComplexObject.Address) + "/" + nameof(ReflectableAddress.City)); + nameof(ReflectableModel.ComplexICollection) + "/" + nameof(ReflectableComplexObject.Address) + "/" + nameof(ReflectableAddress.City), + + // The following fields were added since track 1, since setting the analyzers is only supported on searchable fields: + // https://docs.microsoft.com/rest/api/searchservice/create-index#-field-definitions- +#pragma warning disable SA1115 // Parameter should follow comma + nameof(ReflectableModel.TextWithAnalyzer), + nameof(ReflectableModel.TextWithSearchAnalyzer), + nameof(ReflectableModel.TextWithIndexAnalyzer) +#pragma warning restore SA1115 // Parameter should follow comma + ); } [TestCaseSource(nameof(TestModelTypeTestData))] @@ -266,7 +282,7 @@ public void IsFacetableOnlyOnPropertiesWithIsFacetableAttribute(Type modelType) } [TestCaseSource(nameof(TestModelTypeTestData))] - public void NotIsHiddenOnAllPropertiesExceptOnesWithIsRetrievableAttributeSetToFalse( + public void NotIsHiddenOnAllPropertiesExceptOnesWithIsHiddenSetToTrue( Type modelType) { // Was IsRetrievableOnAllPropertiesExceptOnesWithIsRetrievableAttributeSetToFalse @@ -346,15 +362,29 @@ public void NestedKeyAttributesAreIgnored() { var expectedFields = new SearchField[] { +#if EXPERIMENTAL_FIELDBUILDER + new SimpleField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, + new ComplexField(nameof(ModelWithNestedKey.Inner)) + { + Fields = + { + new SimpleField(nameof(InnerModelWithKey.InnerID), SearchFieldDataType.String), + + // Use a SimpleField helper since the property is attributed with a SimpleFieldAttribute with the same behavior of defaulting property values. + new SimpleField(nameof(InnerModelWithKey.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, + } + } +#else new SearchField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, new SearchField(nameof(ModelWithNestedKey.Inner), SearchFieldDataType.Complex) { Fields = { new SearchField(nameof(InnerModelWithKey.InnerID), SearchFieldDataType.String), - new SearchField(nameof(InnerModelWithKey.OtherField), SearchFieldDataType.Int32) { IsFilterable = true } + new SearchField(nameof(InnerModelWithKey.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, } } +#endif }; IList actualFields = BuildForType(typeof(ModelWithNestedKey)); @@ -367,14 +397,26 @@ public void PropertiesMarkedAsIgnoredAreIgnored() { var expectedFields = new SearchField[] { +#if EXPERIMENTAL_FIELDBUILDER + new SimpleField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, + new ComplexField(nameof(ModelWithNestedKey.Inner), collection: true) + { + Fields = + { + // Use a SimpleField helper since the property is attributed with a SimpleFieldAttribute with the same behavior of defaulting property values. + new SimpleField(nameof(InnerModelWithIgnoredProperties.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, + } + } +#else new SearchField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, new SearchField(nameof(ModelWithNestedKey.Inner), SearchFieldDataType.Collection(SearchFieldDataType.Complex)) { Fields = { - new SearchField(nameof(InnerModelWithIgnoredProperties.OtherField), SearchFieldDataType.Int32) { IsFilterable = true } + new SearchField(nameof(InnerModelWithIgnoredProperties.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, } } +#endif }; IList actualFields = BuildForType(typeof(ModelWithIgnoredProperties)); @@ -388,16 +430,17 @@ public void PropertiesMarkedAsIgnoredAreIgnored() [TestCase(typeof(ModelWithUnsupportedCollectionType), nameof(ModelWithUnsupportedCollectionType.Buffer))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(Type modelType, string invalidPropertyName) { - ArgumentException e = Assert.Throws(() => FieldBuilder.BuildForType(modelType)); + ArgumentException e = Assert.Throws(() => BuildForType(modelType)); string expectedErrorMessage = $"Property '{invalidPropertyName}' is of type '{modelType.GetProperty(invalidPropertyName).PropertyType}', " + "which does not map to an Azure Search data type. Please use a supported data type or mark the property with " + - "[JsonIgnore] or [FieldBuilderIgnore] and define the field by creating a SearchField object." + + "[FieldBuilderIgnore] and define the field by creating a SearchField object. See https://aka.ms/azsdk/net/search/fieldbuilder for more information." + $"{Environment.NewLine}Parameter name: {nameof(modelType)}"; Assert.AreEqual(nameof(modelType), e.ParamName); Assert.AreEqual(expectedErrorMessage, e.Message); + Assert.AreEqual("https://aka.ms/azsdk/net/search/fieldbuilder", e.HelpLink); } [TestCase(typeof(int))] @@ -410,9 +453,11 @@ public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(T [TestCase(typeof(IList))] [TestCase(typeof(List))] [TestCase(typeof(ICollection))] + [TestCase(typeof(decimal))] + [TestCase(typeof(float))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedTypes(Type modelType) { - ArgumentException e = Assert.Throws(() => FieldBuilder.BuildForType(modelType)); + ArgumentException e = Assert.Throws(() => BuildForType(modelType)); string expectedErrorMessage = $"Type '{modelType}' does not have properties which map to fields of an Azure Search index. Please use a " + @@ -429,7 +474,11 @@ from type in modelTypes from tuple in testData select (type, tuple.dataType, tuple.fieldName); +#if EXPERIMENTAL_FIELDBUILDER + private static IList BuildForType(Type modelType) => new FieldBuilder().Build(modelType); +#else private static IList BuildForType(Type modelType) => FieldBuilder.BuildForType(modelType); +#endif private enum Direction { @@ -498,7 +547,11 @@ private class ModelWithEnum [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)] +#else [IsFilterable, IsSearchable, IsSortable, IsFacetable] +#endif public Direction Direction { get; set; } } @@ -507,7 +560,11 @@ private class ModelWithUnsupportedPrimitiveType [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public decimal Price { get; set; } } @@ -516,7 +573,11 @@ private class ModelWithUnsupportedEnumerableType [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public IEnumerable Buffer { get; set; } } @@ -525,7 +586,11 @@ private class ModelWithUnsupportedCollectionType [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public ICollection Buffer { get; set; } } @@ -534,7 +599,11 @@ private class InnerModelWithKey [KeyField] public string InnerID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public int OtherField { get; set; } } @@ -548,7 +617,11 @@ private class ModelWithNestedKey private class InnerModelWithIgnoredProperties { +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public int OtherField { get; set; } [JsonIgnore] diff --git a/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs b/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs new file mode 100644 index 0000000000000..fdb2c368789b6 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Reflection; +using Azure.Search.Documents.Indexes.Models; +using NUnit.Framework; + +namespace Azure.Search.Documents.Tests.Models +{ + public class LexicalAnalyzerNameTests + { +#if EXPERIMENTAL_FIELDBUILDER + [Test] + public void PropertiesEqualConstantFields() + { + var properties = typeof(LexicalAnalyzerName) + .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(p => p.PropertyType == p.DeclaringType) + .Select(p => new { p.Name, Value = p.GetValue(null)?.ToString() }); + + var fields = typeof(LexicalAnalyzerName.Values) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(p => p.FieldType == typeof(string)) + .Select(p => new { p.Name, Value = (string)p.GetRawConstantValue() }); + + // Note: tested that declaring an extra property or field does fail the assert. + CollectionAssert.AreEquivalent(properties, fields); + } +#endif + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs index c9645131dff12..65473f5a7c54f 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs @@ -5,11 +5,20 @@ #pragma warning disable SA1649 // File name should match first type name // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. -namespace Azure.Search.Documents.Samples.Tests +using Azure.Search.Documents.Indexes; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif + +namespace Azure.Search.Documents.Tests { public class RecursiveModel { +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public int Data { get; set; } // This is to test that FieldBuilder gracefully fails on recursive models. @@ -18,7 +27,11 @@ public class RecursiveModel public class OtherRecursiveModel { +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true, IsFacetable = true)] +#else [IsFilterable, IsFacetable] +#endif public double Data { get; set; } public RecursiveModel RecursiveReference { get; set; } diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs index e27fa250462b6..14456d6be71f5 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs @@ -1,31 +1,43 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif using KeyFieldAttribute = System.ComponentModel.DataAnnotations.KeyAttribute; #pragma warning disable SA1402 // File may only contain a single type #pragma warning disable SA1649 // File name should match first type name // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. -namespace Azure.Search.Documents.Samples.Tests +namespace Azure.Search.Documents.Tests { +#if !EXPERIMENTAL_FIELDBUILDER [SerializePropertyNamesAsCamelCase] +#endif public class ReflectableInnerCamelCaseModel { + [JsonPropertyName("name")] public string Name { get; set; } } +#if !EXPERIMENTAL_FIELDBUILDER [SerializePropertyNamesAsCamelCase] +#endif public class ReflectableCamelCaseModel { [KeyField] + [JsonPropertyName("id")] public int Id { get; set; } + [JsonPropertyName("myProperty")] public string MyProperty { get; set; } + [JsonPropertyName("inner")] public ReflectableInnerCamelCaseModel Inner { get; set; } + [JsonPropertyName("innerCollection")] public ReflectableInnerCamelCaseModel[] InnerCollection { get; set; } } } diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs index 3c3848e2721be..01c607037f858 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs @@ -4,6 +4,11 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #else @@ -15,27 +20,54 @@ #pragma warning disable SA1649 // File name should match first type name // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. -namespace Azure.Search.Documents.Samples.Tests +namespace Azure.Search.Documents.Tests { public class ReflectableAddress { +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField] +#else [IsSearchable] +#endif public string City { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true, IsFacetable = true)] +#else [IsFilterable, IsFacetable] +#endif public string Country { get; set; } } public class ReflectableComplexObject { +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#else [IsSearchable] [Analyzer("en.microsoft")] +#endif public string Name { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public int Rating { get; set; } // Ensure that leaf-field-specific attributes are ignored by FieldBuilder on complex fields. +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField( + IsFilterable = true, + IsSortable = true, + IsFacetable = true, + IsHidden = true, + AnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + SynonymMapNames = new[] { "myMap" })] +#else [IsSearchable] [IsFilterable] [IsSortable] @@ -45,6 +77,7 @@ public class ReflectableComplexObject [IndexAnalyzer("zh-Hant.lucene")] [SearchAnalyzer("zh-Hant.lucene")] [SynonymMaps("myMap")] +#endif public ReflectableAddress Address { get; set; } } @@ -63,37 +96,80 @@ public class ReflectableModel public DateTime TimeWithoutOffset { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(SynonymMapNames = new[] { "myMap" })] +#else [IsSearchable] [SynonymMaps("myMap")] +#endif public string Text { get; set; } public string UnsearchableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField] +#else [IsSearchable] +#endif public string MoreText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public string FilterableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsSortable = true)] +#else [IsSortable] +#endif public string SortableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFacetable = true)] +#else [IsFacetable] +#endif public string FacetableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsHidden = true)] +#else [IsRetrievable(false)] +#endif public string IrretrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsHidden = false)] +#else [IsRetrievable(true)] +#endif public string ExplicitlyRetrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#else + [IsSearchable] [Analyzer("en.microsoft")] +#endif public string TextWithAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.Values.EsLucene)] +#else + [IsSearchable] [SearchAnalyzer("es.lucene")] +#endif public string TextWithSearchAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.Values.Whitespace)] +#else + [IsSearchable] [IndexAnalyzer("whitespace")] +#endif public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } @@ -209,7 +285,11 @@ public class ReflectableModel public ICollection ComplexICollection { get; set; } [JsonIgnore] +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsHidden = true)] +#else [IsRetrievable(false)] +#endif #pragma warning disable IDE1006 // Naming Styles public RecordEnum recordEnum { get; set; } #pragma warning restore IDE1006 // Naming Styles diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs index 95aa2723e4cc2..f15b9ea23436c 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs @@ -1,31 +1,43 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif using KeyFieldAttribute = System.ComponentModel.DataAnnotations.KeyAttribute; #pragma warning disable SA1402 // File may only contain a single type #pragma warning disable SA1649 // File name should match first type name // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. -namespace Azure.Search.Documents.Samples.Tests +namespace Azure.Search.Documents.Tests { +#if !EXPERIMENTAL_FIELDBUILDER [SerializePropertyNamesAsCamelCase] +#endif public struct ReflectableInnerStructCamelCaseModel { + [JsonPropertyName("name")] public string Name { get; set; } } +#if !EXPERIMENTAL_FIELDBUILDER [SerializePropertyNamesAsCamelCase] +#endif public struct ReflectableStructCamelCaseModel { [KeyField] + [JsonPropertyName("id")] public int Id { get; set; } + [JsonPropertyName("myProperty")] public string MyProperty { get; set; } + [JsonPropertyName("inner")] public ReflectableInnerStructCamelCaseModel Inner { get; set; } + [JsonPropertyName("innerCollection")] public ReflectableInnerStructCamelCaseModel[] InnerCollection { get; set; } } } diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs index e9170515cf824..8655a831c339e 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs @@ -4,6 +4,11 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #else @@ -15,27 +20,54 @@ #pragma warning disable SA1649 // File name should match first type name // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. -namespace Azure.Search.Documents.Samples.Tests +namespace Azure.Search.Documents.Tests { public struct ReflectableAddressStruct { +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField] +#else [IsSearchable] +#endif public string City { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true, IsFacetable = true)] +#else [IsFilterable, IsFacetable] +#endif public string Country { get; set; } } public struct ReflectableComplexStruct { +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#else [IsSearchable] [Analyzer("en.microsoft")] +#endif public string Name { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public int Rating { get; set; } // Ensure that leaf-field-specific attributes are ignored by FieldBuilder on complex fields. +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField( + IsFilterable = true, + IsSortable = true, + IsFacetable = true, + IsHidden = true, + AnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + SynonymMapNames = new[] { "myMap" })] +#else [IsSearchable] [IsFilterable] [IsSortable] @@ -45,6 +77,7 @@ public struct ReflectableComplexStruct [IndexAnalyzer("zh-Hant.lucene")] [SearchAnalyzer("zh-Hant.lucene")] [SynonymMaps("myMap")] +#endif public ReflectableAddressStruct Address { get; set; } } @@ -63,37 +96,80 @@ public struct ReflectableStructModel public DateTime TimeWithoutOffset { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(SynonymMapNames = new[] { "myMap" })] +#else [IsSearchable] [SynonymMaps("myMap")] +#endif public string Text { get; set; } public string UnsearchableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField] +#else [IsSearchable] +#endif public string MoreText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFilterable = true)] +#else [IsFilterable] +#endif public string FilterableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsSortable = true)] +#else [IsSortable] +#endif public string SortableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsFacetable = true)] +#else [IsFacetable] +#endif public string FacetableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsHidden = true)] +#else [IsRetrievable(false)] +#endif public string IrretrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsHidden = false)] +#else [IsRetrievable(true)] +#endif public string ExplicitlyRetrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#else + [IsSearchable] [Analyzer("en.microsoft")] +#endif public string TextWithAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.Values.EsLucene)] +#else + [IsSearchable] [SearchAnalyzer("es.lucene")] +#endif public string TextWithSearchAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.Values.Whitespace)] +#else + [IsSearchable] [IndexAnalyzer("whitespace")] +#endif public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } @@ -209,7 +285,11 @@ public struct ReflectableStructModel public ICollection ComplexICollection { get; set; } [JsonIgnore] +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsHidden = true)] +#else [IsRetrievable(false)] +#endif #pragma warning disable IDE1006 // Naming Styles public RecordEnum recordEnum { get; set; } #pragma warning restore IDE1006 // Naming Styles diff --git a/sdk/search/Azure.Search.Documents/tests/Models/SearchIndexTests.cs b/sdk/search/Azure.Search.Documents/tests/Models/SearchIndexTests.cs index e9a2861e2a083..477cb4087f73a 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/SearchIndexTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/SearchIndexTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using Azure.Search.Documents.Indexes.Models; using NUnit.Framework; @@ -16,5 +17,34 @@ public void ParsesETag(string value, string expected) SearchIndex sut = new SearchIndex(null, new SearchField[0], null, null, null, null, null, null, null, null, null, null, value); Assert.AreEqual(expected, sut.ETag?.ToString()); } + + [Test] + public void SettingFieldsNullThrows() + { + SearchIndex sut = new SearchIndex("test"); + + ArgumentNullException ex = Assert.Throws(() => sut.Fields = null); + Assert.AreEqual("value", ex.ParamName); + } + + [Test] + public void SettingFieldsOverwrites() + { + SearchIndex sut = new SearchIndex("test", new SearchField[] + { + new SimpleField("a", SearchFieldDataType.String) { IsKey = true }, + new SearchableField("b") { IsSortable = true }, + }); + + SearchField[] fields = new SearchField[] + { + new SimpleField("id", SearchFieldDataType.String) { IsKey = true }, + new SearchableField("name") { IsSortable = true }, + }; + + sut.Fields = fields; + + Assert.That(sut.Fields, Is.EqualTo(fields).Using(SearchFieldComparer.SharedFieldsCollection)); + } } } diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs index dee1ba235a546..2eb319bc129df 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs @@ -129,9 +129,15 @@ public async Task CreateAndQuery() public class Hotel { [JsonPropertyName("hotelId")] +#if EXPERIMENTAL_FIELDBUILDER + [SimpleField(IsKey = true, IsFilterable = true, IsSortable = true)] +#endif public string Id { get; set; } [JsonPropertyName("hotelName")] +#if EXPERIMENTAL_FIELDBUILDER + [SearchableField(IsFilterable = true, IsSortable = true)] +#endif public string Name { get; set; } } #endregion Snippet:Azure_Search_Tests_Samples_Readme_StaticType @@ -184,6 +190,7 @@ public async Task Options() #endregion Snippet:Azure_Search_Tests_Samples_Readme_Options } +#if EXPERIMENTAL_FIELDBUILDER // This won't condition the README.md file, which will require manual effort. [Test] [SyncOnly] public async Task CreateIndex() @@ -201,7 +208,37 @@ public async Task CreateIndex() SearchIndexClient client = new SearchIndexClient(endpoint, credential); /*@@*/ client = resources.GetIndexClient(); - // Create the index + // Create the index using FieldBuilder. + #region Snippet:Azure_Search_Tests_Samples_Readme_CreateIndex_New_SearchIndex + //@@SearchIndex index = new SearchIndex("hotels") + /*@@*/ SearchIndex index = new SearchIndex(Recording.Random.GetName()) + { + Fields = new FieldBuilder().Build(typeof(Hotel)), + Suggesters = + { + // Suggest query terms from the hotelName field. + new SearchSuggester("sg", "hotelName") + } + }; + #endregion Snippet:Azure_Search_Tests_Samples_Readme_CreateIndex_New_SearchIndex + + client.CreateIndex(index); + #endregion Snippet:Azure_Search_Tests_Samples_Readme_CreateIndex + + resources.IndexName = index.Name; + } +#endif + + [Test] + [SyncOnly] + public async Task CreateManualIndex() + { + await using SearchResources resources = SearchResources.CreateWithNoIndexes(this); + SearchIndexClient client = resources.GetIndexClient(); + + #region Snippet:Azure_Search_Tests_Samples_Readme_CreateManualIndex + // Create the index using field definitions. + #region Snippet:Azure_Search_Tests_Samples_Readme_CreateManualIndex_New_SearchIndex //@@SearchIndex index = new SearchIndex("hotels") /*@@*/ SearchIndex index = new SearchIndex(Recording.Random.GetName()) { @@ -225,13 +262,16 @@ public async Task CreateIndex() }, Suggesters = { - // Suggest query terms from both the hotelName and description fields. - new SearchSuggester("sg", "hotelName", "description") + // Suggest query terms from the hotelName field. + new SearchSuggester("sg", "hotelName") } }; + #endregion Snippet:Azure_Search_Tests_Samples_Readme_CreateManualIndex_New_SearchIndex client.CreateIndex(index); - #endregion Snippet:Azure_Search_Tests_Samples_Readme_CreateIndex + #endregion Snippet:Azure_Search_Tests_Samples_Readme_CreateManualIndex + + resources.IndexName = index.Name; } [Test] diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/HelloWorld.Data.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Sample01_HelloWorld.Data.cs similarity index 100% rename from sdk/search/Azure.Search.Documents/tests/Samples/HelloWorld.Data.cs rename to sdk/search/Azure.Search.Documents/tests/Samples/Sample01_HelloWorld.Data.cs diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/HelloWorld.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Sample01_HelloWorld.cs similarity index 100% rename from sdk/search/Azure.Search.Documents/tests/Samples/HelloWorld.cs rename to sdk/search/Azure.Search.Documents/tests/Samples/Sample01_HelloWorld.cs diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilderIgnore.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilderIgnore.cs new file mode 100644 index 0000000000000..49df2db12d6bb --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilderIgnore.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Azure.Core.Serialization; +using Azure.Core.TestFramework; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +using NUnit.Framework; + +namespace Azure.Search.Documents.Tests.Samples +{ +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + public class FieldBuilderIgnore : SearchTestBase + { + public FieldBuilderIgnore(bool async, SearchClientOptions.ServiceVersion serviceVersion) + : base(async, serviceVersion, null /* RecordedTestMode.Record /* to re-record */) + { + } + + [Test] + [SyncOnly] + public async Task CreateIndex() + { + await using SearchResources resources = SearchResources.CreateWithNoIndexes(this); + Environment.SetEnvironmentVariable("SEARCH_ENDPOINT", resources.Endpoint.ToString()); + Environment.SetEnvironmentVariable("SEARCH_API_KEY", resources.PrimaryApiKey); + + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_CreateIndex + Uri endpoint = new Uri(Environment.GetEnvironmentVariable("SEARCH_ENDPOINT")); + string key = Environment.GetEnvironmentVariable("SEARCH_API_KEY"); + + // Define client options to use camelCase when serializing property names. + SearchClientOptions options = new SearchClientOptions + { + Serializer = new JsonObjectSerializer( + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }) + }; + + // Create a service client. + AzureKeyCredential credential = new AzureKeyCredential(key); + SearchIndexClient indexClient = new SearchIndexClient(endpoint, credential, options); + /*@@*/ indexClient = resources.GetIndexClient(options); + + // Create the FieldBuilder using the same serializer. + FieldBuilder fieldBuilder = new FieldBuilder + { + Serializer = options.Serializer + }; + + // Create the index using FieldBuilder. + //@@SearchIndex index = new SearchIndex("movies") + /*@@*/ SearchIndex index = new SearchIndex(Recording.Random.GetName()) + { + Fields = fieldBuilder.Build(typeof(Movie)), + Suggesters = + { + // Suggest query terms from the "name" field. + new SearchSuggester("n", "name") + } + }; + + // Define the "genre" field as a string. + SearchableField genreField = new SearchableField("genre") + { + AnalyzerName = LexicalAnalyzerName.Values.EnLucene, + IsFacetable = true, + IsFilterable = true + }; + index.Fields.Add(genreField); + + // Create the index. + indexClient.CreateIndex(index); + #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_CreateIndex + + // Make sure the index is removed. + resources.IndexName = index.Name; + + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_UploadDocument + Movie movie = new Movie + { + Id = Guid.NewGuid().ToString("n"), + Name = "The Lord of the Rings: The Return of the King", + Genre = MovieGenre.Fantasy, + Year = 2003, + Rating = 9.1 + }; + + // Add a movie to the index. + SearchClient searchClient = indexClient.GetSearchClient(index.Name); + searchClient.UploadDocuments(new[] { movie }); + #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_UploadDocument + } + } + + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_Types + public class Movie + { + [SimpleField(IsKey = true)] + public string Id { get; set; } + + [SearchableField(IsSortable = true, AnalyzerName = LexicalAnalyzerName.Values.EnLucene)] + public string Name { get; set; } + + [FieldBuilderIgnore] + [JsonConverter(typeof(JsonStringEnumConverter))] + public MovieGenre Genre { get; set; } + + [SimpleField(IsFacetable = true, IsFilterable = true, IsSortable = true)] + public int Year { get; set; } + + [SimpleField(IsFilterable = true, IsSortable = true)] + public double Rating { get; set; } + } + + public enum MovieGenre + { + Unknown, + Action, + Comedy, + Drama, + Fantasy, + Horror, + Romance, + SciFi, + } + #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_Types +#pragma warning restore SA1649 // File name should match first type name +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/sdk/search/Azure.Search.Documents/tests/SearchableFieldAttributeTests.cs b/sdk/search/Azure.Search.Documents/tests/SearchableFieldAttributeTests.cs new file mode 100644 index 0000000000000..a201876386bee --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/SearchableFieldAttributeTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +using NUnit.Framework; + +namespace Azure.Search.Documents.Tests +{ + public class SearchableFieldAttributeTests + { + [Test] + [Parallelizable] + public void CreatesEquivalentField( + [Values] bool key, + [Values] bool hidden, + [Values] bool filterable, + [Values] bool facetable, + [Values] bool sortable, + [Values(null, "AnalyzerName")] string analyzerName, + [Values(null, "SearchAnalyzerName")] string searchAnalyzerName, + [Values(null, "IndexAnalyzerName")] string indexAnalyzerName, + [Values(null, new[] { "synonynMapName" })] string[] synonymMapNames) + { + SearchableFieldAttribute sut = new SearchableFieldAttribute + { + IsKey = key, + IsHidden = hidden, + IsFilterable = filterable, + IsFacetable = facetable, + IsSortable = sortable, + }; + + if (analyzerName != null) + { + sut.AnalyzerName = analyzerName; + } + + if (searchAnalyzerName != null) + { + sut.SearchAnalyzerName = searchAnalyzerName; + } + + if (indexAnalyzerName != null) + { + sut.IndexAnalyzerName = indexAnalyzerName; + } + + if (synonymMapNames != null) + { + sut.SynonymMapNames = synonymMapNames; + } + + SearchField field = new SearchField("test", SearchFieldDataType.String); + ((ISearchFieldAttribute)sut).SetField(field); + + Assert.AreEqual("test", field.Name); + Assert.AreEqual(SearchFieldDataType.String, field.Type); + Assert.AreEqual(key, field.IsKey ?? false); + Assert.AreEqual(hidden, field.IsHidden ?? false); + Assert.AreEqual(filterable, field.IsFilterable ?? false); + Assert.AreEqual(facetable, field.IsFacetable ?? false); + Assert.IsTrue(field.IsSearchable); + Assert.AreEqual(sortable, field.IsSortable ?? false); + Assert.AreEqual(analyzerName, field.AnalyzerName?.ToString()); + Assert.AreEqual(searchAnalyzerName, field.SearchAnalyzerName?.ToString()); + Assert.AreEqual(indexAnalyzerName, field.IndexAnalyzerName?.ToString()); + Assert.AreEqual(synonymMapNames ?? Array.Empty(), field.SynonymMapNames); + } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderIgnore/CreateIndex.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderIgnore/CreateIndex.json new file mode 100644 index 0000000000000..f240844f405e0 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderIgnore/CreateIndex.json @@ -0,0 +1,292 @@ +{ + "Entries": [ + { + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes?api-version=2020-06-30", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json; odata.metadata=minimal", + "api-key": "Sanitized", + "Content-Length": "855", + "Content-Type": "application/json", + "User-Agent": [ + "azsdk-net-Search.Documents/11.1.0-dev.20200807.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" + ], + "x-ms-client-request-id": "6ee3306ff1b8a71feee3bfcc41bf1926", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "name": "dtnismrn", + "fields": [ + { + "name": "id", + "type": "Edm.String", + "key": true, + "retrievable": true, + "searchable": false, + "filterable": false, + "sortable": false, + "facetable": false + }, + { + "name": "name", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": false, + "sortable": true, + "facetable": false, + "analyzer": "en.lucene" + }, + { + "name": "year", + "type": "Edm.Int32", + "key": false, + "retrievable": true, + "searchable": false, + "filterable": true, + "sortable": true, + "facetable": true + }, + { + "name": "rating", + "type": "Edm.Double", + "key": false, + "retrievable": true, + "searchable": false, + "filterable": true, + "sortable": true, + "facetable": false + }, + { + "name": "genre", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": false, + "facetable": true, + "analyzer": "en.lucene" + } + ], + "suggesters": [ + { + "name": "n", + "searchMode": "analyzingInfixMatching", + "sourceFields": [ + "name" + ] + } + ] + }, + "StatusCode": 201, + "ResponseHeaders": { + "Cache-Control": "no-cache", + "client-request-id": "6ee3306f-f1b8-a71f-eee3-bfcc41bf1926", + "Content-Length": "1579", + "Content-Type": "application/json; odata.metadata=minimal", + "Date": "Sat, 08 Aug 2020 02:08:49 GMT", + "elapsed-time": "687", + "ETag": "W/\u00220x8D83B3FFD36EB80\u0022", + "Expires": "-1", + "Location": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027dtnismrn\u0027)?api-version=2020-06-30", + "OData-Version": "4.0", + "Pragma": "no-cache", + "Preference-Applied": "odata.include-annotations=\u0022*\u0022", + "request-id": "6ee3306f-f1b8-a71f-eee3-bfcc41bf1926", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "x-ms-client-request-id": "6ee3306f-f1b8-a71f-eee3-bfcc41bf1926" + }, + "ResponseBody": { + "@odata.context": "https://azs-net-heathsrchtst.search.windows.net/$metadata#indexes/$entity", + "@odata.etag": "\u00220x8D83B3FFD36EB80\u0022", + "name": "dtnismrn", + "defaultScoringProfile": null, + "fields": [ + { + "name": "id", + "type": "Edm.String", + "searchable": false, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": true, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "name", + "type": "Edm.String", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": true, + "facetable": false, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": "en.lucene", + "synonymMaps": [] + }, + { + "name": "year", + "type": "Edm.Int32", + "searchable": false, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "rating", + "type": "Edm.Double", + "searchable": false, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": false, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "genre", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": false, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": "en.lucene", + "synonymMaps": [] + } + ], + "scoringProfiles": [], + "corsOptions": null, + "suggesters": [ + { + "name": "n", + "searchMode": "analyzingInfixMatching", + "sourceFields": [ + "name" + ] + } + ], + "analyzers": [], + "tokenizers": [], + "tokenFilters": [], + "charFilters": [], + "encryptionKey": null, + "similarity": { + "@odata.type": "#Microsoft.Azure.Search.BM25Similarity", + "k1": null, + "b": null + } + } + }, + { + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027dtnismrn\u0027)/docs/search.index?api-version=2020-06-30", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json; odata.metadata=none", + "api-key": "Sanitized", + "Content-Length": "192", + "Content-Type": "application/json", + "User-Agent": [ + "azsdk-net-Search.Documents/11.1.0-dev.20200807.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" + ], + "x-ms-client-request-id": "20d307503030e75cf39e6616b9d0f81f", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "value": [ + { + "@search.action": "upload", + "id": "70860962223742448d57a993c8db6fc1", + "name": "The Lord of the Rings: The Return of the King", + "genre": "Fantasy", + "year": 2003, + "rating": 9.0999999999999996 + } + ] + }, + "StatusCode": 200, + "ResponseHeaders": { + "Cache-Control": "no-cache", + "client-request-id": "20d30750-3030-e75c-f39e-6616b9d0f81f", + "Content-Length": "105", + "Content-Type": "application/json; odata.metadata=none", + "Date": "Sat, 08 Aug 2020 02:08:49 GMT", + "elapsed-time": "64", + "Expires": "-1", + "OData-Version": "4.0", + "Pragma": "no-cache", + "Preference-Applied": "odata.include-annotations=\u0022*\u0022", + "request-id": "20d30750-3030-e75c-f39e-6616b9d0f81f", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "x-ms-client-request-id": "20d30750-3030-e75c-f39e-6616b9d0f81f" + }, + "ResponseBody": { + "value": [ + { + "key": "70860962223742448d57a993c8db6fc1", + "status": true, + "errorMessage": null, + "statusCode": 201 + } + ] + } + }, + { + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027dtnismrn\u0027)?api-version=2020-06-30", + "RequestMethod": "DELETE", + "RequestHeaders": { + "Accept": "application/json; odata.metadata=minimal", + "api-key": "Sanitized", + "traceparent": "00-b85565c7e8851a44a45ec15dfb9c3619-4a68a2b3c740a149-00", + "User-Agent": [ + "azsdk-net-Search.Documents/11.1.0-dev.20200807.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" + ], + "x-ms-client-request-id": "b305b470010f19d3bdb938b227febbc5", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": null, + "StatusCode": 204, + "ResponseHeaders": { + "Cache-Control": "no-cache", + "client-request-id": "b305b470-010f-19d3-bdb9-38b227febbc5", + "Date": "Sat, 08 Aug 2020 02:08:50 GMT", + "elapsed-time": "831", + "Expires": "-1", + "Pragma": "no-cache", + "request-id": "b305b470-010f-19d3-bdb9-38b227febbc5", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "x-ms-client-request-id": "b305b470-010f-19d3-bdb9-38b227febbc5" + }, + "ResponseBody": [] + } + ], + "Variables": { + "RandomSeed": "683846869", + "SearchIndexName": "dtnismrn", + "SEARCH_ADMIN_API_KEY": "Sanitized", + "SEARCH_SERVICE_NAME": "azs-net-heathsrchtst" + } +} \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializer.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializer.json index e0d145a13914e..e3ef1b1d0dc9e 100644 --- a/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializer.json +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializer.json @@ -1,17 +1,17 @@ { "Entries": [ { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027nbledbxk\u0027)/docs/search.index?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027xaapwvgv\u0027)/docs/search.index?api-version=2020-06-30", "RequestMethod": "POST", "RequestHeaders": { "Accept": "application/json; odata.metadata=none", "api-key": "Sanitized", "Content-Length": "1723", "Content-Type": "application/json", - "traceparent": "00-ef7d206a84b923449da6cbb7f4f93f86-116eabd16d73e94b-00", + "traceparent": "00-3d9accc566eb564eac9cd95dd7e035c9-467289e29c3ad04a-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "c8e9b9c67f63aadb0e6bf6eb18bc47be", "x-ms-return-client-request-id": "true" @@ -84,8 +84,8 @@ "client-request-id": "c8e9b9c6-7f63-aadb-0e6b-f6eb18bc47be", "Content-Length": "74", "Content-Type": "application/json; odata.metadata=none", - "Date": "Mon, 08 Jun 2020 14:43:38 GMT", - "elapsed-time": "107", + "Date": "Sat, 01 Aug 2020 13:21:30 GMT", + "elapsed-time": "206", "Expires": "-1", "OData-Version": "4.0", "Pragma": "no-cache", @@ -106,15 +106,15 @@ } }, { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027nbledbxk\u0027)/docs(\u00271\u0027)?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027xaapwvgv\u0027)/docs(\u00271\u0027)?api-version=2020-06-30", "RequestMethod": "GET", "RequestHeaders": { "Accept": "application/json; odata.metadata=none", "api-key": "Sanitized", - "traceparent": "00-1bf62192bf74674585123ef789a25c60-c311844218f05145-00", + "traceparent": "00-0aa6c79ba5c5ef4ea855d2cb6b201313-a816e21376630a47-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "49f86160a889777af9bc347f99fadf50", "x-ms-return-client-request-id": "true" @@ -126,8 +126,8 @@ "client-request-id": "49f86160-a889-777a-f9bc-347f99fadf50", "Content-Length": "1664", "Content-Type": "application/json; odata.metadata=none", - "Date": "Mon, 08 Jun 2020 14:43:40 GMT", - "elapsed-time": "14", + "Date": "Sat, 01 Aug 2020 13:21:33 GMT", + "elapsed-time": "24", "Expires": "-1", "OData-Version": "4.0", "Pragma": "no-cache", @@ -139,15 +139,15 @@ "ResponseBody": "{\u0022hotelId\u0022:\u00221\u0022,\u0022hotelName\u0022:\u0022Secret Point Motel\u0022,\u0022description\u0022:\u0022The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time\u0027s Square and the historic centre of the city, as well as other places of interest that make New York one of America\u0027s most attractive and cosmopolitan cities.\u0022,\u0022descriptionFr\u0022:\u0022L\u0027h\\u00f4tel est id\\u00e9alement situ\\u00e9 sur la principale art\\u00e8re commerciale de la ville en plein c\\u0153ur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d\u0027autres lieux d\u0027int\\u00e9r\\u00eat qui font de New York l\u0027une des villes les plus attractives et cosmopolites de l\u0027Am\\u00e9rique.\u0022,\u0022category\u0022:\u0022Boutique\u0022,\u0022tags\u0022:[\u0022pool\u0022,\u0022air conditioning\u0022,\u0022concierge\u0022],\u0022parkingIncluded\u0022:false,\u0022smokingAllowed\u0022:true,\u0022lastRenovationDate\u0022:\u00221970-01-18T05:00:00Z\u0022,\u0022rating\u0022:4,\u0022location\u0022:{\u0022type\u0022:\u0022Point\u0022,\u0022coordinates\u0022:[-73.975403,40.760586],\u0022crs\u0022:{\u0022type\u0022:\u0022name\u0022,\u0022properties\u0022:{\u0022name\u0022:\u0022EPSG:4326\u0022}}},\u0022address\u0022:{\u0022streetAddress\u0022:\u0022677 5th Ave\u0022,\u0022city\u0022:\u0022New York\u0022,\u0022stateProvince\u0022:\u0022NY\u0022,\u0022country\u0022:\u0022USA\u0022,\u0022postalCode\u0022:\u002210022\u0022},\u0022rooms\u0022:[{\u0022description\u0022:\u0022Budget Room, 1 Queen Bed (Cityside)\u0022,\u0022descriptionFr\u0022:\u0022Chambre \\u00c9conomique, 1 grand lit (c\\u00f4t\\u00e9 ville)\u0022,\u0022type\u0022:\u0022Budget Room\u0022,\u0022baseRate\u0022:9.69,\u0022bedOptions\u0022:\u00221 Queen Bed\u0022,\u0022sleepsCount\u0022:2,\u0022smokingAllowed\u0022:true,\u0022tags\u0022:[\u0022vcr/dvd\u0022]},{\u0022description\u0022:\u0022Budget Room, 1 King Bed (Mountain View)\u0022,\u0022descriptionFr\u0022:\u0022Chambre \\u00c9conomique, 1 tr\\u00e8s grand lit (Mountain View)\u0022,\u0022type\u0022:\u0022Budget Room\u0022,\u0022baseRate\u0022:8.09,\u0022bedOptions\u0022:\u00221 King Bed\u0022,\u0022sleepsCount\u0022:2,\u0022smokingAllowed\u0022:true,\u0022tags\u0022:[\u0022vcr/dvd\u0022,\u0022jacuzzi tub\u0022]}]}" }, { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027nbledbxk\u0027)?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027xaapwvgv\u0027)?api-version=2020-06-30", "RequestMethod": "DELETE", "RequestHeaders": { "Accept": "application/json; odata.metadata=minimal", "api-key": "Sanitized", - "traceparent": "00-4f9116f41e4523428133e4ef2fe10c52-7cb5ebb630051841-00", + "traceparent": "00-a62e50cb7f7b3243bf3d6c5e3495a46a-eacf715ae123e54a-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "39742dd9385e6cc960a5d241bb91dc0d", "x-ms-return-client-request-id": "true" @@ -157,8 +157,8 @@ "ResponseHeaders": { "Cache-Control": "no-cache", "client-request-id": "39742dd9-385e-6cc9-60a5-d241bb91dc0d", - "Date": "Mon, 08 Jun 2020 14:43:40 GMT", - "elapsed-time": "640", + "Date": "Sat, 01 Aug 2020 13:21:33 GMT", + "elapsed-time": "205", "Expires": "-1", "Pragma": "no-cache", "request-id": "39742dd9-385e-6cc9-60a5-d241bb91dc0d", @@ -170,9 +170,9 @@ ], "Variables": { "RandomSeed": "1820603944", - "SearchIndexName": "nbledbxk", + "SearchIndexName": "xaapwvgv", "SEARCH_ADMIN_API_KEY": "Sanitized", "SEARCH_QUERY_API_KEY": "Sanitized", - "SEARCH_SERVICE_NAME": "azs-net-teglaza" + "SEARCH_SERVICE_NAME": "azs-net-heathsrchtst" } -} +} \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializerAsync.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializerAsync.json index 177fad718a01e..6131da272db12 100644 --- a/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializerAsync.json +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/IndexingTests/StaticDocumentsWithCustomSerializerAsync.json @@ -1,17 +1,17 @@ { "Entries": [ { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027shbrxcgf\u0027)/docs/search.index?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027vkqkuwud\u0027)/docs/search.index?api-version=2020-06-30", "RequestMethod": "POST", "RequestHeaders": { "Accept": "application/json; odata.metadata=none", "api-key": "Sanitized", "Content-Length": "1723", "Content-Type": "application/json", - "traceparent": "00-4ce3deb07d116543b8643569ef0de6c1-d34fef8e64355848-00", + "traceparent": "00-03a5f761563aa6408108695fb8717976-432c97f7820f1247-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "237d40090ba058628eadb62cfb545a2f", "x-ms-return-client-request-id": "true" @@ -84,8 +84,8 @@ "client-request-id": "237d4009-0ba0-5862-8ead-b62cfb545a2f", "Content-Length": "74", "Content-Type": "application/json; odata.metadata=none", - "Date": "Mon, 08 Jun 2020 14:43:47 GMT", - "elapsed-time": "114", + "Date": "Sat, 01 Aug 2020 13:21:56 GMT", + "elapsed-time": "127", "Expires": "-1", "OData-Version": "4.0", "Pragma": "no-cache", @@ -106,15 +106,15 @@ } }, { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027shbrxcgf\u0027)/docs(\u00271\u0027)?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027vkqkuwud\u0027)/docs(\u00271\u0027)?api-version=2020-06-30", "RequestMethod": "GET", "RequestHeaders": { "Accept": "application/json; odata.metadata=none", "api-key": "Sanitized", - "traceparent": "00-1bc910470064954ab061664145098d50-160a0f858178bd46-00", + "traceparent": "00-39fdd8cd5c4ea9479abfc8d190412cc5-d9128239e41aa34a-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "8535fcb3fde47b276e81545ce15932d4", "x-ms-return-client-request-id": "true" @@ -126,8 +126,8 @@ "client-request-id": "8535fcb3-fde4-7b27-6e81-545ce15932d4", "Content-Length": "1664", "Content-Type": "application/json; odata.metadata=none", - "Date": "Mon, 08 Jun 2020 14:43:50 GMT", - "elapsed-time": "12", + "Date": "Sat, 01 Aug 2020 13:21:58 GMT", + "elapsed-time": "10", "Expires": "-1", "OData-Version": "4.0", "Pragma": "no-cache", @@ -139,15 +139,15 @@ "ResponseBody": "{\u0022hotelId\u0022:\u00221\u0022,\u0022hotelName\u0022:\u0022Secret Point Motel\u0022,\u0022description\u0022:\u0022The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time\u0027s Square and the historic centre of the city, as well as other places of interest that make New York one of America\u0027s most attractive and cosmopolitan cities.\u0022,\u0022descriptionFr\u0022:\u0022L\u0027h\\u00f4tel est id\\u00e9alement situ\\u00e9 sur la principale art\\u00e8re commerciale de la ville en plein c\\u0153ur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d\u0027autres lieux d\u0027int\\u00e9r\\u00eat qui font de New York l\u0027une des villes les plus attractives et cosmopolites de l\u0027Am\\u00e9rique.\u0022,\u0022category\u0022:\u0022Boutique\u0022,\u0022tags\u0022:[\u0022pool\u0022,\u0022air conditioning\u0022,\u0022concierge\u0022],\u0022parkingIncluded\u0022:false,\u0022smokingAllowed\u0022:true,\u0022lastRenovationDate\u0022:\u00221970-01-18T05:00:00Z\u0022,\u0022rating\u0022:4,\u0022location\u0022:{\u0022type\u0022:\u0022Point\u0022,\u0022coordinates\u0022:[-73.975403,40.760586],\u0022crs\u0022:{\u0022type\u0022:\u0022name\u0022,\u0022properties\u0022:{\u0022name\u0022:\u0022EPSG:4326\u0022}}},\u0022address\u0022:{\u0022streetAddress\u0022:\u0022677 5th Ave\u0022,\u0022city\u0022:\u0022New York\u0022,\u0022stateProvince\u0022:\u0022NY\u0022,\u0022country\u0022:\u0022USA\u0022,\u0022postalCode\u0022:\u002210022\u0022},\u0022rooms\u0022:[{\u0022description\u0022:\u0022Budget Room, 1 Queen Bed (Cityside)\u0022,\u0022descriptionFr\u0022:\u0022Chambre \\u00c9conomique, 1 grand lit (c\\u00f4t\\u00e9 ville)\u0022,\u0022type\u0022:\u0022Budget Room\u0022,\u0022baseRate\u0022:9.69,\u0022bedOptions\u0022:\u00221 Queen Bed\u0022,\u0022sleepsCount\u0022:2,\u0022smokingAllowed\u0022:true,\u0022tags\u0022:[\u0022vcr/dvd\u0022]},{\u0022description\u0022:\u0022Budget Room, 1 King Bed (Mountain View)\u0022,\u0022descriptionFr\u0022:\u0022Chambre \\u00c9conomique, 1 tr\\u00e8s grand lit (Mountain View)\u0022,\u0022type\u0022:\u0022Budget Room\u0022,\u0022baseRate\u0022:8.09,\u0022bedOptions\u0022:\u00221 King Bed\u0022,\u0022sleepsCount\u0022:2,\u0022smokingAllowed\u0022:true,\u0022tags\u0022:[\u0022vcr/dvd\u0022,\u0022jacuzzi tub\u0022]}]}" }, { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027shbrxcgf\u0027)?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027vkqkuwud\u0027)?api-version=2020-06-30", "RequestMethod": "DELETE", "RequestHeaders": { "Accept": "application/json; odata.metadata=minimal", "api-key": "Sanitized", - "traceparent": "00-cca98efc027d254d874b3ed8670ef192-bec6a583a3092f4b-00", + "traceparent": "00-c8b3c2708470244f9d83551017e5a021-b297850d8a02fe45-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "45f7a50138f6355a3b1da0fb8dc52d3e", "x-ms-return-client-request-id": "true" @@ -157,8 +157,8 @@ "ResponseHeaders": { "Cache-Control": "no-cache", "client-request-id": "45f7a501-38f6-355a-3b1d-a0fb8dc52d3e", - "Date": "Mon, 08 Jun 2020 14:43:50 GMT", - "elapsed-time": "525", + "Date": "Sat, 01 Aug 2020 13:21:58 GMT", + "elapsed-time": "281", "Expires": "-1", "Pragma": "no-cache", "request-id": "45f7a501-38f6-355a-3b1d-a0fb8dc52d3e", @@ -170,9 +170,9 @@ ], "Variables": { "RandomSeed": "1121913965", - "SearchIndexName": "shbrxcgf", + "SearchIndexName": "vkqkuwud", "SEARCH_ADMIN_API_KEY": "Sanitized", "SEARCH_QUERY_API_KEY": "Sanitized", - "SEARCH_SERVICE_NAME": "azs-net-teglaza" + "SEARCH_SERVICE_NAME": "azs-net-heathsrchtst" } -} +} \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateIndex.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateIndex.json index b9560f9aa8a14..617219aec17ab 100644 --- a/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateIndex.json +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateIndex.json @@ -1,16 +1,16 @@ { "Entries": [ { - "RequestUri": "https://azs-net-pakrymsearch2.search.windows.net/indexes?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrch.search.windows.net/indexes?api-version=2020-06-30", "RequestMethod": "POST", "RequestHeaders": { "Accept": "application/json; odata.metadata=minimal", "api-key": "Sanitized", - "Content-Length": "1505", + "Content-Length": "408", "Content-Type": "application/json", "User-Agent": [ - "azsdk-net-Search.Documents/11.1.0-dev.20200717.1", - "(.NET Core 4.6.28516.03; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200728.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "ec8f1b224af8d9bff1f430bc1b15db2c", "x-ms-return-client-request-id": "true" @@ -37,83 +37,6 @@ "filterable": true, "sortable": true, "facetable": false - }, - { - "name": "description", - "type": "Edm.String", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": false, - "sortable": false, - "facetable": false, - "analyzer": "en.lucene" - }, - { - "name": "tags", - "type": "Collection(Edm.String)", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": true, - "sortable": false, - "facetable": true - }, - { - "name": "address", - "type": "Edm.ComplexType", - "fields": [ - { - "name": "streetAddress", - "type": "Edm.String", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": false, - "sortable": false, - "facetable": false - }, - { - "name": "city", - "type": "Edm.String", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": true, - "sortable": true, - "facetable": true - }, - { - "name": "stateProvince", - "type": "Edm.String", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": true, - "sortable": true, - "facetable": true - }, - { - "name": "country", - "type": "Edm.String", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": true, - "sortable": true, - "facetable": true - }, - { - "name": "postalCode", - "type": "Edm.String", - "key": false, - "retrievable": true, - "searchable": true, - "filterable": true, - "sortable": true, - "facetable": true - } - ] } ], "suggesters": [ @@ -121,8 +44,7 @@ "name": "sg", "searchMode": "analyzingInfixMatching", "sourceFields": [ - "hotelName", - "description" + "hotelName" ] } ] @@ -131,13 +53,13 @@ "ResponseHeaders": { "Cache-Control": "no-cache", "client-request-id": "ec8f1b22-4af8-d9bf-f1f4-30bc1b15db2c", - "Content-Length": "2550", + "Content-Length": "933", "Content-Type": "application/json; odata.metadata=minimal", - "Date": "Fri, 17 Jul 2020 21:45:51 GMT", - "elapsed-time": "1077", - "ETag": "W/\u00220x8D82A9AC65BB875\u0022", + "Date": "Tue, 28 Jul 2020 23:58:11 GMT", + "elapsed-time": "1256", + "ETag": "W/\u00220x8D8335215ABF632\u0022", "Expires": "-1", - "Location": "https://azs-net-pakrymsearch2.search.windows.net/indexes(\u0027wldfvoaj\u0027)?api-version=2020-06-30", + "Location": "https://azs-net-heathsrch.search.windows.net/indexes(\u0027wldfvoaj\u0027)?api-version=2020-06-30", "OData-Version": "4.0", "Pragma": "no-cache", "Preference-Applied": "odata.include-annotations=\u0022*\u0022", @@ -146,8 +68,8 @@ "x-ms-client-request-id": "ec8f1b22-4af8-d9bf-f1f4-30bc1b15db2c" }, "ResponseBody": { - "@odata.context": "https://azs-net-pakrymsearch2.search.windows.net/$metadata#indexes/$entity", - "@odata.etag": "\u00220x8D82A9AC65BB875\u0022", + "@odata.context": "https://azs-net-heathsrch.search.windows.net/$metadata#indexes/$entity", + "@odata.etag": "\u00220x8D8335215ABF632\u0022", "name": "wldfvoaj", "defaultScoringProfile": null, "fields": [ @@ -178,110 +100,6 @@ "searchAnalyzer": null, "analyzer": null, "synonymMaps": [] - }, - { - "name": "description", - "type": "Edm.String", - "searchable": true, - "filterable": false, - "retrievable": true, - "sortable": false, - "facetable": false, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": "en.lucene", - "synonymMaps": [] - }, - { - "name": "tags", - "type": "Collection(Edm.String)", - "searchable": true, - "filterable": true, - "retrievable": true, - "sortable": false, - "facetable": true, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": null, - "synonymMaps": [] - }, - { - "name": "address", - "type": "Edm.ComplexType", - "fields": [ - { - "name": "streetAddress", - "type": "Edm.String", - "searchable": true, - "filterable": false, - "retrievable": true, - "sortable": false, - "facetable": false, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": null, - "synonymMaps": [] - }, - { - "name": "city", - "type": "Edm.String", - "searchable": true, - "filterable": true, - "retrievable": true, - "sortable": true, - "facetable": true, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": null, - "synonymMaps": [] - }, - { - "name": "stateProvince", - "type": "Edm.String", - "searchable": true, - "filterable": true, - "retrievable": true, - "sortable": true, - "facetable": true, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": null, - "synonymMaps": [] - }, - { - "name": "country", - "type": "Edm.String", - "searchable": true, - "filterable": true, - "retrievable": true, - "sortable": true, - "facetable": true, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": null, - "synonymMaps": [] - }, - { - "name": "postalCode", - "type": "Edm.String", - "searchable": true, - "filterable": true, - "retrievable": true, - "sortable": true, - "facetable": true, - "key": false, - "indexAnalyzer": null, - "searchAnalyzer": null, - "analyzer": null, - "synonymMaps": [] - } - ] } ], "scoringProfiles": [], @@ -291,8 +109,7 @@ "name": "sg", "searchMode": "analyzingInfixMatching", "sourceFields": [ - "hotelName", - "description" + "hotelName" ] } ], @@ -307,12 +124,41 @@ "b": null } } + }, + { + "RequestUri": "https://azs-net-heathsrch.search.windows.net/indexes(\u0027wldfvoaj\u0027)?api-version=2020-06-30", + "RequestMethod": "DELETE", + "RequestHeaders": { + "Accept": "application/json; odata.metadata=minimal", + "api-key": "Sanitized", + "traceparent": "00-b7d6a0a2d5608043b53f91980f5dda8d-55c2fba0b5474941-00", + "User-Agent": [ + "azsdk-net-Search.Documents/11.1.0-dev.20200728.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" + ], + "x-ms-client-request-id": "04ea96019403abe77144bfc0b4162512", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": null, + "StatusCode": 204, + "ResponseHeaders": { + "Cache-Control": "no-cache", + "client-request-id": "04ea9601-9403-abe7-7144-bfc0b4162512", + "Date": "Tue, 28 Jul 2020 23:58:11 GMT", + "elapsed-time": "385", + "Expires": "-1", + "Pragma": "no-cache", + "request-id": "04ea9601-9403-abe7-7144-bfc0b4162512", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "x-ms-client-request-id": "04ea9601-9403-abe7-7144-bfc0b4162512" + }, + "ResponseBody": [] } ], "Variables": { "RandomSeed": "2102937546", - "SearchIndexName": null, + "SearchIndexName": "wldfvoaj", "SEARCH_ADMIN_API_KEY": "Sanitized", - "SEARCH_SERVICE_NAME": "azs-net-pakrymsearch2" + "SEARCH_SERVICE_NAME": "azs-net-heathsrch" } } \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateManualIndex.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateManualIndex.json new file mode 100644 index 0000000000000..997eab4915b66 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateManualIndex.json @@ -0,0 +1,345 @@ +{ + "Entries": [ + { + "RequestUri": "https://azs-net-heathsrch.search.windows.net/indexes?api-version=2020-06-30", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json; odata.metadata=minimal", + "api-key": "Sanitized", + "Content-Length": "1491", + "Content-Type": "application/json", + "User-Agent": [ + "azsdk-net-Search.Documents/11.1.0-dev.20200728.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" + ], + "x-ms-client-request-id": "062ad9f862e213513b68f2e5dc858b77", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": { + "name": "nillbdny", + "fields": [ + { + "name": "hotelId", + "type": "Edm.String", + "key": true, + "retrievable": true, + "searchable": false, + "filterable": true, + "sortable": true, + "facetable": false + }, + { + "name": "hotelName", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": true, + "facetable": false + }, + { + "name": "description", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": false, + "sortable": false, + "facetable": false, + "analyzer": "en.lucene" + }, + { + "name": "tags", + "type": "Collection(Edm.String)", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": false, + "facetable": true + }, + { + "name": "address", + "type": "Edm.ComplexType", + "fields": [ + { + "name": "streetAddress", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": false, + "sortable": false, + "facetable": false + }, + { + "name": "city", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": true, + "facetable": true + }, + { + "name": "stateProvince", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": true, + "facetable": true + }, + { + "name": "country", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": true, + "facetable": true + }, + { + "name": "postalCode", + "type": "Edm.String", + "key": false, + "retrievable": true, + "searchable": true, + "filterable": true, + "sortable": true, + "facetable": true + } + ] + } + ], + "suggesters": [ + { + "name": "sg", + "searchMode": "analyzingInfixMatching", + "sourceFields": [ + "hotelName" + ] + } + ] + }, + "StatusCode": 201, + "ResponseHeaders": { + "Cache-Control": "no-cache", + "client-request-id": "062ad9f8-62e2-1351-3b68-f2e5dc858b77", + "Content-Length": "2532", + "Content-Type": "application/json; odata.metadata=minimal", + "Date": "Tue, 28 Jul 2020 23:58:15 GMT", + "elapsed-time": "1085", + "ETag": "W/\u00220x8D8335218003C2A\u0022", + "Expires": "-1", + "Location": "https://azs-net-heathsrch.search.windows.net/indexes(\u0027nillbdny\u0027)?api-version=2020-06-30", + "OData-Version": "4.0", + "Pragma": "no-cache", + "Preference-Applied": "odata.include-annotations=\u0022*\u0022", + "request-id": "062ad9f8-62e2-1351-3b68-f2e5dc858b77", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "x-ms-client-request-id": "062ad9f8-62e2-1351-3b68-f2e5dc858b77" + }, + "ResponseBody": { + "@odata.context": "https://azs-net-heathsrch.search.windows.net/$metadata#indexes/$entity", + "@odata.etag": "\u00220x8D8335218003C2A\u0022", + "name": "nillbdny", + "defaultScoringProfile": null, + "fields": [ + { + "name": "hotelId", + "type": "Edm.String", + "searchable": false, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": false, + "key": true, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "hotelName", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": false, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "description", + "type": "Edm.String", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": "en.lucene", + "synonymMaps": [] + }, + { + "name": "tags", + "type": "Collection(Edm.String)", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": false, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "address", + "type": "Edm.ComplexType", + "fields": [ + { + "name": "streetAddress", + "type": "Edm.String", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "city", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "stateProvince", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "country", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + }, + { + "name": "postalCode", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": true, + "key": false, + "indexAnalyzer": null, + "searchAnalyzer": null, + "analyzer": null, + "synonymMaps": [] + } + ] + } + ], + "scoringProfiles": [], + "corsOptions": null, + "suggesters": [ + { + "name": "sg", + "searchMode": "analyzingInfixMatching", + "sourceFields": [ + "hotelName" + ] + } + ], + "analyzers": [], + "tokenizers": [], + "tokenFilters": [], + "charFilters": [], + "encryptionKey": null, + "similarity": { + "@odata.type": "#Microsoft.Azure.Search.BM25Similarity", + "k1": null, + "b": null + } + } + }, + { + "RequestUri": "https://azs-net-heathsrch.search.windows.net/indexes(\u0027nillbdny\u0027)?api-version=2020-06-30", + "RequestMethod": "DELETE", + "RequestHeaders": { + "Accept": "application/json; odata.metadata=minimal", + "api-key": "Sanitized", + "traceparent": "00-ec3a5d5ec32ca84ca99e45e0ff7cd50c-1b423c661177d746-00", + "User-Agent": [ + "azsdk-net-Search.Documents/11.1.0-dev.20200728.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" + ], + "x-ms-client-request-id": "e487c37abac16ac1ab43997c9d35868a", + "x-ms-return-client-request-id": "true" + }, + "RequestBody": null, + "StatusCode": 204, + "ResponseHeaders": { + "Cache-Control": "no-cache", + "client-request-id": "e487c37a-bac1-6ac1-ab43-997c9d35868a", + "Date": "Tue, 28 Jul 2020 23:58:15 GMT", + "elapsed-time": "424", + "Expires": "-1", + "Pragma": "no-cache", + "request-id": "e487c37a-bac1-6ac1-ab43-997c9d35868a", + "Strict-Transport-Security": "max-age=15724800; includeSubDomains", + "x-ms-client-request-id": "e487c37a-bac1-6ac1-ab43-997c9d35868a" + }, + "ResponseBody": [] + } + ], + "Variables": { + "RandomSeed": "1446516247", + "SearchIndexName": "nillbdny", + "SEARCH_ADMIN_API_KEY": "Sanitized", + "SEARCH_SERVICE_NAME": "azs-net-heathsrch" + } +} \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializer.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializer.json index 5cf4004b2c43c..d701d164cd9b7 100644 --- a/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializer.json +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializer.json @@ -1,24 +1,22 @@ { "Entries": [ { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027pybitseg\u0027)/docs/search.post.search?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027wnpngsjg\u0027)/docs/search.post.search?api-version=2020-06-30", "RequestMethod": "POST", "RequestHeaders": { "Accept": "application/json; odata.metadata=none", "api-key": "Sanitized", - "Content-Length": "49", + "Content-Length": "14", "Content-Type": "application/json", - "traceparent": "00-a136ace68a5519409a3b3c74fe5f6472-9e26abb0f640c145-00", + "traceparent": "00-7caadf92b7cdec4bb965cbb084f63f57-52c04f8e52817343-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "af18bd458a8748f174608b8f7fd86649", "x-ms-return-client-request-id": "true" }, "RequestBody": { - "facets": [], - "scoringParameters": [], "search": "*" }, "StatusCode": 200, @@ -27,8 +25,8 @@ "client-request-id": "af18bd45-8a87-48f1-7460-8b8f7fd86649", "Content-Length": "7076", "Content-Type": "application/json; odata.metadata=none", - "Date": "Mon, 08 Jun 2020 14:32:32 GMT", - "elapsed-time": "120", + "Date": "Sat, 01 Aug 2020 13:22:23 GMT", + "elapsed-time": "39", "Expires": "-1", "OData-Version": "4.0", "Pragma": "no-cache", @@ -42,9 +40,9 @@ ], "Variables": { "RandomSeed": "251878370", - "SearchIndexName": "pybitseg", + "SearchIndexName": "wnpngsjg", "SEARCH_ADMIN_API_KEY": "Sanitized", "SEARCH_QUERY_API_KEY": "Sanitized", - "SEARCH_SERVICE_NAME": "azs-net-teglaza" + "SEARCH_SERVICE_NAME": "azs-net-heathsrchtst" } -} +} \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializerAsync.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializerAsync.json index 3c433dfac5371..08e1310d7b0d9 100644 --- a/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializerAsync.json +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializerAsync.json @@ -1,24 +1,22 @@ { "Entries": [ { - "RequestUri": "https://azs-net-teglaza.search.windows.net/indexes(\u0027pybitseg\u0027)/docs/search.post.search?api-version=2020-06-30", + "RequestUri": "https://azs-net-heathsrchtst.search.windows.net/indexes(\u0027wnpngsjg\u0027)/docs/search.post.search?api-version=2020-06-30", "RequestMethod": "POST", "RequestHeaders": { "Accept": "application/json; odata.metadata=none", "api-key": "Sanitized", - "Content-Length": "49", + "Content-Length": "14", "Content-Type": "application/json", - "traceparent": "00-d7a650c0abe5a7468c6d9d1c7b9826c6-5cbee725ed368445-00", + "traceparent": "00-170bf2ca9346c74997a7c58ef2250040-57b9e483e3b90e4e-00", "User-Agent": [ - "azsdk-net-Search.Documents/1.0.0-dev.20200608.1", - "(.NET Core 4.6.28801.04; Microsoft Windows 10.0.18363 )" + "azsdk-net-Search.Documents/11.1.0-dev.20200801.1", + "(.NET Core 4.6.29017.01; Microsoft Windows 10.0.19041 )" ], "x-ms-client-request-id": "f7b96544cc84bac6bda5e8eb2d107dcc", "x-ms-return-client-request-id": "true" }, "RequestBody": { - "facets": [], - "scoringParameters": [], "search": "*" }, "StatusCode": 200, @@ -27,8 +25,8 @@ "client-request-id": "f7b96544-cc84-bac6-bda5-e8eb2d107dcc", "Content-Length": "7076", "Content-Type": "application/json; odata.metadata=none", - "Date": "Mon, 08 Jun 2020 14:32:32 GMT", - "elapsed-time": "7", + "Date": "Sat, 01 Aug 2020 13:22:23 GMT", + "elapsed-time": "6", "Expires": "-1", "OData-Version": "4.0", "Pragma": "no-cache", @@ -42,8 +40,8 @@ ], "Variables": { "RandomSeed": "716347721", - "SearchIndexName": "pybitseg", + "SearchIndexName": "wnpngsjg", "SEARCH_QUERY_API_KEY": "Sanitized", - "SEARCH_SERVICE_NAME": "azs-net-teglaza" + "SEARCH_SERVICE_NAME": "azs-net-heathsrchtst" } -} +} \ No newline at end of file diff --git a/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs b/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs new file mode 100644 index 0000000000000..9e4d5676d5f12 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.TestFramework; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; +using NUnit.Framework; + +namespace Azure.Search.Documents.Tests +{ + public class SimpleFieldAttributeTests + { + [Test] + [Parallelizable] + public void CreatesEquivalentField( + [EnumValues] SearchFieldDataType type, + [Values] bool key, + [Values] bool hidden, + [Values] bool filterable, + [Values] bool facetable, + [Values] bool sortable) + { + SimpleFieldAttribute sut = new SimpleFieldAttribute + { + IsKey = key, + IsHidden = hidden, + IsFilterable = filterable, + IsFacetable = facetable, + IsSortable = sortable, + }; + + SearchField field = new SearchField("test", type); + ((ISearchFieldAttribute)sut).SetField(field); + + Assert.AreEqual("test", field.Name); + Assert.AreEqual(type, field.Type); + Assert.AreEqual(key, field.IsKey ?? false); + Assert.AreEqual(hidden, field.IsHidden ?? false); + Assert.AreEqual(filterable, field.IsFilterable ?? false); + Assert.AreEqual(facetable, field.IsFacetable ?? false); + Assert.AreEqual(sortable, field.IsSortable ?? false); + } + + [Test] + [Parallelizable] + public void IsSearchableNotOverwritten() + { + SearchField field = new SearchField("test", SearchFieldDataType.String); + ISearchFieldAttribute attribute = new SearchableFieldAttribute + { + AnalyzerName = LexicalAnalyzerName.Values.EnLucene, + IsFilterable = true, + IsSortable = true, + }; + + attribute.SetField(field); + + Assert.AreEqual("test", field.Name); + Assert.AreEqual(SearchFieldDataType.String, field.Type); + Assert.IsFalse(field.IsFacetable); + Assert.IsTrue(field.IsFilterable); + Assert.IsFalse(field.IsHidden); + Assert.IsFalse(field.IsKey); + Assert.IsTrue(field.IsSearchable); + Assert.IsTrue(field.IsSortable); + Assert.AreEqual(LexicalAnalyzerName.EnLucene.ToString(), field.AnalyzerName?.ToString()); + Assert.IsNull(field.IndexAnalyzerName); + Assert.IsNull(field.SearchAnalyzerName); + Assert.IsEmpty(field.SynonymMapNames); + + // Make sure that if a SimpleFieldAttribute were also specified, it does not overwrite IsSearchable + // but does overwrite every other SimpleField property not set otherwise. + attribute = new SimpleFieldAttribute + { + IsKey = true, + }; + + attribute.SetField(field); + + Assert.AreEqual("test", field.Name); + Assert.AreEqual(SearchFieldDataType.String, field.Type); + Assert.IsFalse(field.IsFacetable); + Assert.IsFalse(field.IsFilterable); + Assert.IsFalse(field.IsHidden); + Assert.IsTrue(field.IsKey); + Assert.IsTrue(field.IsSearchable); + Assert.IsFalse(field.IsSortable); + Assert.AreEqual(LexicalAnalyzerName.EnLucene.ToString(), field.AnalyzerName?.ToString()); + Assert.IsNull(field.IndexAnalyzerName); + Assert.IsNull(field.SearchAnalyzerName); + Assert.IsEmpty(field.SynonymMapNames); + } + } +}