Подтвердить что ты не робот

Entity Framework, первый код и полный текстовый поиск

Я понимаю, что было задано много вопросов относительно полнотекстового поиска и Entity Framework, но я надеюсь, что этот вопрос немного отличается.

Я использую Entity Framework, Code First и должен выполнять полный текстовый поиск. Когда мне нужно выполнить полный текстовый поиск, обычно у меня будут другие критерии/ограничения - например, пропустить первые 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. Но чтобы быть конкретным:

  • Помимо динамического SQL (например, через System.Data.Entity.DbSet.SqlQuery) существуют ли какие-либо опции для Entity Framework 4.3?

  • Если я обновляюсь до Entity Framework 5.0, есть ли способ, с помощью которого я могу сначала использовать функции с табличным значением?

Спасибо, Эрик

4b9b3361

Ответ 1

Используя перехватчики, введенные в 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);
    }
}

Например, если у вас есть класс Note с полем FTS-indexed NoteText:

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")));

Ответ 2

Я обнаружил, что самый простой способ реализовать это - настроить и настроить полнотекстовый поиск в SQL Server, а затем использовать хранимую процедуру. Передайте свои аргументы SQL, позвольте БД выполнять свою работу и вернуть либо сложный объект, либо сопоставить результаты с сущностью. Вам необязательно иметь динамический SQL, но он может быть оптимальным. Например, если вам нужен пейджинг, вы можете передать PageNumber и PageSize для каждого запроса без необходимости динамического SQL. Однако, если количество аргументов колеблется для каждого запроса, это будет оптимальное решение.

Ответ 3

Как говорили другие ребята, я бы сказал, начинаю использовать Lucene.NET

Lucene имеет довольно высокую кривую обучения, но я нашел для нее обертку под названием " SimpleLucene", которую можно найти на CodePlex

Позвольте мне привести несколько кодовых блоков из блога, чтобы показать вам, как легко это использовать. Я только начал использовать его, но получил его очень быстро.

Во-первых, получите некоторые сущности из вашего репозитория или в вашем случае используйте Entity Framework

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

Ответ 4

Недавно у меня было аналогичное требование, и в итоге я написал расширение IQueryable специально для полнотекстового доступа к текстовому индексу Microsoft, его доступно здесь IQueryableFreeTextExtensions

Ответ 5

Пример здесь http://www.entityframework.info/Home/FullTextSearch не является полным решением. Вам нужно будет понять, как работает полнотекстовый поиск. Представьте, что у вас есть поле поиска, и пользователь набирает 2 слова для поиска. Вышеприведенный код выдает исключение. Сначала необходимо выполнить предварительную обработку в поисковой фразе, чтобы передать ее в запрос с помощью логического И или ИЛИ.

Например, ваша поисковая фраза "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.

Вы должны избегать букв-буквенно-цифровых символов, иначе он будет генерировать исключения, когда пользователь вводит буквенные числовые символы, и у вас нет проверки уровня модели сайта на сервере.