Skip to content

Instantly share code, notes, and snippets.

@danbarua
Created April 23, 2013 10:10
Show Gist options
  • Save danbarua/5442373 to your computer and use it in GitHub Desktop.
Save danbarua/5442373 to your computer and use it in GitHub Desktop.
Simple text search with ServiceStack.Redis, based on Antirez's 'AutoComplete with Redis' post http://oldblog.antirez.com/post/autocomplete-with-redis.html Add POCOs to the index like this: client.AddToTextIndex(dto, x => x.Name, x => x.Address); and search like this: client.SearchText<TModel>("foo");
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using ServiceStack.Common.Utils;
using ServiceStack.Redis;
using ServiceStack.Text;
namespace RedisSearch
{
public static class RedisTextSearchExtensions
{
//https://gist.github.com/antirez/574044 - Ruby impl
//https://gist.github.com/j4mie/577852 - Python impl
//http://oldblog.antirez.com/post/autocomplete-with-redis.html - original post
public static string WordSetKey = "textSearch:{0}:words";
public static string LookupSetKey = "textSearch:{0}:word:{1}";
public static int MaxRangeToReturnFromWordSet = 50; //see antirez's post
public static int MinPrefixLength = 3;
public static string StripNonAlphanumericChars(this string input)
{
char[] arr = input.ToCharArray();
arr = Array.FindAll<char>(arr, (c => (char.IsLetterOrDigit(c) || char.IsWhiteSpace(c) || c == '-')));
return new string(arr);
}
public static void AddToTextIndex<TModel>(this IRedisClient redis, TModel model,
params Expression<Func<TModel, string>>[] memberExpressions)
{
if (memberExpressions.Length == 0)
throw new ArgumentOutOfRangeException("memberExpressions",
"Did you forget to include an expression indicating what property to index?");
object id = model.GetId();
string modelType = typeof(TModel).Name;
foreach (var memberExpression in memberExpressions)
{
string stringToIndex =
memberExpression.Compile().Invoke(model).ToLowerInvariant().StripNonAlphanumericChars();
if (string.IsNullOrEmpty(stringToIndex))
continue;
foreach (string word in stringToIndex.Split(' '))
{
//index all the words in the names
//by generating a set with all the prefixes >3chars
for (int i = MinPrefixLength; i <= word.Length; i++)
{
string prefix = word.Substring(0, i);
redis.AddItemToSortedSet(WordSetKey.Fmt(modelType), prefix, 0);
}
//add stop word
redis.AddItemToSortedSet(WordSetKey.Fmt(modelType), word + "*", 0);
//now add lookup from word to model
redis.AddItemToSet(LookupSetKey.Fmt(modelType, word), id.ToString());
}
}
}
public static IEnumerable<TModel> SearchText<TModel>(this IRedisClient redis, string filter)
{
string modelType = typeof(TModel).Name;
filter = filter.ToLowerInvariant().StripNonAlphanumericChars();
int start = redis.GetItemIndexInSortedSet(WordSetKey.Fmt(modelType), filter);
var range = redis.GetRangeFromSortedSet(WordSetKey.Fmt(modelType), start, start + MaxRangeToReturnFromWordSet);
var words = (from r in range
where r.StartsWith(filter.ToLowerInvariant()) || r == filter.ToLowerInvariant()
where r.Contains("*")
select r.Remove(r.Length - 1, 1));
IEnumerable<string> sets = words.Select(word => LookupSetKey.Fmt(modelType, word));
HashSet<string> ids = redis.GetUnionFromSets(sets.ToArray());
return redis.As<TModel>().GetByIds(ids);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using ServiceStack.Redis;
using ServiceStack.Text;
namespace RedisSearch
{
public class ViewModel
{
public ViewModel()
{
}
public ViewModel(string name, string address)
{
Id = Guid.NewGuid();
Name = name;
Address = address;
}
public Guid Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
[TestFixture]
public class RedisSearchTests
{
private IEnumerable<ViewModel> GetData()
{
yield return new ViewModel("Alice", "London, England");
yield return new ViewModel("Bob", "New York, America");
yield return new ViewModel("Charles", "York, England");
yield return new ViewModel("David", "Paris, France");
}
[Test]
public void Can_do_text_search_using_redis()
{
List<ViewModel> data = GetData().ToList();
var clientsManager = new BasicRedisClientManager("localhost");
using (IRedisClient client = clientsManager.GetClient())
{
client.FlushDb();
foreach (ViewModel dto in data)
{
client.Store(dto);
client.AddToTextIndex(dto, x => x.Name, x => x.Address);
}
Console.WriteLine("Searching 'england'");
IEnumerable<ViewModel> livesInEngland = client.SearchText<ViewModel>("england");
livesInEngland.PrintDump();
Assert.That(livesInEngland.Count(), Is.EqualTo(2));
Console.WriteLine("Searching 'char'");
IEnumerable<ViewModel> charles = client.SearchText<ViewModel>("char");
charles.PrintDump();
Assert.That(charles.Count(), Is.EqualTo(1));
Console.WriteLine("Searching 'york'");
IEnumerable<ViewModel> york = client.SearchText<ViewModel>("york");
york.PrintDump();
Assert.That(york.Count(), Is.EqualTo(2));
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment