Полнотекстовый поиск в Linq

В Linq нет встроенного полнотекстового поиска, и, похоже, не так много сообщений на эту тему, поэтому я поиграл и придумал этот метод для своего класса утилиты:

public static IEnumerable<TSource> GenericFullTextSearch<TSource>(string text, MyDataContext context)
{
    //Find LINQ Table attribute
    object[] info = typeof(TSource).GetCustomAttributes(typeof(System.Data.Linq.Mapping.TableAttribute), true);
    //Get table name
    String table = (info[0] as System.Data.Linq.Mapping.TableAttribute).Name;
    //Full text search on that table
    return context.ExecuteQuery<TSource>(String.Concat("SELECT * FROM ", table, " WHERE CONTAINS(*, {0})"), text);
}

И добавил эту оболочку в каждый частичный класс Linq, где есть полнотекстовый индекс

public static IEnumerable<Pet> FullTextSearch(string text, MyDataContext context)
{
    return (LinqUtilities.GenericFullTextSearch<Pet>(text, context) as IEnumerable<Pet>);
}

Итак, теперь я могу выполнять полнотекстовый поиск с помощью изящных элементов, таких как

var Pets = Pet.FullTextSearch(helloimatextbox.Text, MyDataContext).Skip(10).Take(10);

Я предполагаю, что в настоящее время необходим только очень простой поиск. Может ли кто-нибудь улучшить это? Можно ли реализовать как метод расширения и избежать оболочки?


person Community    schedule 19.02.2009    source источник
comment
Одна опасная/неоптимальная проблема, связанная с вашим запросом, заключается в том, что .Skip().Take() будет выполняться на стороне клиента, а не на стороне сервера. Поэтому, если вы выполняете FTS, который возвращает 10^6 результатов, и вы хотите получить только первые 10, все 10^6 из них будут возвращены из базы данных, и только после этого вы будете выполнять фильтрацию.   -  person Mark S. Rasmussen    schedule 19.02.2009
comment
Да, для такого большого набора данных я бы рассмотрел другой метод;)   -  person ctrlalt3nd    schedule 19.02.2009
comment
Возможный дубликат Можно ли использовать полный текст Поиск (FTS) с помощью LINQ?   -  person Sruit A.Suk    schedule 13.06.2016


Ответы (8)


Лучшее решение - использовать встроенную табличную функцию в sql и добавить ее в свою модель.

http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL---Enabling-Fulltext-searching.aspx

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

create function udf_sessionSearch
      (@keywords nvarchar(4000)) returns table as   return (select [SessionId],[rank]
            from containstable(Session,(description,title),@keywords))

Затем вы добавляете эту функцию в свою модель LINQ 2 SQL, и теперь вы можете писать такие запросы, как.

var sessList = from s   in DB.Sessions
               join fts in DB.udf_sessionSearch(SearchText) on s.sessionId equals fts.SessionId
               select s;
person Community    schedule 25.02.2010
comment
Насколько я вижу, для этого также требуется udf для каждой таблицы, решение OP должно работать со всеми таблицами. Хотя спасибо за пост - person Smudge202; 09.12.2011

Меня очень расстроило отсутствие четких примеров... особенно когда есть потенциально большие наборы данных и требуется пейджинг. Итак, вот пример, который, надеюсь, охватывает все, что вам может понадобиться :-)

create function TS_projectResourceSearch
    (   @KeyStr nvarchar(4000), 
        @OwnId int,
        @SkipN int,
        @TakeN int )
    returns @srch_rslt table (ProjectResourceId bigint not null, Ranking int not null )
    as 
    begin

        declare @TakeLast int
        set @TakeLast = @SkipN + @TakeN
        set @SkipN = @SkipN + 1

        insert into @srch_rslt  
        select pr.ProjectResourceId, Ranking
        from 
        (
            select t.[KEY] as ProjectResourceId, t.[RANK] as Ranking, ROW_NUMBER() over (order by t.[Rank] desc) row_num
            from containstable( ProjectResource,(ResourceInfo, ResourceName), @KeyStr )
            as t        
        ) as r
        join ProjectResource pr on r.ProjectResourceId = pr.ProjectResourceId
        where (pr.CreatorPersonId = @OwnId
            or pr.ResourceAvailType < 40)
            and r.row_num between @SkipN and @TakeLast
        order by r.Ranking desc 

        return
    end
    go


    select * from ts_projectResourceSearch(' "test*" ',1002, 0,1)

Наслаждайся, Патрик

person Community    schedule 07.03.2011

Я использую небольшой прием, используя методы Provider Wrapper. У меня есть код С#, который переписывает волшебное слово в SQL с поиском FTS для MS SQL (вы можете настроить любой сервер, который вам нравится).

