Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

* Modifying Lucene to store latest and latest stable packages for

  prerelease filtering
* Fixing bug where using multiple IndexWriters would lock the index
* Improving search perf by adding boosts to fields at index time
* Removing the need to recreate index by updating the index when
  aggregate download counts are updated.
  • Loading branch information...
commit 3baaa034c17e229002810c7120b447e50c5b81a9 1 parent 08211b2
@pranavkm pranavkm authored
View
8 Facts/Controllers/PackagesControllerFacts.cs
@@ -307,7 +307,9 @@ public void TrimsSearchTerm()
{
var fakeIdentity = new Mock<IIdentity>();
var httpContext = new Mock<HttpContextBase>();
- var controller = CreateController(fakeIdentity: fakeIdentity, httpContext: httpContext);
+ var searchService = new Mock<ISearchService>();
+
+ var controller = CreateController(fakeIdentity: fakeIdentity, httpContext: httpContext, searchService: searchService);
var result = controller.ListPackages(" test ") as ViewResult;
@@ -1263,8 +1265,8 @@ public void RedirectsToUploadPageAfterDelete()
private static Mock<ISearchService> CreateSearchService()
{
var searchService = new Mock<ISearchService>();
- searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<string>())).Returns((IQueryable<Package> p, string searchTerm) => p);
- searchService.Setup(s => s.SearchWithRelevance(It.IsAny<IQueryable<Package>>(), It.IsAny<string>())).Returns((IQueryable<Package> p, string searchTerm) => p);
+ int total;
+ searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<SearchFilter>(), out total)).Returns((IQueryable<Package> p, string searchTerm) => p);
return searchService;
}
View
9 Facts/Services/FeedServiceFacts.cs
@@ -22,7 +22,8 @@ public void V1FeedSearchDoesNotReturnPrereleasePackages()
var configuration = new Mock<IConfiguration>(MockBehavior.Strict);
configuration.Setup(c => c.GetSiteRoot(It.IsAny<bool>())).Returns("https://localhost:8081/");
var searchService = new Mock<ISearchService>(MockBehavior.Strict);
- searchService.Setup(s => s.SearchWithRelevance(It.IsAny<IQueryable<Package>>(), It.IsAny<String>())).Returns<IQueryable<Package>, string>((_, __) => _);
+ int total;
+ searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<SearchFilter>(), out total)).Returns<IQueryable<Package>, string>((_, __) => _);
var v1Service = new TestableV1Feed(repo.Object, configuration.Object, searchService.Object);
// Act
@@ -47,7 +48,8 @@ public void V1FeedSearchDoesNotReturnUnlistedPackages()
new Package { PackageRegistration = new PackageRegistration { Id ="baz" }, Version = "2.0", Listed = false, DownloadStatistics = new List<PackageStatistics>() },
}.AsQueryable());
var searchService = new Mock<ISearchService>(MockBehavior.Strict);
- searchService.Setup(s => s.SearchWithRelevance(It.IsAny<IQueryable<Package>>(), It.IsAny<String>())).Returns<IQueryable<Package>, string>((_, __) => _);
+ int total;
+ searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<SearchFilter>(), out total)).Returns<IQueryable<Package>, string>((_, __) => _);
var configuration = new Mock<IConfiguration>(MockBehavior.Strict);
configuration.Setup(c => c.GetSiteRoot(It.IsAny<bool>())).Returns("http://test.nuget.org/");
var v1Service = new TestableV1Feed(repo.Object, configuration.Object, searchService.Object);
@@ -75,7 +77,8 @@ public void V2FeedSearchDoesNotReturnPrereleasePackagesIfFlagIsFalse()
new Package { PackageRegistration = packageRegistration, Version = "1.0.1-a", IsPrerelease = true, Listed = true, DownloadStatistics = new List<PackageStatistics>() },
}.AsQueryable());
var searchService = new Mock<ISearchService>(MockBehavior.Strict);
- searchService.Setup(s => s.SearchWithRelevance(It.IsAny<IQueryable<Package>>(), It.IsAny<String>())).Returns<IQueryable<Package>, string>((_, __) => _);
+ int total;
+ searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<SearchFilter>(), out total)).Returns<IQueryable<Package>, string>((_, __) => _);
var configuration = new Mock<IConfiguration>(MockBehavior.Strict);
configuration.Setup(c => c.GetSiteRoot(It.IsAny<bool>())).Returns("https://staged.nuget.org/");
var v2Service = new TestableV2Feed(repo.Object, configuration.Object, searchService.Object);
View
75 Website/Controllers/PackagesController.cs
@@ -136,7 +136,6 @@ public virtual ActionResult ListPackages(string q, string sortOrder = null, int
page = 1;
}
-
IQueryable<Package> packageVersions = packageSvc.GetLatestPackageVersions(allowPrerelease: true);
q = (q ?? "").Trim();
@@ -151,20 +150,12 @@ public virtual ActionResult ListPackages(string q, string sortOrder = null, int
int totalHits;
if (!String.IsNullOrEmpty(q))
{
- if (sortOrder.Equals(Constants.RelevanceSortOrder, StringComparison.OrdinalIgnoreCase))
- {
- packageVersions = searchSvc.SearchWithRelevance(packageVersions, q, take: page * Constants.DefaultPackageListPageSize, totalHits: out totalHits);
- if (page == 1 && !packageVersions.Any())
- {
- // In the event the index wasn't updated, we may get an incorrect count.
- totalHits = 0;
- }
- }
- else
+ var searchFilter = GetSearchFilter(q, sortOrder, page);
+ packageVersions = searchSvc.Search(packageVersions, searchFilter, out totalHits);
+ if (page == 1 && !packageVersions.Any())
{
- packageVersions = searchSvc.Search(packageVersions, q)
- .SortBy(GetSortExpression(sortOrder));
- totalHits = packageVersions.Count();
+ // In the event the index wasn't updated, we may get an incorrect count.
+ totalHits = 0;
}
}
else
@@ -186,20 +177,6 @@ public virtual ActionResult ListPackages(string q, string sortOrder = null, int
return View(viewModel);
}
- private static string GetSortExpression(string sortOrder)
- {
- switch (sortOrder)
- {
- case Constants.AlphabeticSortOrder:
- return "PackageRegistration.Id";
- case Constants.RecentSortOrder:
- return "Published desc";
- case Constants.PopularitySortOrder:
- default:
- return "PackageRegistration.DownloadCount desc";
- }
- }
-
// NOTE: Intentionally NOT requiring authentication
public virtual ActionResult ReportAbuse(string id, string version)
{
@@ -543,5 +520,47 @@ protected internal virtual IPackage ReadNuGetPackage(Stream stream)
{
return new ZipPackage(stream);
}
+
+ private SearchFilter GetSearchFilter(string q, string sortOrder, int page)
+ {
+ var searchFilter = new SearchFilter
+ {
+ SearchTerm = q,
+ Skip = (page - 1) * Constants.DefaultPackageListPageSize, // pages are 1-based.
+ Take = Constants.DefaultPackageListPageSize
+ };
+
+ switch (sortOrder)
+ {
+ case Constants.AlphabeticSortOrder:
+ searchFilter.SortProperty = SortProperty.DisplayName;
+ searchFilter.SortDirection = SortDirection.Ascending;
+ break;
+ case Constants.RecentSortOrder:
+ searchFilter.SortProperty = SortProperty.Recent;
+ break;
+ case Constants.PopularitySortOrder:
+ searchFilter.SortProperty = SortProperty.DownloadCount;
+ break;
+ default:
+ searchFilter.SortProperty = SortProperty.Relevance;
+ break;
+ }
+ return searchFilter;
+ }
+
+ private static string GetSortExpression(string sortOrder)
+ {
+ switch (sortOrder)
+ {
+ case Constants.AlphabeticSortOrder:
+ return "PackageRegistration.Id";
+ case Constants.RecentSortOrder:
+ return "Published desc";
+ case Constants.PopularitySortOrder:
+ default:
+ return "PackageRegistration.DownloadCount desc";
+ }
+ }
}
}
View
4 Website/DataServices/V1Feed.svc.cs
@@ -70,8 +70,8 @@ public IQueryable<V1FeedPackage> Search(string searchTerm, string targetFramewor
{
return packages.ToV1FeedPackageQuery(Configuration.GetSiteRoot(UseHttps()));
}
- return SearchService.Search(packages, searchTerm)
- .ToV1FeedPackageQuery(Configuration.GetSiteRoot(UseHttps()));
+ return packages.Search(searchTerm)
+ .ToV1FeedPackageQuery(Configuration.GetSiteRoot(UseHttps()));
}
}
}
View
115 Website/Infrastructure/Lucene/LuceneIndexingService.cs
@@ -5,10 +5,10 @@
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
-using Lucene.Net.Search;
namespace NuGetGallery
{
@@ -54,69 +54,92 @@ protected internal virtual DbContext CreateContext()
return new EntitiesContext();
}
- protected internal virtual List<PackageIndexEntity> GetPackages(DbContext context, DateTime? dateTime)
+ protected internal virtual List<PackageIndexEntity> GetPackages(DbContext context, DateTime? lastIndexTime)
{
- if (dateTime == null)
+ string sql = @"SELECT p.[Key], pr.Id, p.Title, p.Description, p.Tags, p.FlattenedAuthors as Authors, pr.DownloadCount,
+ p.IsLatestStable, p.IsLatest, p.Published
+ FROM Packages p JOIN PackageRegistrations pr on p.PackageRegistrationKey = pr.[Key]";
+
+ object[] parameters;
+
+ if (lastIndexTime == null)
{
- // If we're creating the index for the first time, fetch the new packages.
- string sql = @"Select p.[Key], pr.Id, p.Title, p.Description, p.Tags, p.FlattenedAuthors as Authors, pr.DownloadCount, p.[Key] as LatestKey
- from Packages p join PackageRegistrations pr on p.PackageRegistrationKey = pr.[Key]
- where p.IsLatestStable = 1 or (p.IsLatest = 1 and Not exists (Select 1 from Packages iP where iP.PackageRegistrationKey = p.PackageRegistrationKey and iP.IsLatestStable = 1))";
- return context.Database.SqlQuery<PackageIndexEntity>(sql).ToList();
+ // First time creation. Only pull the latest packages.
+ sql += " WHERE ((p.IsLatest = 1) or (p.IsLatestStable = 1)) ";
+ parameters = new object[0];
}
else
{
- string sql = @"Select p.[Key], pr.Id, p.Title, p.Description, p.Tags, p.FlattenedAuthors as Authors, pr.DownloadCount,
- LatestKey = CASE When p.IsLatest = 1 then p.[Key] Else (Select pLatest.[Key] from Packages pLatest where pLatest.PackageRegistrationKey = pr.[Key] and pLatest.IsLatest = 1) End
- from Packages p join PackageRegistrations pr on p.PackageRegistrationKey = pr.[Key]
- where p.LastUpdated > @UpdatedDate";
- return context.Database.SqlQuery<PackageIndexEntity>(sql, new SqlParameter("UpdatedDate", dateTime.Value)).ToList();
+ sql += " WHERE p.LastUpdated > @UpdatedDate";
+ parameters = new[] { new SqlParameter("UpdatedDate", lastIndexTime.Value) };
}
+ return context.Database.SqlQuery<PackageIndexEntity>(sql, parameters).ToList();
}
private static void AddPackages(List<PackageIndexEntity> packages)
{
- foreach (var package in packages)
- {
- if (package.Key != package.LatestKey)
- {
- indexWriter.DeleteDocuments(new TermQuery(new Term("Key", package.Key.ToString(CultureInfo.InvariantCulture))));
- continue;
- }
+ var packagesToDelete = from package in packages
+ where !(package.IsLatest || package.IsLatestStable)
+ select new Term("Key", package.Key.ToString(CultureInfo.InvariantCulture));
+ indexWriter.DeleteDocuments(packagesToDelete.ToArray());
- // If there's an older entry for this package, remove it.
- var document = new Document();
+ // As per http://stackoverflow.com/a/3894582. The IndexWriter is CPU bound, so we can try and write multiple packages in parallel.
+ var packagesToUpdate = from package in packages
+ where package.IsLatest || package.IsLatestStable
+ select package;
+ // The IndexWriter is thread safe and is primarily CPU-bound.
+ Parallel.ForEach(packagesToUpdate, UpdatePackage);
- document.Add(new Field("Key", package.Key.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO));
- document.Add(new Field("Id-Exact", package.Id.ToLowerInvariant(), Field.Store.NO, Field.Index.NOT_ANALYZED));
+ indexWriter.Commit();
+ }
- document.Add(new Field("Description", package.Description, Field.Store.NO, Field.Index.ANALYZED));
+ private static void UpdatePackage(PackageIndexEntity package)
+ {
+ string key = package.Key.ToString(CultureInfo.InvariantCulture);
+ var document = new Document();
- var tokenizedId = TokenizeId(package.Id);
- foreach (var idToken in tokenizedId)
- {
- document.Add(new Field("Id", idToken, Field.Store.NO, Field.Index.ANALYZED));
- }
+ var field = new Field("Id-Exact", package.Id.ToLowerInvariant(), Field.Store.NO, Field.Index.NOT_ANALYZED);
+ field.SetBoost(2.5f);
+ document.Add(field);
- // If an element does not have a Title, then add all the tokenized Id components as Title.
- // Lucene's StandardTokenizer does not tokenize items of the format a.b.c which does not play well with things like "xunit.net".
- // We will feed it values that are already tokenized.
- var titleTokens = String.IsNullOrEmpty(package.Title) ? tokenizedId : package.Title.Split(idSeparators, StringSplitOptions.RemoveEmptyEntries);
- foreach (var idToken in titleTokens)
- {
- document.Add(new Field("Title", idToken, Field.Store.NO, Field.Index.ANALYZED));
- }
+ field = new Field("Description", package.Description, Field.Store.NO, Field.Index.ANALYZED);
+ field.SetBoost(0.1f);
+ document.Add(field);
- if (!String.IsNullOrEmpty(package.Tags))
- {
- document.Add(new Field("Tags", package.Tags, Field.Store.NO, Field.Index.ANALYZED));
- }
- document.Add(new Field("Author", package.Authors, Field.Store.NO, Field.Index.ANALYZED));
- document.Add(new Field("DownloadCount", package.DownloadCount.ToString(CultureInfo.InvariantCulture), Field.Store.NO, Field.Index.ANALYZED_NO_NORMS));
+ var tokenizedId = TokenizeId(package.Id);
+ foreach (var idToken in tokenizedId)
+ {
+ field = new Field("Id", idToken, Field.Store.NO, Field.Index.ANALYZED);
+ field.SetBoost(1.2f);
+ document.Add(field);
+ }
- indexWriter.AddDocument(document);
+ // If an element does not have a Title, then add all the tokenized Id components as Title.
+ // Lucene's StandardTokenizer does not tokenize items of the format a.b.c which does not play well with things like "xunit.net".
+ // We will feed it values that are already tokenized.
+ var titleTokens = String.IsNullOrEmpty(package.Title) ? tokenizedId : package.Title.Split(idSeparators, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var idToken in titleTokens)
+ {
+ document.Add(new Field("Title", idToken, Field.Store.NO, Field.Index.ANALYZED));
}
- indexWriter.Commit();
+
+ if (!String.IsNullOrEmpty(package.Tags))
+ {
+ field = new Field("Tags", package.Tags, Field.Store.NO, Field.Index.ANALYZED);
+ field.SetBoost(0.8f);
+ document.Add(field);
+ }
+ document.Add(new Field("Author", package.Authors, Field.Store.NO, Field.Index.ANALYZED));
+
+ // Fields meant for filtering and sorting
+ document.Add(new Field("Key", key, Field.Store.YES, Field.Index.NO));
+ document.Add(new Field("IsLatestStable", package.IsLatestStable.ToString(), Field.Store.NO, Field.Index.NOT_ANALYZED));
+ document.Add(new Field("PublishedDate", package.Published.Ticks.ToString(), Field.Store.NO, Field.Index.NOT_ANALYZED));
+ document.Add(new Field("DownloadCount", package.DownloadCount.ToString(CultureInfo.InvariantCulture), Field.Store.NO, Field.Index.NOT_ANALYZED));
+ string displayName = String.IsNullOrEmpty(package.Title) ? package.Id : package.Title;
+ document.Add(new Field("DisplayName", displayName.ToLower(CultureInfo.CurrentCulture), Field.Store.NO, Field.Index.NOT_ANALYZED));
+
+ indexWriter.UpdateDocument(new Term("Key", key), document);
}
protected static void EnsureIndexWriter(bool creatingIndex)
View
133 Website/Infrastructure/Lucene/LuceneSearchService.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using Lucene.Net.Analysis.Standard;
@@ -15,38 +14,46 @@ public class LuceneSearchService : ISearchService
{
private const int MaximumRecordsToReturn = 1000;
- public IQueryable<Package> Search(IQueryable<Package> packages, string searchTerm)
+ public IQueryable<Package> Search(IQueryable<Package> packages, SearchFilter searchFilter, out int totalHits)
{
- if (String.IsNullOrEmpty(searchTerm))
+ if (packages == null)
{
- return packages;
+ throw new ArgumentNullException("packages");
}
- var keys = SearchCore(searchTerm);
- return SearchByKeys(packages, keys);
- }
- public IQueryable<Package> SearchWithRelevance(IQueryable<Package> packages, string searchTerm)
- {
- int numberOfHits;
- return SearchWithRelevance(packages, searchTerm, MaximumRecordsToReturn, out numberOfHits);
- }
+ if (searchFilter == null)
+ {
+ throw new ArgumentNullException("searchFilter");
+ }
- public IQueryable<Package> SearchWithRelevance(IQueryable<Package> packages, string searchTerm, int take, out int numberOfHits)
- {
- numberOfHits = 0;
- if (String.IsNullOrEmpty(searchTerm))
+ if (String.IsNullOrEmpty(searchFilter.SearchTerm))
{
- return packages;
+ throw new ArgumentException("No term to search for.");
}
- var keys = SearchCore(searchTerm);
- if (!keys.Any())
+ if (searchFilter.Skip < 0)
+ {
+ throw new ArgumentOutOfRangeException("skip");
+ }
+
+ if (searchFilter.Take < 0)
+ {
+ throw new ArgumentOutOfRangeException("take");
+ }
+
+ // For the given search term, find the keys that match.
+ var keys = SearchCore(searchFilter);
+ totalHits = keys.Count;
+ if (keys.Count == 0)
{
return Enumerable.Empty<Package>().AsQueryable();
}
- numberOfHits = keys.Count();
- var results = SearchByKeys(packages, keys.Take(take));
+ // Query the source for each of the keys that need to be taken.
+ var results = packages.Where(p => keys.Contains(p.Key));
+
+ // When querying the database, these keys are returned in no particular order. We use the original order of queries
+ // and retrieve each of the packages from the result in the same order.
var lookup = results.ToDictionary(p => p.Key, p => p);
return keys.Select(key => LookupPackage(lookup, key))
@@ -61,51 +68,62 @@ private static Package LookupPackage(Dictionary<int, Package> dict, int key)
return package;
}
- private static IQueryable<Package> SearchByKeys(IQueryable<Package> packages, IEnumerable<int> keys)
- {
- return packages.Where(p => keys.Contains(p.Key));
- }
-
- private static IEnumerable<int> SearchCore(string searchTerm)
+ private static IList<int> SearchCore(SearchFilter searchFilter)
{
if (!Directory.Exists(LuceneCommon.IndexDirectory))
{
- return Enumerable.Empty<int>();
+ return new int[0];
}
+ SortField sortField = GetSortProperties(searchFilter);
+ int numRecords = Math.Min((1 + searchFilter.Skip) * searchFilter.Take, MaximumRecordsToReturn);
+
using (var directory = new LuceneFileSystem(LuceneCommon.IndexDirectory))
{
var searcher = new IndexSearcher(directory, readOnly: true);
- var query = ParseQuery(searchTerm);
- var results = searcher.Search(query, filter: null, n: 1000, sort: new Sort(new[] { SortField.FIELD_SCORE, new SortField("DownloadCount", SortField.INT, reverse: true) }));
- var keys = results.scoreDocs.Select(c => Int32.Parse(searcher.Doc(c.doc).Get("Key"), CultureInfo.InvariantCulture))
+ var query = ParseQuery(searchFilter);
+
+ Filter filter = null;
+ if (!searchFilter.IncludePrerelease)
+ {
+ var isLatestStableQuery = new TermQuery(new Term("IsLatestStable", Boolean.TrueString));
+ filter = new QueryWrapperFilter(isLatestStableQuery);
+ }
+
+ var results = searcher.Search(query, filter: filter, n: numRecords, sort: new Sort(sortField));
+ var keys = results.scoreDocs.Skip(searchFilter.Skip)
+ .Select(c => ParseKey(searcher.Doc(c.doc).Get("Key")))
.ToList();
searcher.Close();
return keys;
}
}
- private static Query ParseQuery(string searchTerm)
+ private static Query ParseQuery(SearchFilter searchFilter)
{
- var fields = new Dictionary<string, float> { { "Id", 1.2f }, { "Title", 1.0f }, { "Tags", 0.8f }, { "Description", 0.1f },
- { "Author", 1.0f } };
+ var fields = new[] { "Id", "Title", "Tags", "Description", "Author" };
var analyzer = new StandardAnalyzer(LuceneCommon.LuceneVersion);
- var queryParser = new MultiFieldQueryParser(LuceneCommon.LuceneVersion, fields.Keys.ToArray(), analyzer, fields);
+ var queryParser = new MultiFieldQueryParser(LuceneCommon.LuceneVersion, fields, analyzer);
+ // All terms in the multi-term query appear in at least one of the fields.
var conjuctionQuery = new BooleanQuery();
conjuctionQuery.SetBoost(2.0f);
+
+ // Some terms in the multi-term query appear in at least one of the fields.
var disjunctionQuery = new BooleanQuery();
disjunctionQuery.SetBoost(0.1f);
+
+ // Suffix wildcard search e.g. jquer*
var wildCardQuery = new BooleanQuery();
wildCardQuery.SetBoost(0.5f);
// Escape the entire term we use for exact searches.
- var escapedSearchTerm = Escape(searchTerm);
+ var escapedSearchTerm = Escape(searchFilter.SearchTerm);
var exactIdQuery = new TermQuery(new Term("Id-Exact", escapedSearchTerm));
exactIdQuery.SetBoost(2.5f);
var wildCardIdQuery = new WildcardQuery(new Term("Id-Exact", "*" + escapedSearchTerm + "*"));
-
- foreach(var term in GetSearchTerms(searchTerm))
+
+ foreach (var term in GetSearchTerms(searchFilter.SearchTerm))
{
var termQuery = queryParser.Parse(term);
conjuctionQuery.Add(termQuery, BooleanClause.Occur.MUST);
@@ -113,15 +131,22 @@ private static Query ParseQuery(string searchTerm)
foreach (var field in fields)
{
- var wildCardTermQuery = new WildcardQuery(new Term(field.Key, term + "*"));
- wildCardTermQuery.SetBoost(0.7f * field.Value);
+ var wildCardTermQuery = new WildcardQuery(new Term(field, term + "*"));
+ wildCardTermQuery.SetBoost(0.7f);
wildCardQuery.Add(wildCardTermQuery, BooleanClause.Occur.SHOULD);
}
}
-
- var downloadCountBooster = new FieldScoreQuery("DownloadCount", FieldScoreQuery.Type.INT);
- return new CustomScoreQuery(conjuctionQuery.Combine(new Query[] { exactIdQuery, wildCardIdQuery, conjuctionQuery, disjunctionQuery, wildCardQuery }),
- downloadCountBooster);
+
+ // Create an OR of all the queries that we have
+ var combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, wildCardIdQuery, conjuctionQuery, disjunctionQuery, wildCardQuery });
+
+ if (searchFilter.SortProperty == SortProperty.Relevance)
+ {
+ // If searching by relevance, boost scores by download count.
+ var downloadCountBooster = new FieldScoreQuery("DownloadCount", FieldScoreQuery.Type.INT);
+ return new CustomScoreQuery(combinedQuery, downloadCountBooster);
+ }
+ return combinedQuery;
}
private static IEnumerable<string> GetSearchTerms(string searchTerm)
@@ -132,9 +157,29 @@ private static IEnumerable<string> GetSearchTerms(string searchTerm)
.Select(Escape);
}
+ private static SortField GetSortProperties(SearchFilter searchFilter)
+ {
+ switch (searchFilter.SortProperty)
+ {
+ case SortProperty.DisplayName:
+ return new SortField("DisplayName", SortField.STRING, reverse: searchFilter.SortDirection == SortDirection.Descending);
+ case SortProperty.DownloadCount:
+ return new SortField("DownloadCount", SortField.INT, reverse: true);
+ case SortProperty.Recent:
+ return new SortField("PublishedDate", SortField.LONG, reverse: true);
+ }
+ return SortField.FIELD_SCORE;
+ }
+
private static string Escape(string term)
{
return QueryParser.Escape(term).ToLowerInvariant();
}
+
+ private static int ParseKey(string value)
+ {
+ int key;
+ return Int32.TryParse(value, out key) ? key : 0;
+ }
}
}
View
9 Website/Infrastructure/Lucene/PackageIndexEntity.cs
@@ -1,4 +1,5 @@
-namespace NuGetGallery
+using System;
+namespace NuGetGallery
{
public class PackageIndexEntity
{
@@ -16,6 +17,10 @@ public class PackageIndexEntity
public int DownloadCount { get; set; }
- public int? LatestKey { get; set; }
+ public bool IsLatest { get; set; }
+
+ public bool IsLatestStable { get; set; }
+
+ public DateTime Published { get; set; }
}
}
View
15 Website/Services/ISearchService.cs
@@ -1,14 +1,15 @@
-using System.Collections.Generic;
-using System.Linq;
+using System.Linq;
namespace NuGetGallery
{
public interface ISearchService
{
- IQueryable<Package> Search(IQueryable<Package> packages, string searchTerm);
-
- IQueryable<Package> SearchWithRelevance(IQueryable<Package> packages, string searchTerm);
-
- IQueryable<Package> SearchWithRelevance(IQueryable<Package> packages, string searchTerm, int take, out int totalHits);
+ /// <summary>
+ /// Searches for packages that match the search filter and returns a set of results.
+ /// </summary>
+ /// <param name="packages">A query representing the packages to be searched for.</param>
+ /// <param name="filter">The filter to be used.</param>
+ /// <param name="totalHits">The total number of packages discovered.</param>
+ IQueryable<Package> Search(IQueryable<Package> packages, SearchFilter filter, out int totalHits);
}
}
View
32 Website/Services/SearchFilter.cs
@@ -0,0 +1,32 @@
+
+namespace NuGetGallery
+{
+ public class SearchFilter
+ {
+ public string SearchTerm { get; set; }
+
+ public int Skip { get; set; }
+
+ public int Take { get; set; }
+
+ public bool IncludePrerelease { get; set; }
+
+ public SortProperty SortProperty { get; set; }
+
+ public SortDirection SortDirection { get; set; }
+ }
+
+ public enum SortProperty
+ {
+ Relevance,
+ DownloadCount,
+ DisplayName,
+ Recent,
+ }
+
+ public enum SortDirection
+ {
+ Descending,
+ Ascending,
+ }
+}
View
1  Website/Website.csproj
@@ -416,6 +416,7 @@
<Compile Include="RequireRemoteHttpsAttribute.cs" />
<Compile Include="Services\PackageSearchResults.cs" />
<Compile Include="Services\AggregateStatsService.cs" />
+ <Compile Include="Services\SearchFilter.cs" />
<Compile Include="Services\TestableStorageClientException.cs" />
<Compile Include="Services\UploadFileService.cs" />
<Compile Include="SharedController.generated.cs">
Please sign in to comment.
Something went wrong with that request. Please try again.