Поиск с помощью Lucene в Playframework 1.x

в 9:38, , рубрики: java, lucene, playframework, Поисковые машины и технологии, метки: , ,

В моем веб-проекте на Playframework-e в один прекрасный момент потребовался поиск. Идею искать в базе через like я сразу отмел, потому что хотелось ранжирования и прочих плюшек «умного» поиска, а изобретать свой велосипед не было ни времени ни желания.
Так как проект на Java — было очень соблазнительно использовать для этого Lucene.
В гугле я сразу нашел замечательный модуль для Playframework-а под названием Search, также был найден модуль Elastic Search, который тоже использует Lucene, но он требует установки отдельного сервера, и потому был отметен. Модуль Search понравился мне своей простотой — все «навороты» в нем инкапсулированы, так что пользоваться им очень легко.
С установкой модуля, как и всегда в Play-e, проблем не возникло, команда play install search отработала на «ура» и выкачала модуль из репозитория.
Добавив module.search=${play.path}/modules/search-2.0 в application.conf я уже мог использовать его в приложении.
Следуя краткому руководству, я добавил к сущности Entry, по которой собственно и следовало осуществлять поиск, аннотацию @Indexed, а полю description — аннотацию @Field.
Написав в контроллере примерно следующий код:

public static void search(String phrase, int page) {
        int pageSize = PAGE_SIZE;
        Query query = Search.search("description:" + phrase, Entry.class);
        List<Entry> entries = query.page(page*pageSize, pageSize).fetch();
        long totalCount = query.count();
        render(entries, totalCount, page, pageSize, phrase);
}

Я уже был готов делать первые тесты и наращивать функционал, но тут начались проблемы…

Поиск не работал, то есть метод count() возвращал 0, а список entries был пуст. Я пытался осуществлять поиск и на русском и на английском, и вызывал Search.rebuildAllIndexes(), и много чего еще пытался, но результат был неизменен.
Благо модуль в play-e скачивается вместе с исходниками и можно было задебажить. Непродолжительный дебаг показал, что в индекс не кладется поле description. Я залез чуть глубже и увидел, что в поисках аннотации @Field у поля сущности используется метод object.getClass().getFields(), но стоп, метод этот возвращает только public поля, а у меня в сущности поля как и положено имеют protected доступ, и автору модуля следовало использовать метод getDeclaredFields().
Тут я сделаю некоторое отступление: мне совершенно не хотелось пересобирать модуль и менять в нем код, как минимум по тому, что я бы потерял возможность делать play install search на «боевом» сервере, а пришлось бы руками подкладывать пересобраный модуль. Написать баг-репорт или предложить патч, было не быстрой затеей, а функционал нужен был уже сейчас.
В общем было принято решение сделать поле description public-ом, и написать todo, до момента исправления этого бага в модуле. И, о чудо, поиск заработал!
Следующей проблемой, о которой я уже догадывался прочитав tutorial, было то, что в модуле была «киллер фича» — автоматическое обновление индекса(и добавление новых записей) при CRUD операциях с сущностью — мне это было не нужно. Дело в том, что записи, которые добавляются в систему сначала проходят премодерацию и мне совершенно не нужно, чтобы еще «неотмодерированные» записи попадали в поиск.
Я хочу сам вызывать Search.index(entry) после модерации. Я очень надеялся на то, что я найду в исходниках проверку на какую-нибудь строчку в конфиге: делать автоматическое обновление индекса или нет, но я наткнулся на это в коде SearchPlugin:

@Override
public void onEvent(String message, Object context) {
      if (!message.startsWith("JPASupport")) 
          return;
      if (message.equals("JPASupport.objectPersisted") || message.equals("JPASupport.objectUpdated")) {
          Search.index (context);
      } else if (message.equals("JPASupport.objectDeleted")) {
          Search.unIndex(context);
      }
}

Отключить это было никак нельзя, и вроде бы пора уже думать о том, что надо слать патч и пинать, чтобы быстрее выпустили версию с фиксом, но тут родилась гениальная идея: а ведь мне по сути все равно будет это модулем или просто набором библиотек, которые я положу в папку lib.
Копание в коде показало, что при старте приложения нужно выполнить:

Search.init();
FileExtractor.init();

А при остановке:

try {
    Search.shutdown();
} catch (Exception e) {
    throw new UnexpectedException (e);
}

Это легко делается с помощью Job-ов с аннотациями @OnApplicationStart/Stop.
Следующим действием было «отучить» play думать, что это плагин. Playframework находит модули в classpath-е с помощью файлика play.plugins, собственно обычным архиватором этот файлик был удален из jar файла, и все завертелось-закрутилось.
Надеюсь мой опыт будет полезен и поможет сохранить время людям, которые использует Playframework в своей работе.
PS: Раз уж мне пришлось класть модуль в папку lib, я уж заодно и пересобрал его поправив баг с public полями. :)

Автор: paralainer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js