если у вас есть контекстный класс MyEntities, создайте подкласс, например

public class MyEntitiesWithWrappers : MyEntities
{
    private IEFTraceListener listener;
    public string FullTextPrefix = "-FTSPREFIX-";

    public MyEntitiesWithWrappers(): this("name=MyEntities")
    {
    }

    public MyEntitiesWithWrappers(string connectionString)
        : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(connectionString,"EFTracingProvider"))
    {
        TracingConnection.CommandExecuting += RewriteFullTextQuery;
    }

    /// <summary>
    /// Rewrites query that contains predefined prefix like: where n.NOTETEXT.Contains(Db.FullTextPrefix  + text) with SQL server FTS 
    /// To be removed when EF will support FTS
    /// </summary>
    /// <param name="o"></param>
    /// <param name="args"></param>
    public void RewriteFullTextQuery(object o, CommandExecutionEventArgs args)
    {
        var text = args.Command.CommandText;
        for (int i = 0; i < args.Command.Parameters.Count; i++)
        {
            DbParameter parameter = args.Command.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;
                parameter.Size = 4096;
                if (value.IndexOf(FullTextPrefix) >= 0)
                {
                    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;
                    args.Command.CommandText = Regex.Replace(text,
                        string.Format(@"\(\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE '~')\)", parameter.ParameterName), 
                        string.Format(@"contains([$1].[$2], @{0})", parameter.ParameterName));
                }
            }
        }
    }
}

А затем используйте это так:

var fullTextSearch = Db.FullTextPrefix + textToSearch;
var q = Db.Notes.Where(n => !n.Private && n.NoteText.Contains(fullTextSearch));
person Community    schedule 19.10.2012

Немного более приятный метод (применяет ранг) с использованием CONTAINSTABLE.

String pkey = context.Mapping.GetTable(typeof(TSource)).RowType.DataMembers.SingleOrDefault(x => x.IsPrimaryKey).Name;
string query = String.Concat(@"SELECT *
    FROM ", table, @" AS FT_TBL INNER JOIN
    CONTAINSTABLE(", table, @", *, {0}) AS KEY_TBL
    ON FT_TBL.", pkey, @" = KEY_TBL.[KEY]
    ORDER BY KEY_TBL.[RANK] DESC");
return context.ExecuteQuery<TSource>(query, text);
person Community    schedule 19.02.2009

.NET Core 2.1 и выше поддерживает метод расширения, который позволяет использовать поиск FREETEXT и FREETEXTTABLE.

using Microsoft.EntityFrameworkCore;

var results = dbContext.MyTable
        .Where(e => EF.Functions.FreeText("*", "search criteria"));

Функция FreeText определена в Microsoft.EntityFrameworkCore.SqlServer, поэтому ваш проект должен ссылаться на этот пакет.

Документация

person Community    schedule 06.05.2021

Я пытался решить точную проблему. Мне нравится писать свою логику SQL в моем LINQtoSQL, но мне нужен был способ выполнять полнотекстовый поиск. прямо сейчас я просто использую функции SQL, а затем вызываю пользовательские функции, встроенные в запросы linq. не уверен, что это самый эффективный способ. Что вы думаете, ребята?

person Community    schedule 25.02.2009

Вы можете просто сделать что-то вроде этого

    var results = (from tags in _dataContext.View_GetDeterminationTags
                   where tags.TagName.Contains(TagName) ||
                   SqlMethods.Like(tags.TagName,TagName)
                   select new DeterminationTags
                   {
                       Row = tags.Row,
                       Record = tags.Record,
                       TagID = tags.TagID,
                       TagName = tags.TagName,
                       DateTagged = tags.DateTagged,
                       DeterminationID = tags.DeterminationID,
                       DeterminationMemberID = tags.DeterminationMemberID,
                       MemberID = tags.MemberID,
                       TotalTagged = tags.TotalTagged.Value
                   }).ToList();

Обратите внимание, где TagName.Contains также SQLMethods.Like просто использует

using System.Data.Linq.SqlClient;

чтобы получить доступ к этим SQLMethods.

person Community    schedule 19.02.2009
comment
Это .contains переводится как LIKE '%TAGNAME%', что неоптимально. - person Jarret Raim; 14.11.2010
comment
LIKE не является FULLTEXT поиском. - person Rafael Herscovici; 14.01.2015

dswatik — полнотекстовый поиск нужен потому, что .contains переводится как

SELECT * FROM MYTABLE WHERE COLUMNNAME LIKE '%TEXT%'

Который игнорирует любые индексы и ужасен для большой таблицы.

person Community    schedule 19.02.2009