Статьи

Добавление поиска в AppFuse с помощью Compass

Более 5 лет назад я понял, что AppFuse необходимо иметь функцию поиска, и решил проблему в JIRA . Почти 4 года спустя было создано Учебное пособие по компасу, и вскоре после него Шей Банон (основатель компаса) прислал патч . Из сообщения, которое он мне отправил:

Краткое описание включения поиска:

  1. Добавлены аннотации для поиска к пользователю и адресу.
  2. Определен компонент Compass, автоматически сканирующий пакет моделей на предмет сопоставляемых классов поиска. Он также автоматически интегрируется с диспетчером транзакций Spring и сохраняет индекс в файловой системе ([рабочий каталог] / target / test-index).
  3. Определен CompassTemplate (аналогично по концепции HibernateTemplate).
  4. Определен CompassSearchHelper. Действительно помогает выполнять поиск, так как выполняет пагинацию и так далее.
  5. Определяется CompassGps, в основном он позволяет работать с индексами, позволяя полностью переиндексировать данные из базы данных. JPA и Hiberante также автоматически отражают изменения, внесенные через их API, в индекс. iBatis использует AOP.

Перенесемся на два года вперед, и я наконец-то нашел время / желание добавить пользовательский интерфейс в базовую реализацию Compass, которую предоставил Шэй. Да, я понимаю, что Compass заменяется ElasticSearch . Я могу изменить использование ElasticSearch в будущем; теперь , что функция поиска существует, я надеюсь увидеть его развиваться и совершенствоваться.

Так как патч Шэя интегрировал необходимые компоненты Spring для индексации и поиска, единственное, что мне нужно было сделать, — реализовать пользовательский интерфейс. Вместо того, чтобы иметь «все объекты» страницу результатов, я решил реализовать его, чтобы вы могли найти на экране списка хозяйствующего субъекта. Я начал с Spring MVC и добавил метод поиска () к UserController:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView handleRequest(@RequestParam(required = false, value = "q") String query) throws Exception {
if (query != null && !"".equals(query.trim())) {
return new ModelAndView("admin/userList", Constants.USER_LIST, search(query));
} else {
return new ModelAndView("admin/userList", Constants.USER_LIST, mgr.getUsers());
}
}

public List<User> search(String query) {
List<User> results = new ArrayList<User>();
CompassDetachedHits hits = compassTemplate.findWithDetach(query);
log.debug("No. of results for '" + query + "': " + hits.length());
for (int i = 0; i < hits.length(); i++) {
results.add((User) hits.data(i));
}
return results;
}

Сначала я использовал compassTemplate.find () , но получил ошибку, потому что я не использовал OpenSessionInViewFilter. Я решил пойти с findWithDetach () и добавил следующую форму поиска в верхней части страницы userList.jsp:

<div id="search">
<form method="get" action="${ctx}/admin/users" id="searchForm">
<input type="text" size="20" name="q" id="query" value="${param.q}"
placeholder="Enter search terms"/>
<input type="submit" value="<fmt:message key="button.search"/>"/>
</form>
</div>

ПРИМЕЧАНИЕ. Я пытался использовать <input type = «search»> в HTML5, но обнаружил, что Canoo WebTest не поддерживает его.

Затем я написал модульное тестирование, чтобы проверить все работало, как ожидалось. Я обнаружил, что должен был вызвать compassGps.index () как часть моего теста, чтобы убедиться, что мой индекс был создан и обновлен.

public class UserControllerTest extends BaseControllerTestCase {
@Autowired
private CompassGps compassGps;
@Autowired
private UserController controller;

public void testSearch() throws Exception {
compassGps.index();
ModelAndView mav = controller.handleRequest("admin");
Map m = mav.getModel();
List results = (List) m.get(Constants.USER_LIST);
assertNotNull(results);
assertTrue(results.size() >= 1);
assertEquals("admin/userList", mav.getViewName());
}
}

После этого я начал интегрировать аналогичный код в другие модули веб-фреймворка AppFuse (Struts, JSF и Tapestry). Когда я закончил, все они выглядели очень похоже с точки зрения пользовательского интерфейса.

