Tuesday, September 15, 2015

Find text position in Lucene.Net

Здравствуйте, уважаемые читатели. И снова сегодня будем рассматривать, как можно использовать Lucene.Net для своих проектов. Со времени последних статей по Lucene.Net многое изменилось. Основной причиной, по которой я начал работать с Lucene.Net, было внедрение его в проект, над которым я работал. Но это все отошло на дальний план и вряд-ли когда-нибудь вернется, так как для бизнеса важнее запилить какую-то UI красивую фишку для клиента, чем сделать что-то полезное. Такая, к сожалению, суровая тенденция. Но приоритеты приоритетами, а мне Lucene.Net очень нравится, и бросать его не хочется, поэтому я разбираюсь с ним в свободное от работы время, чтобы заставить свой мозг работать и не давать ему покрываться пылью. Поэтому если вы интересуетесь работой Lucene.Net и хотите узнать больше, чем описано в книге “Lucene in Action”, буду рад вам помочь. Надеюсь, я еще напишу парочку статей, главное – найти время на это все. Сегодня мы рассмотрим, как найти позицию текста, который вы ищете с помощью запросов. Когда-то я писал в статье Using text analyzers in Lucene.NET” о том, как раньше можно было получить позицию текста, начало/конец и сам текст. Ниже приведен этот пример.
public static void DisplayDetailsTokens()
{
    const string test = "This is a test. How about that?! Huh?";
    var analyzer = new StandardAnalyzer(Version.LUCENE_30);
    TokenStream stream = analyzer.TokenStream("Context"new StringReader(test));

    var termAttr = stream.GetAttribute<ITermAttribute>();
    var offsetAttr = stream.GetAttribute<OffsetAttribute>();
    var typeAttr = stream.GetAttribute<TypeAttribute>();

    while (stream.IncrementToken())
    {
        var startOffset = offsetAttr.StartOffset;
        var endOffset = offsetAttr.EndOffset;
        var token = termAttr.Term + ":" + startOffset + "->" + endOffset + ":" + typeAttr.Type;
        Console.WriteLine("[" + token + "]  ");
    }
}
Раньше такой код до версии 3.0.3 Lucene.Net, конечно, работал бы, но времена меняются, код устаревает и т.д. Поэтому наша задача на сегодня – рассмотреть способ, как это можно сделать без корявого механизма доставания этих данных с атрибутов.
Сначала создадим новое консольное предложение “FindTextPositionInLucene”, как показано для примера на скриншоте ниже.
Затем установим с помощью NuGet Package Manager пакет Lucene.Net и Lucene.Net Contrib.
Для того чтобы было более удобно работать с классом Document в 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);
    }
}
Затем реализуем класс ContentDocument, который наследуем от класса ADocument. Реализация приведена ниже.
public class ContentDocument : ADocument
{
    private string _content;

    public string Content
    {
        get { return _content; }
        set
        {
            _content = value;
            AddParameterToDocument("Content", _content, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
        }
    }
}
А теперь приступим к самой реализации логики.
Рассмотрим функцию, которая занимается поиском слова и нахождением позиций текста.
public static List<FieldTermStack> Search(Lucene.Net.Store.Directory directory, string field, string searchQuery)
{
    var termList = new List<FieldTermStack>();
    using (var searcher = new IndexSearcher(directory))
    {
        var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
        var parser =
            new QueryParser(Lucene.Net.Util.Version.LUCENE_30, field, 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 ftl = new FieldTermStack(indexReader, hits.ScoreDocs[i].Doc, field, fieldQuery);
            termList.Add(ftl);
        }

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

        return termList;
    };
}
Основная логика ложится на класс FieldTermStack. Класс FieldTermStack реализует всю необходимую логику поверх работы с классами TermFreqVector, TermPositionVector и TermVectorOffsetInfo. Вы можете посмотреть данную реализацию по ссылке FieldTermStack.cs. То есть, если вы хотите, то можете написать все то же самое самиостоятельно, используя механизмы более высокого уровня. Я, признаться, тоже хотел вначале так сделать, но лень победила, и я решил показать более простой способ.
Также нам понадобится сгенерировать какие-то тестовые данные для теста, например, как показано ниже.
private static IEnumerable<ContentDocument> GenerateDocuments()
{
    var documents = new List<ContentDocument>();

    documents.Add(new ContentDocument
    {
        Id = 1,
        Content = @"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"
    });

    documents.Add(new ContentDocument
    {
        Id = 2,
        Content = @"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"
    });

    documents.Add(new ContentDocument
    {
        Id = 3,
        Content = @"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"
    });

    documents.Add(new ContentDocument
    {
        Id = 4,
        Content = @"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."
    });

    documents.Add(new ContentDocument
    {
        Id = 5,
        Content = "Nevermind, the answer was given via a Google Group."
    });
    documents.Add(new ContentDocument
    {
        Id = 6,
        Content = "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"
    });
    return documents;
}
Текст я взял просто из статьи со stackoverflow. Теперь осталось привести саму реализацию того, что у нас получилось.
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 res = Search(luceneDirectory, "Content""page");
    foreach (var token in res)
    {
        foreach (var term in token.termList)
        {
            var tokenInfo = term.Text + "   Start: " + term.StartOffset.ToString().PadLeft(5) +
                "  End: " + term.EndOffset.ToString().PadLeft(5) + "   Position :" 
                + term.Position.ToString().PadLeft(5) +"\r\n" ;

            Console.WriteLine(tokenInfo);
        }
               
    }

