If you are using Episerver Social on your site, it's possible to "personalize" search results for a logged in user.
Read my blog here
namespace EPiServer.SocialAlloy.Web.Business.FindHelpers | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using EPiServer.Core; | |
using EPiServer.Find; | |
using EPiServer.Find.Api.Facets; | |
using EPiServer.Find.Api.Querying.Queries; | |
/// <summary> | |
/// Class FindExtensions. | |
/// </summary> | |
public static class FindExtensions | |
{ | |
/// <summary> | |
/// Adds category boosts to the search. | |
/// </summary> | |
/// <typeparam name="T">The type to query for.</typeparam> | |
/// <param name="query">The query.</param> | |
/// <param name="favoriteCategories">The categories to boost, with the boost factor.</param> | |
/// <returns>The <see cref="IQueriedSearch{T}"/> with added BoostMatching.</returns> | |
/// <remarks><para>The BoostMatching method must be called before any method not related to the search query (such as Filter, Take, and Skip).</para> | |
/// <para>This is enforced by the fact that the For method in the above sample returns a IQueriedSearch object.</para> | |
/// </remarks> | |
public static IQueriedSearch<T> AddCategoryBoosts<T>( | |
this IQueriedSearch<T> query, | |
Dictionary<int, int> favoriteCategories) | |
where T : ICategorizable | |
{ | |
return favoriteCategories.Aggregate( | |
query, | |
(current, favoriteCategory) => current.BoostMatching(x => x.Category.In(new[] { favoriteCategory.Key }), favoriteCategory.Value)); | |
} | |
/// <summary> | |
/// Adds content type boosts to the search. | |
/// </summary> | |
/// <typeparam name="T">The type to query for.</typeparam> | |
/// <param name="query">The query.</param> | |
/// <param name="favoriteUserContent">The content types to boost, with the boost factor.</param> | |
/// <returns>The <see cref="IQueriedSearch{T}"/> with added BoostMatching.</returns> | |
/// <remarks><para>The BoostMatching method must be called before any method not related to the search query (such as Filter, Take, and Skip).</para> | |
/// <para>This is enforced by the fact that the For method in the above sample returns a IQueriedSearch object.</para> | |
/// </remarks> | |
public static IQueriedSearch<T> AddContentTypeBoosts<T>( | |
this IQueriedSearch<T> query, | |
Dictionary<int, int> favoriteUserContent) | |
where T : IContent | |
{ | |
return favoriteUserContent.Aggregate( | |
query, | |
(current, favoriteContent) => current.BoostMatching(x => x.ContentTypeID.Match(favoriteContent.Key), favoriteContent.Value)); | |
} | |
} | |
} |
namespace EPiServer.SocialAlloy.Web.Models.ViewModels | |
{ | |
using System; | |
using System.Web; | |
using EPiServer; | |
using EPiServer.Find.Cms; | |
using EPiServer.Find.Framework.Statistics; | |
using EPiServer.ServiceLocation; | |
using EPiServer.SocialAlloy.Web.Models.Pages; | |
public class FindSearchContentModel : PageViewModel<FindSearchPage> | |
{ | |
public FindSearchContentModel(FindSearchPage currentPage) | |
: base(currentPage) | |
{ | |
} | |
/// <summary> | |
/// Gets or sets the content result. | |
/// </summary> | |
/// <value>The content result.</value> | |
public IContentResult<StandardPage> ContentResult { get; set; } | |
/// <summary> | |
/// Public proxy path mainly used for constructing url's in javascript | |
/// </summary> | |
public string PublicProxyPath { get; set; } | |
/// <summary> | |
/// Flag to indicate if both Find serviceUrl and defaultIndex are configured | |
/// </summary> | |
public bool IsConfigured { get; set; } | |
/// <summary> | |
/// Constructs a url for a section group | |
/// </summary> | |
/// <param name="groupName">Name of group</param> | |
/// <returns>Url</returns> | |
public string GetSectionGroupUrl(string groupName) | |
{ | |
return UriSupport.AddQueryString(this.RemoveQueryStringByKey(HttpContext.Current.Request.Url.AbsoluteUri,"p"), "t", HttpContext.Current.Server.UrlEncode(groupName)); | |
} | |
/// <summary> | |
/// Removes specified query string from url | |
/// </summary> | |
/// <param name="url">Url from which to remove query string</param> | |
/// <param name="key">Key of query string to remove</param> | |
/// <returns>New url that excludes the specified query string</returns> | |
private string RemoveQueryStringByKey(string url, string key) | |
{ | |
var uri = new Uri(url); | |
var newQueryString = HttpUtility.ParseQueryString(uri.Query); | |
newQueryString.Remove(key); | |
string pagePathWithoutQueryString = uri.GetLeftPart(UriPartial.Path); | |
return newQueryString.Count > 0 | |
? String.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString) | |
: pagePathWithoutQueryString; | |
} | |
/// <summary> | |
/// Number of matching hits | |
/// </summary> | |
public int NumberOfHits | |
{ | |
get { return this.ContentResult.TotalMatching; } | |
} | |
/// <summary> | |
/// Current active section filter | |
/// </summary> | |
public string SectionFilter | |
{ | |
get { return HttpContext.Current.Request.QueryString["t"] ?? string.Empty; } | |
} | |
/// <summary> | |
/// Retrieve the paging page from the query string parameter "p". | |
/// If no parameter exists, default to the first page. | |
/// </summary> | |
public int PagingPage | |
{ | |
get | |
{ | |
int pagingPage; | |
if (!int.TryParse(HttpContext.Current.Request.QueryString["p"], out pagingPage)) | |
{ | |
pagingPage = 1; | |
} | |
return pagingPage; | |
} | |
} | |
/// <summary> | |
/// Retrieve the paging section from the query string parameter "ps". | |
/// If no parameter exists, default to the first paging section. | |
/// </summary> | |
public int PagingSection | |
{ | |
get | |
{ | |
return 1 + (this.PagingPage - 1) / this.PagingSectionSize; | |
} | |
} | |
/// <summary> | |
/// Calculate the number of pages required to list results | |
/// </summary> | |
public int TotalPagingPages | |
{ | |
get | |
{ | |
if (CurrentPage.PageSize > 0) | |
{ | |
return 1 + (this.ContentResult.TotalMatching - 1)/CurrentPage.PageSize; | |
} | |
return 0; | |
} | |
} | |
public int PagingSectionSize | |
{ | |
get { return 10; } | |
} | |
/// <summary> | |
/// Calculate the number of paging sections required to list page links | |
/// </summary> | |
public int TotalPagingSections | |
{ | |
get | |
{ | |
return 1 + (this.TotalPagingPages - 1) / this.PagingSectionSize; | |
} | |
} | |
/// <summary> | |
/// Number of first page in current paging section | |
/// </summary> | |
public int PagingSectionFirstPage | |
{ | |
get { return 1 + (this.PagingSection - 1) * this.PagingSectionSize; } | |
} | |
/// <summary> | |
/// Number of last page in current paging section | |
/// </summary> | |
public int PagingSectionLastPage | |
{ | |
get { return Math.Min((this.PagingSection * this.PagingSectionSize), this.TotalPagingPages); } | |
} | |
/// <summary> | |
/// Create URL for a specified paging page. | |
/// </summary> | |
/// <param name="pageNumber">Number of page for which to get a url</param> | |
/// <returns>Url for specified page</returns> | |
public string GetPagingUrl(int pageNumber) | |
{ | |
return UriSupport.AddQueryString(HttpContext.Current.Request.RawUrl, "p", pageNumber.ToString()); | |
} | |
/// <summary> | |
/// Create URL for the next paging section. | |
/// </summary> | |
/// <returns>Url for next paging section</returns> | |
public string GetNextPagingSectionUrl() | |
{ | |
return UriSupport.AddQueryString(HttpContext.Current.Request.RawUrl, "p", ((this.PagingSection * this.PagingSectionSize) + 1).ToString()); | |
} | |
/// <summary> | |
/// Create URL for the previous paging section. | |
/// </summary> | |
/// <returns>Url for previous paging section</returns> | |
public string GetPreviousPagingSectionUrl() | |
{ | |
return UriSupport.AddQueryString(HttpContext.Current.Request.RawUrl, "p", ((this.PagingSection - 1) * this.PagingSectionSize).ToString()); | |
} | |
/// <summary> | |
/// User query to search | |
/// </summary> | |
public string Query | |
{ | |
get { return (HttpContext.Current.Request.QueryString["q"] ?? string.Empty).Trim(); } | |
} | |
/// <summary> | |
/// Search tags like language and site | |
/// </summary> | |
public string Tags | |
{ | |
get { return string.Join(",", ServiceLocator.Current.GetInstance<IStatisticTagsHelper>().GetTags()); } | |
} | |
/// <summary> | |
/// Length of excerpt | |
/// </summary> | |
public int ExcerptLength | |
{ | |
get { return CurrentPage.ExcerptLength; } | |
} | |
/// <summary> | |
/// Height of hit images | |
/// </summary> | |
public int HitImagesHeight | |
{ | |
get { return CurrentPage.HitImagesHeight; } | |
} | |
/// <summary> | |
/// Flag retrieved from editor settings to determine if it should | |
/// use AND as the operator for multiple search terms | |
/// </summary> | |
public bool UseAndForMultipleSearchTerms | |
{ | |
get { return CurrentPage.UseAndForMultipleSearchTerms; } | |
} | |
} | |
} |
namespace EPiServer.SocialAlloy.Web.Controllers | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.Web.Mvc; | |
using EPiServer.Core; | |
using EPiServer.Find; | |
using EPiServer.Find.Api.Facets; | |
using EPiServer.Find.Api.Querying.Filters; | |
using EPiServer.Find.Api.Querying.Queries; | |
using EPiServer.Find.Cms; | |
using EPiServer.Find.Framework.Statistics; | |
using EPiServer.Find.Helpers.Text; | |
using EPiServer.Find.UI; | |
using EPiServer.Framework.Web.Resources; | |
using EPiServer.Globalization; | |
using EPiServer.SocialAlloy.Web.Business.FindHelpers; | |
using EPiServer.SocialAlloy.Web.Models.Pages; | |
using EPiServer.SocialAlloy.Web.Models.ViewModels; | |
using EPiServer.SocialAlloy.Web.Social.Repositories; | |
using EPiServer.Web; | |
using Configuration = EPiServer.Find.Configuration; | |
public class FindSearchPageController : PageControllerBase<FindSearchPage> | |
{ | |
private readonly IClient searchClient; | |
private readonly IFindUIConfiguration findUIConfiguration; | |
private readonly IRequiredClientResourceList requiredClientResourceList; | |
private readonly ISocialRatingRepository socialRatingRepository; | |
private readonly IUserRepository userRepository; | |
public FindSearchPageController(IClient searchClient, IFindUIConfiguration findUIConfiguration, IRequiredClientResourceList requiredClientResourceList, ISocialRatingRepository socialRatingRepository, IUserRepository userRepository) | |
{ | |
this.searchClient = searchClient; | |
this.findUIConfiguration = findUIConfiguration; | |
this.requiredClientResourceList = requiredClientResourceList; | |
this.socialRatingRepository = socialRatingRepository; | |
this.userRepository = userRepository; | |
} | |
[ValidateInput(false)] | |
public ViewResult Index(FindSearchPage currentPage, string q) | |
{ | |
FindSearchContentModel model = new FindSearchContentModel(currentPage) | |
{ | |
PublicProxyPath = this.findUIConfiguration.AbsolutePublicProxyPath() | |
}; | |
// detect if serviceUrl and/or defaultIndex is configured. | |
model.IsConfigured = this.SearchIndexIsConfigured(EPiServer.Find.Configuration.GetConfiguration()); | |
if (model.IsConfigured && !string.IsNullOrWhiteSpace(model.Query)) | |
{ | |
ITypeSearch<StandardPage> queryFor = this.BuildBoostedQuery(model); | |
model.ContentResult = queryFor.GetContentResult(); | |
} | |
this.RequireClientResources(); | |
return View(model); | |
} | |
private ITypeSearch<StandardPage> BuildBoostedQuery(FindSearchContentModel model) | |
{ | |
string userId = this.userRepository.GetUserId(this.User); | |
Dictionary<int, int> cats = this.socialRatingRepository.GetFavoriteCategoriesForUser(userId); | |
Dictionary<int, int> types = this.socialRatingRepository.GetFavoriteContentTypesForUser(userId); | |
ITypeSearch<StandardPage> query = | |
this.searchClient.Search<StandardPage>( | |
this.searchClient.Settings.Languages.GetSupportedLanguage(ContentLanguage.PreferredCulture) | |
?? Language.None).For(model.Query) | |
// Boost the users favorite categories | |
.AddCategoryBoosts(cats) | |
// Boost the users favorite content types | |
.AddContentTypeBoosts(types) | |
// Fetch the specific paging page. | |
.Skip((model.PagingPage - 1) * model.CurrentPage.PageSize).Take(model.CurrentPage.PageSize) | |
// Allow editors (from the Find/Optimizations view) to push specific hits to the top | |
// for certain search phrases. | |
.ApplyBestBets(); | |
// obey DNT | |
string doNotTrackHeader = System.Web.HttpContext.Current.Request.Headers.Get("DNT"); | |
// Should not track when value equals 1 | |
if (doNotTrackHeader == null || doNotTrackHeader.Equals("0")) | |
{ | |
query = query.Track(); | |
} | |
return query; | |
} | |
/// <summary> | |
/// Checks if service url and index are configured | |
/// </summary> | |
/// <param name="configuration">Find configuration</param> | |
/// <returns>True if configured, false otherwise</returns> | |
private bool SearchIndexIsConfigured(Configuration configuration) | |
{ | |
return !configuration.ServiceUrl.IsNullOrEmpty() | |
&& !configuration.ServiceUrl.Contains("YOUR_URI") | |
&& !configuration.DefaultIndex.IsNullOrEmpty() | |
&& !configuration.DefaultIndex.Equals("YOUR_INDEX"); | |
} | |
/// <summary> | |
/// Requires the client resources used in the view. | |
/// </summary> | |
private void RequireClientResources() | |
{ | |
// jQuery.UI is used in autocomplete example. | |
// Add jQuery.UI files to existing client resource bundles or load it from CDN or use any other alternative library. | |
// We use local resources for demo purposes without Internet connection. | |
this.requiredClientResourceList.RequireStyle(VirtualPathUtilityEx.ToAbsolute("~/Static/css/jquery-ui.css")); | |
this.requiredClientResourceList.RequireScript(VirtualPathUtilityEx.ToAbsolute("~/Static/js/jquery-ui.js")).AtFooter(); | |
} | |
} | |
} |