web-dev-qa-db-fra.com

Est-il possible d'utiliser la recherche en texte intégral (FTS) avec LINQ?

Je me demande s'il est possible d'utiliser FTS avec LINQ en utilisant .NET Framework 3.5. Je cherche dans la documentation que je n'ai encore rien trouvé d'utile.

Quelqu'un at-il une expérience à ce sujet?

75
Edwin Jarvis

Oui. Cependant, vous devez d'abord créer la fonction de serveur SQL et l'appeler car, par défaut, LINQ utilisera une fonction similaire.

Ce article de blog qui expliquera les détails mais voici l'extrait:

Pour le faire fonctionner, vous devez créer une fonction de valeur de table qui ne fait rien de plus qu'une requête CONTAINSTABLE basée sur les mots clés que vous transmettez,

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

Vous ajoutez ensuite cette fonction à votre modèle SQL LINQ 2 et il vous permet désormais d'écrire des requêtes comme.

    var sessList = from s   in DB.Sessions
                   join fts in DB.udf_sessionSearch(SearchText) 
                   on s.sessionId equals fts.SessionId
                 select s;
76
John

Non. La recherche en texte intégral n'est pas prise en charge par LINQ To SQL.

Cela dit, vous pouvez utilisez une procédure stockée qui utilise FTS et demandez à la requête LINQ To SQL d'extraire des données de cela.

12
Gabriel Isenberg

si vous ne souhaitez pas créer de jointures et que vous souhaitez simplifier votre code C #, vous pouvez créer une fonction SQL et l'utiliser dans la clause "from":

CREATE FUNCTION ad_Search
(
      @keyword nvarchar(4000)
)
RETURNS TABLE
AS
RETURN
(
      select * from Ad where 
      (CONTAINS(Description, @keyword) OR CONTAINS(Title, @keyword))
)

Après avoir mis à jour votre DBML, utilisez-le dans linq:

string searchKeyword = "Word and subword";
var result = from ad in context.ad_Search(searchKeyword)
                 select ad;

Cela produira du SQL simple comme ceci:

SELECT [t0].ID, [t0].Title, [t0].Description
FROM [dbo].[ad_Search](@p0) AS [t0]

Cela fonctionne dans la recherche par plusieurs colonnes comme vous pouvez le voir dans l'implémentation de la fonction ad_Search.

9
Victor Gelmutdinov

Je ne le crois pas. Vous pouvez utiliser "contient" sur un champ, mais il ne génère qu'une requête LIKE. Si vous souhaitez utiliser le texte intégral, je vous recommande d'utiliser un proc stocké pour effectuer la requête, puis de le renvoyer à LINQ

9
Glenn Slaven

Non, la recherche en texte intégral est quelque chose de très spécifique au serveur SQL (dans lequel le texte est indexé par des mots et les requêtes atteignent cet index par rapport à la traversée d'un tableau de caractères). Linq ne prend pas en charge cela, tous les appels .Contains () atteindront les fonctions de chaîne non gérées mais ne bénéficieront pas de l'indexation.

5
Will

J'ai fait un prototype fonctionnel, pour SQL Server CONTIENT uniquement et aucune colonne générique. Ce qu'il permet de faire est d'utiliser CONTIENT comme les fonctions LINQ ordinaires:

var query = context.CreateObjectSet<MyFile>()
    .Where(file => file.FileName.Contains("pdf")
        && FullTextFunctions.ContainsBinary(file.FileTable_Ref.file_stream, "Hello"));

Tu auras besoin de:

1. Définitions des fonctions dans le code et EDMX pour prendre en charge le mot clé CONTIENT .

2.Réécrivez EF SQL par EFProviderWrapperToolkit/EFTracingProvider, car CONTAINS n'est pas une fonction et par défaut le SQL généré traite son résultat comme bit.

MAIS:

1.Contains n'est pas vraiment une fonction et vous ne pouvez pas en sélectionner de résultats booléens. Il ne peut être utilisé que dans des conditions.

Le code de réécriture SQL ci-dessous est susceptible de se casser si les requêtes contiennent des chaînes non paramétrées avec des caractères spéciaux.

Source de mon prototype

Définitions des fonctions: (EDMX)

Sous edmx: StorageModels/Schema

<Function Name="conTAINs" BuiltIn="true" IsComposable="true" ParameterTypeSemantics="AllowImplicitConversion" ReturnType="bit" Schema="dbo">
    <Parameter Name="dataColumn" Type="varbinary" Mode="In" />
    <Parameter Name="keywords" Type="nvarchar" Mode="In" />
</Function>
<Function Name="conTAInS" BuiltIn="true" IsComposable="true" ParameterTypeSemantics="AllowImplicitConversion" ReturnType="bit" Schema="dbo">
    <Parameter Name="textColumn" Type="nvarchar" Mode="In" />
    <Parameter Name="keywords" Type="nvarchar" Mode="In" />
</Function>

PS: les cas étranges de caractères sont utilisés pour activer la même fonction avec différents types de paramètres (varbinary et nvarchar)

Définitions des fonctions: (code)

using System.Data.Objects.DataClasses;

public static class FullTextFunctions
{
    [EdmFunction("MyModel.Store", "conTAINs")]
    public static bool ContainsBinary(byte[] dataColumn, string keywords)
    {
        throw new System.NotSupportedException("Direct calls are not supported.");
    }

    [EdmFunction("MyModel.Store", "conTAInS")]
    public static bool ContainsString(string textColumn, string keywords)
    {
        throw new System.NotSupportedException("Direct calls are not supported.");
    }
}

PS: "MyModel.Store" est identique à la valeur dans edmx: StorageModels/Schema/@ Namespace

Réécrire EF SQL: (par EFProviderWrapperToolkit)

using EFProviderWrapperToolkit;
using EFTracingProvider;

public class TracedMyDataContext : MyDataContext
{
    public TracedMyDataContext()
        : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
            "name=MyDataContext", "EFTracingProvider"))
    {
        var tracingConnection = (EFTracingConnection) ((EntityConnection) Connection).StoreConnection;
        tracingConnection.CommandExecuting += TracedMyDataContext_CommandExecuting;
    }

    protected static void TracedMyDataContext_CommandExecuting(object sender, CommandExecutionEventArgs e)
    {
        e.Command.CommandText = FixFullTextContainsBinary(e.Command.CommandText);
        e.Command.CommandText = FixFullTextContainsString(e.Command.CommandText);
    }


    private static string FixFullTextContainsBinary(string commandText, int startIndex = 0)
    {
        var patternBeg = "(conTAINs(";
        var patternEnd = ")) = 1";
        var exprBeg = commandText.IndexOf(patternBeg, startIndex, StringComparison.Ordinal);
        if (exprBeg == -1)
            return commandText;
        var exprEnd = FindEnd(commandText, exprBeg + patternBeg.Length, ')');
        if (commandText.Substring(exprEnd).StartsWith(patternEnd))
        {
            var newCommandText = commandText.Substring(0, exprEnd + 2) + commandText.Substring(exprEnd + patternEnd.Length);
            return FixFullTextContainsBinary(newCommandText, exprEnd + 2);
        }
        return commandText;
    }

    private static string FixFullTextContainsString(string commandText, int startIndex = 0)
    {
        var patternBeg = "(conTAInS(";
        var patternEnd = ")) = 1";
        var exprBeg = commandText.IndexOf(patternBeg, startIndex, StringComparison.Ordinal);
        if (exprBeg == -1)
            return commandText;
        var exprEnd = FindEnd(commandText, exprBeg + patternBeg.Length, ')');
        if (exprEnd != -1 && commandText.Substring(exprEnd).StartsWith(patternEnd))
        {
            var newCommandText = commandText.Substring(0, exprEnd + 2) + commandText.Substring(exprEnd + patternEnd.Length);
            return FixFullTextContainsString(newCommandText, exprEnd + 2);
        }
        return commandText;
    }

    private static int FindEnd(string commandText, int startIndex, char endChar)
    {
        // TODO: handle escape chars between parens/squares/quotes
        var lvlParan = 0;
        var lvlSquare = 0;
        var lvlQuoteS = 0;
        var lvlQuoteD = 0;
        for (var i = startIndex; i < commandText.Length; i++)
        {
            var c = commandText[i];
            if (c == endChar && lvlParan == 0 && lvlSquare == 0
                && (lvlQuoteS % 2) == 0 && (lvlQuoteD % 2) == 0)
                return i;
            switch (c)
            {
                case '(':
                    ++lvlParan;
                    break;
                case ')':
                    --lvlParan;
                    break;
                case '[':
                    ++lvlSquare;
                    break;
                case ']':
                    --lvlSquare;
                    break;
                case '\'':
                    ++lvlQuoteS;
                    break;
                case '"':
                    ++lvlQuoteD;
                    break;
            }
        }
        return -1;
    }
}

Activez EFProviderWrapperToolkit:

Si vous l'obtenez par nuget, il devrait ajouter ces lignes dans votre app.config ou web.config:

<system.data>
    <DbProviderFactories>
        <add name="EFTracingProvider" invariant="EFTracingProvider" description="Tracing Provider Wrapper" type="EFTracingProvider.EFTracingProviderFactory, EFTracingProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=def642f226e0e59b" />
        <add name="EFProviderWrapper" invariant="EFProviderWrapper" description="Generic Provider Wrapper" type="EFProviderWrapperToolkit.EFProviderWrapperFactory, EFProviderWrapperToolkit, Version=1.0.0.0, Culture=neutral, PublicKeyToken=def642f226e0e59b" />
    </DbProviderFactories>
</system.data>
0
AqD