    Console.ReadLine();
}
После запуска нашего проекта мы получим результат, как показано на рисунке ниже.
Ну и напоследок полный исходный код ниже.
class Program
{
    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 res = Search(luceneDirectory, "Content""page");
        foreach (var token in res)
        {
            foreach (var term in token.termList)
            {
                var tokenInfo = term.Text + "   Start: " + term.StartOffset.ToString().PadLeft(5) +
                    "  End: " + term.EndOffset.ToString().PadLeft(5) + "   Position :" 
                    + term.Position.ToString().PadLeft(5) +"\r\n" ;

                Console.WriteLine(tokenInfo);
            }
               
        }

        Console.ReadLine();
    }

    public static List<FieldTermStack> Search(Lucene.Net.Store.Directory directory, string field, string searchQuery)
    {
        var termList = new List<FieldTermStack>();
        using (var searcher = new IndexSearcher(directory))
        {
            var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
            var parser =
                new QueryParser(Lucene.Net.Util.Version.LUCENE_30, field, 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 ftl = new FieldTermStack(indexReader, hits.ScoreDocs[i].Doc, field, fieldQuery);
                termList.Add(ftl);
            }

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

            return termList;
        };
    }

    private static IEnumerable<ContentDocument> GenerateDocuments()
    {
        var documents = new List<ContentDocument>();

        documents.Add(new ContentDocument
        {
            Id = 1,
            Content = @"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"
        });

        documents.Add(new ContentDocument
        {
            Id = 2,
            Content = @"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"
        });

        documents.Add(new ContentDocument
        {
            Id = 3,
            Content = @"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"
        });

        documents.Add(new ContentDocument
        {
            Id = 4,
            Content = @"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."
        });

        documents.Add(new ContentDocument
        {
            Id = 5,
            Content = "Nevermind, the answer was given via a Google Group."
        });
        documents.Add(new ContentDocument
        {
            Id = 6,
            Content = "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"
        });
        return documents;
    }

    public class ContentDocument : ADocument
    {
        private string _content;

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

    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. Этот вариант может пригодиться, например, если вам нужно определить, в каком поле найден нужный результат, для того чтобы подсветить его, сделать какой-то вложенный подзапрос и т.д. Для подсветки текста есть другие механизмы, такие как Highlighter для подсветки текста, но иногда вам нужно знать позицию текста, чтобы заменить существующую запись. Можно найти множество разных применений такому подходу, и один из них мы рассмотрим в следующих статьях, а именно будем делать запрос по нескольким полям и искать, в каком поле найден исходный текст. А на сегодня для данной статьи все. Получилась она небольшой и надеюсь, что будет полезной вам в работе. Буду рад услышать ваши замечания и предложения по поводу того, что бы вы хотели еще услышать об Lucene.Net, и есть ли смысл в подобного рода статьях. 

No comments:

Post a Comment