Created
April 23, 2013 10:10
-
-
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");
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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