Wednesday, 8 November 2017

Match Against MariaDB MySQL

To take advantage of Full text indexing on MariaDB 10, I need to use this new "MATCH AGAINST" syntax in the sql string.
I think it would be really cool if, for certain columns only, I could override linq-to-nhibernate to change the sql it generates when I use
.Where(x => FullTextIndexedStringProperty.Contains("Some word")).ToList().
Who can give me some general directions on how to get started?
shareimprove this question
This will get you a very simple MATCH ... AGAINST clause. If you want to get more complex (more arguments, specifying the search modifier), you'll have to make some bigger changes. Hopefully this will get you started though:
  1. Create a new dialect and register a simple MATCH (...) AGAINST (...) function:
    public class CustomMySQLDialect : MySQLDialect
    {
        public CustomMySQLDialect()
        {
            this.RegisterFunction(
                "matchagainst",
                new SQLFunctionTemplate(
                    NHibernateUtil.Boolean,
                    "match (?1) against (?2)"));
        }
    }
  2. Create a static extension method on string that you'll use in LINQ statements:
    public static class LinqExtensions
    {
        public static bool MatchAgainst(this string source, string against)
        {
            throw new NotImplementedException();
        }
    }
  3. Create a new LINQ to HQL generator class that associates the method with the SQL function we registered in the custom dialect:
    public class MatchAgainstGenerator : BaseHqlGeneratorForMethod
    {
        public MatchAgainstGenerator()
        {
            this.SupportedMethods = new[]
            {
                ReflectionHelper.GetMethod(() => LinqExtensions.MatchAgainst(null, null))
            };
        }
    
        public override HqlTreeNode BuildHql(
            MethodInfo method,
            System.Linq.Expressions.Expression targetObject,
            ReadOnlyCollection<System.Linq.Expressions.Expression> arguments,
            HqlTreeBuilder treeBuilder,
            IHqlExpressionVisitor visitor)
        {
    
            return treeBuilder.BooleanMethodCall(
                "matchagainst",
                arguments.Select(visitor.Visit).Cast<HqlExpression>());
        }
    }
  4. Create a custom LinqToHqlGeneratorsRegistry:
    public class MyLinqToHqlRegistry : DefaultLinqToHqlGeneratorsRegistry
    {
        public MyLinqToHqlRegistry()
        {
           var generator = new MatchAgainstGenerator();
           RegisterGenerator(typeof(LinqExtensions).GetMethod("MatchAgainst"), generator);
        }
    }
  5. Use your custom dialect, and Linq to HQL registry either in your cfg.xml file or in code:
    var cfg = new Configuration()
        .DataBaseIntegration(db =>
    {
        db.Dialect<CustomMySQLDialect>();
    })
    .LinqToHqlGeneratorsRegistry<MyLinqToHqlRegistry>();
  6. Finally, use your extension method in a LINQ-to-NHibernate query:
    session.Query<Article>()
        .Where(a => a.Body.MatchAgainst("configured"))
        .ToList()
        .Dump();
    This will generate SQL that looks like this:
    select 
        userquery_0_.Id as Id51_, 
        userquery_0_.Title as Title51_, 
        userquery_0_.Body as Body51_ 
    from 
        articles userquery_0_ 
    where 
        match (userquery_0_.Body) against ('configured');
Again, this won't help if you have more complicated requirements. But hopefully this is at least a good starting point.
In case anyone is curious about how to make this support more complex scenarios, here are the problems I think you'd run into:
  • Separating the arguments to MATCH from those to AGAINST.
  • Registering a custom SQL function with NHibernate that can take an arbitrary number of arguments in different places
  • Creating the correct HQL even after solving the two issues above.

No comments:

Post a Comment

Note: only a member of this blog may post a comment.