Skip to content

Instantly share code, notes, and snippets.

@tuespetre
Last active June 29, 2017 08:06
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tuespetre/f6951bb665c79abbb7c8 to your computer and use it in GitHub Desktop.
Save tuespetre/f6951bb665c79abbb7c8 to your computer and use it in GitHub Desktop.
A utility class and general pattern for creating Github Issues-style Elasticsearch query string builder UIs.

Usage

Pretty straightforward. Follow the pattern in the files of this Gist. They've only demonstrated one type of filter though: a 0-1 selection from a group of options. Here are the types of filters you might have:

  • A single toggleable option
  • 0 or 1 of a group of options
  • 1 of a group of options (enforced by HasOption/AppendOption/PrependOption in a controller action)
  • 0 to n of a group of options (think Github label filters)
  • 1 to n of a group of options (enforced by HasOption/AppendOption/PrependOption in a controller action)
  • user-entered options

For any 'custom' options where the user can type in a filter, create a small form that POSTs to the current page -- the server can then manipulate the query string (by calling AppendOption/PrependOption) and redirect the user to the page with the appropriate query string. You can also use HasGenericOption, GetGenericOption, and RemoveGenericOption like so to handle these custom filters:

var query = "volvo tire.status:(deflated OR punctured)";

var test = FilterHelper.HasGenericOption(query, "tire.status"); // true

var value = FilterHelper.GetGenericOption(query, "tire.status"); // "(deflated OR punctured)";

var cleared = FilterHelper.RemoveGenericOption(query, "tire.status"); // "volvo";

How Github and Stack Overflow do their "is:..." filters

This is pretty simple but I admit I had to pause to think about it. Just create an 'is' property on your Elasticsearch documents that contains an array of strings.

using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Your.Web.Project
{
public static class FilterHelper
{
public static bool HasOption(string query, string option)
{
var regex = GetRegex(option);
return regex.IsMatch(query);
}
public static bool HasOption(string query, params string[] options)
{
foreach (var option in options)
{
if (HasOption(query, option))
{
return true;
}
}
return false;
}
public static bool HasGenericOption(string query, string field)
{
var regex = GetGenericRegex(field);
return regex.IsMatch(query);
}
public static bool HasGenericOption(string query, params string[] fields)
{
foreach (var field in fields)
{
if (HasGenericOption(query, field))
{
return true;
}
}
return false;
}
public static string GetGenericOption(string query, string field)
{
var value = string.Empty;
var regex = GetGenericRegex(field);
var matches = regex.Matches(query);
foreach (Match match in matches)
{
var group = match.Groups["value"];
if (group.Success)
{
value = group.Value;
break;
}
}
return value.Trim();
}
public static string PrependOption(string query, string option)
{
return Compact(string.Format("{0} {1}", option, query));
}
public static string PrependGenericOption(string query, string field, string value)
{
return PrependOption(query, FormatGenericOption(field, value));
}
public static string AppendOption(string query, string option)
{
return Compact(string.Format("{0} {1}", query, option));
}
public static string AppendGenericOption(string query, string field, string value)
{
return AppendOption(query, FormatGenericOption(field, value));
}
public static string RemoveOption(string query, string option)
{
return Compact(query.Replace(option, " "));
}
public static string RemoveOption(string query, params string[] options)
{
foreach (var option in options)
{
query = RemoveOption(query, option);
}
return query;
}
public static string RemoveGenericOption(string query, string field)
{
var regex = GetGenericRegex(field);
return Compact(regex.Replace(query, " "));
}
public static string RemoveGenericOption(string query, params string[] fields)
{
foreach (var field in fields)
{
query = RemoveGenericOption(query, field);
}
return query;
}
public static string TogglePrependOption(string query, string option, params string[] options)
{
if (HasOption(query, option))
{
return RemoveOption(query, option);
}
else
{
foreach (var _option in options)
{
query = RemoveOption(query, _option);
}
return PrependOption(query, option);
}
}
public static string ToggleAppendOption(string query, string option, params string[] options)
{
if (HasOption(query, option))
{
return RemoveOption(query, option);
}
else
{
foreach (var _option in options)
{
query = RemoveOption(query, _option);
}
return AppendOption(query, option);
}
}
public static string FormatGenericOption(string field, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
else if (value.Contains(" "))
{
value = value.Trim('"');
value = string.Format("\"{0}\"", value);
}
return string.Format("{0}:{1}", field, value);
}
private static Regex GetRegex(string option, bool escape = true)
{
Regex regex;
if (escape)
{
option = Regex.Escape(option);
}
if (!compiled.TryGetValue(option, out regex))
{
var pattern = string.Format(specificOptionTemplate, option);
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
lock (compiled)
{
if (!compiled.ContainsKey(option))
{
compiled.Add(option, regex);
}
}
}
return regex;
}
private static Regex GetGenericRegex(string field)
{
var option = string.Format(genericOptionTemplate, Regex.Escape(field));
var regex = GetRegex(option, escape: false);
return regex;
}
private static string Compact(string query)
{
return compacter.Replace(query, " ").Trim();
}
private static readonly Dictionary<string, Regex> compiled = new Dictionary<string, Regex>();
private static readonly Regex compacter = new Regex(@" +", RegexOptions.Compiled);
private static readonly string specificOptionTemplate = @"
(^|\ +)
{0}
(\ +|$)";
/// <summary>
/// Note the double curly braces; this string is intended to be used with
/// string.Format so they will be collapsed. This string should cover the following:
///
/// field:value
/// field:(value)
/// field:(value OR value)
/// field:"value value"
/// field:[range1 TO range2]
/// field:{range1 TO range2}
/// field:{range1 TO range2]
/// field:"{any of the above examples}"
/// </summary>
private static readonly string genericOptionTemplate = @"
{0}:
(?<value>
(?<surround>(
(?<quote> "" )|
(?<escquote> \\"" )|
(?<square> \[ )|
(?<curly> \{{ )|
(?<paren> \( )
))?
(?<inner>
.*?
)
(?(surround)(
(?(quote) (?<!\\)"" | $ )|
(?(escquote) \\"" | $ )|
(?(square) (\}}|\]) | $ )|
(?(curly) (\}}|\]) | $ )|
(?(paren) \) | $ )
))
)
";
}
}
@model SearchModel
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Status <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li>
<a href="@Url.Action("Index", new { Query = Model.QueryOptionActive })">
<span class="glyphicon glyphicon-ok @Model.ClassOptionActive"></span>
Active
</a>
</li>
<li>
<a href="@Url.Action("Index", new { Query = Model.QueryOptionInactive })">
<span class="glyphicon glyphicon-ok @Model.ClassOptionInactive"></span>
Inactive
</a>
</li>
</ul>
</div>
public class SearchModel
{
private const string is_active = "is:active";
private const string is_inactive = "is:inactive";
private static readonly string[] actives = { is_active, is_inactive };
public string Query { get; set; }
public string QueryOptionActive
{
get
{
return FilterHelper.TogglePrependOption(Query, is_active, actives);
}
}
public string ClassOptionActive
{
get
{
return ActiveClass(is_active);
}
}
public string QueryOptionInactive
{
get
{
return FilterHelper.TogglePrependOption(Query, is_inactive, actives);
}
}
public string ClassOptionActive
{
get
{
return ActiveClass(is_inactive);
}
}
private string ActiveClass(option)
{
return FilterHelper.HasOption(Query, option) ? "visible" : "invisible";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment