Thursday, October 1, 2015

Sorting documents in Lucene.Net

Сегодня у нас снова будет тема об использовании Lucene.Net. И поговорим мы о том, как использовать сортировку полей в документах Lucene.Net при поиске. Мы рассмотрим один способ как можно сортировать данные при поиске, и один пример, как можно эти данные группировать. Также разберемся, как сделать так, чтобы некоторые поля при сортировке имели больший приоритет с установкой свойства Boost. А также попробуем реализовать нестандартный способ сортировки полей после выборки. Например, это было одно из моих заданий при изучении Lucene.Net, – понять, как реализовать сортировку после выборки. Это самый нестандартный случай, и его реализация потребует ручной работы, к сожалению, без средств Lucene. Давайте приступим к работе. Для начала нам необходимо создать новое консольное приложение “SortFieldsInLucene”, как показано на рисунке ниже.
Следующим делом ставим с помощью NuGet Package Manager пакет Lucene.Net и Lucene.Net Contrib, как показано ниже в примере.
Так как работа с добавлением полей в документы Lucene.Net выглядит не очень удобной, предлагаю сразу для нее написать высокоуровневую оболочку – класс ADocument, который будет инкапсулировать в себе логику по добавлению новых полей.
public abstract class ADocument
{
    private long _id;

    public long Id
    {
        set
        {
            _id = value;

            AddParameterToDocument("Id", _id, Field.Store.YES, Field.Index.NOT_ANALYZED);
        }

        get { return _id; }

    }

    private readonly Document _document;

    public Document Document { get { return _document; } }

    protected ADocument()
    {
        _document = new Document();
    }

    protected void AddParameterToDocument(string name, dynamic value, Field.Store store, Field.Index index, Field.TermVector vector = Field.TermVector.NO)
    {
        _document.Add(new Field(name, value.ToString(), store, index, vector));
    }

    protected void AddParameterToDocumentStoreParameter(string name, dynamic value)
    {
        AddParameterToDocument(name, value, Field.Store.YES, Field.Index.ANALYZED);
    }

    protected void AddParameterToDocumentNoStoreParameter(string name, dynamic value)
    {
        AddParameterToDocument(name, value, Field.Store.NO, Field.Index.ANALYZED);
    }
}
Теперь реализуем сам класс, который реализует логику по добавлению новых полей в Lucene документы и сохранение их в индекс.
public class ContentDocument : ADocument
{
    private string _content1;

    public string Content1
    {
        get { return _content1; }
        set
        {
            _content1 = value;
            AddParameterToDocument("Content1", _content1, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }

    private string _content2;
    public string Content2
    {
        get { return _content2; }
        set
        {
            _content2 = value;
            AddParameterToDocument("Content2", _content2, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }

    private string _content3;
    public string Content3
    {
        get { return _content3; }
        set
        {
            _content3 = value;
            AddParameterToDocument("Content3", _content3, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }
}
Затем нам нужна функция, которая сгенерирует для нас какие-то тестовые данные. Например, вы можете сами написать такую функцию, или использовать мой пример.
private static IEnumerable<ContentDocument> GenerateDocuments()
{
    var documents = new List<ContentDocument>();

    documents.Add(new ContentDocument
    {
        Id = 1,
        Content1 = @"I created custom styles by just overriding jQuery classes in inline style. So on top of the page, you have the jQuery CSS linked and right after that override the classes you need to modify",
        Content2 = "hello world",
        Content3 = "hello kitty"
    });

    documents.Add(new ContentDocument
    {
        Id = 2,
        Content1 = @"right, but .ui-dialog-titlebar doesn't have the class .dialog1. .dialog1 {display:none;} will hide the entire contents of the dialog box, which I don't want",
        Content2 = "one style",
        Content3 = "Could anyone tell me"
    });

    documents.Add(new ContentDocument
    {
        Id = 3,
        Content1 = @"The best recommendation I can give for you is to load the page in Firefox, open the dialog and inspect it with Firebug, then try different selectors in the console, and see what works. You may need to use some of the other descendant selectors",
        Content2 = "newer version of Lucene for Java",
        Content3 = "to be able to access the text "
    });

    documents.Add(new ContentDocument
    {
        Id = 4,
        Content1 = @"I'm sure i'm missing something obvious, but the other examples write to the input it is linked too, but there seems to be no obvious way the data is output from the inline version.",
        Content2 = "hi world",
        Content3 = "hi, guys"
    });

    documents.Add(new ContentDocument
    {
        Id = 5,
        Content1 = "Nevermind, the answer was given via a Google Group.",
        Content2 = "First of all, I discovered that Lucene is still faster than SQL query",
        Content3 = "Lucene.Net solves most of those problems for you"
    });
    documents.Add(new ContentDocument
    {
        Id = 6,
        Content1 = "For the Highlighter, TermVectors need to be available and you have a choice of either computing and storing them with the index at index time or computing them as you need them when the search is performed. Above, Field.TermVector.WITH_POSITIONS_OFFSETS indicates were are computing and storing them in the index at index time",
        Content2 = "and this article is not only about Lucene",
        Content3 = "search querries long and complicated"
    });
    return documents;
}

public class ContentDocument : ADocument
{
    private string _content1;

    public string Content1
    {
        get { return _content1; }
        set
        {
            _content1 = value;
            AddParameterToDocument("Content1", _content1, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }

    private string _content2;
    public string Content2
    {
        get { return _content2; }
        set
        {
            _content2 = value;
            AddParameterToDocument("Content2", _content2, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }

    private string _content3;
    public string Content3
    {
        get { return _content3; }
        set
        {
            _content3 = value;
            AddParameterToDocument("Content3", _content3, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }
}
Теперь приступим к реализации функции, которая будет возвращать класс Sort с библиотеки Lucene со списком полей, по которым мы будем проводить сортировку. Выглядит эта функция следующим образом.
static Sort GetSort(IEnumerable<string> fields)
{
    var sortFields = fields.Select(x => new SortField(x, SortField.STRING)).ToArray();
    return new Sort(sortFields); 
}
То есть, мы передаем список полей, которые хотим отсортировать, а уже функция сортировки будет их сортировать.
Теперь приступим к реализации функции, которая будет сортировать найденный результат.
public static void SearchWithSorting(Lucene.Net.Store.Directory directory, List<string> fields, string searchQuery)
{
    using (var searcher = new IndexSearcher(directory))
    {
        var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
        QueryParser parser = fields.Count == 1 ?
            new QueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.First(), analyzer) :
            new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.ToArray(), analyzer);
        var query = parser.Parse(searchQuery);

        Sort sort = GetSort(fields);

        var hits = searcher.Search(query, null, 200, sort);

        for (var i = 0; i < hits.TotalHits; i++)
        {
            var docId = hits.ScoreDocs[i].Doc;
            var doc = searcher.Doc(docId);
            var id = doc.Get("Id");

            foreach (var field in fields)
            {
                var originalIformation = "Id: " + id.PadLeft(5) +
                    "  FieldName: " + field.PadLeft(5) +
                    "  Original Text: " + doc.Get(field).PadLeft(5);
                Console.WriteLine(originalIformation);
            }

        }

        analyzer.Close();
        searcher.Dispose();
    };
}
Посмотрим, как же это работает. Для этого немного поправим метод Main, как показано ниже, чтобы вызывать функцию, приведенную выше.
static void Main(string[] args)
{
    const string luceneIndexFolder = "LuceneIndex";
    var dataFolder = Path.Combine(Environment.CurrentDirectory, luceneIndexFolder);
    var di = new DirectoryInfo(dataFolder);

    if (!di.Exists)
    {
        di.Create();
    }

    var luceneDirectory = FSDirectory.Open(di.FullName);
    var docs = GenerateDocuments();
    using (var writer = new IndexWriter(luceneDirectory, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED))
    {
        foreach (var doc in docs)
        {
            writer.AddDocument(doc.Document);
        }

        writer.Dispose();
    }

    var fields = new List<string>()
    {
        "Content1",
        "Content2",
        "Content3"
    };

    //Sorting
    SearchWithSorting(luceneDirectory, fields, "Lucene");

    Console.ReadLine();
}
Затем запустим пример и посмотрим на результат.
Пока ничего сложного не должно было быть.
Пример 2
Следующий пример позволяет группировать полученный результат с помощью класса SimpleFacetedSearch с пакета Lucene.Net Contrib. Для того чтобы посмотреть пример использования этого класса, нам достаточно всех тех данных, что мы привели выше, с генераций документов и т.д. Единственное, что нам нужно дописать, – это функцию, которая будет производить группировку. Ниже приведена реализация данной функции.
public static void SearchWithGrouping(Lucene.Net.Store.Directory directory, List<string> fields, string searchQuery)
{
    using (var searcher = new IndexSearcher(directory))
    {
        var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
        QueryParser parser = fields.Count == 1 ?
            new QueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.First(), analyzer) :
            new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.ToArray(), analyzer);
        var indexReader = IndexReader.Open(directory, true);
        var query = parser.Parse(searchQuery);

        SimpleFacetedSearch.MAX_FACETS = int.MaxValue;
        var sfs = new SimpleFacetedSearch(indexReader, fields.ToArray());

        // then pass in the query into the search like you normally would with a typical search class.

        SimpleFacetedSearch.Hits hits = sfs.Search(query, 10);

        // what comes back is different than normal.
        // the result documents & hits are grouped by facets.

        // you'll need to iterate over groups of hits-per-facet.

        foreach (SimpleFacetedSearch.HitsPerFacet hpg in hits.HitsPerFacet)
        {
            SimpleFacetedSearch.FacetName facetName = hpg.Name;

            foreach (Document doc in hpg.Documents)
            {
                var id = doc.Get("Id");
                foreach (var field in fields)
                {
                    var originalIformation = "Id: " + id.PadLeft(5) +
                        "  FieldName: " + field.PadLeft(5) +
                        "  Original Text: " + doc.Get(field).PadLeft(5);
                    Console.WriteLine(">>" + facetName + ": " + originalIformation);
                }

            }
        }

        analyzer.Close();
        searcher.Dispose();
    };
}
После этого немного поправим функцию Main, как показано ниже.
static void Main(string[] args)
{
    const string luceneIndexFolder = "LuceneIndex";
    var dataFolder = Path.Combine(Environment.CurrentDirectory, luceneIndexFolder);
    var di = new DirectoryInfo(dataFolder);

    if (!di.Exists)
    {
        di.Create();
    }

    var luceneDirectory = FSDirectory.Open(di.FullName);
    var docs = GenerateDocuments();
    using (var writer = new IndexWriter(luceneDirectory, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED))
    {
        foreach (var doc in docs)
        {
            writer.AddDocument(doc.Document);
        }

        writer.Dispose();
    }

    var fields = new List<string>()
    {
        "Content1",
        "Content2",
        "Content3"
    };

    //Sorting
    //SearchWithSorting(luceneDirectory, fields, "Lucene");

    //Ordered with SimpleFacetedSearch
    SearchWithGrouping(luceneDirectory, fields, "Lucene");

    Console.ReadLine();
}
Мы всего-навсего закомментировали старую функцию и добавили новую. После этого мы можем запустить наш пример и посмотреть на то, что у нас получилось.
Здесь побольше информации, поскольку мы выводим дополнительную информацию по facetNames и другим параметрам. Но если вы посмотрите на то, что получилось, то вы увидите, что все, в принципе, работает.
Пример 3
Теперь расскажу о нестандартных вариантах. Например, когда-то у меня была задача сделать сортировку по тем полям, которые чаще всего попадают в результат поиска, когда поиск выполнен по нескольким полям. Что для этого нужно. Сначала находим все поля, по которым у вас есть совпадения, как показано в функции ниже.
public static List<SearchResultHelper> Search(Lucene.Net.Store.Directory directory, List<string> fields, string searchQuery)
{
    var termList = new List<SearchResultHelper>();
    using (var searcher = new IndexSearcher(directory))
    {
        var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
        QueryParser parser = fields.Count == 1 ?
            new QueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.First(), analyzer) :
            new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.ToArray(), analyzer);
        var indexReader = IndexReader.Open(directory, true);
        var query = parser.Parse(searchQuery);
        var fieldQuery = new FieldQuery(query, truefalse);

        var hits = searcher.Search(query, 200);

        for (var i = 0; i < hits.TotalHits; i++)
        {
            var docId = hits.ScoreDocs[i].Doc;
            var doc = searcher.Doc(docId);
            var id = doc.Get("Id");
            foreach (var field in fields)
            {
                var ftl = new FieldTermStack(indexReader, docId, field, fieldQuery);
                        
                if (!ftl.IsEmpty())
                {
                    var findData = doc.Get(field);
                            
                    termList.Add(new SearchResultHelper
                    {
                        Id = id,
                        FieldName = field,
                        OriginalText = findData,
                        FieldTerm = ftl
                    });
                }
            }
                    
        }

        analyzer.Close();
        searcher.Dispose();

        return termList;
    };
}
Здесь используется класс FieldTermStack, который помогает найти позицию искомого текста в документах Lucene. Класс SearchResultHelper – вспомогательный класс, который просто сохраняет необходимую информацию.
public class SearchResultHelper
{
    public string Id { getset; }
    public string FieldName { getset; }
    public string OriginalText { getset; }
    public FieldTermStack FieldTerm { getset; }
}
После этого вы сможете сделать сортировку по FieldName по количеству их совпадений. Это легко посчитать, и вы можете выполнить это как домашнее задание. У вас получится в таком случае сортировка постфактум, поскольку вы сортируете уже полученный результат так, как вам нужно.
Пример 4
Есть еще один вариант, когда мы указываем для некоторых полей приоритет выше, чем для остальных. Для этого в Lucene есть понятие "коэффициент усиления". Поле Boost для класса Field. Пример использования этого поля приведен ниже.
var field = new Field(name, value.ToString(), store, index, vector);
field.Boost = 2;
Благодаря этому полю вы можете указывать Lucene, что некоторые поля более весомы, чем остальные. Только пожалуйста используйте этот коэффициент очень осторожно, так как это играет очень большую роль при поиске ваших документов.

Итоги
Сегодня мы с вами рассмотрели 4 небольших примера о том, как можно делать сортировку в Lucene.Net после поиска. Первый – стандартный вариант, и три остальных, довольно специфических. Надеюсь, что данная статья поможет вам в решение подобного рода вопросов, когда нужно построить сортировку результата поиска, и вы не знаете, с какой стороны к этому подойти.
Исходные коды к статье вы можете скачать по ссылке: Sort Fields in Lucene.Net.

No comments:

Post a Comment