Entity Framework, Code First and Full Text Search

Using interceptors introduced in EF6, you could mark the full text search in linq and then replace it in dbcommand as described in http://www.entityframework.info/Home/FullTextSearch:

public class FtsInterceptor : IDbCommandInterceptor
{
    private const string FullTextPrefix = "-FTSPREFIX-";

    public static string Fts(string search)
    {
        return string.Format("({0}{1})", FullTextPrefix, search);
    }

    public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
    }

    public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
    }

    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        RewriteFullTextQuery(command);
    }

    public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
    }

    public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        RewriteFullTextQuery(command);
    }

    public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
    }

    public static void RewriteFullTextQuery(DbCommand cmd)
    {
        string text = cmd.CommandText;
        for (int i = 0; i < cmd.Parameters.Count; i++)
        {
            DbParameter parameter = cmd.Parameters[i];
            if (parameter.DbType.In(DbType.String, DbType.AnsiString, DbType.StringFixedLength, DbType.AnsiStringFixedLength))
            {
                if (parameter.Value == DBNull.Value)
                    continue;
                var value = (string)parameter.Value;
                if (value.IndexOf(FullTextPrefix) >= 0)
                {
                    parameter.Size = 4096;
                    parameter.DbType = DbType.AnsiStringFixedLength;
                    value = value.Replace(FullTextPrefix, ""); // remove prefix we added n linq query
                    value = value.Substring(1, value.Length - 2);
                    // remove %% escaping by linq translator from string.Contains to sql LIKE
                    parameter.Value = value;
                    cmd.CommandText = Regex.Replace(text,
                        string.Format(
                            @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')",
                            parameter.ParameterName),
                        string.Format(@"contains([$1].[$2], @{0})",
                                    parameter.ParameterName));
                    if (text == cmd.CommandText)
                        throw new Exception("FTS was not replaced on: " + text);
                    text = cmd.CommandText;
                }
            }
        }
    }

}
static class LanguageExtensions
{
    public static bool In<T>(this T source, params T[] list)
    {
        return (list as IList<T>).Contains(source);
    }
}

For example, if you have class Note with FTS-indexed field NoteText:

public class Note
{
    public int NoteId { get; set; }
    public string NoteText { get; set; }
}

and EF map for it

public class NoteMap : EntityTypeConfiguration<Note>
{
    public NoteMap()
    {
        // Primary Key
        HasKey(t => t.NoteId);
    }
}

and context for it:

public class MyContext : DbContext
{
    static MyContext()
    {
        DbInterception.Add(new FtsInterceptor());
    }

    public MyContext(string nameOrConnectionString) : base(nameOrConnectionString)
    {
    }

    public DbSet<Note> Notes { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new NoteMap());
    }
}

you can have quite simple syntax to FTS query:

class Program
{
    static void Main(string[] args)
    {
        var s = FtsInterceptor.Fts("john");

        using (var db = new MyContext("CONNSTRING"))
        {
            var q = db.Notes.Where(n => n.NoteText.Contains(s));
            var result = q.Take(10).ToList();
        }
    }
}

That will generate SQL like

exec sp_executesql N'SELECT TOP (10) 
[Extent1].[NoteId] AS [NoteId], 
[Extent1].[NoteText] AS [NoteText]
FROM [NS].[NOTES] AS [Extent1]
WHERE contains([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 char(4096)',@p__linq__0='(john)   

Please notice that you should use local variable and cannot move FTS wrapper inside expression like

var q = db.Notes.Where(n => n.NoteText.Contains(FtsInterceptor.Fts("john")));

Leave a Comment