diff --git a/src/NuGet.Services.AzureSearch/DocumentUtilities.cs b/src/NuGet.Services.AzureSearch/DocumentUtilities.cs index 1067076489..9f712eaa86 100644 --- a/src/NuGet.Services.AzureSearch/DocumentUtilities.cs +++ b/src/NuGet.Services.AzureSearch/DocumentUtilities.cs @@ -28,6 +28,11 @@ internal static class DocumentUtilities NuGetFramework.UnsupportedFramework }; + public static string GetSearchFilterString(SearchFilters searchFilters) + { + return searchFilters.ToString(); + } + public static void PopulateMetadata( IBaseMetadataDocument document, string packageId, @@ -55,6 +60,7 @@ public static void PopulateMetadata( document.ReleaseNotes = package.ReleaseNotes; document.RequiresLicenseAcceptance = package.RequiresLicenseAcceptance; document.SemVerLevel = package.SemVerLevelKey; + document.SortableTitle = GetSortableTitle(package.Title, packageId); document.Summary = package.Summary; document.Tags = Utils.SplitTags(package.Tags ?? string.Empty); document.Title = package.Title; @@ -85,18 +91,29 @@ public static void PopulateMetadata( document.ProjectUrl = leaf.ProjectUrl; document.Published = leaf.Published; document.ReleaseNotes = leaf.ReleaseNotes; - document.RequiresLicenseAcceptance = leaf.RequireLicenseAgreement; + document.RequiresLicenseAcceptance = leaf.RequireLicenseAgreement ?? false; document.SemVerLevel = leaf.IsSemVer2() ? 2 : (int?)null; + document.SortableTitle = GetSortableTitle(leaf.Title, leaf.PackageId); document.Summary = leaf.Summary; document.Tags = leaf.Tags == null ? null : leaf.Tags.ToArray(); document.Title = leaf.Title; } + private static string GetSortableTitle(string title, string packageId) + { + if (string.IsNullOrWhiteSpace(title)) + { + return packageId; + } + + return title; + } + public static string GetSearchDocumentKey(string packageId, SearchFilters searchFilters) { var lowerId = packageId.ToLowerInvariant(); var encodedId = EncodeKey(lowerId); - return $"{encodedId}-{searchFilters}"; + return $"{encodedId}-{GetSearchFilterString(searchFilters)}"; } public static string GetHijackDocumentKey(string packageId, string normalizedVersion) diff --git a/src/NuGet.Services.AzureSearch/HijackDocumentBuilder.cs b/src/NuGet.Services.AzureSearch/HijackDocumentBuilder.cs index 8e31981ca5..d0056529ae 100644 --- a/src/NuGet.Services.AzureSearch/HijackDocumentBuilder.cs +++ b/src/NuGet.Services.AzureSearch/HijackDocumentBuilder.cs @@ -38,7 +38,7 @@ public HijackDocument.Full Full( { var document = new HijackDocument.Full(); - PopulateFull(document, leaf.PackageId, normalizedVersion, changes, leaf.Title); + PopulateLatest(document, leaf.PackageId, normalizedVersion, changes); DocumentUtilities.PopulateMetadata(document, normalizedVersion, leaf); return document; @@ -51,23 +51,12 @@ public HijackDocument.Full Full( { var document = new HijackDocument.Full(); - PopulateFull(document, packageId, package.NormalizedVersion, changes, package.Title); + PopulateLatest(document, packageId, package.NormalizedVersion, changes); DocumentUtilities.PopulateMetadata(document, packageId, package); return document; } - private static void PopulateFull( - HijackDocument.Full document, - string packageId, - string normalizedVersion, - HijackDocumentChanges changes, - string title) - { - PopulateLatest(document, packageId, normalizedVersion, changes); - document.SortableTitle = title ?? packageId; - } - private static void PopulateLatest( T document, string packageId, diff --git a/src/NuGet.Services.AzureSearch/Models/BaseMetadataDocument.cs b/src/NuGet.Services.AzureSearch/Models/BaseMetadataDocument.cs index c14dc1f582..cd49acae47 100644 --- a/src/NuGet.Services.AzureSearch/Models/BaseMetadataDocument.cs +++ b/src/NuGet.Services.AzureSearch/Models/BaseMetadataDocument.cs @@ -3,39 +3,64 @@ using System; using Microsoft.Azure.Search; +using Microsoft.Azure.Search.Models; namespace NuGet.Services.AzureSearch { - /// - /// Implements most of . Some fields are not defined here since they have - /// different Azure Search attributes. - /// - public abstract class BaseMetadataDocument : KeyedDocument + public abstract class BaseMetadataDocument : KeyedDocument, IBaseMetadataDocument { [IsFilterable] public int? SemVerLevel { get; set; } + [IsSearchable] public string Authors { get; set; } + public string Copyright { get; set; } public DateTimeOffset? Created { get; set; } + + [IsSearchable] public string Description { get; set; } + public long? FileSize { get; set; } public string FlattenedDependencies { get; set; } public string Hash { get; set; } public string HashAlgorithm { get; set; } public string IconUrl { get; set; } public string Language { get; set; } + + [IsSortable] + public DateTimeOffset? LastEdited { get; set; } + public string LicenseUrl { get; set; } public string MinClientVersion { get; set; } + + [IsSearchable] public string NormalizedVersion { get; set; } + public string OriginalVersion { get; set; } + + [IsSearchable] public string PackageId { get; set; } + public bool? Prerelease { get; set; } public string ProjectUrl { get; set; } + + [IsSortable] + public DateTimeOffset? Published { get; set; } + public string ReleaseNotes { get; set; } public bool? RequiresLicenseAcceptance { get; set; } + + [IsSortable] + public string SortableTitle { get; set; } + + [IsSearchable] public string Summary { get; set; } + + [IsSearchable] public string[] Tags { get; set; } + + [IsSearchable] public string Title { get; set; } } } diff --git a/src/NuGet.Services.AzureSearch/Models/HijackDocument.cs b/src/NuGet.Services.AzureSearch/Models/HijackDocument.cs index 4f577b7d46..b215465b9d 100644 --- a/src/NuGet.Services.AzureSearch/Models/HijackDocument.cs +++ b/src/NuGet.Services.AzureSearch/Models/HijackDocument.cs @@ -19,13 +19,6 @@ public static class HijackDocument [SerializePropertyNamesAsCamelCase] public class Full : BaseMetadataDocument, ILatest, IBaseMetadataDocument { - [IsSortable] - public DateTimeOffset? LastEdited { get; set; } - [IsSortable] - public DateTimeOffset? Published { get; set; } - [IsSortable] - public string SortableTitle { get; set; } - public bool? IsLatestStableSemVer1 { get; set; } public bool? IsLatestSemVer1 { get; set; } public bool? IsLatestStableSemVer2 { get; set; } diff --git a/src/NuGet.Services.AzureSearch/Models/IBaseMetadataDocument.cs b/src/NuGet.Services.AzureSearch/Models/IBaseMetadataDocument.cs index bee28e0397..5d0a386017 100644 --- a/src/NuGet.Services.AzureSearch/Models/IBaseMetadataDocument.cs +++ b/src/NuGet.Services.AzureSearch/Models/IBaseMetadataDocument.cs @@ -27,11 +27,12 @@ public interface IBaseMetadataDocument string OriginalVersion { get; set; } string PackageId { get; set; } bool? Prerelease { get; set; } - DateTimeOffset? Published { get; set; } string ProjectUrl { get; set; } + DateTimeOffset? Published { get; set; } string ReleaseNotes { get; set; } bool? RequiresLicenseAcceptance { get; set; } int? SemVerLevel { get; set; } + string SortableTitle { get; set; } string Summary { get; set; } string[] Tags { get; set; } string Title { get; set; } diff --git a/src/NuGet.Services.AzureSearch/Models/SearchDocument.cs b/src/NuGet.Services.AzureSearch/Models/SearchDocument.cs index 9cc53722fd..25ec1a37e8 100644 --- a/src/NuGet.Services.AzureSearch/Models/SearchDocument.cs +++ b/src/NuGet.Services.AzureSearch/Models/SearchDocument.cs @@ -29,6 +29,7 @@ public class Full : AddFirst [SerializePropertyNamesAsCamelCase] public class AddFirst : UpdateLatest { + [IsSearchable] public string[] Owners { get; set; } } @@ -37,14 +38,12 @@ public class AddFirst : UpdateLatest /// . /// [SerializePropertyNamesAsCamelCase] - public class UpdateLatest : BaseMetadataDocument, IVersions, IBaseMetadataDocument + public class UpdateLatest : BaseMetadataDocument, IVersions { [IsFilterable] public string SearchFilters { get; set; } public string FullVersion { get; set; } - public DateTimeOffset? LastEdited { get; set; } - public DateTimeOffset? Published { get; set; } public string[] Versions { get; set; } public bool? IsLatestStable { get; set; } public bool? IsLatest { get; set; } diff --git a/src/NuGet.Services.AzureSearch/SearchDocumentBuilder.cs b/src/NuGet.Services.AzureSearch/SearchDocumentBuilder.cs index 12badeae4a..f54a2ec62a 100644 --- a/src/NuGet.Services.AzureSearch/SearchDocumentBuilder.cs +++ b/src/NuGet.Services.AzureSearch/SearchDocumentBuilder.cs @@ -170,7 +170,7 @@ private static void PopulateUpdateLatest( string fullVersion) { PopulateVersions(document, packageId, searchFilters, versions, isLatestStable, isLatest); - document.SearchFilters = searchFilters.ToString(); + document.SearchFilters = DocumentUtilities.GetSearchFilterString(searchFilters); document.FullVersion = fullVersion; } diff --git a/tests/NuGet.Services.AzureSearch.Tests/HijackDocumentBuilderFacts.cs b/tests/NuGet.Services.AzureSearch.Tests/HijackDocumentBuilderFacts.cs index 9a5789b674..91a31a7c20 100644 --- a/tests/NuGet.Services.AzureSearch.Tests/HijackDocumentBuilderFacts.cs +++ b/tests/NuGet.Services.AzureSearch.Tests/HijackDocumentBuilderFacts.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Threading.Tasks; using NuGet.Services.AzureSearch.Support; using Xunit; @@ -63,11 +64,12 @@ public async Task SetsExpectedProperties() Assert.Equal(_fullJson, json); } - [Fact] - public void UsesIdForNullTitleInHijackIndex() + [Theory] + [MemberData(nameof(MissingTitles))] + public void UsesIdWhenMissingForSortableTitle(string title) { var package = Data.PackageEntity; - package.Title = null; + package.Title = title; var document = _target.Full(Data.PackageId, _changes, package); @@ -137,15 +139,28 @@ public async Task SetsExpectedProperties() Assert.Equal(_fullJson, json); } - [Fact] - public void UsesIdForNullTitleInHijackIndex() + [Theory] + [MemberData(nameof(MissingTitles))] + public void UsesIdWhenMissingForSortableTitle(string title) { var leaf = Data.Leaf; - leaf.Title = null; + leaf.Title = title; + var document = _target.Full(Data.NormalizedVersion, _changes, leaf); Assert.Equal(Data.PackageId, document.SortableTitle); } + + [Fact] + public void DefaultsRequiresLicenseAcceptanceToFalse() + { + var leaf = Data.Leaf; + leaf.RequireLicenseAgreement = null; + + var document = _target.Full(Data.NormalizedVersion, _changes, leaf); + + Assert.False(document.RequiresLicenseAcceptance); + } } public abstract class BaseFacts @@ -154,6 +169,14 @@ public abstract class BaseFacts protected readonly string _fullJson; protected readonly HijackDocumentBuilder _target; + public static IEnumerable MissingTitles = new[] + { + new object[] { null }, + new object[] { string.Empty }, + new object[] { " " }, + new object[] { " \t"}, + }; + public BaseFacts() { _changes = new HijackDocumentChanges( @@ -167,9 +190,6 @@ public BaseFacts() ""value"": [ { ""@search.action"": ""upload"", - ""lastEdited"": ""2017-01-02T00:00:00+00:00"", - ""published"": ""2017-01-03T00:00:00+00:00"", - ""sortableTitle"": ""Windows Azure Storage"", ""isLatestStableSemVer1"": false, ""isLatestSemVer1"": true, ""isLatestStableSemVer2"": false, @@ -185,6 +205,7 @@ public BaseFacts() ""hashAlgorithm"": ""SHA512"", ""iconUrl"": ""http://go.microsoft.com/fwlink/?LinkID=288890"", ""language"": ""en-US"", + ""lastEdited"": ""2017-01-02T00:00:00+00:00"", ""licenseUrl"": ""http://go.microsoft.com/fwlink/?LinkId=331471"", ""minClientVersion"": ""2.12"", ""normalizedVersion"": ""7.1.2-alpha"", @@ -192,8 +213,10 @@ public BaseFacts() ""packageId"": ""WindowsAzure.Storage"", ""prerelease"": true, ""projectUrl"": ""https://github.com/Azure/azure-storage-net"", + ""published"": ""2017-01-03T00:00:00+00:00"", ""releaseNotes"": ""Release notes."", ""requiresLicenseAcceptance"": true, + ""sortableTitle"": ""Windows Azure Storage"", ""summary"": ""Summary."", ""tags"": [ ""Microsoft"", diff --git a/tests/NuGet.Services.AzureSearch.Tests/SearchDocumentBuilderFacts.cs b/tests/NuGet.Services.AzureSearch.Tests/SearchDocumentBuilderFacts.cs index 4721c75283..99089bae99 100644 --- a/tests/NuGet.Services.AzureSearch.Tests/SearchDocumentBuilderFacts.cs +++ b/tests/NuGet.Services.AzureSearch.Tests/SearchDocumentBuilderFacts.cs @@ -162,6 +162,25 @@ public async Task SetsExpectedProperties(bool isLatestStable, bool isLatest) public class UpdateLatest : BaseFacts { + [Theory] + [MemberData(nameof(MissingTitles))] + public void UsesIdWhenMissingForSortableTitle(string title) + { + var leaf = Data.Leaf; + leaf.Title = title; + + var document = _target.UpdateLatest( + _searchFilters, + _versions, + isLatestStable: false, + isLatest: true, + normalizedVersion: Data.NormalizedVersion, + fullVersion: Data.FullVersion, + leaf: leaf); + + Assert.Equal(Data.PackageId, document.SortableTitle); + } + [Theory] [MemberData(nameof(AllSearchFilters))] public async Task SetsExpectedProperties(SearchFilters searchFilters, string expected) @@ -182,8 +201,6 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ""@search.action"": ""upload"", ""searchFilters"": """ + expected + @""", ""fullVersion"": ""7.1.2-alpha+git"", - ""lastEdited"": ""2017-01-02T00:00:00+00:00"", - ""published"": ""2017-01-03T00:00:00+00:00"", ""versions"": [ ""1.0.0"", ""2.0.0+git"", @@ -203,6 +220,7 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ""hashAlgorithm"": ""SHA512"", ""iconUrl"": ""http://go.microsoft.com/fwlink/?LinkID=288890"", ""language"": ""en-US"", + ""lastEdited"": ""2017-01-02T00:00:00+00:00"", ""licenseUrl"": ""http://go.microsoft.com/fwlink/?LinkId=331471"", ""minClientVersion"": ""2.12"", ""normalizedVersion"": ""7.1.2-alpha"", @@ -210,8 +228,10 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ""packageId"": ""WindowsAzure.Storage"", ""prerelease"": true, ""projectUrl"": ""https://github.com/Azure/azure-storage-net"", + ""published"": ""2017-01-03T00:00:00+00:00"", ""releaseNotes"": ""Release notes."", ""requiresLicenseAcceptance"": true, + ""sortableTitle"": ""Windows Azure Storage"", ""summary"": ""Summary."", ""tags"": [ ""Microsoft"", @@ -230,10 +250,49 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ] }", json); } + + [Fact] + public void DefaultsRequiresLicenseAcceptanceToFalse() + { + var leaf = Data.Leaf; + leaf.RequireLicenseAgreement = null; + + var document = _target.UpdateLatest( + _searchFilters, + _versions, + isLatestStable: false, + isLatest: true, + normalizedVersion: Data.NormalizedVersion, + fullVersion: Data.FullVersion, + leaf: leaf); + + Assert.False(document.RequiresLicenseAcceptance); + } } public class Full : BaseFacts { + [Theory] + [MemberData(nameof(MissingTitles))] + public void UsesIdWhenMissingForSortableTitle(string title) + { + var package = Data.PackageEntity; + package.Title = title; + + var document = _target.Full( + Data.PackageId, + _searchFilters, + _versions, + isLatestStable: false, + isLatest: true, + fullVersion: Data.FullVersion, + package: package, + owners: _owners, + totalDownloadCount: _totalDownloadCount); + + Assert.Equal(Data.PackageId, document.SortableTitle); + } + [Theory] [MemberData(nameof(AllSearchFilters))] public async Task SetsExpectedProperties(SearchFilters searchFilters, string expected) @@ -261,8 +320,6 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ], ""searchFilters"": """ + expected + @""", ""fullVersion"": ""7.1.2-alpha+git"", - ""lastEdited"": ""2017-01-02T00:00:00+00:00"", - ""published"": ""2017-01-03T00:00:00+00:00"", ""versions"": [ ""1.0.0"", ""2.0.0+git"", @@ -282,6 +339,7 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ""hashAlgorithm"": ""SHA512"", ""iconUrl"": ""http://go.microsoft.com/fwlink/?LinkID=288890"", ""language"": ""en-US"", + ""lastEdited"": ""2017-01-02T00:00:00+00:00"", ""licenseUrl"": ""http://go.microsoft.com/fwlink/?LinkId=331471"", ""minClientVersion"": ""2.12"", ""normalizedVersion"": ""7.1.2-alpha"", @@ -289,8 +347,10 @@ public async Task SetsExpectedProperties(SearchFilters searchFilters, string exp ""packageId"": ""WindowsAzure.Storage"", ""prerelease"": true, ""projectUrl"": ""https://github.com/Azure/azure-storage-net"", + ""published"": ""2017-01-03T00:00:00+00:00"", ""releaseNotes"": ""Release notes."", ""requiresLicenseAcceptance"": true, + ""sortableTitle"": ""Windows Azure Storage"", ""summary"": ""Summary."", ""tags"": [ ""Microsoft"", @@ -338,6 +398,14 @@ public abstract class BaseFacts protected readonly int _totalDownloadCount; protected readonly SearchDocumentBuilder _target; + public static IEnumerable MissingTitles = new[] + { + new object[] { null }, + new object[] { string.Empty }, + new object[] { " " }, + new object[] { " \t"}, + }; + public static IEnumerable AllSearchFilters => new[] { new object[] { SearchFilters.Default, "Default" },