Date Range Facets

Faceted search adds powerful search functionality to websites and allows web users to narrow down the results in a listing page, letting the users find what they really want. Sometimes, we need to add a date range facet as part of our search page. This post will give you some guidance on how to create facets based on DateTime fields using Episerver Search & Navigation (formerly called Episerver Find).

First, we will define the ranges or options we want to display.
In this case, we will define Monday as the first day of the week and Sunday will be the last.

For events we have defined the following rules:
– Upcoming events: event date greater or equal than today
– This week: event date happening this week (monday as the first day of the week)
– Old events: event date less than today

For articles we have defined these rules:
– Latest stories: publish date set for the current week
– Current year: publish date in between of a defined first Day (can be January 1st) and end day
– Oldest stories: publish date less than the first day defined

private static DateTime StartOfWeek => DateTime.Now.StartOfWeek(DayOfWeek.Monday);
private static DateTime EndOfWeek => StartOfWeek.AddDays(6);
private static DateTime FirstDay => new DateTime(DateTime.Now.Year, 1, 1);
private static DateTime LastDay => new DateTime(DateTime.Now.Year, 12, 31);

private readonly List<DateRange> _eventDateRanges = new List<DateRange>
{
	new DateRange { From = DateTime.Now},
	new DateRange { From = StartOfWeek, To = EndOfWeek},
	new DateRange { From = DateTime.MinValue, To = DateTime.Now.AddDays(-1) }
};

private readonly List<DateRange> _publishDateRanges = new List<DateRange>
{
	new DateRange { From = DateTime.Now.AddMonths(-3)},
	new DateRange { From = FirstDay, To = LastDay},
	new DateRange { From = DateTime.MinValue, To = FirstDay }
};

Once we’ve defined the custom ranges, now we need to contruct the search logic

public YourResponseModel GetResults(FilterOption filter)
{
	var query = _findClient.Search<BasePageData>();


	if (!string.IsNullOrWhiteSpace(filter.Keyword))
	{
		query = query.For(filter.Keyword);
	}
	
	//Get articles and events
	query = query.FilterByExactTypes(new List<Type> { typeof(EventDetailPage), typeof(ArticleDetailPage) });
	
	if (filter.EventDate != null)
	{
		var facetFilter = _findClient.BuildFilter<BasePageData>();
		query = query.FilterByExactTypes(new List<Type> { typeof(EventDetailPage) });

		facetFilter = filter.EventDate switch
		{
			"1" => facetFilter.Or(x =>
				((EventDetailPage)x).StartDateTime().Exists() &
				((EventDetailPage)x).StartDateTime().GreaterThanNow()),
			"2" => facetFilter.Or(x =>
				((EventDetailPage)x).StartDateTime().Exists() &
				((EventDetailPage)x).StartDateTime().GreaterThan(StartOfWeek) &
				((EventDetailPage)x).StartDateTime().LessThan(EndOfWeek)),
			"3" => facetFilter.Or(x =>
				((EventDetailPage)x).StartDateTime().Exists() &
				((EventDetailPage)x).StartDateTime().LessThanNow()),
			_ => facetFilter
		};

		query = query.Filter(facetFilter);
	}
	
	if (filter.PublishDate != null)
	{
		var facetFilter = _findClient.BuildFilter<FoundationPageData>();
		query = query.FilterByExactTypes(new List<Type> { typeof(ArticleDetailPage) });

		facetFilter = filter.PublishDate switch
		{
			"1" => facetFilter.Or(x =>
				((ArticleDetailPage)x).PublishDate.Exists() &
				((ArticleDetailPage)x).PublishDate.GreaterThan(DateTime.Now.AddMonths(-3))),
			"2" => facetFilter.Or(x =>
				((ArticleDetailPage)x).PublishDate.Exists() &
				((ArticleDetailPage)x).PublishDate.GreaterThan(FirstDay) &
				((ArticleDetailPage)x).PublishDate.LessThan(LastDay)),
			"3" => facetFilter.Or(x =>
				((ArticleDetailPage)x).PublishDate.Exists() &
				((ArticleDetailPage)x).PublishDate.LessThan(FirstDay)),
			_ => facetFilter
		};

		query = query.Filter(facetFilter);
	}

	query = query.FilterForVisitor()
		.RangeFacetFor(x => ((EventDetailPage)x).StartDateTime(), _eventDateRanges.ToArray())
		.RangeFacetFor(x => ((ArticleDetailPage)x).PublishDate, _publishDateRanges.ToArray());

	query = query.Skip((filter.Page - 1) * filter.PageSize)
			.Take(filter.PageSize);
	
	var results = query.GetContentResult();
	
	//Process your response model
	...
	//Generate Facet List	
	...	

	return response;
}