Struts:

<div id="search">
<form method="get" action="${ctx}/admin/users" id="searchForm">
<input type="text" size="20" name="q" id="query" value="${param.q}"
placeholder="Enter search terms..."/>
<input type="submit" value="<fmt:message key="button.search"/>"/>
</form>
</div>

JSF:

<div id="search">
<h:form id="searchForm">
<h:inputText id="q" name="q" size="20" value="#{userList.query}"/>
<h:commandButton value="#{text['button.search']}" action="#{userList.search}"/>
</h:form>
</div>

Гобелен:

<div id="search">
<t:form method="get" t:id="searchForm">
<t:textfield size="20" name="q" t:id="q"/>
<input t:type="submit" value="${message:button.search}"/>
</t:form>
</div>

Одно расстраивает , что я обнаружил, что Гобелен не поддерживает метод = «получить» и AFAICT, ни делает JSF 2. С помощью JSF, я должен был сделать мой UserList боб контекст сеанса или параметр запроса будет нулевым , когда он перечислил результаты , Гобелен взял меня дольше , чтобы осуществить, в основном потому , что я имел проблемы выяснить , как это легко для понимания, когда вы-знаете onSubmit () обработчики работали , и я имел надлежащую @property и @Persist аннотаций на мою собственность «Q» , Этот учебник был самым большим подспорьем для меня. Конечно, теперь, когда все закончено, код выглядит довольно интуитивно понятным.

Чувствуя себя гордым за то, что я получил эту работу, я начал интегрировать эту функцию в генерацию кода AppFuse и обнаружил, что мне нужно было добавить немало кода в сгенерированные страницы / контроллеры списка.

Итак, я поехал на велосипеде …

Во время езды, я думал гораздо лучшее решение, добавив следующий метод поиска в GenericManagerImpl.java AppFuse в. В коде, который я добавил на страницы / контроллеры ранее, я уже реорганизовал использование CompassSearchHelper и продолжал делать это в реализации уровня обслуживания.

@Autowired
private CompassSearchHelper compass;

public List<T> search(String q, Class clazz) {
if (q == null || "".equals(q.trim())) {
return getAll();
}

List<T> results = new ArrayList<T>();

CompassSearchCommand command = new CompassSearchCommand(q);
CompassSearchResults compassResults = compass.search(command);
CompassHit[] hits = compassResults.getHits();

if (log.isDebugEnabled() && clazz != null) {
log.debug("Filtering by type: " + clazz.getName());
}

for (CompassHit hit : hits) {
if (clazz != null) {
if (hit.data().getClass().equals(clazz)) {
results.add((T) hit.data());
}
} else {
results.add((T) hit.data());
}
}

if (log.isDebugEnabled()) {
log.debug("Number of results for '" + q + "': " + results.size());
}

return results;
}

Это значительно упростило мою логику страницы / контроллера, потому что теперь все, что мне нужно было сделать, это вызвать manager.search (query, User.class) вместо того, чтобы выполнять вход Compass в контроллер. Конечно, было бы замечательно, если бы мне не пришлось передавать класс для фильтрации по объекту, но это характер универсальных и стирание типов .

Другие вещи, которые я узнал по пути:

  • Чтобы индексировать при запуске, я добавил compassGps.index () в StartupListener.
  • В модульных тестах, в которых использовались транзакции вокруг методов, мне приходилось вызывать compassGps.index () перед началом любых транзакций.
  • Чтобы сканировать несколько пакетов для поиска классов, мне пришлось добавить LocalCompassBeanPostProcessor .

Но больше всего мне напомнили, что всегда полезно прокатиться на велосипеде, когда вам не нравится дизайн вашего кода. ;-)

Эта функция и многое другое будет в AppFuse 2.1, который я надеюсь
завершить к концу месяца . А пока, пожалуйста, не стесняйтесь попробовать
последний снимок .

От http://raibledesigns.com/rd/entry/adding_search_to_appfuse