Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Personalize Find with Social

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

Powered by ReSharper image

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();
}
}
}
/// <summary>
/// The SocialRatingRepository class defines the operations that can be issued
/// against the EPiServer Social rating repository.
/// </summary>
public class SocialRatingRepository : ISocialRatingRepository
{
/// <summary>
/// The content repository
/// </summary>
private readonly IContentRepository contentRepository;
/// <summary>
/// The rating service
/// </summary>
private readonly IRatingService ratingService;
/// <summary>
/// The category repository
/// </summary>
private readonly CategoryRepository categoryRepository;
/// <summary>
/// Initializes a new instance of the <see cref="SocialRatingRepository"/> class.
/// </summary>
/// <param name="ratingService">The rating service.</param>
/// <param name="contentRepository">The content repository.</param>
/// <param name="synchronizedObjectInstanceCache">The synchronized object instance cache.</param>
/// <param name="categoryRepository">The category repository.</param>
public SocialRatingRepository(IRatingService ratingService, IContentRepository contentRepository, CategoryRepository categoryRepository)
{
this.ratingService = ratingService;
this.contentRepository = contentRepository;
this.categoryRepository = categoryRepository;
}
/// <summary>
/// Gets the favorite categories for user.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>A dictionary with the category id and the times it occurred.</returns>
public Dictionary<int, int> GetFavoriteCategoriesForUser(string userId)
{
Dictionary<int, int> favoriteCategories = new Dictionary<int, int>();
if (string.IsNullOrWhiteSpace(userId))
{
return favoriteCategories;
}
List<IContent> topRated = this.GetTopRatedPagesForUser(userId).ToList();
foreach (ICategorizable page in topRated.OfType<ICategorizable>())
{
foreach (int categoryId in page.Category)
{
if (favoriteCategories.ContainsKey(categoryId))
{
favoriteCategories[categoryId] += 1;
}
else
{
favoriteCategories.Add(categoryId, 1);
}
}
}
return favoriteCategories.OrderByDescending(c => c.Value).Take(5).ToDictionary(pair => pair.Key, pair => pair.Value);
}
/// <summary>
/// Gets the favorite content types for user.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>A dictionary with the content type id and the times it occurred.</returns>
public Dictionary<int, int> GetFavoriteContentTypesForUser(string userId)
{
Dictionary<int, int> favoriteContentTypes = new Dictionary<int, int>();
if (string.IsNullOrWhiteSpace(userId))
{
return favoriteContentTypes;
}
List<IContent> topRated = this.GetTopRatedPagesForUser(userId).ToList();
foreach (IContent content in topRated)
{
int contentTypeId = content.ContentTypeID;
if (favoriteContentTypes.ContainsKey(contentTypeId))
{
favoriteContentTypes[contentTypeId] += 1;
}
else
{
favoriteContentTypes.Add(contentTypeId, 1);
}
}
return favoriteContentTypes.OrderByDescending(c => c.Value).Take(5).ToDictionary(pair => pair.Key, pair => pair.Value);
}
/// <summary>
/// Gets the top rated pages for user.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>A list of favorite content for the user.</returns>
public IEnumerable<IContent> GetTopRatedPagesForUser(string userId)
{
if (string.IsNullOrWhiteSpace(userId))
{
return new List<IContent>();
}
Reference rater = Reference.Create(userId);
ResultPage<Rating> ratingPage = this.ratingService.Get(
new Criteria<RatingFilter>()
{
Filter = new RatingFilter { Rater = rater },
PageInfo =
new PageInfo
{
PageSize = 25
},
OrderBy =
new List<SortInfo>
{
new SortInfo(RatingSortFields.Value, false),
new SortInfo(
RatingSortFields.Created,
false),
}
});
return ratingPage.Results.Select(result => this.contentRepository.Get<IContent>(Guid.Parse(result.Target.Id)));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.