【问题标题】:Entity Framework, Code First and Full Text Search实体框架、代码优先和全文搜索
【发布时间】:2012-07-21 21:52:38
【问题描述】:

我知道有人问了很多关于全文搜索和实体框架的问题,但我希望这个问题有点不同。

我正在使用实体框架,代码优先,需要进行全文搜索。当我需要执行全文搜索时,我通常还会有其他条件/限制 - 例如跳过前 500 行,或过滤另一列等。

我看到这已使用表值函数处理 - 请参阅http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL---Enabling-Fulltext-searching.aspx。这似乎是正确的想法。

不幸的是,直到 Entity Framework 5.0 才支持表值函数(即使那样,我相信 Code First 也不支持它们)。

我真正的问题是对于 Entity Framework 4.3 和 Entity Framework 5.0 处理此问题的最佳方法的建议是什么。但具体来说:

  1. 除了动态 SQL(例如,通过 System.Data.Entity.DbSet.SqlQuery)之外,还有其他可用于 Entity Framework 4.3 的选项吗?

  2. 如果我升级到 Entity Framework 5.0,有没有一种方法可以让我先在代码中使用表值函数?

谢谢, 埃里克

【问题讨论】:

标签: linq entity-framework full-text-search ef-code-first


【解决方案1】:

我发现实现此功能的最简单方法是在 SQL Server 中设置和配置全文搜索,然后使用存储过程。将您的参数传递给 SQL,让数据库完成它的工作并返回一个复杂的对象或将结果映射到一个实体。您不一定必须拥有动态 SQL,但它可能是最佳的。例如,如果您需要分页,您可以在每个请求中传入 PageNumberPageSize,而无需动态 SQL。但是,如果每个查询的参数数量有所波动,这将是最佳解决方案。

【讨论】:

  • 有时我们忘记了我们总是可以依靠久经考验的真实存储过程!我也更喜欢这种方法而不是拦截器黑客。
【解决方案2】:

正如其他人提到的,我想说开始使用 Lucene.NET

Lucene 的学习曲线相当高,但我找到了一个名为“SimpleLucene”的包装器,可以在 CodePlex 上找到它

让我引用博客中的几个代码块来向您展示它的易用性。我刚刚开始使用它,但很快就掌握了它。

首先,从您的存储库中获取一些实体,或者在您的情况下,使用实体框架

public class Repository
{
    public IList<Product> Products {
        get {
            return new List<Product> {
                new Product { Id = 1, Name = "Football" },
                new Product { Id = 2, Name = "Coffee Cup"},
                new Product { Id = 3, Name = "Nike Trainers"},
                new Product { Id = 4, Name = "Apple iPod Nano"},
                new Product { Id = 5, Name = "Asus eeePC"},
            };
        }
    }
}

接下来要做的是创建一个索引定义

public class ProductIndexDefinition : IIndexDefinition<Product> {
    public Document Convert(Product p) {
        var document = new Document();
        document.Add(new Field("id", p.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
        document.Add(new Field("name", p.Name, Field.Store.YES, Field.Index.ANALYZED));
        return document;
    }

    public Term GetIndex(Product p) {
        return new Term("id", p.Id.ToString());
    }
}

并为其创建搜索索引。

var writer = new DirectoryIndexWriter(
    new DirectoryInfo(@"c:\index"), true);

var service = new IndexService();
service.IndexEntities(writer, Repository().Products, ProductIndexDefinition());

所以,您现在有了一个可搜索的索引。唯一剩下要做的就是..,搜索!你可以做一些非常了不起的事情,但它可以像这样简单:(更多示例请参阅博客或 codeplex 上的文档)

var searcher = new DirectoryIndexSearcher(
                new DirectoryInfo(@"c:\index"), true);

var query = new TermQuery(new Term("name", "Football"));

var searchService = new SearchService();

Func<Document, ProductSearchResult> converter = (doc) => {
    return new ProductSearchResult {
        Id = int.Parse(doc.GetValues("id")[0]),
        Name = doc.GetValues("name")[0]
    };
};

IList<Product> results = searchService.SearchIndex(searcher, query, converter);

【讨论】:

    【解决方案3】:

    我最近有一个类似的需求,最后写了一个 IQueryable 扩展,专门用于 Microsoft 全文索引访问,它可以在这里IQueryableFreeTextExtensions

    【讨论】:

    【解决方案4】:

    使用 EF6 中引入的拦截器,您可以在 linq 中标记全文搜索,然后在 dbcommand 中替换它,如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);
        }
    }
    

    例如,如果您有带有 FTS 索引字段 NoteText 的类 Note:

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

    和它的EF映射

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

    及其上下文:

    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());
        }
    }
    

    您可以使用非常简单的 FTS 查询语法:

    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();
            }
        }
    }
    

    这将生成类似的 SQL

    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)   
    

    请注意,您应该使用局部变量,并且不能像

    那样在表达式中移动 FTS 包装器
    var q = db.Notes.Where(n => n.NoteText.Contains(FtsInterceptor.Fts("john")));
    

    【讨论】:

    • 我添加了示例 NoteMap 类
    • 感谢@Ben,没有意识到 EF 可以以这种方式配置。
    • 不错的解决方案,但是当输入字符串包含多个单词时它会失败。 john doe 搜索查询将导致 "Syntax error near 'doe' in the full-text search condition '(john doe)
    • 由于您使用的是OnModelCreating,我想这仅适用于Code First 方法。如何使它与Database First Approach 一起工作?
    • @MaksimVi。您必须将RewriteFullTextQuery 函数中的“contains”更改为“freetext”(将Regex.Replace 分配给cmd.CommandText)。
    【解决方案5】:

    这里的例子http://www.entityframework.info/Home/FullTextSearch 不是完整的解决方案。您将需要了解全文搜索的工作原理。想象一下,您有一个搜索字段,并且用户输入 2 个单词来进行搜索。上面的代码会抛出异常。您需要先对搜索短语进行预处理,然后使用逻辑 AND 或 OR 将其传递给查询。

    例如,您的搜索词组是“blah blah2”,那么您需要将其转换为:

    var searchTerm = @"\"blah\" AND/OR \"blah2\" "; 
    

    完整的解决方案是:

     value = Regex.Replace(value, @"\s+", " "); //replace multiplespaces
                        value = Regex.Replace(value, @"[^a-zA-Z0-9 -]", "").Trim();//remove non-alphanumeric characters and trim spaces
    
                        if (value.Any(Char.IsWhiteSpace))
                        {
                            value = PreProcessSearchKey(value);
                        }
    
    
     public static string PreProcessSearchKey(string searchKey)
        {
            var splitedKeyWords = searchKey.Split(null); //split from whitespaces
    
            // string[] addDoubleQuotes = new string[splitedKeyWords.Length];
    
            for (int j = 0; j < splitedKeyWords.Length; j++)
            {
                splitedKeyWords[j] = $"\"{splitedKeyWords[j]}\"";
            }
    
            return string.Join(" AND ", splitedKeyWords);
        }
    

    此方法使用 AND 逻辑运算符。您可以将其作为参数传递,并将该方法用于 AND 或 OR 运算符。

    您必须转义非字母数字字符,否则当用户输入字母数字字符并且您没有适当的服务器站点模型级别验证时,它将引发异常。

    【讨论】:

    • 或者您可以只使用 Freetext 而不是 Contains?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-25
    相关资源
    最近更新 更多