Статьи

Использование Lucene в Grails

Apache Lucene является ведущей поисковой системой с открытым исходным кодом и используется во многих компаниях, проектах и ​​продуктах. У Lucene есть подпроекты, которые предоставляют дополнительные функции, такие как веб-сканер Nutch и служба поиска Solr. В этой статье дается введение в Lucene, учебное пособие по трем плагинам Grails Lucene и сравнение между ними.

Эта статья первоначально появилась в сентябрьском выпуске GroovyMag .

Lucene

Apache Lucene Core обеспечивает реализацию индексации и поиска на основе Java. По сути, индекс состоит из документов, которые состоят из полей.

индексирование

При индексации документа поля обрабатываются с помощью токенизатора, чтобы разбить его на термины. На рисунке 1 показано влияние токенайзера на пробелы при разбиении «у Марии был маленький ягненок» на токены: у Мэри был маленький ягненок.

Рисунок 1: Токенизация пробелов

Термины могут подвергаться дальнейшему анализу с помощью классов TokenFilter. На рисунке 2 показан фильтр стоп-слов, удаляющий общие термины, которые могут повлиять на релевантность.

Рисунок 2: Стоп-слова

На рисунке 3 показан PorterStemFilter, применяющий (основанный на английском) алгоритм портежа Портера, чтобы привести токены к основам их слов, чтобы они были приравнены. Этот TokenFilter должен работать на вводе нижнего регистра — поэтому нужно использовать LowerCaseFilter / Tokenizer перед тем, как будет получен ствол.

Рисунок 3: Слово stemming

Наконец, термины затем сопоставляются с их документами, как показано на рисунке 4.

Рисунок 4: Термин индекс

Обновления индекса

Обновления индексных документов обрабатываются как операция удаления, за которой следует операция добавления. Со временем сегменты индекса могут стать фрагментированными — это можно исправить, выполнив операцию оптимизации для упаковки индекса.

Запросы

Запросы в Lucene должны проходить через те же анализаторы, которые использовались при индексации, иначе идентичные термины могут не совпадать.
Запрос с одним словом (TermQuery) требует поиска в термине index для возврата соответствующих документов.

например, запрос термина «индекс», показанного на рис. 4, для «консигнации» вернул бы документы 1, 4 и 7.

Запрос из двух слов (BooleanQuery) требует, чтобы Lucene выполнил два поиска по индексу термина, а затем отфильтровал результирующие документы на основе И или ИЛИ (из явного оператора или оператора по умолчанию). например, запрос индекса сроков на рисунке 4 для «consign AND ship» вернет документ 4, тогда как «consign OR ship» вернет документы 1, 4 и 7.

Фразовый запрос обозначается двойными кавычками (например, «Нью-Йорк») и соответствует документам, содержащим определенную последовательность терминов. Это зависит от позиционной информации в индексе. Отклонение фразы, или близость, задается с помощью оператора ~ и целого числа, чтобы указать, насколько близко должны быть слова в фразе. например, «большой банан» ~ 5 будет соответствовать документам, содержащим «большой банан», «большой зеленый банан» и «большой прямой желтый банан»