THe FilterOption object has basically a common information you would pass to hanlde pagination plus the filters you display.

public class FilterOption
{
	public int Page { get; set; } = 1;
	public string Keyword { get; set; }
	public int PageSize { get; set; } = 10;       
	public string EventDate { get; set; }
	public string PublishDate { get; set; }
}


If you are wondering how you can generate the facet list, below is a sample, and it will get the facets based on the results that you get from Find and then you can process them to add labels and the number of items.

var eventDateFacets = results.RangeFacetFor(x => ((EventDetailPage)x).StartDateTime());
var publishDateFacets = results.RangeFacetFor(x => ((ArticleDetailPage)x).PublishDate);

//event Date
if (eventDateFacets?.Ranges != null && eventDateFacets.Ranges.Any(x => x.Count > 0))
{
	var facet = new ListingFacet
	{
		Keyword = nameof(filter.EventDate),
		Name = "Event Date",
		Options = GetEventDateFacetOptions(eventDateFacets.Ranges, page)
	};

	facets.Add(facet);
}

//publish Date
if (publishDateFacets?.Ranges != null && publishDateFacets.Ranges.Any(x => x.Count > 0))
{
	var facet = new ListingFacet
	{
		Keyword = nameof(filter.PublishDate),
		Name = "Publish Date",
		Options = GetPublishDateFacetOptions(publishDateFacets.Ranges, page)
	};

	facets.Add(facet);
}
......

private IEnumerable<FacetOption> GetEventDateFacetOptions(IEnumerable<DateRangeResult> ranges)
{
	var options = new List<FacetOption>();
	int cont = 0;

	foreach (var range in ranges)
	{
		cont++;
		if (range.Count == 0) continue;

		options.Add(new FacetOption
		{
			Value = cont.ToString(),
			Name = cont switch
			{
				1 => "Upcoming Events",
				2 => "This Week",
				_ => "Old Events"
			},
			Matches = range.Count
		});

	}

	return options;
}

private IEnumerable<FacetOption> GetPublishDateFacetOptions(IEnumerable<DateRangeResult> ranges)
{
	var options = new List<FacetOption>();
	int cont = 0;

	foreach (var range in ranges)
	{
		cont++;
		if (range.Count == 0) continue;

		options.Add(new FacetOption
		{
			Value = cont.ToString(),
			Name = cont switch
			{
				1 => "Latest Stories",
				2 => "Current Year",
				_ => "Oldest Stories"
			},
			Matches = range.Count
		});
	}

	return options;
}


And here are some models that probably you will require to generate the response:

public partial class ListingFacet
{
	public string Name { get; set; }

	public string Keyword { get; set; }

	public IEnumerable<FacetOption> Options { get; set; }
}

public partial class FacetOption
{
	public string Name { get; set; }

	public long Matches { get; set; }

	public string Value { get; set; }
}

This is a sample of the response you could generate as JSON

{"Name":"Event Date","Keyword":"EventDate","Options":[{"Name":"Upcoming Events","Matches":73,"Value":"1"},{"Name":"This Week","Matches":8,"Value":"2"},{"Name":"Old Events","Matches":232,"Value":"3"}]},{"Name":"Publish Date","Keyword":"PublishDate","Options":[{"Name":"Latest Stories","Matches":46,"Value":"1"},{"Name":"Oldest Stories","Matches":198,"Value":"3"}]}

And that’s how you can generate custom date ranges using Find.

You can create a free demo index search if you don’t have one, check https://find.episerver.com/ and register.

Happy coding 😉

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: