NHibernate-search Sort with Analyzer

Hello everyone,
I’m trying to sort a field with lowercase and with their full content in NHibernate-Search.
I downloaded the code from NHibernate-Search github directory, to update our codebase to latest NHibernate version to prevent vulnerabilities.

Anyway, I’m unable sort a text as lowercase and with full content. It was working before but I guess the tokenization changed and now sorting doesnt work like before.

Here is the code:

using System;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Mapping;
using NHibernate;
using System.Linq;
using NHibernate.Search;
using NHibernate.Search.Attributes;
using NHibernate.Search.Store;
using NHibernate.Search.Event;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Search;
using Lucene.Net.Analysis.Core;
using Lucene.Net.Analysis;
using Lucene.Net.Util;
using System.IO;

namespace MyNHibernate.LuceneSort
{
    internal class BasicStringSort
    {
        static void Main()
        {
            Console.WriteLine("Testing NHibernate Sort");

            Console.WriteLine($"NHibernate Version: {typeof(ISession).Assembly.GetName().Version}");

            Account A1, A2, A3, A4, A5, A6;

            var sessionFactory = CreateSessionFactory();

            DeleteAccounts(sessionFactory);

            var allAccounts = new Account[] { };

            using (var session = sessionFactory.OpenSession())
            {
                using (var transaction = session.BeginTransaction())
                {

                    A1 = new Account("mgn");
                    A2 = new Account("MGN");
                    A3 = new Account("Third");
                    A4 = new Account("MGN");
                    A5 = new Account("qwewqewq");
                    A6 = new Account("EUROPE");


                    allAccounts = [A1, A2, A3, A4, A5, A6];

                    session.SaveOrUpdate(A1);
                    session.SaveOrUpdate(A2);
                    session.SaveOrUpdate(A3);
                    session.SaveOrUpdate(A4);
                    session.SaveOrUpdate(A5);
                    session.SaveOrUpdate(A6);

                    transaction.Commit();
                }
            }

            using (var session = sessionFactory.OpenSession())
            {
                var fullTextSession = Search.CreateFullTextSession(session);

                var fullTextQuery = fullTextSession.CreateFullTextQuery<Account>("*:*")
                            .SetSort(new Sort(new SortField("Name_sort", SortFieldType.STRING, false)));
                var nhSearchResults = fullTextQuery.List<Account>();

                foreach (var result in nhSearchResults)
                {
                    Console.WriteLine($"Found Account: {result.Id}-{result.Name}");
                }

                var sortedAccounts = allAccounts.OrderBy(w => w.Name.ToLower()).ToArray();
                if (sortedAccounts.Select(a => a.Name).SequenceEqual(nhSearchResults.Select(a => a.Name)))
                {
                    Console.WriteLine("Sort worked properly.");
                }
                else
                {
                    Console.WriteLine("Error! Sort didn't work properly!");
                }
            }

            DeleteAccounts(sessionFactory);

        }

        private static void DeleteAccounts(ISessionFactory sessionFactory)
        {
            using var session = sessionFactory.OpenSession();
            using var transaction = session.BeginTransaction();
            session.CreateQuery("DELETE FROM Account").ExecuteUpdate();

            transaction.Commit();
        }

        public static ISessionFactory CreateSessionFactory()
        {
            var factory = Fluently.Configure()
                .Database(MsSqlConfiguration.MsSql2005
                                            .ShowSql()
                                            .ConnectionString("Data Source=.;Initial Catalog=test_db;User Id=id;Password=pw"))
                .Mappings(m =>
                    m.FluentMappings
                     .Add<AccountMap>()
                )
                .ExposeConfiguration(cfg =>
                {
                    cfg.SetProperty("hibernate.search.default.directory_provider", typeof(RAMDirectoryProvider).AssemblyQualifiedName);
                    cfg.SetProperty("hibernate.search.default.indexBase.create", "true");
                    cfg.SetProperty("hibernate.search.analyzer", typeof(StandardAnalyzer).AssemblyQualifiedName);
                    cfg.SetProperty("hibernate.search.indexing_strategy", "event");

                    cfg.SetListener(NHibernate.Event.ListenerType.PostUpdate, new FullTextIndexEventListener());
                    cfg.SetListener(NHibernate.Event.ListenerType.PostInsert, new FullTextIndexEventListener());
                    cfg.SetListener(NHibernate.Event.ListenerType.PostDelete, new FullTextIndexEventListener());
                })
                .BuildSessionFactory();

            using (var session = factory.OpenSession())
            {
                var searchFactory = Search.CreateFullTextSession(session).SearchFactory;
                searchFactory.Optimize();
            }

            return factory;
        }
    }

    [Indexed]
    public class Account
    {
        [DocumentId]
        public virtual int Id { get; protected set; }

        public virtual int LastDataVersion { get; set; }

        [Field(Name = "Name_sort", Index = Index.UnTokenized)]
        [Analyzer(typeof(LowerKeywordAnalyzer))]
        public virtual string Name { get; set; }
        public Account()
        {
            Name = string.Empty;
        }
        public Account(string name)
        {
            Name = name;
        }
        public override string ToString() => $"Name:{Name}";
    }

    public class AccountMap : ClassMap<Account>
    {
        public AccountMap()
        {
            Schema("accounts");
            Table("Accounts");
            Id(x => x.Id);
            Map(x => x.LastDataVersion);
            Map(x => x.Name, "Name");
        }
    }

    public class LowerKeywordAnalyzer : Analyzer
    {
        protected override TokenStreamComponents CreateComponents(string fieldName, TextReader reader)
        {
            Tokenizer tokenizer = new KeywordTokenizer(reader);
            TokenStream tokenStream = new LowerCaseFilter(LuceneVersion.LUCENE_48, tokenizer);
            return new TokenStreamComponents(tokenizer, tokenStream);
        }
    }
}

I looked at the docs and I found that I need to use Normalizer, but I couldnt find the Normalizer in NHibernate: Hibernate Search 5.8.2.Final: Reference Guide

All I want is something works similar to this:

var sortedAccounts = allAccounts.OrderBy(w => w.Name.ToLower());

Hey @kasap

This forum is for the Hibernate Search, and we do not actually maintain the port NHibernate-search…
With that in mind … if you want to get a behavior similar to a normalizer from an analyzer try using the KeywordTokenizer [1] (link to Lucene 4.8 as you have this version mentioned in your code). This keyword tokenizer returns the entire input as a single output token.

[1] KeywordTokenizer (Lucene 4.8.0 API)