28 March 2013

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

Search enginesJava
Sandbox
В моем веб-проекте на 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 полями. :)
Tags:playframeworkjavalucene
Hubs: Search engines Java
+4
2.7k 11
Comments 1
Popular right now
Тренер JAVA
from 350,000 to 400,000 ₽ИЦ «Ай-Теко»Remote job
Java Developer
from 120,000 to 200,000 ₽HighTeamНижний НовгородRemote job
Java разработчик
from 140,000 to 230,000 ₽МойСкладМоскваRemote job
Java developer
from 240,000 to 270,000 ₽ОТП БанкМоскваRemote job
Java developer
to 180,000 ₽King Bird StudioМоскваRemote job