Skip to content

Instantly share code, notes, and snippets.

@javafun
Forked from jstemerdink/FindExtensions.cs
Created June 23, 2019 23:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save javafun/00f696ad9d837d77ab3c7043a9d091ac to your computer and use it in GitHub Desktop.
Save javafun/00f696ad9d837d77ab3c7043a9d091ac to your computer and use it in GitHub Desktop.

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