private static IList<int> SearchCore(SearchFilter searchFilter, out int totalHits) { if (!Directory.Exists(LuceneCommon.IndexDirectory)) { totalHits = 0; return new int[0]; } SortField sortField = GetSortField(searchFilter); int numRecords = searchFilter.Skip + searchFilter.Take; using (var directory = new LuceneFileSystem(LuceneCommon.IndexDirectory)) { var searcher = new IndexSearcher(directory, readOnly: true); var query = ParseQuery(searchFilter); var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; var termQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString)); Filter filter = new QueryWrapperFilter(termQuery); 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(); totalHits = results.totalHits; searcher.Close(); return keys; } }
public static SearchFilter GetSearchFilter(string q, string sortOrder, int page, bool includePrerelease) { var searchFilter = new SearchFilter { SearchTerm = q, Skip = (page - 1) * Constants.DefaultPackageListPageSize, // pages are 1-based. Take = Constants.DefaultPackageListPageSize, IncludePrerelease = includePrerelease }; 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 IQueryable<Package> SearchCore(SearchFilter searchFilter, out int totalHits) { int numRecords = searchFilter.Skip + searchFilter.Take; var searcher = new IndexSearcher(_directory, readOnly: true); var query = ParseQuery(searchFilter); // IF searching by relevance, boost scores by download count. if (searchFilter.SortProperty == SortProperty.Relevance) { var downloadCountBooster = new FieldScoreQuery("DownloadCount", FieldScoreQuery.Type.INT); query = new CustomScoreQuery(query, downloadCountBooster); } var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; var termQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString)); var filter = new QueryWrapperFilter(termQuery); var results = searcher.Search(query, filter: filter, n: numRecords, sort: new Sort(GetSortField(searchFilter))); totalHits = results.totalHits; if (results.totalHits == 0 || searchFilter.CountOnly) { return Enumerable.Empty<Package>().AsQueryable(); } var packages = results.scoreDocs .Skip(searchFilter.Skip) .Select(sd => PackageFromDoc(searcher.Doc(sd.doc))) .ToList(); return packages.AsQueryable(); }
public static SearchFilter GetSearchFilter(string q, int page, string sortOrder, string context) { var searchFilter = new SearchFilter(context) { SearchTerm = q, Skip = (page - 1) * Constants.DefaultPackageListPageSize, // pages are 1-based. Take = Constants.DefaultPackageListPageSize, IncludePrerelease = true }; switch (sortOrder) { case Constants.AlphabeticSortOrder: searchFilter.SortOrder = SortOrder.TitleAscending; break; case Constants.RecentSortOrder: searchFilter.SortOrder = SortOrder.Published; break; default: searchFilter.SortOrder = SortOrder.Relevance; break; } return searchFilter; }
private static SortField GetSortField(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; }
public static async Task<IQueryable<Package>> GetResultsFromSearchService(ISearchService searchService, SearchFilter searchFilter) { var result = await searchService.Search(searchFilter); // For count queries, we can ask the SearchService to not filter the source results. This would avoid hitting the database and consequently make // it very fast. if (searchFilter.CountOnly) { // At this point, we already know what the total count is. We can have it return this value very quickly without doing any SQL. return result.Data.InterceptWith(new CountInterceptor(result.Hits)); } // For relevance search, Lucene returns us a paged\sorted list. OData tries to apply default ordering and Take \ Skip on top of this. // We avoid it by yanking these expressions out of out the tree. return result.Data.InterceptWith(new DisregardODataInterceptor()); }
public IQueryable<Package> Search(IQueryable<Package> packages, SearchFilter searchFilter, out int totalHits) { if (packages == null) { throw new ArgumentNullException("packages"); } if (searchFilter == null) { throw new ArgumentNullException("searchFilter"); } if (String.IsNullOrEmpty(searchFilter.SearchTerm)) { throw new ArgumentException("No term to search for."); } 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 || searchFilter.CountOnly) { return Enumerable.Empty<Package>().AsQueryable(); } // 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)) .Where(p => p != null) .AsQueryable(); }
private SearchResults SearchCore(SearchFilter searchFilter) { // Get index timestamp DateTime timestamp = File.GetLastWriteTimeUtc(LuceneCommon.GetIndexMetadataPath()); int numRecords = searchFilter.Skip + searchFilter.Take; var searcher = new IndexSearcher(_directory, readOnly: true); var query = ParseQuery(searchFilter); // IF searching by relevance, boost scores by download count. if (searchFilter.SortOrder == SortOrder.Relevance) { var downloadCountBooster = new FieldScoreQuery("DownloadCount", FieldScoreQuery.Type.INT); query = new CustomScoreQuery(query, downloadCountBooster); } var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; Query filterQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString)); if (searchFilter.CuratedFeed != null) { var feedFilterQuery = new TermQuery(new Term("CuratedFeedKey", searchFilter.CuratedFeed.Key.ToString(CultureInfo.InvariantCulture))); BooleanQuery conjunctionQuery = new BooleanQuery(); conjunctionQuery.Add(filterQuery, Occur.MUST); conjunctionQuery.Add(feedFilterQuery, Occur.MUST); filterQuery = conjunctionQuery; } Filter filter = new QueryWrapperFilter(filterQuery); var results = searcher.Search(query, filter: filter, n: numRecords, sort: new Sort(GetSortField(searchFilter))); if (results.TotalHits == 0 || searchFilter.CountOnly) { return new SearchResults(results.TotalHits, timestamp); } var packages = results.ScoreDocs .Skip(searchFilter.Skip) .Select(sd => PackageFromDoc(searcher.Doc(sd.Doc))) .ToList(); return new SearchResults( results.TotalHits, timestamp, packages.AsQueryable()); }
public SearchResults Search(SearchFilter searchFilter) { if (searchFilter == null) { throw new ArgumentNullException("searchFilter"); } if (searchFilter.Skip < 0) { throw new ArgumentOutOfRangeException("searchFilter"); } if (searchFilter.Take < 0) { throw new ArgumentOutOfRangeException("searchFilter"); } return SearchCore(searchFilter); }
public Task<SearchResults> Search(SearchFilter searchFilter) { if (searchFilter == null) { throw new ArgumentNullException("searchFilter"); } if (searchFilter.Skip < 0) { throw new ArgumentOutOfRangeException("searchFilter"); } if (searchFilter.Take < 0) { throw new ArgumentOutOfRangeException("searchFilter"); } return Task.FromResult(SearchCore(searchFilter)); }
public IQueryable<Package> Search(SearchFilter searchFilter, out int totalHits) { if (searchFilter == null) { throw new ArgumentNullException("searchFilter"); } if (searchFilter.Skip < 0) { throw new ArgumentOutOfRangeException("searchFilter"); } if (searchFilter.Take < 0) { throw new ArgumentOutOfRangeException("searchFilter"); } return SearchCore(searchFilter, out totalHits); }
private IQueryable<Package> SearchCore(SearchFilter searchFilter, out int totalHits) { int numRecords = searchFilter.Skip + searchFilter.Take; var searcher = new IndexSearcher(_directory, readOnly: true); var query = ParseQuery(searchFilter); // IF searching by relevance, boost scores by download count. if (searchFilter.SortProperty == SortProperty.Relevance) { var downloadCountBooster = new FieldScoreQuery("DownloadCount", FieldScoreQuery.Type.INT); query = new CustomScoreQuery(query, downloadCountBooster); } var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; Query filterQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString)); if (searchFilter.CuratedFeedKey.HasValue) { var feedFilterQuery = new TermQuery(new Term("CuratedFeedKey", searchFilter.CuratedFeedKey.Value.ToString(CultureInfo.InvariantCulture))); BooleanQuery conjunctionQuery = new BooleanQuery(); conjunctionQuery.Add(filterQuery, Occur.MUST); conjunctionQuery.Add(feedFilterQuery, Occur.MUST); filterQuery = conjunctionQuery; } Filter filter = new QueryWrapperFilter(filterQuery); var results = searcher.Search(query, filter: filter, n: numRecords, sort: new Sort(GetSortField(searchFilter))); totalHits = results.TotalHits; if (results.TotalHits == 0 || searchFilter.CountOnly) { return Enumerable.Empty<Package>().AsQueryable(); } var packages = results.ScoreDocs .Skip(searchFilter.Skip) .Select(sd => PackageFromDoc(searcher.Doc(sd.Doc))) .ToList(); return packages.AsQueryable(); }
private SearchResults SearchCore(SearchFilter searchFilter) { // Get index timestamp DateTime timestamp = File.GetLastWriteTimeUtc(LuceneCommon.IndexMetadataPath); int numRecords = searchFilter.Skip + searchFilter.Take; var searcher = new IndexSearcher(_directory, readOnly: true); var query = ParseQuery(searchFilter); // If searching by relevance, boost scores by download count. if (searchFilter.SortProperty == SortProperty.Relevance) { var downloadCountBooster = new FieldScoreQuery("DownloadCount", FieldScoreQuery.Type.INT); query = new CustomScoreQuery(query, downloadCountBooster); } var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; Query filterQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString)); Filter filter = new QueryWrapperFilter(filterQuery); var results = searcher.Search(query, filter: filter, n: numRecords, sort: new Sort(GetSortField(searchFilter))); if (results.TotalHits == 0 || searchFilter.CountOnly) { return new SearchResults(results.TotalHits, timestamp); } var packages = results.ScoreDocs .Skip(searchFilter.Skip) .Select(sd => PackageFromDoc(searcher.Doc(sd.Doc))) .ToList(); return new SearchResults( results.TotalHits, timestamp, packages.AsQueryable()); }
static void TestSearch(string query, string expectedPackageId, int maxExpectedPosition) { var searchFilter = new SearchFilter { Skip = 0, Take = maxExpectedPosition, SearchTerm = query, }; int totalHits; var results = luceneSearchService.Search(searchFilter, out totalHits).ToList(); Assert.NotEqual(0, results.Count); Assert.Contains(expectedPackageId, results.Select(p => p.PackageRegistration.Id), StringComparer.InvariantCultureIgnoreCase); Assert.True(results.Count <= maxExpectedPosition); }
protected virtual IQueryable <Package> SearchCore(IQueryable <Package> packages, string searchTerm, string targetFramework, bool includePrerelease, SearchFilter searchFilter, bool useCache) { // we don't allow an empty search for all versions. if (searchFilter.FilterInvalidReason == SearchFilterInvalidReason.DueToAllVersionsRequested && string.IsNullOrWhiteSpace(searchTerm)) { searchFilter.IsValid = true; } // When the SearchFilter is valid, we can use Lucene // We can only use Lucene if the client queries for the latest versions (IsLatest \ IsLatestStable) versions of a package // and specific sort orders that we have in the index. if (searchFilter.IsValid) { searchFilter.SearchTerm = searchTerm; searchFilter.IncludePrerelease = includePrerelease; return(GetResultsFromSearchService(searchFilter)); } var invalidSearchFilterMessage = "Search filter was invalid ('{0}') Raw Url: '{1}' .".format_with(searchFilter.FilterInvalidReason, HttpContext.Request.RawUrl); Trace.WriteLine(invalidSearchFilterMessage); // raise this as an exception for even better visibility ErrorSignal.FromCurrentContext().Raise(new SystemException(invalidSearchFilterMessage)); if (!includePrerelease) { packages = packages.Where(p => !p.IsPrerelease); } if (useCache) { return(NugetGallery.Cache.Get( string.Format( "V2Feed-Search-{0}-{1}-{2}", searchTerm.to_lower(), targetFramework.to_lower(), includePrerelease ), DateTime.UtcNow.AddSeconds(3600), () => { Trace.WriteLine("Database search results hit for API (caching results) Search term: '{0}' (prerelease? {1}).".format_with(searchTerm, includePrerelease)); return packages.Search(searchTerm, lowerCaseExpression: false).ToList(); }).AsQueryable()); } Trace.WriteLine("Database search results hit for API (not caching results) Search term: '{0}' (prerelease? {1}).".format_with(searchTerm, includePrerelease)); if (searchFilter.Skip != 0) { packages = packages.Skip(searchFilter.Skip); } packages = packages.Take(MaxPageSize); if (searchFilter.Take != 0) { packages = packages.Take(searchFilter.Take); } switch (searchFilter.SortProperty) { case SortProperty.Relevance: //do not change the search order break; case SortProperty.DownloadCount: packages = packages.OrderByDescending(p => p.PackageRegistration.DownloadCount); break; case SortProperty.DisplayName: packages = searchFilter.SortDirection == SortDirection.Ascending ? packages.OrderBy(p => p.Title) : packages.OrderByDescending(p => p.Title); break; case SortProperty.Recent: packages = packages.OrderByDescending(p => p.Published); break; default: //do not change the search order break; } return(packages.Search(searchTerm, lowerCaseExpression: false)); }
private static Query ParseQuery(SearchFilter searchFilter) { // 1. parse the query into field clauses and general terms // We imagine that mostly, field clauses are meant to 'filter' results found searching for general terms. // The resulting clause collections may be empty. var queryParser = new NuGetQueryParser(); var clauses = queryParser.Parse(searchFilter.SearchTerm).Select(StandardizeSearchTerms).ToList(); var fieldSpecificTerms = clauses.Where(a => a.Field != null); var generalTerms = clauses.Where(a => a.Field == null); // Convert terms to appropriate Lucene Query objects var analyzer = new PerFieldAnalyzer(); var fieldSpecificQueries = fieldSpecificTerms .Select(c => AnalysisHelper.GetFieldQuery(analyzer, c.Field, c.TermOrPhrase)) .Where(q => !IsDegenerateQuery(q)) .ToList(); var generalQueries = generalTerms .Select(c => AnalysisHelper.GetMultiFieldQuery(analyzer, Fields, c.TermOrPhrase)) .Where(q => !IsDegenerateQuery(q)) .ToList(); if (fieldSpecificQueries.Count == 0 && generalQueries.Count == 0) { return new MatchAllDocsQuery(); } // At this point we try to detect user intent... // a) General search? [foo bar] // b) Id-targeted search? [id:Foo bar] // c) Other Field-targeted search? [author:Foo bar] bool doExactId = !fieldSpecificQueries.Any(); Query generalQuery = BuildGeneralQuery(doExactId, searchFilter.SearchTerm, analyzer, generalTerms, generalQueries); // IF field targeting is done, we should basically want to AND their field specific queries with all other query terms if (fieldSpecificQueries.Any()) { var combinedQuery = new BooleanQuery(); if (!IsDegenerateQuery(generalQuery)) { combinedQuery.Add(generalQuery, Occur.MUST); } foreach (var fieldQuery in fieldSpecificQueries) { if (!IsDegenerateQuery(fieldQuery)) { combinedQuery.Add(fieldQuery, Occur.MUST); } } generalQuery = combinedQuery; } return generalQuery; }
public IQueryable <V2FeedPackage> Search(string searchTerm, string targetFramework, bool includePrerelease) { var packages = PackageRepo.GetAll().Where(p => p.StatusForDatabase == PackageStatusType.Submitted.GetDescriptionOrValue()); var packageVersions = SearchCore(packages, searchTerm, targetFramework, includePrerelease, SearchFilter.Empty()).ToV2FeedPackageQuery(GetSiteRoot()).ToList(); if (!includePrerelease) { return(packageVersions.Where(p => !p.IsPrerelease).AsQueryable()); } return(packageVersions.AsQueryable()); }
protected virtual IQueryable <Package> SearchCore(IQueryable <Package> packages, string searchTerm, string targetFramework, bool includePrerelease, SearchFilter searchFilter) { // we don't allow an empty search for all versions. if (searchFilter.FilterInvalidReason == SearchFilterInvalidReason.DueToAllVersionsRequested && string.IsNullOrWhiteSpace(searchTerm)) { searchFilter.IsValid = true; } // We can only use Lucene if the client queries for the latest versions (IsLatest \ IsLatestStable) versions of a package // and specific sort orders that we have in the index. if (searchFilter.IsValid) { searchFilter.SearchTerm = searchTerm; searchFilter.IncludePrerelease = includePrerelease; return(GetResultsFromSearchService(searchFilter)); } Trace.WriteLine("Database hit"); if (!includePrerelease) { packages = packages.Where(p => !p.IsPrerelease); } return(packages.Search(searchTerm)); }
private static SortField GetSortField(SearchFilter searchFilter) { switch (searchFilter.SortOrder) { case SortOrder.TitleAscending: return new SortField("DisplayName", SortField.STRING, reverse: false); case SortOrder.TitleDescending: return new SortField("DisplayName", SortField.STRING, reverse: false); case SortOrder.Published: return new SortField("PublishedDate", SortField.LONG, reverse: true); case SortOrder.LastEdited: return new SortField("EditedDate", SortField.LONG, reverse: true); } return SortField.FIELD_SCORE; }
protected virtual IQueryable <Package> SearchCore(IQueryable <Package> packages, string searchTerm, string targetFramework, bool includePrerelease, SearchFilter searchFilter, bool useCache) { // we don't allow an empty search for all versions. if (searchFilter.FilterInvalidReason == SearchFilterInvalidReason.DueToAllVersionsRequested && string.IsNullOrWhiteSpace(searchTerm)) { searchFilter.IsValid = true; } // We can only use Lucene if the client queries for the latest versions (IsLatest \ IsLatestStable) versions of a package // and specific sort orders that we have in the index. if (searchFilter.IsValid) { searchFilter.SearchTerm = searchTerm; searchFilter.IncludePrerelease = includePrerelease; return(GetResultsFromSearchService(searchFilter)); } if (!includePrerelease) { packages = packages.Where(p => !p.IsPrerelease); } //if (searchFilter.Skip != 0) //{ // packages = packages.Skip(searchFilter.Skip); //} //packages = packages.Take(MaxPageSize); //if (searchFilter.Take != 0) //{ // packages = packages.Take(searchFilter.Take); //} //switch (searchFilter.SortProperty) //{ // case SortProperty.Relevance: // //do not change the search order // break; // case SortProperty.DownloadCount: // packages = packages.OrderByDescending(p => p.PackageRegistration.DownloadCount); // break; // case SortProperty.DisplayName: // packages = searchFilter.SortDirection == SortDirection.Ascending ? packages.OrderBy(p => p.Title) : packages.OrderByDescending(p => p.Title); // break; // case SortProperty.Recent: // packages = packages.OrderByDescending(p => p.Published); // break; // default: // //do not change the search order // break; //} if (useCache) { return(NugetGallery.Cache.Get( string.Format( "V2Feed-Search-{0}-{1}-{2}", searchTerm.to_lower(), targetFramework.to_lower(), includePrerelease ), DateTime.UtcNow.AddSeconds(3600), () => { Trace.WriteLine("Database search results hit for API (caching results) Search term: '{0}' (prerelease? {1}).".format_with(searchTerm, includePrerelease)); return packages.Search(searchTerm).ToList(); }).AsQueryable()); } Trace.WriteLine("Database search results hit for API (not caching results) Search term: '{0}' (prerelease? {1}).".format_with(searchTerm, includePrerelease)); return(packages.Search(searchTerm)); }
private static bool TryReadSearchFilter(bool allVersionsInIndex, string url, out SearchFilter searchFilter) { if (url == null) { searchFilter = null; return(false); } int indexOfQuestionMark = url.IndexOf('?'); if (indexOfQuestionMark == -1) { searchFilter = null; return(false); } string path = url.Substring(0, indexOfQuestionMark); string query = url.Substring(indexOfQuestionMark + 1); if (string.IsNullOrEmpty(query)) { searchFilter = null; return(false); } searchFilter = new SearchFilter(SearchFilter.ODataSearchContext) { // The way the default paging works is WCF attempts to read up to the MaxPageSize elements. If it finds as many, it'll assume there // are more elements to be paged and generate a continuation link. Consequently we'll always ask to pull MaxPageSize elements so WCF generates the // link for us and then allow it to do a Take on the results. Further down, we'll also parse $skiptoken as a custom IDataServicePagingProvider // sneakily injects the Skip value in the continuation token. Take = MaxPageSize, Skip = 0, CountOnly = path.EndsWith("$count", StringComparison.Ordinal) }; string[] props = query.Split('&'); IDictionary <string, string> queryTerms = new Dictionary <string, string>(); foreach (string prop in props) { string[] nameValue = prop.Split('='); if (nameValue.Length == 2) { queryTerms[nameValue[0]] = nameValue[1]; } } // We'll only use the index if we the query searches for latest \ latest-stable packages string filter; if (queryTerms.TryGetValue("$filter", out filter)) { if (!(filter.Equals("IsLatestVersion", StringComparison.Ordinal) || filter.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal))) { searchFilter = null; return(false); } } else if (!allVersionsInIndex) { searchFilter = null; return(false); } string skipStr; if (queryTerms.TryGetValue("$skip", out skipStr)) { int skip; if (int.TryParse(skipStr, out skip)) { searchFilter.Skip = skip; } } string topStr; if (queryTerms.TryGetValue("$top", out topStr)) { int top; if (int.TryParse(topStr, out top)) { searchFilter.Take = Math.Min(top, MaxPageSize); } } string skipTokenStr; if (queryTerms.TryGetValue("$skiptoken", out skipTokenStr)) { var skipTokenParts = skipTokenStr.Split(','); if (skipTokenParts.Length == 3) // this means our custom IDataServicePagingProvider did its magic by sneaking the Skip value into the SkipToken { int skip; if (int.TryParse(skipTokenParts[2], out skip)) { searchFilter.Skip = skip; } } } // only certain orderBy clauses are supported from the Lucene search string orderBy; if (queryTerms.TryGetValue("$orderby", out orderBy)) { if (string.IsNullOrEmpty(orderBy)) { searchFilter.SortOrder = SortOrder.Relevance; } else if (orderBy.StartsWith("DownloadCount", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.Relevance; } else if (orderBy.StartsWith("Published", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.Published; } else if (orderBy.StartsWith("LastEdited", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.LastEdited; } else if (orderBy.StartsWith("Id", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.TitleAscending; } else if (orderBy.StartsWith("concat", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.TitleAscending; if (orderBy.Contains("%20desc")) { searchFilter.SortOrder = SortOrder.TitleDescending; } } else { searchFilter = null; return(false); } } else { searchFilter.SortOrder = SortOrder.Relevance; } return(true); }
private static Query ParseQuery(SearchFilter searchFilter) { if (String.IsNullOrWhiteSpace(searchFilter.SearchTerm)) { return(new MatchAllDocsQuery()); } var fields = new[] { "Id", "Title", "Tags", "Description", "Author" }; //var analyzer = new StandardAnalyzer(LuceneCommon.LuceneVersion); var analyzer = new PerFieldAnalyzer(); 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.Boost = 2.0f; // Some terms in the multi-term query appear in at least one of the fields. var disjunctionQuery = new BooleanQuery(); disjunctionQuery.Boost = 0.1f; // Escape the entire term we use for exact searches. var escapedSearchTerm = Escape(searchFilter.SearchTerm).Replace("id\\:", string.Empty).Replace("author\\:", string.Empty).Replace("tag\\:", string.Empty); // Do not escape id when used against Id-Exact. The results will return incorrectly var idExactSearchTerm = searchFilter.SearchTerm.Replace("id:", string.Empty).Replace("author:", string.Empty).Replace("tag:", string.Empty); var exactIdQuery = new TermQuery(new Term("Id-Exact", idExactSearchTerm)); exactIdQuery.Boost = 7.0f; var relatedIdQuery = new WildcardQuery(new Term("Id-Exact", idExactSearchTerm + ".*")); relatedIdQuery.Boost = 6.5f; var startsIdQuery = new WildcardQuery(new Term("Id-Exact", idExactSearchTerm + "*")); startsIdQuery.Boost = 6.0f; var wildCardIdQuery = new WildcardQuery(new Term("Id-Exact", "*" + idExactSearchTerm + "*")); wildCardIdQuery.Boost = 3.0f; var exactTitleQuery = new TermQuery(new Term("Title-Exact", escapedSearchTerm)); exactTitleQuery.Boost = 6.5f; var startsTitleQuery = new WildcardQuery(new Term("Title-Exact", escapedSearchTerm + "*")); startsTitleQuery.Boost = 5.5f; var wildCardTitleQuery = new WildcardQuery(new Term("Title-Exact", "*" + escapedSearchTerm + "*")); wildCardTitleQuery.Boost = 2.5f; // Suffix wildcard search e.g. jquer* var wildCardQuery = new BooleanQuery(); wildCardQuery.Boost = 0.5f; // GetSearchTerms() escapes the search terms, so do not escape again var terms = GetSearchTerms(searchFilter.SearchTerm).ToList(); bool onlySearchById = searchFilter.ByIdOnly || searchFilter.ExactIdOnly || terms.AnySafe(t => t.StartsWith("id\\:")); bool onlySearchByExactId = searchFilter.ExactIdOnly; bool onlySearchByAuthor = terms.AnySafe(t => t.StartsWith("author\\:")); bool onlySearchByTag = terms.AnySafe(t => t.StartsWith("tag\\:")); bool searchLimiter = onlySearchById || onlySearchByAuthor || onlySearchByTag; foreach (var term in terms) { var localTerm = term.Replace("id\\:", string.Empty).Replace("author\\:", string.Empty).Replace("tag\\:", string.Empty); var termQuery = queryParser.Parse(localTerm); conjuctionQuery.Add(termQuery, Occur.MUST); disjunctionQuery.Add(termQuery, Occur.SHOULD); foreach (var field in fields) { if (onlySearchById && field != "Id") { continue; } if (onlySearchByAuthor && field != "Author") { continue; } if (onlySearchByTag && field != "Tags") { continue; } var wildCardTermQuery = new WildcardQuery(new Term(field, localTerm + "*")); wildCardTermQuery.Boost = searchLimiter ? 7.0f : 0.7f; wildCardQuery.Add(wildCardTermQuery, Occur.MUST); } } // Create an OR of all the queries that we have var combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, relatedIdQuery, exactTitleQuery, startsIdQuery, startsTitleQuery, wildCardIdQuery, wildCardTitleQuery, conjuctionQuery, wildCardQuery }); if (onlySearchByExactId) { combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery }); } else if (onlySearchById) { combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, relatedIdQuery, startsIdQuery, wildCardIdQuery, wildCardQuery }); } else if (onlySearchByAuthor || onlySearchByTag) { combinedQuery = conjuctionQuery.Combine(new Query[] { 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 Query ParseQuery(SearchFilter searchFilter) { if (String.IsNullOrWhiteSpace(searchFilter.SearchTerm)) { return new MatchAllDocsQuery(); } var fields = new[] { "Id", "Title", "Tags", "Description", "Author" }; var analyzer = new StandardAnalyzer(LuceneCommon.LuceneVersion); 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.Boost = 2.0f; // Some terms in the multi-term query appear in at least one of the fields. var disjunctionQuery = new BooleanQuery(); disjunctionQuery.Boost = 0.1f; // Suffix wildcard search e.g. jquer* var wildCardQuery = new BooleanQuery(); wildCardQuery.Boost = 0.5f; // Escape the entire term we use for exact searches. var escapedSearchTerm = Escape(searchFilter.SearchTerm).Replace("id\\:", string.Empty).Replace("author\\:", string.Empty).Replace("tag\\:", string.Empty); bool searchLimiter = false; bool onlySearchById = false; bool onlySearchByAuthor = false; bool onlySearchByTag = false; var exactIdQuery = new TermQuery(new Term("Id-Exact", escapedSearchTerm)); exactIdQuery.Boost = 2.5f; var wildCardIdQuery = new WildcardQuery(new Term("Id-Exact", "*" + escapedSearchTerm + "*")); foreach (var term in GetSearchTerms(searchFilter.SearchTerm)) { var localTerm = term.to_lower_invariant(); onlySearchById = localTerm.StartsWith("id\\:"); onlySearchByAuthor = localTerm.StartsWith("author\\:"); onlySearchByTag = localTerm.StartsWith("tag\\:"); if (onlySearchById || onlySearchByAuthor || onlySearchByTag) searchLimiter = true; localTerm = term.Replace("id\\:", string.Empty).Replace("author\\:", string.Empty).Replace("tag\\:", string.Empty); var termQuery = queryParser.Parse(localTerm); conjuctionQuery.Add(termQuery, Occur.MUST); disjunctionQuery.Add(termQuery, Occur.SHOULD); foreach (var field in fields) { if (onlySearchById && field != "Id") continue; if (onlySearchByAuthor && field != "Author") continue; if (onlySearchByTag && field != "Tags") continue; var wildCardTermQuery = new WildcardQuery(new Term(field, localTerm + "*")); wildCardTermQuery.Boost = searchLimiter ? 2.5f : 0.7f; wildCardQuery.Add(wildCardTermQuery, Occur.SHOULD); } } // Create an OR of all the queries that we have var combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, wildCardIdQuery, conjuctionQuery, disjunctionQuery, wildCardQuery }); if (onlySearchById) { combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, wildCardIdQuery, wildCardQuery }); } else if (onlySearchByAuthor || onlySearchByTag) { combinedQuery = conjuctionQuery.Combine(new Query[] { 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 IList<int> SearchCore(SearchFilter searchFilter) { if (!Directory.Exists(LuceneCommon.IndexDirectory)) { return new int[0]; } SortField sortField = GetSortField(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(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(SearchFilter searchFilter) { if (String.IsNullOrWhiteSpace(searchFilter.SearchTerm)) { return(new MatchAllDocsQuery()); } var fields = new[] { "Id", "Title", "Tags", "Description", "Author" }; var analyzer = new StandardAnalyzer(LuceneCommon.LuceneVersion); 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.Boost = 2.0f; // Some terms in the multi-term query appear in at least one of the fields. var disjunctionQuery = new BooleanQuery(); disjunctionQuery.Boost = 0.1f; // Suffix wildcard search e.g. jquer* var wildCardQuery = new BooleanQuery(); wildCardQuery.Boost = 0.5f; // Escape the entire term we use for exact searches. var escapedSearchTerm = Escape(searchFilter.SearchTerm).Replace("id\\:", string.Empty).Replace("author\\:", string.Empty).Replace("tag\\:", string.Empty); bool searchLimiter = false; bool onlySearchById = false; bool onlySearchByAuthor = false; bool onlySearchByTag = false; var exactIdQuery = new TermQuery(new Term("Id-Exact", escapedSearchTerm)); exactIdQuery.Boost = 2.5f; var wildCardIdQuery = new WildcardQuery(new Term("Id-Exact", "*" + escapedSearchTerm + "*")); foreach (var term in GetSearchTerms(searchFilter.SearchTerm)) { var localTerm = term.to_lower_invariant(); onlySearchById = localTerm.StartsWith("id\\:"); onlySearchByAuthor = localTerm.StartsWith("author\\:"); onlySearchByTag = localTerm.StartsWith("tag\\:"); if (onlySearchById || onlySearchByAuthor || onlySearchByTag) { searchLimiter = true; } localTerm = term.Replace("id\\:", string.Empty).Replace("author\\:", string.Empty).Replace("tag\\:", string.Empty); var termQuery = queryParser.Parse(localTerm); conjuctionQuery.Add(termQuery, Occur.MUST); disjunctionQuery.Add(termQuery, Occur.SHOULD); foreach (var field in fields) { if (onlySearchById && field != "Id") { continue; } if (onlySearchByAuthor && field != "Author") { continue; } if (onlySearchByTag && field != "Tags") { continue; } var wildCardTermQuery = new WildcardQuery(new Term(field, localTerm + "*")); wildCardTermQuery.Boost = searchLimiter ? 2.5f : 0.7f; wildCardQuery.Add(wildCardTermQuery, Occur.SHOULD); } } // Create an OR of all the queries that we have var combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, wildCardIdQuery, conjuctionQuery, disjunctionQuery, wildCardQuery }); if (onlySearchById) { combinedQuery = conjuctionQuery.Combine(new Query[] { exactIdQuery, wildCardIdQuery, wildCardQuery }); } else if (onlySearchByAuthor || onlySearchByTag) { combinedQuery = conjuctionQuery.Combine(new Query[] { 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 Query ParseQuery(SearchFilter searchFilter) { var fields = new[] { "Id", "Title", "Tags", "Description", "Author" }; var analyzer = new StandardAnalyzer(LuceneCommon.LuceneVersion); 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(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(searchFilter.SearchTerm)) { var termQuery = queryParser.Parse(term); conjuctionQuery.Add(termQuery, BooleanClause.Occur.MUST); disjunctionQuery.Add(termQuery, BooleanClause.Occur.SHOULD); foreach (var field in fields) { var wildCardTermQuery = new WildcardQuery(new Term(field, term + "*")); wildCardTermQuery.SetBoost(0.7f); wildCardQuery.Add(wildCardTermQuery, BooleanClause.Occur.SHOULD); } } // 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 bool TryReadSearchFilter(HttpRequestBase request, string siteRoot, out SearchFilter searchFilter) { var odataQuery = SyntacticTree.ParseUri(new Uri(siteRoot + request.RawUrl), new Uri(siteRoot)); var keywordPath = odataQuery.Path as KeywordSegmentQueryToken; searchFilter = new SearchFilter { // HACK: The way the default paging works is WCF attempts to read up to the MaxPageSize elements. If it finds as many, it'll assume there // are more elements to be paged and generate a continuation link. Consequently we'll always ask to pull MaxPageSize elements so WCF generates the // link for us and then allow it to do a Take on the results. The alternative to do is roll our IDataServicePagingProvider, but we run into // issues since we need to manage state over concurrent requests. This seems like an easier solution. Take = MaxPageSize, Skip = odataQuery.Skip ?? 0, CountOnly = keywordPath != null && keywordPath.Keyword == KeywordKind.Count, SortDirection = SortDirection.Ascending }; var filterProperty = odataQuery.Filter as PropertyAccessQueryToken; if (filterProperty == null || !(filterProperty.Name.Equals("IsLatestVersion", StringComparison.Ordinal) || filterProperty.Name.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal))) { // We'll only use the index if we the query searches for latest \ latest-stable packages return false; } var orderBy = odataQuery.OrderByTokens.FirstOrDefault(); if (orderBy == null || orderBy.Expression == null) { searchFilter.SortProperty = SortProperty.Relevance; } else if (orderBy.Expression.Kind == QueryTokenKind.PropertyAccess) { var propertyAccess = (PropertyAccessQueryToken)orderBy.Expression; if (propertyAccess.Name.Equals("DownloadCount", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.DownloadCount; } else if (propertyAccess.Name.Equals("Published", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.Recent; } else if (propertyAccess.Name.Equals("Id", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.DisplayName; } else { Debug.WriteLine("Order by clause {0} is unsupported", propertyAccess.Name); return false; } } else if (orderBy.Expression.Kind == QueryTokenKind.FunctionCall) { var functionCall = (FunctionCallQueryToken)orderBy.Expression; if (functionCall.Name.Equals("concat", StringComparison.OrdinalIgnoreCase)) { // We'll assume this is concat(Title, Id) searchFilter.SortProperty = SortProperty.DisplayName; searchFilter.SortDirection = orderBy.Direction == OrderByDirection.Descending ? SortDirection.Descending : SortDirection.Ascending; } else { Debug.WriteLine("Order by clause {0} is unsupported", functionCall.Name); return false; } } else { Debug.WriteLine("Order by clause {0} is unsupported", orderBy.Expression.Kind); return false; } return true; }
private static bool TryReadSearchFilter(string url, out SearchFilter searchFilter) { if (url == null) { searchFilter = null; return(false); } int indexOfQuestionMark = url.IndexOf('?'); if (indexOfQuestionMark == -1) { searchFilter = null; return(false); } string path = url.Substring(0, indexOfQuestionMark); string query = url.Substring(indexOfQuestionMark + 1); if (string.IsNullOrEmpty(query)) { searchFilter = null; return(false); } searchFilter = new SearchFilter { // The way the default paging works is WCF attempts to read up to the MaxPageSize elements. If it finds as many, it'll assume there // are more elements to be paged and generate a continuation link. Consequently we'll always ask to pull MaxPageSize elements so WCF generates the // link for us and then allow it to do a Take on the results. The alternative to do is roll our IDataServicePagingProvider, but we run into // issues since we need to manage state over concurrent requests. This seems like an easier solution. Take = MaxPageSize, Skip = 0, CountOnly = path.EndsWith("$count", StringComparison.Ordinal), SortDirection = SortDirection.Ascending }; string[] props = query.Split('&'); IDictionary <string, string> queryTerms = new Dictionary <string, string>(); foreach (string prop in props) { string[] nameValue = prop.Split('='); if (nameValue.Length == 2) { queryTerms[nameValue[0]] = nameValue[1]; } } // We'll only use the index if we the query searches for latest \ latest-stable packages string filter; if (queryTerms.TryGetValue("$filter", out filter)) { if (!(filter.Equals("IsLatestVersion", StringComparison.Ordinal) || filter.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal))) { searchFilter = null; return(false); } } else { searchFilter = null; return(false); } string skip; if (queryTerms.TryGetValue("$skip", out skip)) { int result; if (int.TryParse(skip, out result)) { searchFilter.Skip = result; } } // only certain orderBy clauses are supported from the Lucene search string orderBy; if (queryTerms.TryGetValue("$orderby", out orderBy)) { if (string.IsNullOrEmpty(orderBy)) { searchFilter.SortProperty = SortProperty.Relevance; } else if (orderBy.StartsWith("DownloadCount", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.DownloadCount; } else if (orderBy.StartsWith("Published", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.Recent; } else if (orderBy.StartsWith("LastEdited", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.RecentlyEdited; } else if (orderBy.StartsWith("Id", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.DisplayName; } else if (orderBy.StartsWith("concat", StringComparison.Ordinal)) { searchFilter.SortProperty = SortProperty.DisplayName; if (orderBy.Contains("%20desc")) { searchFilter.SortDirection = SortDirection.Descending; } } else { searchFilter = null; return(false); } } else { searchFilter.SortProperty = SortProperty.Relevance; } return(true); }
public static IQueryable <Package> GetResultsFromSearchService(ISearchService searchService, SearchFilter searchFilter) { int totalHits; var result = searchService.Search(searchFilter, out totalHits); // For count queries, we can ask the SearchService to not filter the source results. This would avoid hitting the database and consequently make // it very fast. if (searchFilter.CountOnly) { // At this point, we already know what the total count is. We can have it return this value very quickly without doing any SQL. return(result.InterceptWith(new CountInterceptor(totalHits))); } // For relevance search, Lucene returns us a paged\sorted list. OData tries to apply default ordering and Take \ Skip on top of this. // We avoid it by yanking these expressions out of out the tree. return(result.InterceptWith(new DisregardODataInterceptor())); }
private static bool TryReadSearchFilter(bool allVersionsInIndex, string url, out SearchFilter searchFilter) { if (url == null) { searchFilter = null; return false; } int indexOfQuestionMark = url.IndexOf('?'); if (indexOfQuestionMark == -1) { searchFilter = null; return false; } string path = url.Substring(0, indexOfQuestionMark); string query = url.Substring(indexOfQuestionMark + 1); if (string.IsNullOrEmpty(query)) { searchFilter = null; return false; } searchFilter = new SearchFilter(SearchFilter.ODataSearchContext) { // The way the default paging works is WCF attempts to read up to the MaxPageSize elements. If it finds as many, it'll assume there // are more elements to be paged and generate a continuation link. Consequently we'll always ask to pull MaxPageSize elements so WCF generates the // link for us and then allow it to do a Take on the results. The alternative to do is roll our IDataServicePagingProvider, but we run into // issues since we need to manage state over concurrent requests. This seems like an easier solution. Take = MaxPageSize, Skip = 0, CountOnly = path.EndsWith("$count", StringComparison.Ordinal) }; string[] props = query.Split('&'); IDictionary<string, string> queryTerms = new Dictionary<string, string>(); foreach (string prop in props) { string[] nameValue = prop.Split('='); if (nameValue.Length == 2) { queryTerms[nameValue[0]] = nameValue[1]; } } // We'll only use the index if we the query searches for latest \ latest-stable packages string filter; if (queryTerms.TryGetValue("$filter", out filter)) { if (!(filter.Equals("IsLatestVersion", StringComparison.Ordinal) || filter.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal))) { searchFilter = null; return false; } } else if(!allVersionsInIndex) { searchFilter = null; return false; } string skipStr; if (queryTerms.TryGetValue("$skip", out skipStr)) { int skip; if (int.TryParse(skipStr, out skip)) { searchFilter.Skip = skip; } } string topStr; if (queryTerms.TryGetValue("$top", out topStr)) { int top; if(int.TryParse(topStr, out top)) { searchFilter.Take = Math.Min(top, SearchAdaptor.MaxPageSize); } } // only certain orderBy clauses are supported from the Lucene search string orderBy; if (queryTerms.TryGetValue("$orderby", out orderBy)) { if (string.IsNullOrEmpty(orderBy)) { searchFilter.SortOrder = SortOrder.Relevance; } else if (orderBy.StartsWith("DownloadCount", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.Relevance; } else if (orderBy.StartsWith("Published", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.Published; } else if (orderBy.StartsWith("LastEdited", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.LastEdited; } else if (orderBy.StartsWith("Id", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.TitleAscending; } else if (orderBy.StartsWith("concat", StringComparison.Ordinal)) { searchFilter.SortOrder = SortOrder.TitleAscending; if (orderBy.Contains("%20desc")) { searchFilter.SortOrder = SortOrder.TitleDescending; } } else { searchFilter = null; return false; } } else { searchFilter.SortOrder = SortOrder.Relevance; } return true; }