По умолчанию результаты возвращаются в порядке релевантности, а оценка рассчитывается по формуле (если вы действительно заинтересованы, она находится в JavaDoc для класса подобия — http://lucene.apache.org/java/3_3_0/api/ core / org / apache / lucene / search / Similarity.html ). Оценка может зависеть от повышения условий. например, запрос ‘subject: lucene ИЛИ author: bramley ^ 2 ′ увеличит вклад в оценку поля автора в два раза для документов, авторские поля которых содержат’ bramley ‘.

инструменты

Прежде чем мы перейдем к практическому применению, стоит упомянуть, что Luke, Lucene Index Toolbox, является бесценным инструментом для проверки, поиска и просмотра индексов Lucene.

Он доступен по адресу http://code.google.com/p/luke/, но имейте в виду, что вам нужно использовать правильную версию, соответствующую версии Lucene, которая создала ваши индексы. Это стало проще и очевиднее, поскольку номера версий Luke теперь напрямую соответствуют версиям Lucene (например, Luke 3.3.0 для Lucene 3.3.0, однако Luke 0.9.9.1 основан на Lucene 2.9.1).

Внедрение в Grails

Вместо приложения в стиле Twitter мы создадим простое приложение со списком дел с одним классом домена для представления элемента списка дел, показанного в листинге 1, которое будет использовать сгенерированный контроллер и представления (вы можете включить леса вместо).

Мы будем использовать это вместе с тестовыми данными на рисунке 5 в 3 похожих приложениях, чтобы продемонстрировать различные плагины Grails, связанные с Lucene. Все примеры приложений доступны на GitHub по адресу https://github.com/rbramley/GroovyMagLucene.

package com.rbramley.todo
 
class Item {
    Date dateCreated
    String subject
    Date startDate
    Date dueDate
    String status
    String priority
    boolean completed = false
    int percentComplete = 0
    String body
 
    static constraints = {
        subject(blank:false, nullable:false)
        startDate()
        dueDate()
        status(inList:["Not Started","In Progress","Completed","Waiting on someone else","Deferred"])
        priority(inList:["Low","Normal","High"])
        completed()
        percentComplete(range:0..100)
        body(nullable:true, maxSize:1000)
    }
}

Листинг 1: класс предметного предмета

Рисунок 5: Тестовые данные

Плагин для поиска

Плагин Searchable использует Compass :: GPS для индексирования объектов домена Hibernate с помощью событий жизненного цикла GORM / Hibernate. Плагин также предоставляет контроллер поиска по умолчанию и просмотр. Цель, которая соответствует рекомендациям Марка Палмера, состоит в том, чтобы сделать полнотекстовый поиск объектов вашего домена максимально простым.

Первым шагом является установка плагина с помощью поиска grails install-plugin.

После того как мы создали класс домена (grails create-domain-class com.rbramley.todo.Item) и заполнили его кодом из листинга 1, мы добавили свойство статического поиска в класс домена: static searchable = [кроме : ‘Дата создания’]

Запустив приложение, выберите ссылку com.rbramley.todo.ItemController на домашней странице и введите тестовые данные с рисунка 5.

Вернитесь на домашнюю страницу и выберите ссылку grails.plugin.searchable.SearchableController.

Введите запрос «расписание» в поле поиска, затем нажмите кнопку «Поиск» — это даст вам результаты, как показано на рисунке 6.

Рисунок 6: Результаты поиска

Вот и все — быстрый и простой полнотекстовый поиск по модели вашего домена. Хотя стиль может не соответствовать нашему приложению, по крайней мере, он дает нам над чем поработать.

Обратите внимание, что плагин хранит индексы в ~ / .grails / projects / <project-name> / searchable-index / <environment>

Solr плагин

Версия 0.2 плагина была выпущена в январе 2010 года, совместима с Grails 1.1+ и включает в себя старые версии 1.4 Solr и SolrJ. Было бы неплохо увидеть новую версию этого плагина с текущей версией Solr и обновленной для более новых механизмов разрешения зависимостей — если у вас есть время помочь, вы можете раскошелиться на GitHub по адресу http://github.com/ mbrevoort / Grails-Solr-плагин

После того, как вы установили плагин, используя ‘grails install-plugin solr’, скрипт ‘grails start-solr’ запустит экземпляр Solr по умолчанию, используя Jetty. Он находится в ‘solr-home’ в рабочем каталоге проекта (на основе настроек сборки Grails), используя пример схемы и конфигурации Solr. Хотя этот пример конфигурации подходит для оценки, я (и Lucid Imagination) настоятельно рекомендую не использовать эту конфигурацию в рабочей среде. Например, вам нужно изменить это, если вы хотите использовать обработчик dismax (в этот момент вам, вероятно, лучше с отдельной установкой и конфигурацией с управлением версиями).

Плагин хорошо использует соглашения (используя динамические поля Solr) и метапрограммирование для добавления методов в классы предметной области.

Как это делает авто-индексацию?

Если вы посмотрите на плагин doWithApplicationContext, то вы увидите, что прослушиватель зарегистрирован для событий post-insert / update / delete.

Как насчет поиска?

Это обрабатывается SolrService, который использует клиент SolrJ и создает простой запрос (здесь было бы неплохо использовать dismax). Однако автор плагина (Mike Brevoort) также предоставил возможность создать собственный сложный запрос и передать его SolrService.

Теперь мы обнаружили, как это работает «из коробки» — пришло время написать некоторый код, чтобы мы могли взять его для тест-драйва.

Плагин может быть установлен grails install-plugin solr.

Снова мы будем использовать класс домена из листинга 1, но на этот раз добавим два свойства: static enableSolrSearch = true и static solrAutoIndex = true

Когда класс домена завершен, мы можем сгенерировать контроллеры и представления:

grails generate-all com.rbramley.todo.Item

Мы добавим окно поиска в макет main.gsp. Это показано в листинге 2, а стилизация была оставлена ​​читателю в качестве упражнения (обратите внимание, что вы можете предоставить изображение для кнопки поиска). Он отправляет поисковому контроллеру (созданному с использованием grails create-controller com.rbramley.todo.Search — и заполняется в листинге 3) файл search.gsp, отображающий результаты (в листинге 4 показан фрагмент).

<div>
     <strong>Quick search</strong>
    <g:form url='[controller: "search", action: "search"]' id="searchForm" name="searchForm" method="get">
         <input type="text" name="query" value="${query ?: 'Keywords'}" onClick="javascript:if (this.value=='Keywords') { this.value='' }">
        <input type="image" src="${resource(dir:'images',file:'go_quick_search.png')}">
    </g:form>
</div>

Листинг 2: окно поиска main.gsp

package com.rbramley.todo
 
class SearchController {
    def solrService
 
    def search = {
        def res = solrService.search("${params.query}")
        [query:params.query, total:res.total, searchResults:res.resultList]
    }
}

Листинг 3: Код контроллера поиска

<g:if test="${haveResults}">
    <g:each var="result" in="${searchResults}">
        <g:set var="className" value="${ClassUtils.getShortName(result.doctype_s)}" />
        <li><solr:resultLink result="${result}">${className}: ${result.id}</solr:resultLink></li>
        ${result.subject?.encodeAsHTML()}
    </g:each>
</g:if>

Листинг 4: Фрагмент страницы результатов

Перед тем, как запустить приложение Grails, вам нужно запустить Solr, используя grails start-solr.

Как только Solr, а затем Grails запущены, мы можем создать элемент, используя тестовые данные, показанные на рисунке 5, а затем выполнить поиск по «расписанию».

Почему нет результатов?

Плагин doc заявляет, что будет искать по полю Solr по умолчанию (которое по умолчанию называется ‘text’).

Попытка еще раз: subject_s: расписание также не возвращает результатов …

Итак, давайте попробуем фразу запроса: subject_s: «Полное расписание», как показано на рисунке 7 — этот работает, потому что мы предоставили всю строку для сопоставления, это связано с тем, что
«тип StrField не анализируется, а индексируется / сохраняется дословно «.

Рисунок 7: Solr с полностью заданным запросом

На рисунке 8 показан результат проверки поля темы с помощью обработчика запросов Solr Luke ( http: // localhost: 8983 / solr / admin / luke ).

Рисунок 8: Solr Luke анализ предметной области

Таким образом, чтобы получить желаемые результаты, нам нужно выполнить некоторые настройки Solr, и у нас есть следующие опции:

  1. Сопоставьте поля с типом текста, чтобы они анализировались (их также можно принудительно преобразовывать в текст с помощью аннотаций) и используйте директивы поля копирования, чтобы скопировать их в текстовое поле по умолчанию.
  2. Настройте обработчик dismax (выходящий за рамки данной статьи), чтобы иметь более гибкий контроль над полями, которые запрашиваются по умолчанию

Если мы изменим схему Solr, нам потребуется выполнить полную переиндексацию — или, в нашем случае, с тестовым приложением в режиме разработки, мы можем просто удалить файлы индекса (обычно в ~ / .grails / <grails-version). > / проекты / GroovyMagSolr / Solr-дома / Solr / данные).

First we’ll modify Item.groovy to add import org.grails.solr.Solr and then add the Solr annotation to the String subject field e.g. @Solr(asText=true) – this will cause the field to be indexed as subject_t. We’d now be able to search for subject_t:timesheet and get a match – but this still doesn’t meet our usability requirements as it requires knowledge of the underlying document fields. We could use an explicit <copyField source=»subject_t» dest=»text»/> in the Solr schema.xml, however if you inspect the schema.xml you will see that the first 3000 characters of all ‘_t’ fields are copied to the ‘text’ field.

Now a search for ‘timesheet’ gives the results shown in Figure 9.

Figure 9: Solr simple query result

ElasticSearch plugin

ElasticSearch (http://www.elasticsearch.org/) is a new Lucene-based distributed, RESTful search engine. It was created by Shay Banon, who created Compass (used by the Searchable Plugin) and has worked on data grid technologies.

Version 0.2 of the Grails plugin uses ElasticSearch 0.15.2 – you can start with an embedded instance in development mode which stores the indices in the project source directory under ‘data’.

The first step (within a clean application) is to install the plugin:

grails install-plugin elasticsearch

Then create the domain class (grails create-domain-class com.rbramley.todo.Item) and fill in using the Listing 1 domain class code with the addition of Listing 5.

 static searchable = {
        except = 'dateCreated'
    }

Listing 5: ElasticSearch mapping

Once that is done and the controller and views generated (grails generate-all com.rbramley.todo.Item), we can then create our Search controller (grails create-controller com.rbramley.todo.Search) and complete it using Listing 6.

package com.rbramley.todo
 
class SearchController {
    def elasticSearchService
 
    def search = {
        def res = elasticSearchService.search("${params.query}")
        [query:params.query, total:res.total, searchResults:res.searchResults]
    }
}

Listing 6: ElasticSearch controller

Let’s re-use the main.gsp from the GroovyMagSolr application (Listing 2) and we’ll adapt the search.gsp fragment from Listing 4 to get Listing 7.

<g:if test="${haveResults}">
    <g:each var="result" in="${searchResults}">
        <g:set var="className" value="${ClassUtils.getShortName(result.class)}" />
        <g:set var="link" value="${createLink(controller: className[0].toLowerCase() + className[1..-1], action: 'show', id: result.id)}" />
        <li><a href="${link}">${className}: ${result.id}</a></li>
        ${result.subject?.encodeAsHTML()}
    </g:each>
</g:if>

Listing 7: Search results page for ElasticSearch

Having started up the application and entered the test data as shown in Figure 5, I hit a problem in that the database record was created but the index record wasn’t and the console was filling up with repeated log entries until I stopped the application:

2011-08-20 22:33:51,630 [elasticsearch[index]-pool-2-thread-1] ERROR index.Index
RequestQueue - Failed bulk item: NullPointerException[null]
2011-08-20 22:33:51,633 [elasticsearch[index]-pool-2-thread-2] ERROR index.Index
RequestQueue - Failed bulk item: NullPointerException[null]

When I looked at the IndexRequestQueue source, the JavaDoc says “If indexing fails, all failed objects are retried. Still no support for max number of retries (todo)”.

The NullPointerException itself is ElasticSearch bug 795 – but the underlying cause seems to be that the JSON document didn’t meet the expectations of ElasticSearch for that type!

I got the plugin working by changing the domain class searchable mapping to only = ‘subject’ – then the results look identical to Figure 9.

In the time available I didn’t manage to track down the initial issue (I also tried with an external ElasticSearch instance and upgrading the ElasticSearch dependencies to 0.16). I’ll be in communication with the plugin authors…

Plugin comparison

So how do they compare? Well this article has given a basic introduction to their usage for English text and hasn’t demonstrated advanced features or the distributed capabilities of the latter two.

The criteria for comparison is partially inspired by Marc Palmer’s views on plugins – a distilled form is “make it work, make it simple, make it magic”; we’ll also add scalability to this list.

Searchable

This is a well established plugin and works very well out of the box with simple domain classes and even relationships. It is simple to use, works as expected and provides a basic search page and controller which includes some administrative actions such as the ability to re-index all searchable domain classes.

I’ve occasionally encountered older versions throwing exceptions on start-up that could be rectified by removing the indices and restarting the application.

On the downside, due to the embedded nature of the indices, it is only really suitable for single-instance applications.

Solr

This plugin requires some configuration to get the best out of it and it could benefit from some attention to update it. However, it does have reasonable documentation and some powerful features such as faceted-search, spatial search and taglibs for facets and result links.

Also it should be possible to index domain classes that use Mongo through the use of the metaClass added indexSolr() method.

ElasticSearch

This plugin has great promise – although I encountered some issues relating to the ElasticSearch expectation of the index document structure, I should mention that the plugin documentation currently contains the warning “you should only use this plugin for testing”.

The main attraction of ElasticSearch is the real time search with a distributed and scalable nature.

As per the Solr plugin, it should also be possible to index Mongo-mapped domain classes using a metaClass added index() method.

References

For further reading the DZone Refcardz provide good overviews: