¿Es posible utilizar la búsqueda de texto completo (FTS) con LINQ?

76

Me pregunto si es posible usar FTS con LINQ usando .NET Framework 3.5. Estoy buscando en la documentación que todavía no encontré nada útil.

¿Alguien tiene alguna experiencia en esto?

Edwin Jarvis
fuente

Respuestas:

77

Si. Sin embargo, primero debe crear la función del servidor SQL y llamarla, ya que, por defecto, LINQ usará un me gusta.

Esta publicación de blog que explicará los detalles, pero este es el extracto:

Para que funcione, debe crear una función con valores de tabla que no haga más que una consulta CONTAINSTABLE basada en las palabras clave que ingresa,

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

Luego agrega esta función a su modelo SQL de LINQ 2 y listo, ahora puede escribir consultas como.

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

No. LINQ To SQL no admite la búsqueda de texto completo.

Dicho esto, puede usar un procedimiento almacenado que utilice FTS y hacer que la consulta LINQ To SQL extraiga datos de eso.

Gabriel Isenberg
fuente
10

si no desea crear uniones y desea simplificar su código C #, puede crear una función SQL y usarla en la cláusula "from":

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

Después de actualizar su DBML, utilícelo en linq:

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

Esto producirá SQL simple como este:

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

Esto funciona en la búsqueda por varias columnas, como puede ver en la implementación de la función ad_Search.

Victor Gelmutdinov
fuente
9

Yo no lo creo. Puede usar 'contiene' en un campo, pero solo genera una LIKEconsulta. Si desea usar texto completo, recomendaría usar un proceso almacenado para hacer la consulta y luego devolverlo a LINQ

Glenn Slaven
fuente
5

No, la búsqueda de texto completo es algo muy específico del servidor SQL (en el que el texto se indexa por palabras y las consultas alcanzan este índice en lugar de atravesar una matriz de caracteres). Linq no admite esto, cualquier llamada .Contains () afectará a las funciones de cadena no administradas pero no se beneficiará de la indexación.


fuente
0

Hice un prototipo funcional, solo para CONTAINS de SQL Server y sin columnas comodín. Lo que logra es que utilice CONTAINS como las funciones LINQ ordinarias:

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

Necesitará:

1.Definiciones de funciones en código y EDMX para admitir la palabra clave CONTAINS .

2.Reescriba EF SQL por EFProviderWrapperToolkit / EFTracingProvider, porque CONTAINS no es una función y por defecto el SQL generado trata su resultado como bit .

PERO:

1.Contains no es realmente una función y no puede seleccionar resultados booleanos de ella. Solo se puede usar en condiciones.

2. Es probable que el código de reescritura de SQL a continuación se rompa si las consultas contienen cadenas no parametrizadas con caracteres especiales.

Fuente de mi prototipo

Definiciones de funciones: (EDMX)

Bajo 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>

PD: los casos extraños de caracteres se utilizan para habilitar la misma función con diferentes tipos de parámetros (varbinary y nvarchar)

Definiciones de funciones: (código)

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

PD: "MyModel.Store" es el mismo que el valor en edmx: StorageModels / Schema / @ Namespace

Reescribir EF SQL: (por 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;
    }
}

Habilite EFProviderWrapperToolkit:

Si lo obtiene por nuget, debe agregar estas líneas en su app.config o 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>
AqD
fuente