From d87e908c5db41693e5cae7625a59c0a42cb49c54 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 24 Jul 2020 19:40:55 -0700 Subject: [PATCH 01/13] Initial implementation of FieldBuilder Fixes #11166. This is a port of the original track 1 FieldBuilder, which was merely a sample for track 2 GA. --- .../Azure.Search.Documents/CHANGELOG.md | 3 + .../Azure.Search.Documents.netstandard2.0.cs | 125 +++++++++ .../src/Azure.Search.Documents.csproj | 15 +- .../Indexes}/FieldBuilder.cs | 243 +++++++++--------- .../Indexes}/FieldBuilderIgnoreAttribute.cs | 2 +- .../src/Indexes/ISearchFieldAttribute.cs | 19 ++ .../src/Indexes/Models/LexicalAnalyzerName.cs | 204 +++++++++++++++ .../src/Indexes/SearchableFieldAttribute.cs | 80 ++++++ .../src/Indexes/SimpleFieldAttribute.cs | 84 ++++++ .../src/Models/IndexDocumentsAction{T}.cs | 4 +- .../src/Models/SearchResult{T}.cs | 4 +- .../src/Models/SearchSuggestion{T}.cs | 4 +- .../src/SearchClient.cs | 4 +- .../src/Serialization/JsonSerialization.cs | 4 +- .../tests/Azure.Search.Documents.Tests.csproj | 3 - .../tests/FieldBuilderTests.cs | 35 ++- .../tests/Models/RecursiveModel.cs | 8 +- .../tests/Models/ReflectableCamelCaseModel.cs | 11 +- .../tests/Models/ReflectableModel.cs | 54 ++-- .../Models/ReflectableStructCamelCaseModel.cs | 11 +- .../tests/Models/ReflectableStructModel.cs | 54 ++-- .../Samples/FieldBuilder/AnalyzerAttribute.cs | 34 --- .../FieldBuilder/IndexAnalyzerAttribute.cs | 34 --- .../FieldBuilder/IsFacetableAttribute.cs | 17 -- .../FieldBuilder/IsFilterableAttribute.cs | 15 -- .../FieldBuilder/IsRetrievableAttribute.cs | 33 --- .../FieldBuilder/IsSearchableAttribute.cs | 16 -- .../FieldBuilder/IsSortableAttribute.cs | 16 -- .../FieldBuilder/SearchAnalyzerAttribute.cs | 34 --- ...ializePropertyNamesAsCamelCaseAttribute.cs | 42 --- .../FieldBuilder/SynonymMapsAttribute.cs | 36 --- .../tests/SearchableFieldAttributeTests.cs | 72 ++++++ .../tests/SimpleFieldAttributeTests.cs | 44 ++++ 33 files changed, 868 insertions(+), 496 deletions(-) rename sdk/search/Azure.Search.Documents/{tests/Samples/FieldBuilder => src/Indexes}/FieldBuilder.cs (66%) rename sdk/search/Azure.Search.Documents/{tests/Samples/FieldBuilder => src/Indexes}/FieldBuilderIgnoreAttribute.cs (95%) create mode 100644 sdk/search/Azure.Search.Documents/src/Indexes/ISearchFieldAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs create mode 100644 sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/SearchableFieldAttributeTests.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs diff --git a/sdk/search/Azure.Search.Documents/CHANGELOG.md b/sdk/search/Azure.Search.Documents/CHANGELOG.md index 360860435a3f..21cf5915d460 100644 --- a/sdk/search/Azure.Search.Documents/CHANGELOG.md +++ b/sdk/search/Azure.Search.Documents/CHANGELOG.md @@ -2,6 +2,9 @@ ## 11.1.0-preview.1 (Unreleased) +### Added + +- Add `FieldBuilder` to easily create `SearchIndex` fields from a model type. ## 11.0.0 (2020-07-07) 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 58723e9b6e43..f550411e130e 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.ObjectSerializer Serializer { get { throw null; } set { } } public Azure.Search.Documents.SearchClientOptions.ServiceVersion Version { get { throw null; } } public enum ServiceVersion { @@ -103,6 +104,24 @@ public SuggestOptions() { } } namespace Azure.Search.Documents.Indexes { + public static partial class FieldBuilder + { + public static System.Collections.Generic.IList Build(System.Type modelType, Azure.Core.ObjectSerializer serializer = null) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Property)] + 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 +221,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 +758,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 AsString + { + 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 { 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 d363d53f954e..facc263b33f2 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 @@ -11,6 +11,10 @@ Azure Cognitive Search;Azure Search Documents;Azure Search;Search;Cognitive;Search Engine;Azure $(RequiredTargetFrameworks) + + $(DefineConstants); + EXPERIMENTAL_SERIALIZER; + $(NoWarn);AZC0007;AZC0004;AZC0001 @@ -34,8 +38,15 @@ - + + + + + + + + diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs similarity index 66% rename from sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs rename to sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs index 9d825e0c9f9b..7fb18c2b13c7 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -5,10 +5,10 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; +using Azure.Core; using Azure.Search.Documents.Indexes.Models; #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; @@ -16,7 +16,7 @@ using Microsoft.Spatial; #endif -namespace Azure.Search.Documents.Samples +namespace Azure.Search.Documents.Indexes { /// /// Builds field definitions for a search index by reflecting over a user-defined model type. @@ -58,76 +58,23 @@ public static class FieldBuilder typeof(decimal), }; - private static JsonNamingPolicy CamelCaseResolver { get; } = JsonNamingPolicy.CamelCase; - - private static JsonNamingPolicy DefaultResolver { get; } = DefaultJsonNamingPolicy.Shared; - /// - /// Creates a collection 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 static IList BuildForType() => BuildForType(typeof(T)); - - /// - /// Creates a collection of objects corresponding to + /// 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 static IList BuildForType(Type modelType) - { - bool useCamelCase = SerializePropertyNamesAsCamelCaseAttribute.IsDefinedOnType(modelType); - JsonNamingPolicy namingPolicy = useCamelCase - ? CamelCaseResolver - : DefaultResolver; - return BuildForType(modelType, namingPolicy); - } - - /// - /// Creates a collection of objects corresponding to - /// the properties of the type supplied. - /// - /// - /// The type for which fields will be created, based on its properties. - /// - /// - /// to use. - /// This ensures that the field names are generated in a way that is - /// consistent with the way the model will be serialized. + /// + /// The to use to generate field names that match JSON property names. + /// You should use the same value as . + /// will be used if no value is provided. /// /// A collection of fields. - public static IList BuildForType(JsonNamingPolicy namingPolicy) => BuildForType(typeof(T), namingPolicy); - - /// - /// Creates a collection of objects corresponding to - /// the properties of the type supplied. - /// - /// - /// The type for which fields will be created, based on its properties. - /// - /// - /// to use. - /// Contract resolver that the SearchIndexClient will use. - /// This ensures that the field names are generated in a way that is - /// consistent with the way the model will be serialized. - /// - /// A collection of fields. - /// or is null. - public static IList BuildForType(Type modelType, JsonNamingPolicy namingPolicy) + /// . + public static IList Build(Type modelType, ObjectSerializer serializer = null) { - if (modelType is null) - { throw new ArgumentNullException(nameof(modelType)); - } - - if (namingPolicy is null) - { throw new ArgumentNullException(nameof(namingPolicy)); - } + Argument.AssertNotNull(modelType, nameof(modelType)); ArgumentException FailOnNonObjectDataType() { @@ -138,7 +85,10 @@ ArgumentException FailOnNonObjectDataType() throw new ArgumentException(errorMessage, nameof(modelType)); } - if (ObjectInfo.TryGet(modelType, out ObjectInfo info)) + serializer ??= new JsonObjectSerializer(); + IMemberNameConverter nameProvider = serializer as IMemberNameConverter ?? DefaultSerializedNameProvider.Shared; + + if (ObjectInfo.TryGet(modelType, nameProvider, out ObjectInfo info)) { if (info.Properties.Length == 0) { @@ -146,22 +96,23 @@ ArgumentException FailOnNonObjectDataType() } // Use Stack to avoid a dependency on ImmutableStack for now. - return BuildForTypeRecursive(modelType, info, namingPolicy, new Stack(new[] { modelType })); + return Build(modelType, info, nameProvider, new Stack(new[] { modelType })); } throw FailOnNonObjectDataType(); } - private static IList BuildForTypeRecursive( + private static IList Build( Type modelType, ObjectInfo info, - JsonNamingPolicy namingPolicy, + IMemberNameConverter nameProvider, Stack processedTypes) { - SearchField BuildField(PropertyInfo prop) + SearchField BuildField(ObjectPropertyInfo prop) { + // The IMemberNameConverter will return null for implementation-specific ways of ignoring members. static bool ShouldIgnore(Attribute attribute) => - attribute is JsonIgnoreAttribute || attribute is FieldBuilderIgnoreAttribute; + attribute is FieldBuilderIgnoreAttribute; IList attributes = prop.GetCustomAttributes(true).Cast().ToArray(); if (attributes.Any(ShouldIgnore)) @@ -181,11 +132,15 @@ SearchField CreateComplexField(SearchFieldDataType dataType, Type underlyingClrT try { IList subFields = - BuildForTypeRecursive(underlyingClrType, info, namingPolicy, processedTypes); + Build(underlyingClrType, info, nameProvider, processedTypes); - string fieldName = namingPolicy.ConvertName(prop.Name); + if (prop.SerializedName is null) + { + // Member is unsupported or ignored. + return null; + } - SearchField field = new SearchField(fieldName, dataType); + SearchField field = new SearchField(prop.SerializedName, dataType); foreach (SearchField subField in subFields) { field.Fields.Add(subField); @@ -201,50 +156,23 @@ SearchField CreateComplexField(SearchFieldDataType dataType, Type underlyingClrT SearchField CreateSimpleField(SearchFieldDataType SearchFieldDataType) { - string fieldName = namingPolicy.ConvertName(prop.Name); + if (prop.SerializedName is null) + { + // Member is unsupported or ignored. + return null; + } - SearchField field = new SearchField(fieldName, SearchFieldDataType); + SearchField field = new SearchField(prop.SerializedName, SearchFieldDataType); foreach (Attribute attribute in attributes) { switch (attribute) { - case IsSearchableAttribute _: - field.IsSearchable = true; - break; - - case IsFilterableAttribute _: - field.IsFilterable = true; - break; - - case IsSortableAttribute _: - field.IsSortable = true; + case SearchableFieldAttribute searchableFieldAttribute: + ((ISearchFieldAttribute)searchableFieldAttribute).SetField(field); break; - case IsFacetableAttribute _: - field.IsFacetable = true; - break; - - case IsRetrievableAttribute isRetrievableAttribute: - field.IsHidden = !isRetrievableAttribute.IsRetrievable; - break; - - case AnalyzerAttribute analyzerAttribute: - field.AnalyzerName = analyzerAttribute.Name; - break; - - case SearchAnalyzerAttribute searchAnalyzerAttribute: - field.SearchAnalyzerName = searchAnalyzerAttribute.Name; - break; - - case IndexAnalyzerAttribute indexAnalyzerAttribute: - field.IndexAnalyzerName = indexAnalyzerAttribute.Name; - break; - - case SynonymMapsAttribute synonymMapsAttribute: - foreach (string synonymMapName in synonymMapsAttribute.SynonymMaps) - { - field.SynonymMapNames.Add(synonymMapName); - } + case SimpleFieldAttribute simpleFieldAttribute: + ((ISearchFieldAttribute)simpleFieldAttribute).SetField(field); break; default: @@ -276,7 +204,7 @@ ArgumentException FailOnUnknownDataType() return new ArgumentException(errorMessage, nameof(modelType)); } - IDataTypeInfo dataTypeInfo = GetDataTypeInfo(prop.PropertyType, namingPolicy); + IDataTypeInfo dataTypeInfo = GetDataTypeInfo(prop.PropertyType, nameProvider); return dataTypeInfo.Match( onUnknownDataType: () => throw FailOnUnknownDataType(), @@ -287,7 +215,7 @@ ArgumentException FailOnUnknownDataType() return info.Properties.Select(BuildField).Where(field => field != null).ToArray(); } - private static IDataTypeInfo GetDataTypeInfo(Type propertyType, JsonNamingPolicy namingPolicy) + private static IDataTypeInfo GetDataTypeInfo(Type propertyType, IMemberNameConverter nameProvider) { static bool IsNullableType(Type type) => type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); @@ -298,14 +226,14 @@ static bool IsNullableType(Type type) => } else if (IsNullableType(propertyType)) { - return GetDataTypeInfo(propertyType.GenericTypeArguments[0], namingPolicy); + return GetDataTypeInfo(propertyType.GenericTypeArguments[0], nameProvider); } else if (TryGetEnumerableElementType(propertyType, out Type elementType)) { - IDataTypeInfo elementTypeInfo = GetDataTypeInfo(elementType, namingPolicy); + IDataTypeInfo elementTypeInfo = GetDataTypeInfo(elementType, nameProvider); return DataTypeInfo.AsCollection(elementTypeInfo); } - else if (ObjectInfo.TryGet(propertyType, out ObjectInfo info)) + else if (ObjectInfo.TryGet(propertyType, nameProvider, out ObjectInfo info)) { return DataTypeInfo.Complex(SearchFieldDataType.Complex, propertyType, info); } @@ -424,32 +352,97 @@ public T Match( private class ObjectInfo { - private ObjectInfo(Type type) + private ObjectInfo(ObjectPropertyInfo[] properties) { - Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + Properties = properties; } - public static bool TryGet(Type type, out ObjectInfo info) + public static bool TryGet(Type type, IMemberNameConverter nameProvider, out ObjectInfo info) { - // Close approximation to Newtonsoft.Json.Serialization.DefaultContractResolver. + // 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)) { - info = new ObjectInfo(type); - return true; + 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 PropertyInfo[] Properties { get; } + public ObjectPropertyInfo[] Properties { get; } } - private class DefaultJsonNamingPolicy : JsonNamingPolicy + private struct ObjectPropertyInfo { - public static JsonNamingPolicy Shared { get; } = new DefaultJsonNamingPolicy(); + 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 override string ConvertName(string name) => name; + public string ConvertMemberName(MemberInfo member) => member?.Name; } } } diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilderIgnoreAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs similarity index 95% rename from sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilderIgnoreAttribute.cs rename to sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs index bc0316d94076..d67f0f02cd41 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilderIgnoreAttribute.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; using Azure.Search.Documents.Indexes.Models; -namespace Azure.Search.Documents.Samples +namespace Azure.Search.Documents.Indexes { /// /// Indicates that the target property should be ignored by . 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 000000000000..5bab61211739 --- /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/LexicalAnalyzerName.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs new file mode 100644 index 000000000000..1e1b5874173e --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Search.Documents.Indexes.Models +{ + public readonly partial struct LexicalAnalyzerName + { +#pragma warning disable CA1034 // Nested types should not be visible + /// + /// The names of all lexical analyzer as string constants. + /// These can be used in and anywhere else constants are required. + /// + public static class AsString + { + /// 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 + } +} 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 000000000000..68f34b3203e3 --- /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 000000000000..3ae37d1e3cf3 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs @@ -0,0 +1,84 @@ +// 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) + { + if (IsKey) + { + field.IsKey = IsKey; + } + + if (IsHidden) + { + field.IsHidden = IsHidden; + } + + if (IsFilterable) + { + field.IsFilterable = IsFilterable; + } + + if (IsFacetable) + { + field.IsFacetable = IsFacetable; + } + + if (IsSortable) + { + field.IsSortable = IsSortable; + } + } + } +} 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 e2e9e564a12e..29e047afd647 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs @@ -100,11 +100,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/SearchResult{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs index d336f14ba449..e54c029be127 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/SearchResult{T}.cs @@ -108,8 +108,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 0e0736c4d5d8..b7e287fd4f59 100644 --- a/sdk/search/Azure.Search.Documents/src/Models/SearchSuggestion{T}.cs +++ b/sdk/search/Azure.Search.Documents/src/Models/SearchSuggestion{T}.cs @@ -87,8 +87,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/SearchClient.cs b/sdk/search/Azure.Search.Documents/src/SearchClient.cs index 6b735f4620a0..2ab0d7fd6093 100644 --- a/sdk/search/Azure.Search.Documents/src/SearchClient.cs +++ b/sdk/search/Azure.Search.Documents/src/SearchClient.cs @@ -183,7 +183,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 +217,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/Serialization/JsonSerialization.cs b/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs index 1257128375fb..18a36a588c46 100644 --- a/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs +++ b/sdk/search/Azure.Search.Documents/src/Serialization/JsonSerialization.cs @@ -362,8 +362,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 a0a09a595296..ed84ac4b71b9 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 @@ -31,9 +31,6 @@ - - - diff --git a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index 94d557b510ea..637eab1e3ce2 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -5,12 +5,12 @@ 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; using NUnit.Framework; using KeyFieldAttribute = System.ComponentModel.DataAnnotations.KeyAttribute; -namespace Azure.Search.Documents.Samples.Tests +namespace Azure.Search.Documents.Tests { public class FieldBuilderTests { @@ -219,7 +219,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 +275,7 @@ public void IsFacetableOnlyOnPropertiesWithIsFacetableAttribute(Type modelType) } [TestCaseSource(nameof(TestModelTypeTestData))] - public void NotIsHiddenOnAllPropertiesExceptOnesWithIsRetrievableAttributeSetToFalse( + public void NotIsHiddenOnAllPropertiesExceptOnesWithIsHiddenSetToTrue( Type modelType) { // Was IsRetrievableOnAllPropertiesExceptOnesWithIsRetrievableAttributeSetToFalse @@ -388,7 +397,7 @@ 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(() => FieldBuilder.Build(modelType)); string expectedErrorMessage = $"Property '{invalidPropertyName}' is of type '{modelType.GetProperty(invalidPropertyName).PropertyType}', " + @@ -412,7 +421,7 @@ public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(T [TestCase(typeof(ICollection))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedTypes(Type modelType) { - ArgumentException e = Assert.Throws(() => FieldBuilder.BuildForType(modelType)); + ArgumentException e = Assert.Throws(() => FieldBuilder.Build(modelType)); string expectedErrorMessage = $"Type '{modelType}' does not have properties which map to fields of an Azure Search index. Please use a " + @@ -429,7 +438,7 @@ from type in modelTypes from tuple in testData select (type, tuple.dataType, tuple.fieldName); - private static IList BuildForType(Type modelType) => FieldBuilder.BuildForType(modelType); + private static IList BuildForType(Type modelType) => FieldBuilder.Build(modelType); private enum Direction { @@ -498,7 +507,7 @@ private class ModelWithEnum [KeyField] public string ID { get; set; } - [IsFilterable, IsSearchable, IsSortable, IsFacetable] + [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)] public Direction Direction { get; set; } } @@ -507,7 +516,7 @@ private class ModelWithUnsupportedPrimitiveType [KeyField] public string ID { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public decimal Price { get; set; } } @@ -516,7 +525,7 @@ private class ModelWithUnsupportedEnumerableType [KeyField] public string ID { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public IEnumerable Buffer { get; set; } } @@ -525,7 +534,7 @@ private class ModelWithUnsupportedCollectionType [KeyField] public string ID { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public ICollection Buffer { get; set; } } @@ -534,7 +543,7 @@ private class InnerModelWithKey [KeyField] public string InnerID { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public int OtherField { get; set; } } @@ -548,7 +557,7 @@ private class ModelWithNestedKey private class InnerModelWithIgnoredProperties { - [IsFilterable] + [SimpleField(IsFilterable = true)] public int OtherField { get; set; } [JsonIgnore] diff --git a/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs index c9645131dff1..66e386fb5891 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs @@ -5,11 +5,13 @@ #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; + +namespace Azure.Search.Documents.Tests { public class RecursiveModel { - [IsFilterable] + [SimpleField(IsFilterable = true)] public int Data { get; set; } // This is to test that FieldBuilder gracefully fails on recursive models. @@ -18,7 +20,7 @@ public class RecursiveModel public class OtherRecursiveModel { - [IsFilterable, IsFacetable] + [SimpleField(IsFilterable = true, IsFacetable = true)] 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 e27fa250462b..3a698de3c2e0 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs @@ -1,31 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; 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 { - [SerializePropertyNamesAsCamelCase] public class ReflectableInnerCamelCaseModel { + [JsonPropertyName("name")] public string Name { get; set; } } - [SerializePropertyNamesAsCamelCase] 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 3c3848e2721b..eeb2850c802f 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #else @@ -15,36 +17,35 @@ #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 { - [IsSearchable] + [SearchableField] public string City { get; set; } - [IsFilterable, IsFacetable] + [SimpleField(IsFilterable = true, IsFacetable = true)] public string Country { get; set; } } public class ReflectableComplexObject { - [IsSearchable] - [Analyzer("en.microsoft")] + [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] public string Name { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public int Rating { get; set; } // Ensure that leaf-field-specific attributes are ignored by FieldBuilder on complex fields. - [IsSearchable] - [IsFilterable] - [IsSortable] - [IsFacetable] - [IsRetrievable(false)] - [Analyzer("zh-Hant.lucene")] - [IndexAnalyzer("zh-Hant.lucene")] - [SearchAnalyzer("zh-Hant.lucene")] - [SynonymMaps("myMap")] + [SearchableField( + IsFilterable = true, + IsSortable = true, + IsFacetable = true, + IsHidden = true, + AnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + SearchAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + IndexAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + SynonymMapNames = new[] { "myMap" })] public ReflectableAddress Address { get; set; } } @@ -63,37 +64,36 @@ public class ReflectableModel public DateTime TimeWithoutOffset { get; set; } - [IsSearchable] - [SynonymMaps("myMap")] + [SearchableField(SynonymMapNames = new[] { "myMap" })] public string Text { get; set; } public string UnsearchableText { get; set; } - [IsSearchable] + [SearchableField] public string MoreText { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public string FilterableText { get; set; } - [IsSortable] + [SimpleField(IsSortable = true)] public string SortableText { get; set; } - [IsFacetable] + [SimpleField(IsFacetable = true)] public string FacetableText { get; set; } - [IsRetrievable(false)] + [SimpleField(IsHidden = true)] public string IrretrievableText { get; set; } - [IsRetrievable(true)] + [SimpleField(IsHidden = false)] public string ExplicitlyRetrievableText { get; set; } - [Analyzer("en.microsoft")] + [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] public string TextWithAnalyzer { get; set; } - [SearchAnalyzer("es.lucene")] + [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.AsString.EsLucene)] public string TextWithSearchAnalyzer { get; set; } - [IndexAnalyzer("whitespace")] + [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.AsString.Whitespace)] public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } @@ -209,7 +209,7 @@ public class ReflectableModel public ICollection ComplexICollection { get; set; } [JsonIgnore] - [IsRetrievable(false)] + [SimpleField(IsHidden = true)] #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 95aa2723e4cc..2681f11039ea 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs @@ -1,31 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; 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 { - [SerializePropertyNamesAsCamelCase] public struct ReflectableInnerStructCamelCaseModel { + [JsonPropertyName("name")] public string Name { get; set; } } - [SerializePropertyNamesAsCamelCase] 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 e9170515cf82..363727e9aeb9 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Indexes.Models; #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; #else @@ -15,36 +17,35 @@ #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 { - [IsSearchable] + [SearchableField] public string City { get; set; } - [IsFilterable, IsFacetable] + [SimpleField(IsFilterable = true, IsFacetable = true)] public string Country { get; set; } } public struct ReflectableComplexStruct { - [IsSearchable] - [Analyzer("en.microsoft")] + [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] public string Name { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public int Rating { get; set; } // Ensure that leaf-field-specific attributes are ignored by FieldBuilder on complex fields. - [IsSearchable] - [IsFilterable] - [IsSortable] - [IsFacetable] - [IsRetrievable(false)] - [Analyzer("zh-Hant.lucene")] - [IndexAnalyzer("zh-Hant.lucene")] - [SearchAnalyzer("zh-Hant.lucene")] - [SynonymMaps("myMap")] + [SearchableField( + IsFilterable = true, + IsSortable = true, + IsFacetable = true, + IsHidden = true, + AnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + SearchAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + IndexAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + SynonymMapNames = new[] { "myMap" })] public ReflectableAddressStruct Address { get; set; } } @@ -63,37 +64,36 @@ public struct ReflectableStructModel public DateTime TimeWithoutOffset { get; set; } - [IsSearchable] - [SynonymMaps("myMap")] + [SearchableField(SynonymMapNames = new[] { "myMap" })] public string Text { get; set; } public string UnsearchableText { get; set; } - [IsSearchable] + [SearchableField] public string MoreText { get; set; } - [IsFilterable] + [SimpleField(IsFilterable = true)] public string FilterableText { get; set; } - [IsSortable] + [SimpleField(IsSortable = true)] public string SortableText { get; set; } - [IsFacetable] + [SimpleField(IsFacetable = true)] public string FacetableText { get; set; } - [IsRetrievable(false)] + [SimpleField(IsHidden = true)] public string IrretrievableText { get; set; } - [IsRetrievable(true)] + [SimpleField(IsHidden = false)] public string ExplicitlyRetrievableText { get; set; } - [Analyzer("en.microsoft")] + [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] public string TextWithAnalyzer { get; set; } - [SearchAnalyzer("es.lucene")] + [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.AsString.EsLucene)] public string TextWithSearchAnalyzer { get; set; } - [IndexAnalyzer("whitespace")] + [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.AsString.Whitespace)] public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } @@ -209,7 +209,7 @@ public struct ReflectableStructModel public ICollection ComplexICollection { get; set; } [JsonIgnore] - [IsRetrievable(false)] + [SimpleField(IsHidden = true)] #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/Samples/FieldBuilder/AnalyzerAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs deleted file mode 100644 index 7524dc27b654..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Azure.Search.Documents.Indexes.Models; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the generated by for - /// the target property should have its property set to the - /// specified analyzer. - /// - [AttributeUsage(AttributeTargets.Property)] - public class AnalyzerAttribute : Attribute - { - /// - /// Indicates that the specified analyzer should be used. - /// - /// - /// The name of the analyzer. Use one of the names on - /// or the name of a custom analyzer. - /// - public AnalyzerAttribute(string analyzerName) - { - Name = analyzerName; - } - - /// - /// The name of the analyzer. - /// - public string Name { get; } - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs deleted file mode 100644 index 422efc39b0a2..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Azure.Search.Documents.Indexes.Models; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the generated by for - /// the target property should have its property set to the - /// specified analyzer. - /// - [AttributeUsage(AttributeTargets.Property)] - public class IndexAnalyzerAttribute : Attribute - { - /// - /// Indicates that the specified analyzer should be used. - /// - /// - /// The name of the analyzer. Use one of the names on - /// or the name of a custom analyzer. - /// - public IndexAnalyzerAttribute(string analyzerName) - { - Name = analyzerName; - } - - /// - /// The name of the analyzer. - /// - public string Name { get; } - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs deleted file mode 100644 index fc90d369b511..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure.Search.Documents.Samples -{ - - /// - /// Indicates that it is possible to facet on this field. Not valid for - /// geo-point fields. - /// - [AttributeUsage(AttributeTargets.Property)] - public class IsFacetableAttribute : Attribute - { - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs deleted file mode 100644 index f644fb92d759..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the field can be used in filter expressions. - /// - [AttributeUsage(AttributeTargets.Property)] - public class IsFilterableAttribute : Attribute - { - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs deleted file mode 100644 index 9d74c75e8957..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Azure.Search.Documents.Indexes.Models; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates whether the field can be returned in a search result. This - /// defaults to true, so this attribute only has any effect if you use it - /// as [IsRetrievable(false)]. - /// - [AttributeUsage(AttributeTargets.Property)] - public class IsRetrievableAttribute : Attribute - { - /// - /// Indicates that the specified value should be used to negate the - /// flag of the target field. - /// - /// true if the target field should be included in - /// search results, false otherwise. - public IsRetrievableAttribute(bool isRetrievable) - { - IsRetrievable = isRetrievable; - } - - /// - /// true if the target field should be included in search results, false otherwise. - /// - public bool IsRetrievable { get; } - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs deleted file mode 100644 index c181db244d52..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Causes the field to be included in full-text searches. Valid only for - /// string or string collection fields. - /// - [AttributeUsage(AttributeTargets.Property)] - public class IsSearchableAttribute : Attribute - { - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs deleted file mode 100644 index acba89259cb4..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the field can be used in orderby expressions. Not valid - /// for string collection fields. - /// - [AttributeUsage(AttributeTargets.Property)] - public class IsSortableAttribute : Attribute - { - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs deleted file mode 100644 index 0a33a3d0a9a8..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Azure.Search.Documents.Indexes.Models; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the generated by for - /// the target property should have its property set to the - /// specified analyzer. - /// - [AttributeUsage(AttributeTargets.Property)] - public class SearchAnalyzerAttribute : Attribute - { - /// - /// Indicates that the specified analyzer should be used. - /// - /// - /// The name of the analyzer. Use one of the names on - /// or the name of a custom analyzer. - /// - public SearchAnalyzerAttribute(string analyzerName) - { - Name = analyzerName; - } - - /// - /// The name of the analyzer. - /// - public string Name { get; } - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs deleted file mode 100644 index b296197cddff..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Linq; -using System.Reflection; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the public properties of a model type should be serialized as camel-case in order to match - /// the field names of a search index. - /// - /// - /// Types without this attribute are expected to have property names that exactly match their corresponding - /// fields names in Azure Cognitive Search. Otherwise, it would not be possible to use instances of the type to populate - /// the index. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] - public class SerializePropertyNamesAsCamelCaseAttribute : Attribute - { - /// - /// Indicates whether the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute. - /// - /// The type to test. - /// true if the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute, - /// false otherwise. - public static bool IsDefinedOnType() => IsDefinedOnType(typeof(T)); - - /// - /// Indicates whether the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute. - /// - /// The type to test. - /// true if the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute, - /// false otherwise. - public static bool IsDefinedOnType(Type modelType) => - modelType - .GetTypeInfo() - .GetCustomAttributes(typeof(SerializePropertyNamesAsCamelCaseAttribute), inherit: true) - .Any(); - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs deleted file mode 100644 index 428fb380ebaf..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using Azure.Search.Documents.Indexes.Models; - -namespace Azure.Search.Documents.Samples -{ - /// - /// Indicates that the generated by for - /// the target property should have its property set to the - /// specified value. - /// - [AttributeUsage(AttributeTargets.Property)] - public class SynonymMapsAttribute : Attribute - { - /// - /// Indicates that the specified synonym maps should be used for searches on the target field. - /// - /// A list of synonym map names that associates synonym maps with the field. - /// This option can be used only with searchable fields. 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. - /// - public SynonymMapsAttribute(params string[] synonymMaps) - { - SynonymMaps = synonymMaps; - } - - /// - /// A list of synonym map names that associates synonym maps with the field. - /// - public IList SynonymMaps { get; } - } -} 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 000000000000..a201876386be --- /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/SimpleFieldAttributeTests.cs b/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs new file mode 100644 index 000000000000..61cc0e350cc3 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs @@ -0,0 +1,44 @@ +// 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); + } + } +} From 3f3c405110331c6f5c70703f65ef32157bdaeb2c Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Tue, 28 Jul 2020 17:14:42 -0700 Subject: [PATCH 02/13] Add FieldBuilder sample --- sdk/search/Azure.Search.Documents/README.md | 31 +- .../Azure.Search.Documents.netstandard2.0.cs | 2 +- .../src/Indexes/Models/SearchIndex.cs | 73 +++- .../src/Indexes/SimpleFieldAttribute.cs | 31 +- .../tests/FieldBuilderTests.cs | 7 +- .../tests/Models/SearchIndexTests.cs | 30 ++ .../tests/Samples/Readme.cs | 42 ++- .../SessionRecords/Readme/CreateIndex.json | 242 +++--------- .../Readme/CreateManualIndex.json | 345 ++++++++++++++++++ 9 files changed, 565 insertions(+), 238 deletions(-) create mode 100644 sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateManualIndex.json diff --git a/sdk/search/Azure.Search.Documents/README.md b/sdk/search/Azure.Search.Documents/README.md index 2a4568e3d401..c99935301b85 100644 --- a/sdk/search/Azure.Search.Documents/README.md +++ b/sdk/search/Azure.Search.Documents/README.md @@ -217,9 +217,11 @@ We can decorate our own C# types with [attributes from `System.Text.Json`](https public class Hotel { [JsonPropertyName("hotelId")] + [SimpleField(IsKey = true, IsFilterable = true, IsSortable = true)] public string Id { get; set; } [JsonPropertyName("hotelName")] + [SearchableField(IsFilterable = true, IsSortable = true)] public string Name { get; set; } } ``` @@ -276,8 +278,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 +289,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 = 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 +331,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 f550411e130e..03bf15323ca0 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 @@ -1288,7 +1288,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/src/Indexes/Models/SearchIndex.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs index 7a2aa45db8eb..23f75c4f6381 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; @@ -25,9 +27,10 @@ public SearchIndex(string name) Name = name; + _fields = new ChangeTrackingList(); + Analyzers = new ChangeTrackingList(); CharFilters = new ChangeTrackingList(); - Fields = new ChangeTrackingList(); ScoringProfiles = new ChangeTrackingList(); Suggesters = new ChangeTrackingList(); TokenFilters = new ChangeTrackingList(); @@ -48,9 +51,11 @@ public SearchIndex(string name, IEnumerable fields) Name = name; + // Allow the underlying list to be mutated. + _fields = new ChangeTrackingList((Optional>)fields.ToList()); + Analyzers = new ChangeTrackingList(); CharFilters = new ChangeTrackingList(); - Fields = new ChangeTrackingList((Optional>)fields.ToArray()); ScoringProfiles = new ChangeTrackingList(); Suggesters = new ChangeTrackingList(); TokenFilters = new ChangeTrackingList(); @@ -74,11 +79,69 @@ public SearchIndex(string name, IEnumerable fields) public IList CharFilters { get; } /// - /// Gets the fields in the index. - /// Use , , and for help defining valid indexes. + /// Gets or sets the fields in the index. + /// Use to define fields based on a model class, + /// or , , and for help defining valid indexes. /// 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 from a model class using : + /// + /// SearchIndex index = new SearchIndex("hotels") + /// { + /// Fields = 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 explicitly 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") + /// } + /// }; + /// + /// + public IList Fields + { + get => _fields; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value), $"{nameof(Fields)} cannot be null. To clear values, call {nameof(Fields.Clear)}."); + } + + // Allow the underlying list to be mutated. + _fields = new ChangeTrackingList((Optional>)value.ToList()); + } + } /// /// Gets the scoring profiles for the index. diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs index 3ae37d1e3cf3..73620add520d 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs @@ -55,30 +55,15 @@ public class SimpleFieldAttribute : Attribute, ISearchFieldAttribute private protected void SetField(SearchField field) { - if (IsKey) - { - field.IsKey = IsKey; - } + field.IsKey = IsKey; + field.IsHidden = IsHidden; + field.IsFilterable = IsFilterable; + field.IsFacetable = IsFacetable; + field.IsSortable = IsSortable; - if (IsHidden) - { - field.IsHidden = IsHidden; - } - - if (IsFilterable) - { - field.IsFilterable = IsFilterable; - } - - if (IsFacetable) - { - field.IsFacetable = IsFacetable; - } - - if (IsSortable) - { - field.IsSortable = IsSortable; - } + // 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/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index 637eab1e3ce2..8f9daeb0346b 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -361,7 +361,9 @@ public void NestedKeyAttributesAreIgnored() Fields = { new SearchField(nameof(InnerModelWithKey.InnerID), SearchFieldDataType.String), - new SearchField(nameof(InnerModelWithKey.OtherField), SearchFieldDataType.Int32) { IsFilterable = true } + + // 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 }, } } }; @@ -381,7 +383,8 @@ public void PropertiesMarkedAsIgnoredAreIgnored() { Fields = { - new SearchField(nameof(InnerModelWithIgnoredProperties.OtherField), SearchFieldDataType.Int32) { IsFilterable = true } + // 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 } } } }; diff --git a/sdk/search/Azure.Search.Documents/tests/Models/SearchIndexTests.cs b/sdk/search/Azure.Search.Documents/tests/Models/SearchIndexTests.cs index e9a2861e2a08..477cb4087f73 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 dee1ba235a54..a822b3acb2aa 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs @@ -129,9 +129,11 @@ public async Task CreateAndQuery() public class Hotel { [JsonPropertyName("hotelId")] + [SimpleField(IsKey = true, IsFilterable = true, IsSortable = true)] public string Id { get; set; } [JsonPropertyName("hotelName")] + [SearchableField(IsFilterable = true, IsSortable = true)] public string Name { get; set; } } #endregion Snippet:Azure_Search_Tests_Samples_Readme_StaticType @@ -201,7 +203,36 @@ 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 = 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; + } + + [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 +256,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/SessionRecords/Readme/CreateIndex.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/Readme/CreateIndex.json index b9560f9aa8a1..617219aec17a 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 000000000000..997eab4915b6 --- /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 From 300b7a5bd22d51cd65efc1c08080b4cad454db12 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 31 Jul 2020 15:15:33 -0700 Subject: [PATCH 03/13] Resolve PR feedback on code --- .../Azure.Search.Documents/CHANGELOG.md | 3 +- .../src/Indexes/FieldBuilder.cs | 38 +++++++++---------- .../Indexes/FieldBuilderIgnoreAttribute.cs | 2 +- .../src/Indexes/Models/LexicalAnalyzerName.cs | 4 +- .../src/Indexes/Models/SearchIndex.cs | 17 ++------- .../src/Indexes/SearchableFieldAttribute.cs | 6 +-- .../tests/FieldBuilderTests.cs | 6 +-- .../tests/Models/LexicalAnalyzerNameTests.cs | 30 +++++++++++++++ .../tests/Models/ReflectableModel.cs | 14 +++---- .../tests/Models/ReflectableStructModel.cs | 14 +++---- .../tests/Samples/Readme.cs | 2 +- 11 files changed, 78 insertions(+), 58 deletions(-) create mode 100644 sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs diff --git a/sdk/search/Azure.Search.Documents/CHANGELOG.md b/sdk/search/Azure.Search.Documents/CHANGELOG.md index 21cf5915d460..6304ce012d92 100644 --- a/sdk/search/Azure.Search.Documents/CHANGELOG.md +++ b/sdk/search/Azure.Search.Documents/CHANGELOG.md @@ -4,7 +4,8 @@ ### Added -- Add `FieldBuilder` to easily create `SearchIndex` fields from a model type. +- Added `SearchClientOptions.Serializer` to set which `ObjectSerializer` to use for serialization. +- Added `FieldBuilder` to easily create `SearchIndex` fields from a model type. ## 11.0.0 (2020-07-07) diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs index 7fb18c2b13c7..e00abd2c0682 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -21,18 +21,7 @@ namespace Azure.Search.Documents.Indexes /// /// Builds field definitions for a search index by reflecting over a user-defined model type. /// - /// - /// - /// This was ported from the Microsoft.Azure.Search.Service package - /// to make migrating from using Microsoft.Azure.Search to Azure.Search.Documents easier. - /// It also uses System.Text.Json instead of Newtonsoft.Json (JSON.NET). - /// - /// - /// This is only a sample you can include in your code and future implementations may change - /// to follow modern guidelines and design principles. - /// - /// - public static class FieldBuilder + public class FieldBuilder { private static readonly IReadOnlyDictionary s_primitiveTypeMap = new ReadOnlyDictionary( @@ -58,6 +47,20 @@ public static class FieldBuilder 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. @@ -65,14 +68,9 @@ public static class FieldBuilder /// /// The type for which fields will be created, based on its properties. /// - /// - /// The to use to generate field names that match JSON property names. - /// You should use the same value as . - /// will be used if no value is provided. - /// /// A collection of fields. /// . - public static IList Build(Type modelType, ObjectSerializer serializer = null) + public IList Build(Type modelType) { Argument.AssertNotNull(modelType, nameof(modelType)); @@ -85,8 +83,8 @@ ArgumentException FailOnNonObjectDataType() throw new ArgumentException(errorMessage, nameof(modelType)); } - serializer ??= new JsonObjectSerializer(); - IMemberNameConverter nameProvider = serializer as IMemberNameConverter ?? DefaultSerializedNameProvider.Shared; + Serializer ??= new JsonObjectSerializer(); + IMemberNameConverter nameProvider = Serializer as IMemberNameConverter ?? DefaultSerializedNameProvider.Shared; if (ObjectInfo.TryGet(modelType, nameProvider, out ObjectInfo info)) { diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs index d67f0f02cd41..ea337a0a25a6 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilderIgnoreAttribute.cs @@ -17,7 +17,7 @@ namespace Azure.Search.Documents.Indexes /// 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.Property)] + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class FieldBuilderIgnoreAttribute : Attribute { } diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs index 1e1b5874173e..9fc3dabfda5a 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs @@ -7,10 +7,10 @@ public readonly partial struct LexicalAnalyzerName { #pragma warning disable CA1034 // Nested types should not be visible /// - /// The names of all lexical analyzer as string constants. + /// The values of all declared properties as string constants. /// These can be used in and anywhere else constants are required. /// - public static class AsString + public static class Values { /// Microsoft analyzer for Arabic. public const string ArMicrosoft = LexicalAnalyzerName.ArMicrosoftValue; 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 23f75c4f6381..0c1d930dc261 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs @@ -27,10 +27,9 @@ public SearchIndex(string name) Name = name; - _fields = new ChangeTrackingList(); - Analyzers = new ChangeTrackingList(); CharFilters = new ChangeTrackingList(); + Fields = new List(); ScoringProfiles = new ChangeTrackingList(); Suggesters = new ChangeTrackingList(); TokenFilters = new ChangeTrackingList(); @@ -51,11 +50,9 @@ public SearchIndex(string name, IEnumerable fields) Name = name; - // Allow the underlying list to be mutated. - _fields = new ChangeTrackingList((Optional>)fields.ToList()); - Analyzers = new ChangeTrackingList(); CharFilters = new ChangeTrackingList(); + Fields = fields.ToList(); ScoringProfiles = new ChangeTrackingList(); Suggesters = new ChangeTrackingList(); TokenFilters = new ChangeTrackingList(); @@ -81,7 +78,7 @@ public SearchIndex(string name, IEnumerable fields) /// /// Gets or sets the fields in the index. /// Use to define fields based on a model class, - /// or , , and for help defining valid indexes. + /// or , , and to manually define fields. /// Index fields have many constraints that are not validated with until the index is created on the server. /// /// @@ -133,13 +130,7 @@ public IList Fields get => _fields; set { - if (value == null) - { - throw new ArgumentNullException(nameof(value), $"{nameof(Fields)} cannot be null. To clear values, call {nameof(Fields.Clear)}."); - } - - // Allow the underlying list to be mutated. - _fields = new ChangeTrackingList((Optional>)value.ToList()); + _fields = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Fields)} cannot be null. To clear values, call {nameof(Fields.Clear)}."); } } diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs index 68f34b3203e3..9f5e4fb860b0 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SearchableFieldAttribute.cs @@ -16,7 +16,7 @@ public class SearchableFieldAttribute : SimpleFieldAttribute, ISearchFieldAttrib /// 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. + /// String values from LexicalAnalyzerName, or the name of a custom analyzer previously uploaded. public string AnalyzerName { get; set; } /// @@ -24,7 +24,7 @@ public class SearchableFieldAttribute : SimpleFieldAttribute, ISearchFieldAttrib /// 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. + /// String values from LexicalAnalyzerName, or the name of a custom analyzer previously uploaded. public string SearchAnalyzerName { get; set; } /// @@ -32,7 +32,7 @@ public class SearchableFieldAttribute : SimpleFieldAttribute, ISearchFieldAttrib /// 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. + /// String values from LexicalAnalyzerName, or the name of a custom analyzer previously uploaded. public string IndexAnalyzerName { get; set; } /// diff --git a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index 8f9daeb0346b..266a781e40d9 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -400,7 +400,7 @@ public void PropertiesMarkedAsIgnoredAreIgnored() [TestCase(typeof(ModelWithUnsupportedCollectionType), nameof(ModelWithUnsupportedCollectionType.Buffer))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(Type modelType, string invalidPropertyName) { - ArgumentException e = Assert.Throws(() => FieldBuilder.Build(modelType)); + ArgumentException e = Assert.Throws(() => new FieldBuilder().Build(modelType)); string expectedErrorMessage = $"Property '{invalidPropertyName}' is of type '{modelType.GetProperty(invalidPropertyName).PropertyType}', " + @@ -424,7 +424,7 @@ public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(T [TestCase(typeof(ICollection))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedTypes(Type modelType) { - ArgumentException e = Assert.Throws(() => FieldBuilder.Build(modelType)); + ArgumentException e = Assert.Throws(() => new FieldBuilder().Build(modelType)); string expectedErrorMessage = $"Type '{modelType}' does not have properties which map to fields of an Azure Search index. Please use a " + @@ -441,7 +441,7 @@ from type in modelTypes from tuple in testData select (type, tuple.dataType, tuple.fieldName); - private static IList BuildForType(Type modelType) => FieldBuilder.Build(modelType); + private static IList BuildForType(Type modelType) => new FieldBuilder().Build(modelType); private enum Direction { 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 000000000000..7ca0c6c443d0 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs @@ -0,0 +1,30 @@ +// 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 + { + [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); + } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs index eeb2850c802f..61dfee3ac7e4 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs @@ -30,7 +30,7 @@ public class ReflectableAddress public class ReflectableComplexObject { - [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] public string Name { get; set; } [SimpleField(IsFilterable = true)] @@ -42,9 +42,9 @@ public class ReflectableComplexObject IsSortable = true, IsFacetable = true, IsHidden = true, - AnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, - SearchAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, - IndexAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + AnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, SynonymMapNames = new[] { "myMap" })] public ReflectableAddress Address { get; set; } } @@ -87,13 +87,13 @@ public class ReflectableModel [SimpleField(IsHidden = false)] public string ExplicitlyRetrievableText { get; set; } - [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] public string TextWithAnalyzer { get; set; } - [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.AsString.EsLucene)] + [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.Values.EsLucene)] public string TextWithSearchAnalyzer { get; set; } - [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.AsString.Whitespace)] + [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.Values.Whitespace)] public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { 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 363727e9aeb9..252f69cd1f6e 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs @@ -30,7 +30,7 @@ public struct ReflectableAddressStruct public struct ReflectableComplexStruct { - [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] public string Name { get; set; } [SimpleField(IsFilterable = true)] @@ -42,9 +42,9 @@ public struct ReflectableComplexStruct IsSortable = true, IsFacetable = true, IsHidden = true, - AnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, - SearchAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, - IndexAnalyzerName = LexicalAnalyzerName.AsString.ZhHantLucene, + AnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, + IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, SynonymMapNames = new[] { "myMap" })] public ReflectableAddressStruct Address { get; set; } } @@ -87,13 +87,13 @@ public struct ReflectableStructModel [SimpleField(IsHidden = false)] public string ExplicitlyRetrievableText { get; set; } - [SearchableField(AnalyzerName = LexicalAnalyzerName.AsString.EnMicrosoft)] + [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] public string TextWithAnalyzer { get; set; } - [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.AsString.EsLucene)] + [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.Values.EsLucene)] public string TextWithSearchAnalyzer { get; set; } - [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.AsString.Whitespace)] + [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.Values.Whitespace)] public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs index a822b3acb2aa..240f8822f046 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs @@ -208,7 +208,7 @@ public async Task CreateIndex() //@@SearchIndex index = new SearchIndex("hotels") /*@@*/ SearchIndex index = new SearchIndex(Recording.Random.GetName()) { - Fields = FieldBuilder.Build(typeof(Hotel)), + Fields = new FieldBuilder().Build(typeof(Hotel)), Suggesters = { // Suggest query terms from the hotelName field. From 65a794ed231b2839b4743bb97d35ea2503ad6453 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Sat, 1 Aug 2020 06:16:03 -0700 Subject: [PATCH 04/13] Conditionalize FieldBuilder implementation --- .../Directory.Build.props | 33 ++++++++++++--- .../src/Azure.Search.Documents.csproj | 21 ++++------ .../src/Indexes/Models/LexicalAnalyzerName.cs | 2 + .../src/Indexes/Models/SearchIndex.cs | 41 ++++++++++++++++++- .../tests/Azure.Search.Documents.Tests.csproj | 7 ++++ .../tests/Models/LexicalAnalyzerNameTests.cs | 2 + .../tests/Models/RecursiveModel.cs | 4 ++ .../tests/Models/ReflectableModel.cs | 32 +++++++++++++++ .../tests/Models/ReflectableStructModel.cs | 32 +++++++++++++++ .../tests/Samples/Readme.cs | 6 +++ 10 files changed, 162 insertions(+), 18 deletions(-) diff --git a/sdk/search/Azure.Search.Documents/Directory.Build.props b/sdk/search/Azure.Search.Documents/Directory.Build.props index 6428c499177e..ddeb308e8a73 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,19 +10,35 @@ $(UseAzureCoreExperimental) EXPERIMENTAL_SPATIAL;$(DefineConstants) - - $(UseAzureCoreExperimental) - EXPERIMENTAL_SERIALIZER;$(DefineConstants) + + true + EXPERIMENTAL_SERIALIZER;$(DefineConstants) $(UseAzureCoreExperimental) EXPERIMENTAL_DYNAMIC;$(DefineConstants) + true + EXPERIMENTAL_FIELDBUILDER;$(DefineConstants) + + + true + - true + true $(UseProjectReferenceToAzureClients) + + + + + + + + + + + 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 facc263b33f2..32a751377d48 100644 --- a/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj +++ b/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj @@ -11,15 +11,20 @@ Azure Cognitive Search;Azure Search Documents;Azure Search;Search;Cognitive;Search Engine;Azure $(RequiredTargetFrameworks) - - $(DefineConstants); - EXPERIMENTAL_SERIALIZER; - $(NoWarn);AZC0007;AZC0004;AZC0001 + + + + + + + + + @@ -38,15 +43,7 @@ - - - - - - - - diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs index 9fc3dabfda5a..6e455d112525 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/LexicalAnalyzerName.cs @@ -5,6 +5,7 @@ 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. @@ -200,5 +201,6 @@ public static class Values 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 0c1d930dc261..2c4af172e05a 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs @@ -75,6 +75,7 @@ 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, @@ -95,7 +96,7 @@ public SearchIndex(string name, IEnumerable fields) /// }; /// /// For this reason, is settable. In scenarios when the model is not known or cannot be modified, you can - /// also create fields explicitly using helper classes: + /// also create fields manually using helper classes: /// /// SearchIndex index = new SearchIndex("hotels") /// { @@ -125,6 +126,44 @@ public SearchIndex(string name, IEnumerable fields) /// }; /// /// +#else + /// + /// 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. + /// + /// + /// 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; 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 ed84ac4b71b9..3acba3d30bd0 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 @@ -37,6 +37,13 @@ + + + + + + + diff --git a/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs b/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs index 7ca0c6c443d0..fdb2c368789b 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/LexicalAnalyzerNameTests.cs @@ -10,6 +10,7 @@ namespace Azure.Search.Documents.Tests.Models { public class LexicalAnalyzerNameTests { +#if EXPERIMENTAL_FIELDBUILDER [Test] public void PropertiesEqualConstantFields() { @@ -26,5 +27,6 @@ public void PropertiesEqualConstantFields() // 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 66e386fb5891..8e191801548f 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs @@ -11,7 +11,9 @@ namespace Azure.Search.Documents.Tests { public class RecursiveModel { +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#endif public int Data { get; set; } // This is to test that FieldBuilder gracefully fails on recursive models. @@ -20,7 +22,9 @@ public class RecursiveModel public class OtherRecursiveModel { +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true, IsFacetable = true)] +#endif public double Data { get; set; } public RecursiveModel RecursiveReference { 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 61dfee3ac7e4..50eb0c62dc0c 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs @@ -21,22 +21,31 @@ namespace Azure.Search.Documents.Tests { public class ReflectableAddress { +#if EXPERIMENTAL_FIELDBUILDER [SearchableField] +#endif public string City { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true, IsFacetable = true)] +#endif public string Country { get; set; } } public class ReflectableComplexObject { +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#endif public string Name { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#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, @@ -46,6 +55,7 @@ public class ReflectableComplexObject SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, SynonymMapNames = new[] { "myMap" })] +#endif public ReflectableAddress Address { get; set; } } @@ -64,36 +74,56 @@ public class ReflectableModel public DateTime TimeWithoutOffset { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(SynonymMapNames = new[] { "myMap" })] +#endif public string Text { get; set; } public string UnsearchableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField] +#endif public string MoreText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] public string FilterableText { get; set; } +#endif +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsSortable = true)] +#endif public string SortableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFacetable = true)] +#endif public string FacetableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = true)] +#endif public string IrretrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = false)] +#endif public string ExplicitlyRetrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#endif public string TextWithAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.Values.EsLucene)] +#endif public string TextWithSearchAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.Values.Whitespace)] +#endif public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } @@ -209,7 +239,9 @@ public class ReflectableModel public ICollection ComplexICollection { get; set; } [JsonIgnore] +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = true)] +#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/ReflectableStructModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs index 252f69cd1f6e..9e4bca7e3ebc 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs @@ -21,22 +21,31 @@ namespace Azure.Search.Documents.Tests { public struct ReflectableAddressStruct { +#if EXPERIMENTAL_FIELDBUILDER [SearchableField] +#endif public string City { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true, IsFacetable = true)] +#endif public string Country { get; set; } } public struct ReflectableComplexStruct { +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#endif public string Name { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#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, @@ -46,6 +55,7 @@ public struct ReflectableComplexStruct SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, SynonymMapNames = new[] { "myMap" })] +#endif public ReflectableAddressStruct Address { get; set; } } @@ -64,36 +74,56 @@ public struct ReflectableStructModel public DateTime TimeWithoutOffset { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(SynonymMapNames = new[] { "myMap" })] +#endif public string Text { get; set; } public string UnsearchableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField] +#endif public string MoreText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] public string FilterableText { get; set; } +#endif +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsSortable = true)] +#endif public string SortableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFacetable = true)] +#endif public string FacetableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = true)] +#endif public string IrretrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = false)] +#endif public string ExplicitlyRetrievableText { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnMicrosoft)] +#endif public string TextWithAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(SearchAnalyzerName = LexicalAnalyzerName.Values.EsLucene)] +#endif public string TextWithSearchAnalyzer { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SearchableField(IndexAnalyzerName = LexicalAnalyzerName.Values.Whitespace)] +#endif public string TextWithIndexAnalyzer { get; set; } public string[] StringArray { get; set; } @@ -209,7 +239,9 @@ public struct ReflectableStructModel public ICollection ComplexICollection { get; set; } [JsonIgnore] +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = true)] +#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/Samples/Readme.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs index 240f8822f046..2eb319bc129d 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Readme.cs @@ -129,11 +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 @@ -186,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() @@ -222,6 +227,7 @@ public async Task CreateIndex() resources.IndexName = index.Name; } +#endif [Test] [SyncOnly] From ac4552ec4dce70d503630bf16a152850b92076f7 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Sat, 1 Aug 2020 06:26:24 -0700 Subject: [PATCH 05/13] Fix recorded serializer tests --- .../StaticDocumentsWithCustomSerializer.json | 42 +++++++++---------- ...ticDocumentsWithCustomSerializerAsync.json | 42 +++++++++---------- .../StaticDocumentsWithCustomSerializer.json | 22 +++++----- ...ticDocumentsWithCustomSerializerAsync.json | 22 +++++----- 4 files changed, 62 insertions(+), 66 deletions(-) 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 e0d145a13914..e3ef1b1d0dc9 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 177fad718a01..6131da272db1 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/SearchTests/StaticDocumentsWithCustomSerializer.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/SearchTests/StaticDocumentsWithCustomSerializer.json index 5cf4004b2c43..d701d164cd9b 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 3c433dfac537..08e1310d7b0d 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 From caa5d4e8738fb8ecb83ae6be818d82ec3ca86ff7 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Sat, 1 Aug 2020 06:29:00 -0700 Subject: [PATCH 06/13] Update public API and samples --- sdk/search/Azure.Search.Documents/README.md | 26 +++++++++++-------- .../Azure.Search.Documents.netstandard2.0.cs | 12 +++++---- .../src/Indexes/FieldBuilder.cs | 3 +++ .../src/Indexes/Models/SearchIndex.cs | 2 +- .../src/Indexes/SearchIndexClient.cs | 3 +++ .../src/Models/IndexDocumentsAction{T}.cs | 3 +++ .../src/Models/IndexDocumentsBatch{T}.cs | 3 +++ .../src/Models/SearchResults{T}.cs | 5 ++-- .../src/Models/SearchResult{T}.cs | 5 ++-- .../src/Models/SearchSuggestion{T}.cs | 5 ++-- .../src/Models/SuggestResults{T}.cs | 7 +++-- .../src/SearchClient.cs | 3 +++ .../src/SearchClientOptions.cs | 3 +++ .../src/Serialization/JsonSerialization.cs | 4 ++- .../tests/DocumentOperations/IndexingTests.cs | 4 ++- .../tests/DocumentOperations/SearchTests.cs | 4 ++- 16 files changed, 62 insertions(+), 30 deletions(-) diff --git a/sdk/search/Azure.Search.Documents/README.md b/sdk/search/Azure.Search.Documents/README.md index c99935301b85..34696f6e64e5 100644 --- a/sdk/search/Azure.Search.Documents/README.md +++ b/sdk/search/Azure.Search.Documents/README.md @@ -214,16 +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")] - [SimpleField(IsKey = true, IsFilterable = true, IsSortable = true)] - public string Id { get; set; } - - [JsonPropertyName("hotelName")] - [SearchableField(IsFilterable = true, IsSortable = true)] - 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: @@ -292,7 +296,7 @@ SearchIndexClient client = new SearchIndexClient(endpoint, credential); // Create the index using FieldBuilder. SearchIndex index = new SearchIndex("hotels") { - Fields = FieldBuilder.Build(typeof(Hotel)), + Fields = new FieldBuilder().Build(typeof(Hotel)), Suggesters = { // Suggest query terms from the hotelName field. 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 03bf15323ca0..6eedb8798072 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,7 +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.ObjectSerializer Serializer { get { throw null; } set { } } + public Azure.Core.Serialization.ObjectSerializer Serializer { get { throw null; } set { } } public Azure.Search.Documents.SearchClientOptions.ServiceVersion Version { get { throw null; } } public enum ServiceVersion { @@ -104,11 +104,13 @@ public SuggestOptions() { } } namespace Azure.Search.Documents.Indexes { - public static partial class FieldBuilder + public partial class FieldBuilder { - public static System.Collections.Generic.IList Build(System.Type modelType, Azure.Core.ObjectSerializer serializer = null) { throw null; } + 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.Property)] + [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] public partial class FieldBuilderIgnoreAttribute : System.Attribute { public FieldBuilderIgnoreAttribute() { } @@ -758,7 +760,7 @@ 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 AsString + public static partial class Values { public const string ArLucene = "ar.lucene"; public const string ArMicrosoft = "ar.microsoft"; diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs index e00abd2c0682..94eac2991524 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -9,6 +9,9 @@ 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; 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 2c4af172e05a..899db7fd762f 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/Models/SearchIndex.cs @@ -87,7 +87,7 @@ public SearchIndex(string name, IEnumerable fields) /// /// SearchIndex index = new SearchIndex("hotels") /// { - /// Fields = FieldBuilder.Build(typeof(Hotel)), + /// Fields = new FieldBuilder().Build(typeof(Hotel)), /// Suggesters = /// { /// // Suggest query terms from the hotelName field. diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/SearchIndexClient.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SearchIndexClient.cs index c4850a77cb1b..b260bd6b6058 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/Models/IndexDocumentsAction{T}.cs b/sdk/search/Azure.Search.Documents/src/Models/IndexDocumentsAction{T}.cs index 29e047afd647..6eaee9c46128 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 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 c17b0f50dbe1..4bf46ff31cba 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 e1c9d515e969..282f86665167 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 e54c029be127..e34663d6ed32 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 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 b7e287fd4f59..ecbd9bde4446 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 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 6acf2ac76910..d318fc54d9a3 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 2ab0d7fd6093..d767825b1d31 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; diff --git a/sdk/search/Azure.Search.Documents/src/SearchClientOptions.cs b/sdk/search/Azure.Search.Documents/src/SearchClientOptions.cs index d7fafce59720..08b5f7fa11bb 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 18a36a588c46..b786d0bc53d6 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 diff --git a/sdk/search/Azure.Search.Documents/tests/DocumentOperations/IndexingTests.cs b/sdk/search/Azure.Search.Documents/tests/DocumentOperations/IndexingTests.cs index adec6c88b9d0..90fa45b81efd 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 f807d18c162f..0be5ee9280fc 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 From 4b4e2bf897bb01b9b56c1aaf3d63381c34a94e53 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Sat, 1 Aug 2020 07:38:47 -0700 Subject: [PATCH 07/13] Add back conditional FieldBuilder sample --- .../tests/Azure.Search.Documents.Tests.csproj | 4 +- .../tests/FieldBuilderTests.cs | 45 +- .../tests/Models/RecursiveModel.cs | 7 + .../tests/Models/ReflectableCamelCaseModel.cs | 9 + .../tests/Models/ReflectableModel.cs | 50 +- .../Models/ReflectableStructCamelCaseModel.cs | 9 + .../tests/Models/ReflectableStructModel.cs | 50 +- .../Samples/FieldBuilder/AnalyzerAttribute.cs | 34 ++ .../Samples/FieldBuilder/FieldBuilder.cs | 455 ++++++++++++++++++ .../FieldBuilderIgnoreAttribute.cs | 24 + .../FieldBuilder/IndexAnalyzerAttribute.cs | 34 ++ .../FieldBuilder/IsFacetableAttribute.cs | 17 + .../FieldBuilder/IsFilterableAttribute.cs | 15 + .../FieldBuilder/IsRetrievableAttribute.cs | 33 ++ .../FieldBuilder/IsSearchableAttribute.cs | 16 + .../FieldBuilder/IsSortableAttribute.cs | 16 + .../FieldBuilder/SearchAnalyzerAttribute.cs | 34 ++ ...ializePropertyNamesAsCamelCaseAttribute.cs | 42 ++ .../FieldBuilder/SynonymMapsAttribute.cs | 36 ++ 19 files changed, 924 insertions(+), 6 deletions(-) create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilderIgnoreAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs 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 3acba3d30bd0..759c98148658 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 @@ -38,8 +38,10 @@ + + + - diff --git a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index 266a781e40d9..b2b767e2997d 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -7,6 +7,9 @@ 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 using NUnit.Framework; using KeyFieldAttribute = System.ComponentModel.DataAnnotations.KeyAttribute; @@ -362,8 +365,12 @@ public void NestedKeyAttributesAreIgnored() { new SearchField(nameof(InnerModelWithKey.InnerID), SearchFieldDataType.String), +#if EXPERIMENTAL_FIELDBUILDER // 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(InnerModelWithKey.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, +#endif } } }; @@ -383,8 +390,12 @@ public void PropertiesMarkedAsIgnoredAreIgnored() { Fields = { +#if EXPERIMENTAL_FIELDBUILDER // 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 } + new SimpleField(nameof(InnerModelWithIgnoredProperties.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, +#else + new SearchField(nameof(InnerModelWithIgnoredProperties.OtherField), SearchFieldDataType.Int32) { IsFilterable = true }, +#endif } } }; @@ -400,7 +411,7 @@ public void PropertiesMarkedAsIgnoredAreIgnored() [TestCase(typeof(ModelWithUnsupportedCollectionType), nameof(ModelWithUnsupportedCollectionType.Buffer))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(Type modelType, string invalidPropertyName) { - ArgumentException e = Assert.Throws(() => new FieldBuilder().Build(modelType)); + ArgumentException e = Assert.Throws(() => BuildForType(modelType)); string expectedErrorMessage = $"Property '{invalidPropertyName}' is of type '{modelType.GetProperty(invalidPropertyName).PropertyType}', " + @@ -424,7 +435,7 @@ public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(T [TestCase(typeof(ICollection))] public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedTypes(Type modelType) { - ArgumentException e = Assert.Throws(() => new FieldBuilder().Build(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 " + @@ -441,7 +452,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 { @@ -510,7 +525,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; } } @@ -519,7 +538,11 @@ private class ModelWithUnsupportedPrimitiveType [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#else + [IsFilterable] +#endif public decimal Price { get; set; } } @@ -528,7 +551,11 @@ private class ModelWithUnsupportedEnumerableType [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#else + [IsFilterable] +#endif public IEnumerable Buffer { get; set; } } @@ -537,7 +564,11 @@ private class ModelWithUnsupportedCollectionType [KeyField] public string ID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#else + [IsFilterable] +#endif public ICollection Buffer { get; set; } } @@ -546,7 +577,11 @@ private class InnerModelWithKey [KeyField] public string InnerID { get; set; } +#if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#else + [IsFilterable] +#endif public int OtherField { get; set; } } @@ -560,7 +595,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/RecursiveModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs index 8e191801548f..65473f5a7c54 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/RecursiveModel.cs @@ -6,6 +6,9 @@ // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. using Azure.Search.Documents.Indexes; +#if !EXPERIMENTAL_FIELDBUILDER +using Azure.Search.Documents.Samples; +#endif namespace Azure.Search.Documents.Tests { @@ -13,6 +16,8 @@ public class RecursiveModel { #if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] +#else + [IsFilterable] #endif public int Data { get; set; } @@ -24,6 +29,8 @@ public class OtherRecursiveModel { #if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true, IsFacetable = true)] +#else + [IsFilterable, IsFacetable] #endif public double Data { 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 3a698de3c2e0..14456d6be71f 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableCamelCaseModel.cs @@ -2,6 +2,9 @@ // Licensed under the MIT License. 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 @@ -10,12 +13,18 @@ // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. 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] diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs index 50eb0c62dc0c..01c607037f85 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableModel.cs @@ -6,6 +6,9 @@ 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 @@ -23,11 +26,15 @@ 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; } } @@ -36,11 +43,16 @@ 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; } @@ -55,6 +67,16 @@ public class ReflectableComplexObject SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, SynonymMapNames = new[] { "myMap" })] +#else + [IsSearchable] + [IsFilterable] + [IsSortable] + [IsFacetable] + [IsRetrievable(false)] + [Analyzer("zh-Hant.lucene")] + [IndexAnalyzer("zh-Hant.lucene")] + [SearchAnalyzer("zh-Hant.lucene")] + [SynonymMaps("myMap")] #endif public ReflectableAddress Address { get; set; } } @@ -76,6 +98,9 @@ public class ReflectableModel #if EXPERIMENTAL_FIELDBUILDER [SearchableField(SynonymMapNames = new[] { "myMap" })] +#else + [IsSearchable] + [SynonymMaps("myMap")] #endif public string Text { get; set; } @@ -83,46 +108,67 @@ public class ReflectableModel #if EXPERIMENTAL_FIELDBUILDER [SearchableField] +#else + [IsSearchable] #endif public string MoreText { get; set; } #if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] - public string FilterableText { get; set; } +#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; } @@ -241,6 +287,8 @@ public class ReflectableModel [JsonIgnore] #if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = true)] +#else + [IsRetrievable(false)] #endif #pragma warning disable IDE1006 // Naming Styles public RecordEnum recordEnum { get; set; } diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs index 2681f11039ea..f15b9ea23436 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructCamelCaseModel.cs @@ -2,6 +2,9 @@ // Licensed under the MIT License. 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 @@ -10,12 +13,18 @@ // TODO: Remove when https://github.com/Azure/azure-sdk-for-net/issues/11166 is completed. 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] diff --git a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs index 9e4bca7e3ebc..8655a831c339 100644 --- a/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs +++ b/sdk/search/Azure.Search.Documents/tests/Models/ReflectableStructModel.cs @@ -6,6 +6,9 @@ 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 @@ -23,11 +26,15 @@ 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; } } @@ -36,11 +43,16 @@ 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; } @@ -55,6 +67,16 @@ public struct ReflectableComplexStruct SearchAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, IndexAnalyzerName = LexicalAnalyzerName.Values.ZhHantLucene, SynonymMapNames = new[] { "myMap" })] +#else + [IsSearchable] + [IsFilterable] + [IsSortable] + [IsFacetable] + [IsRetrievable(false)] + [Analyzer("zh-Hant.lucene")] + [IndexAnalyzer("zh-Hant.lucene")] + [SearchAnalyzer("zh-Hant.lucene")] + [SynonymMaps("myMap")] #endif public ReflectableAddressStruct Address { get; set; } } @@ -76,6 +98,9 @@ public struct ReflectableStructModel #if EXPERIMENTAL_FIELDBUILDER [SearchableField(SynonymMapNames = new[] { "myMap" })] +#else + [IsSearchable] + [SynonymMaps("myMap")] #endif public string Text { get; set; } @@ -83,46 +108,67 @@ public struct ReflectableStructModel #if EXPERIMENTAL_FIELDBUILDER [SearchableField] +#else + [IsSearchable] #endif public string MoreText { get; set; } #if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsFilterable = true)] - public string FilterableText { get; set; } +#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; } @@ -241,6 +287,8 @@ public struct ReflectableStructModel [JsonIgnore] #if EXPERIMENTAL_FIELDBUILDER [SimpleField(IsHidden = true)] +#else + [IsRetrievable(false)] #endif #pragma warning disable IDE1006 // Naming Styles public RecordEnum recordEnum { get; set; } diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs new file mode 100644 index 000000000000..7524dc27b654 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/AnalyzerAttribute.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the generated by for + /// the target property should have its property set to the + /// specified analyzer. + /// + [AttributeUsage(AttributeTargets.Property)] + public class AnalyzerAttribute : Attribute + { + /// + /// Indicates that the specified analyzer should be used. + /// + /// + /// The name of the analyzer. Use one of the names on + /// or the name of a custom analyzer. + /// + public AnalyzerAttribute(string analyzerName) + { + Name = analyzerName; + } + + /// + /// The name of the analyzer. + /// + public string Name { get; } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs new file mode 100644 index 000000000000..9d825e0c9f9b --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilder.cs @@ -0,0 +1,455 @@ +// 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.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Search.Documents.Indexes.Models; +#if EXPERIMENTAL_SPATIAL +using Azure.Core.Spatial; +#else +using Microsoft.Spatial; +#endif + +namespace Azure.Search.Documents.Samples +{ + /// + /// Builds field definitions for a search index by reflecting over a user-defined model type. + /// + /// + /// + /// This was ported from the Microsoft.Azure.Search.Service package + /// to make migrating from using Microsoft.Azure.Search to Azure.Search.Documents easier. + /// It also uses System.Text.Json instead of Newtonsoft.Json (JSON.NET). + /// + /// + /// This is only a sample you can include in your code and future implementations may change + /// to follow modern guidelines and design principles. + /// + /// + public static class 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, +#else + [typeof(GeographyPoint)] = SearchFieldDataType.GeographyPoint, +#endif + }); + + private static readonly ISet s_unsupportedTypes = + new HashSet + { + typeof(decimal), + }; + + private static JsonNamingPolicy CamelCaseResolver { get; } = JsonNamingPolicy.CamelCase; + + private static JsonNamingPolicy DefaultResolver { get; } = DefaultJsonNamingPolicy.Shared; + + /// + /// Creates a collection 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 static IList BuildForType() => BuildForType(typeof(T)); + + /// + /// Creates a collection 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 static IList BuildForType(Type modelType) + { + bool useCamelCase = SerializePropertyNamesAsCamelCaseAttribute.IsDefinedOnType(modelType); + JsonNamingPolicy namingPolicy = useCamelCase + ? CamelCaseResolver + : DefaultResolver; + return BuildForType(modelType, namingPolicy); + } + + /// + /// Creates a collection of objects corresponding to + /// the properties of the type supplied. + /// + /// + /// The type for which fields will be created, based on its properties. + /// + /// + /// to use. + /// This ensures that the field names are generated in a way that is + /// consistent with the way the model will be serialized. + /// + /// A collection of fields. + public static IList BuildForType(JsonNamingPolicy namingPolicy) => BuildForType(typeof(T), namingPolicy); + + /// + /// Creates a collection of objects corresponding to + /// the properties of the type supplied. + /// + /// + /// The type for which fields will be created, based on its properties. + /// + /// + /// to use. + /// Contract resolver that the SearchIndexClient will use. + /// This ensures that the field names are generated in a way that is + /// consistent with the way the model will be serialized. + /// + /// A collection of fields. + /// or is null. + public static IList BuildForType(Type modelType, JsonNamingPolicy namingPolicy) + { + if (modelType is null) + { throw new ArgumentNullException(nameof(modelType)); + } + + if (namingPolicy is null) + { throw new ArgumentNullException(nameof(namingPolicy)); + } + + 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)); + } + + if (ObjectInfo.TryGet(modelType, out ObjectInfo info)) + { + if (info.Properties.Length == 0) + { + throw FailOnNonObjectDataType(); + } + + // Use Stack to avoid a dependency on ImmutableStack for now. + return BuildForTypeRecursive(modelType, info, namingPolicy, new Stack(new[] { modelType })); + } + + throw FailOnNonObjectDataType(); + } + + private static IList BuildForTypeRecursive( + Type modelType, + ObjectInfo info, + JsonNamingPolicy namingPolicy, + Stack processedTypes) + { + SearchField BuildField(PropertyInfo prop) + { + static bool ShouldIgnore(Attribute attribute) => + attribute is JsonIgnoreAttribute || 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 = + BuildForTypeRecursive(underlyingClrType, info, namingPolicy, processedTypes); + + string fieldName = namingPolicy.ConvertName(prop.Name); + + SearchField field = new SearchField(fieldName, dataType); + foreach (SearchField subField in subFields) + { + field.Fields.Add(subField); + } + + return field; + } + finally + { + processedTypes.Pop(); + } + } + + SearchField CreateSimpleField(SearchFieldDataType SearchFieldDataType) + { + string fieldName = namingPolicy.ConvertName(prop.Name); + + SearchField field = new SearchField(fieldName, SearchFieldDataType); + foreach (Attribute attribute in attributes) + { + switch (attribute) + { + case IsSearchableAttribute _: + field.IsSearchable = true; + break; + + case IsFilterableAttribute _: + field.IsFilterable = true; + break; + + case IsSortableAttribute _: + field.IsSortable = true; + break; + + case IsFacetableAttribute _: + field.IsFacetable = true; + break; + + case IsRetrievableAttribute isRetrievableAttribute: + field.IsHidden = !isRetrievableAttribute.IsRetrievable; + break; + + case AnalyzerAttribute analyzerAttribute: + field.AnalyzerName = analyzerAttribute.Name; + break; + + case SearchAnalyzerAttribute searchAnalyzerAttribute: + field.SearchAnalyzerName = searchAnalyzerAttribute.Name; + break; + + case IndexAnalyzerAttribute indexAnalyzerAttribute: + field.IndexAnalyzerName = indexAnalyzerAttribute.Name; + break; + + case SynonymMapsAttribute synonymMapsAttribute: + foreach (string synonymMapName in synonymMapsAttribute.SynonymMaps) + { + field.SynonymMapNames.Add(synonymMapName); + } + 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 [JsonIgnore] or " + + "[FieldBuilderIgnore] and define the field by creating a SearchField object."; + + return new ArgumentException(errorMessage, nameof(modelType)); + } + + IDataTypeInfo dataTypeInfo = GetDataTypeInfo(prop.PropertyType, namingPolicy); + + return dataTypeInfo.Match( + onUnknownDataType: () => throw FailOnUnknownDataType(), + onSimpleDataType: CreateSimpleField, + onComplexDataType: CreateComplexField); + } + + return info.Properties.Select(BuildField).Where(field => field != null).ToArray(); + } + + private static IDataTypeInfo GetDataTypeInfo(Type propertyType, JsonNamingPolicy namingPolicy) + { + 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], namingPolicy); + } + else if (TryGetEnumerableElementType(propertyType, out Type elementType)) + { + IDataTypeInfo elementTypeInfo = GetDataTypeInfo(elementType, namingPolicy); + return DataTypeInfo.AsCollection(elementTypeInfo); + } + else if (ObjectInfo.TryGet(propertyType, 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(Type type) + { + Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + } + + public static bool TryGet(Type type, out ObjectInfo info) + { + // Close approximation to Newtonsoft.Json.Serialization.DefaultContractResolver. + if (!type.IsPrimitive && !type.IsEnum && !s_unsupportedTypes.Contains(type) && !s_primitiveTypeMap.ContainsKey(type) && !typeof(IEnumerable).IsAssignableFrom(type)) + { + info = new ObjectInfo(type); + return true; + } + + info = null; + return false; + } + + public PropertyInfo[] Properties { get; } + } + + private class DefaultJsonNamingPolicy : JsonNamingPolicy + { + public static JsonNamingPolicy Shared { get; } = new DefaultJsonNamingPolicy(); + + public override string ConvertName(string name) => name; + } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilderIgnoreAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/FieldBuilderIgnoreAttribute.cs new file mode 100644 index 000000000000..bc0316d94076 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/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.Samples +{ + /// + /// 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.Property)] + public class FieldBuilderIgnoreAttribute : Attribute + { + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs new file mode 100644 index 000000000000..422efc39b0a2 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IndexAnalyzerAttribute.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the generated by for + /// the target property should have its property set to the + /// specified analyzer. + /// + [AttributeUsage(AttributeTargets.Property)] + public class IndexAnalyzerAttribute : Attribute + { + /// + /// Indicates that the specified analyzer should be used. + /// + /// + /// The name of the analyzer. Use one of the names on + /// or the name of a custom analyzer. + /// + public IndexAnalyzerAttribute(string analyzerName) + { + Name = analyzerName; + } + + /// + /// The name of the analyzer. + /// + public string Name { get; } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs new file mode 100644 index 000000000000..fc90d369b511 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFacetableAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Search.Documents.Samples +{ + + /// + /// Indicates that it is possible to facet on this field. Not valid for + /// geo-point fields. + /// + [AttributeUsage(AttributeTargets.Property)] + public class IsFacetableAttribute : Attribute + { + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs new file mode 100644 index 000000000000..f644fb92d759 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsFilterableAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the field can be used in filter expressions. + /// + [AttributeUsage(AttributeTargets.Property)] + public class IsFilterableAttribute : Attribute + { + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs new file mode 100644 index 000000000000..9d74c75e8957 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsRetrievableAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates whether the field can be returned in a search result. This + /// defaults to true, so this attribute only has any effect if you use it + /// as [IsRetrievable(false)]. + /// + [AttributeUsage(AttributeTargets.Property)] + public class IsRetrievableAttribute : Attribute + { + /// + /// Indicates that the specified value should be used to negate the + /// flag of the target field. + /// + /// true if the target field should be included in + /// search results, false otherwise. + public IsRetrievableAttribute(bool isRetrievable) + { + IsRetrievable = isRetrievable; + } + + /// + /// true if the target field should be included in search results, false otherwise. + /// + public bool IsRetrievable { get; } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs new file mode 100644 index 000000000000..c181db244d52 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSearchableAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Causes the field to be included in full-text searches. Valid only for + /// string or string collection fields. + /// + [AttributeUsage(AttributeTargets.Property)] + public class IsSearchableAttribute : Attribute + { + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs new file mode 100644 index 000000000000..acba89259cb4 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/IsSortableAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the field can be used in orderby expressions. Not valid + /// for string collection fields. + /// + [AttributeUsage(AttributeTargets.Property)] + public class IsSortableAttribute : Attribute + { + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs new file mode 100644 index 000000000000..0a33a3d0a9a8 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SearchAnalyzerAttribute.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the generated by for + /// the target property should have its property set to the + /// specified analyzer. + /// + [AttributeUsage(AttributeTargets.Property)] + public class SearchAnalyzerAttribute : Attribute + { + /// + /// Indicates that the specified analyzer should be used. + /// + /// + /// The name of the analyzer. Use one of the names on + /// or the name of a custom analyzer. + /// + public SearchAnalyzerAttribute(string analyzerName) + { + Name = analyzerName; + } + + /// + /// The name of the analyzer. + /// + public string Name { get; } + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs new file mode 100644 index 000000000000..b296197cddff --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SerializePropertyNamesAsCamelCaseAttribute.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Reflection; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the public properties of a model type should be serialized as camel-case in order to match + /// the field names of a search index. + /// + /// + /// Types without this attribute are expected to have property names that exactly match their corresponding + /// fields names in Azure Cognitive Search. Otherwise, it would not be possible to use instances of the type to populate + /// the index. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] + public class SerializePropertyNamesAsCamelCaseAttribute : Attribute + { + /// + /// Indicates whether the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute. + /// + /// The type to test. + /// true if the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute, + /// false otherwise. + public static bool IsDefinedOnType() => IsDefinedOnType(typeof(T)); + + /// + /// Indicates whether the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute. + /// + /// The type to test. + /// true if the given type is annotated with SerializePropertyNamesAsCamelCaseAttribute, + /// false otherwise. + public static bool IsDefinedOnType(Type modelType) => + modelType + .GetTypeInfo() + .GetCustomAttributes(typeof(SerializePropertyNamesAsCamelCaseAttribute), inherit: true) + .Any(); + } +} diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs new file mode 100644 index 000000000000..428fb380ebaf --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/FieldBuilder/SynonymMapsAttribute.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Azure.Search.Documents.Indexes.Models; + +namespace Azure.Search.Documents.Samples +{ + /// + /// Indicates that the generated by for + /// the target property should have its property set to the + /// specified value. + /// + [AttributeUsage(AttributeTargets.Property)] + public class SynonymMapsAttribute : Attribute + { + /// + /// Indicates that the specified synonym maps should be used for searches on the target field. + /// + /// A list of synonym map names that associates synonym maps with the field. + /// This option can be used only with searchable fields. 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. + /// + public SynonymMapsAttribute(params string[] synonymMaps) + { + SynonymMaps = synonymMaps; + } + + /// + /// A list of synonym map names that associates synonym maps with the field. + /// + public IList SynonymMaps { get; } + } +} From fded56e1b4f48be666e344e9e117781377f96b75 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 7 Aug 2020 16:27:32 -0700 Subject: [PATCH 08/13] Use default attribute property values for FieldBuilder --- .../src/Indexes/FieldBuilder.cs | 6 ++- .../src/Indexes/Models/ComplexField.cs | 15 ++++++ .../src/Indexes/SimpleFieldAttribute.cs | 10 ++-- .../tests/FieldBuilderTests.cs | 33 ++++++++---- .../tests/SimpleFieldAttributeTests.cs | 50 +++++++++++++++++++ 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs index 94eac2991524..b1a7b46eaf5d 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -141,7 +141,8 @@ SearchField CreateComplexField(SearchFieldDataType dataType, Type underlyingClrT return null; } - SearchField field = new SearchField(prop.SerializedName, dataType); + // 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); @@ -163,7 +164,8 @@ SearchField CreateSimpleField(SearchFieldDataType SearchFieldDataType) return null; } - SearchField field = new SearchField(prop.SerializedName, SearchFieldDataType); + // 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) 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 13536e53811f..fdaadfd3ec5f 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/SimpleFieldAttribute.cs b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs index 73620add520d..d48c027de2b4 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/SimpleFieldAttribute.cs @@ -61,9 +61,13 @@ private protected void SetField(SearchField field) field.IsFacetable = IsFacetable; field.IsSortable = IsSortable; - // Use a SearchableFieldAttribute instead, which will override this property. - // The service will return Searchable == false for all non-searchable simple types. - field.IsSearchable = false; + // 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/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index b2b767e2997d..a3fa4c50ec03 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -358,21 +358,29 @@ public void NestedKeyAttributesAreIgnored() { var expectedFields = new SearchField[] { - new SearchField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, - new SearchField(nameof(ModelWithNestedKey.Inner), SearchFieldDataType.Complex) +#if EXPERIMENTAL_FIELDBUILDER + new SimpleField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, + new ComplexField(nameof(ModelWithNestedKey.Inner)) { Fields = { - new SearchField(nameof(InnerModelWithKey.InnerID), SearchFieldDataType.String), + new SimpleField(nameof(InnerModelWithKey.InnerID), SearchFieldDataType.String), -#if EXPERIMENTAL_FIELDBUILDER // 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 }, -#endif } } +#endif }; IList actualFields = BuildForType(typeof(ModelWithNestedKey)); @@ -385,19 +393,26 @@ public void PropertiesMarkedAsIgnoredAreIgnored() { var expectedFields = new SearchField[] { - new SearchField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, - new SearchField(nameof(ModelWithNestedKey.Inner), SearchFieldDataType.Collection(SearchFieldDataType.Complex)) +#if EXPERIMENTAL_FIELDBUILDER + new SimpleField(nameof(ModelWithNestedKey.ID), SearchFieldDataType.String) { IsKey = true }, + new ComplexField(nameof(ModelWithNestedKey.Inner), collection: true) { Fields = { -#if EXPERIMENTAL_FIELDBUILDER // 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 }, -#endif } } +#endif }; IList actualFields = BuildForType(typeof(ModelWithIgnoredProperties)); diff --git a/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs b/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs index 61cc0e350cc3..9e4d5676d5f1 100644 --- a/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/SimpleFieldAttributeTests.cs @@ -40,5 +40,55 @@ public void CreatesEquivalentField( 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); + } } } From 31126196fc0be0a4b97229ab442c2c918ba9ed82 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 7 Aug 2020 16:41:50 -0700 Subject: [PATCH 09/13] Update to Azure.Core package reference --- eng/Packages.Data.props | 4 ++-- .../Azure.Search.Documents/Directory.Build.props | 15 +-------------- .../src/Azure.Search.Documents.csproj | 1 + 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 14b829b72a73..6e4e3497bccd 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -11,8 +11,8 @@ - - + + diff --git a/sdk/search/Azure.Search.Documents/Directory.Build.props b/sdk/search/Azure.Search.Documents/Directory.Build.props index ddeb308e8a73..e3cacff8140d 100644 --- a/sdk/search/Azure.Search.Documents/Directory.Build.props +++ b/sdk/search/Azure.Search.Documents/Directory.Build.props @@ -18,27 +18,14 @@ $(UseAzureCoreExperimental) EXPERIMENTAL_DYNAMIC;$(DefineConstants) - true + true EXPERIMENTAL_FIELDBUILDER;$(DefineConstants) - - true - true $(UseProjectReferenceToAzureClients) - - - - - - + From be217afeceb9976de31f1a777fbf0c6912b16cdd Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 7 Aug 2020 20:48:52 -0700 Subject: [PATCH 10/13] Update FieldBuilder exception message with docs Relates to #14058 --- .../Azure.Search.Documents/samples/README.md | 1 + .../samples/Sample04_FieldBuilder.md | 127 ++++++++ .../src/Indexes/FieldBuilder.cs | 13 +- .../tests/Azure.Search.Documents.Tests.csproj | 5 +- .../tests/FieldBuilderTests.cs | 3 +- ...ld.Data.cs => Sample01_HelloWorld.Data.cs} | 0 .../{HelloWorld.cs => Sample01_HelloWorld.cs} | 0 .../tests/Samples/Sample04_FieldBuilder.cs | 137 ++++++++ .../FieldBuilderSample/CreateIndex.json | 292 ++++++++++++++++++ 9 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md rename sdk/search/Azure.Search.Documents/tests/Samples/{HelloWorld.Data.cs => Sample01_HelloWorld.Data.cs} (100%) rename sdk/search/Azure.Search.Documents/tests/Samples/{HelloWorld.cs => Sample01_HelloWorld.cs} (100%) create mode 100644 sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilder.cs create mode 100644 sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderSample/CreateIndex.json diff --git a/sdk/search/Azure.Search.Documents/samples/README.md b/sdk/search/Azure.Search.Documents/samples/README.md index 78e2d75aa4ab..6691dd0c7407 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). +- Build a Search index from a model using [FieldBuilder](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md). ## Sample Code to Include in your Projects diff --git a/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md b/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md new file mode 100644 index 000000000000..4a74b9ef5741 --- /dev/null +++ b/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md @@ -0,0 +1,127 @@ +# Azure.Search.Documents Samples - FieldBuilder + +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. + +## Model + +Consider the following model, which declares a property of an `enum` type. + +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilder_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 Genre 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 Genre +{ + Unknown, + Action, + Comedy, + Drama, + Fantasy, + Horror, + Romance, + SciFi, +} +``` + +The property is attributed with `[FieldBuilderIgnore]` so that `FieldBuilder` will ignore it +when generating fields for a Search index. + +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 processed by default because you may instead want to serialize them as their +integer values to save space in an index. + +## 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 `Genre` enum +to a string, and define the `genre` index field ourselves. + +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilder_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_FieldBuilder_UploadDocument +Movie movie = new Movie +{ + Id = Guid.NewGuid().ToString("n"), + Name = "The Lord of the Rings: The Return of the King", + Genre = Genre.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 `Genre` enum value. diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs index b1a7b46eaf5d..9bcc896571ee 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -26,6 +26,8 @@ namespace Azure.Search.Documents.Indexes /// public class FieldBuilder { + private const string HelpLink = "https://aka.ms/azsdk/net/search/fieldbuilder"; + private static readonly IReadOnlyDictionary s_primitiveTypeMap = new ReadOnlyDictionary( new Dictionary() @@ -201,10 +203,13 @@ 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 [JsonIgnore] or " + - "[FieldBuilderIgnore] and define the field by creating a SearchField object."; + "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)); + return new ArgumentException(errorMessage, nameof(modelType)) + { + HelpLink = HelpLink, + }; } IDataTypeInfo dataTypeInfo = GetDataTypeInfo(prop.PropertyType, nameProvider); @@ -215,7 +220,7 @@ ArgumentException FailOnUnknownDataType() onComplexDataType: CreateComplexField); } - return info.Properties.Select(BuildField).Where(field => field != null).ToArray(); + return info.Properties.Select(BuildField).Where(field => field != null).ToList(); } private static IDataTypeInfo GetDataTypeInfo(Type propertyType, IMemberNameConverter nameProvider) 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 759c98148658..c293c90cb3ef 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 @@ -34,16 +34,17 @@ - + - + + diff --git a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index a3fa4c50ec03..fe507e48af2b 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -431,11 +431,12 @@ public void FieldBuilderFailsWithHelpfulErrorMessageOnUnsupportedPropertyTypes(T 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))] 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_FieldBuilder.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilder.cs new file mode 100644 index 000000000000..83627b98181f --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilder.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 FieldBuilderSample : SearchTestBase + { + public FieldBuilderSample(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_FieldBuilder_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_FieldBuilder_CreateIndex + + // Make sure the index is removed. + resources.IndexName = index.Name; + + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilder_UploadDocument + Movie movie = new Movie + { + Id = Guid.NewGuid().ToString("n"), + Name = "The Lord of the Rings: The Return of the King", + Genre = Genre.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_FieldBuilder_UploadDocument + } + } + + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilder_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 Genre 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 Genre + { + Unknown, + Action, + Comedy, + Drama, + Fantasy, + Horror, + Romance, + SciFi, + } + #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilder_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/SessionRecords/FieldBuilderSample/CreateIndex.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderSample/CreateIndex.json new file mode 100644 index 000000000000..f240844f405e --- /dev/null +++ b/sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderSample/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 From 0e5ad30598a03d16974f98e7ab3a2b36347d2072 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Fri, 7 Aug 2020 21:14:07 -0700 Subject: [PATCH 11/13] Use relative link to Sample04_FieldBuilder.md Created new issue #14060 to track using a fully-qualified URL once this PR is merged. --- sdk/search/Azure.Search.Documents/samples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/search/Azure.Search.Documents/samples/README.md b/sdk/search/Azure.Search.Documents/samples/README.md index 6691dd0c7407..5d612121d5bd 100644 --- a/sdk/search/Azure.Search.Documents/samples/README.md +++ b/sdk/search/Azure.Search.Documents/samples/README.md @@ -14,7 +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). -- Build a Search index from a model using [FieldBuilder](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md). +- Build a Search index from a model using [FieldBuilder](Sample04_FieldBuilder.md). ## Sample Code to Include in your Projects From 4367e3e6e2714da151600cbd8fc7f5aaa7d52e2b Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Mon, 10 Aug 2020 17:07:28 -0700 Subject: [PATCH 12/13] Remove reference to Microsoft.Spatial in production code --- sdk/search/Azure.Search.Documents/Directory.Build.props | 7 ------- .../Azure.Search.Documents/src/Indexes/FieldBuilder.cs | 4 ---- .../tests/Azure.Search.Documents.Tests.csproj | 3 +++ .../Azure.Search.Documents/tests/FieldBuilderTests.cs | 6 ++++++ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/sdk/search/Azure.Search.Documents/Directory.Build.props b/sdk/search/Azure.Search.Documents/Directory.Build.props index e3cacff8140d..81e574947ae7 100644 --- a/sdk/search/Azure.Search.Documents/Directory.Build.props +++ b/sdk/search/Azure.Search.Documents/Directory.Build.props @@ -36,13 +36,6 @@ Include="Azure.Core.Experimental" /> - - - - - diff --git a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs index 9bcc896571ee..42d695aa80ce 100644 --- a/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/src/Indexes/FieldBuilder.cs @@ -15,8 +15,6 @@ using Azure.Search.Documents.Indexes.Models; #if EXPERIMENTAL_SPATIAL using Azure.Core.Spatial; -#else -using Microsoft.Spatial; #endif namespace Azure.Search.Documents.Indexes @@ -41,8 +39,6 @@ public class FieldBuilder [typeof(DateTimeOffset)] = SearchFieldDataType.DateTimeOffset, #if EXPERIMENTAL_SPATIAL [typeof(PointGeometry)] = SearchFieldDataType.GeographyPoint, -#else - [typeof(GeographyPoint)] = SearchFieldDataType.GeographyPoint, #endif }); 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 c293c90cb3ef..859ff58e0563 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 @@ -31,6 +31,9 @@ + + + diff --git a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs index fe507e48af2b..b58dd4469f36 100644 --- a/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/FieldBuilderTests.cs @@ -68,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 = @@ -122,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)), @@ -449,6 +453,8 @@ 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(() => BuildForType(modelType)); From fdbdb323a3728525e3211901aa5efcc1708ccf15 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Mon, 10 Aug 2020 17:22:17 -0700 Subject: [PATCH 13/13] Refocus FieldBuilder sample on FieldBuilderIgnore --- .../Azure.Search.Documents/samples/README.md | 2 +- ...lder.md => Sample04_FieldBuilderIgnore.md} | 35 +++++++++++-------- ...lder.cs => Sample04_FieldBuilderIgnore.cs} | 22 ++++++------ .../CreateIndex.json | 0 4 files changed, 33 insertions(+), 26 deletions(-) rename sdk/search/Azure.Search.Documents/samples/{Sample04_FieldBuilder.md => Sample04_FieldBuilderIgnore.md} (74%) rename sdk/search/Azure.Search.Documents/tests/Samples/{Sample04_FieldBuilder.cs => Sample04_FieldBuilderIgnore.cs} (90%) rename sdk/search/Azure.Search.Documents/tests/SessionRecords/{FieldBuilderSample => FieldBuilderIgnore}/CreateIndex.json (100%) diff --git a/sdk/search/Azure.Search.Documents/samples/README.md b/sdk/search/Azure.Search.Documents/samples/README.md index 5d612121d5bd..4e5d0558f0df 100644 --- a/sdk/search/Azure.Search.Documents/samples/README.md +++ b/sdk/search/Azure.Search.Documents/samples/README.md @@ -14,7 +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). -- Build a Search index from a model using [FieldBuilder](Sample04_FieldBuilder.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_FieldBuilder.md b/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilderIgnore.md similarity index 74% rename from sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md rename to sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilderIgnore.md index 4a74b9ef5741..258a78ca273a 100644 --- a/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilder.md +++ b/sdk/search/Azure.Search.Documents/samples/Sample04_FieldBuilderIgnore.md @@ -1,13 +1,20 @@ -# Azure.Search.Documents Samples - FieldBuilder +# 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. +Consider the following model, which declares a property of an `enum` type, +which are not currently supported. -```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilder_Types +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_Types public class Movie { [SimpleField(IsKey = true)] @@ -18,7 +25,7 @@ public class Movie [FieldBuilderIgnore] [JsonConverter(typeof(JsonStringEnumConverter))] - public Genre Genre { get; set; } + public MovieGenre Genre { get; set; } [SimpleField(IsFacetable = true, IsFilterable = true, IsSortable = true)] public int Year { get; set; } @@ -27,7 +34,7 @@ public class Movie public double Rating { get; set; } } -public enum Genre +public enum MovieGenre { Unknown, Action, @@ -40,23 +47,23 @@ public enum Genre } ``` -The property is attributed with `[FieldBuilderIgnore]` so that `FieldBuilder` will ignore it -when generating fields for a Search index. - 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 processed by default because you may instead want to serialize them as their +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 `Genre` enum +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_FieldBuilder_CreateIndex +```C# Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_CreateIndex Uri endpoint = new Uri(Environment.GetEnvironmentVariable("SEARCH_ENDPOINT")); string key = Environment.GetEnvironmentVariable("SEARCH_API_KEY"); @@ -109,12 +116,12 @@ indexClient.CreateIndex(index); 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_FieldBuilder_UploadDocument +```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 = Genre.Fantasy, + Genre = MovieGenre.Fantasy, Year = 2003, Rating = 9.1 }; @@ -124,4 +131,4 @@ SearchClient searchClient = indexClient.GetSearchClient(index.Name); searchClient.UploadDocuments(new[] { movie }); ``` -Similarly, deserializing converts it from a string back into a `Genre` enum value. +Similarly, deserializing converts it from a string back into a `MovieGenre` enum value. diff --git a/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilder.cs b/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilderIgnore.cs similarity index 90% rename from sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilder.cs rename to sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilderIgnore.cs index 83627b98181f..49df2db12d6b 100644 --- a/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilder.cs +++ b/sdk/search/Azure.Search.Documents/tests/Samples/Sample04_FieldBuilderIgnore.cs @@ -15,9 +15,9 @@ 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 FieldBuilderSample : SearchTestBase + public class FieldBuilderIgnore : SearchTestBase { - public FieldBuilderSample(bool async, SearchClientOptions.ServiceVersion serviceVersion) + public FieldBuilderIgnore(bool async, SearchClientOptions.ServiceVersion serviceVersion) : base(async, serviceVersion, null /* RecordedTestMode.Record /* to re-record */) { } @@ -30,7 +30,7 @@ public async Task CreateIndex() Environment.SetEnvironmentVariable("SEARCH_ENDPOINT", resources.Endpoint.ToString()); Environment.SetEnvironmentVariable("SEARCH_API_KEY", resources.PrimaryApiKey); - #region Snippet:Azure_Search_Tests_Sample2_FieldBuilder_CreateIndex + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_CreateIndex Uri endpoint = new Uri(Environment.GetEnvironmentVariable("SEARCH_ENDPOINT")); string key = Environment.GetEnvironmentVariable("SEARCH_API_KEY"); @@ -78,17 +78,17 @@ public async Task CreateIndex() // Create the index. indexClient.CreateIndex(index); - #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilder_CreateIndex + #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_CreateIndex // Make sure the index is removed. resources.IndexName = index.Name; - #region Snippet:Azure_Search_Tests_Sample2_FieldBuilder_UploadDocument + #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 = Genre.Fantasy, + Genre = MovieGenre.Fantasy, Year = 2003, Rating = 9.1 }; @@ -96,11 +96,11 @@ public async Task CreateIndex() // Add a movie to the index. SearchClient searchClient = indexClient.GetSearchClient(index.Name); searchClient.UploadDocuments(new[] { movie }); - #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilder_UploadDocument + #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_UploadDocument } } - #region Snippet:Azure_Search_Tests_Sample2_FieldBuilder_Types + #region Snippet:Azure_Search_Tests_Sample2_FieldBuilderIgnore_Types public class Movie { [SimpleField(IsKey = true)] @@ -111,7 +111,7 @@ public class Movie [FieldBuilderIgnore] [JsonConverter(typeof(JsonStringEnumConverter))] - public Genre Genre { get; set; } + public MovieGenre Genre { get; set; } [SimpleField(IsFacetable = true, IsFilterable = true, IsSortable = true)] public int Year { get; set; } @@ -120,7 +120,7 @@ public class Movie public double Rating { get; set; } } - public enum Genre + public enum MovieGenre { Unknown, Action, @@ -131,7 +131,7 @@ public enum Genre Romance, SciFi, } - #endregion Snippet:Azure_Search_Tests_Sample2_FieldBuilder_Types + #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/SessionRecords/FieldBuilderSample/CreateIndex.json b/sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderIgnore/CreateIndex.json similarity index 100% rename from sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderSample/CreateIndex.json rename to sdk/search/Azure.Search.Documents/tests/SessionRecords/FieldBuilderIgnore/CreateIndex.json