21 февраля 2019
Hibernate Search - это мощное решение для реализации возможностей полнотекстового поиска (как google или amazon) в вашем ЕЕ приложении. Под капотом, для построения индекса, будет использоваться Apache Lucene напрямую, или через Elasticsearch. Hibernate Search может быть легко интегрирован с JPA, Hibernate ORM, Infinispan или другими источниками. Если вы используете Wildfly - вам повезло, - Hibernate Search с ним уже интегрирован.
Итак, посмотрим, как это работает на практике ...
1.Давайте начнем с gradle проекта, используя hibernate-search и EE зависимости
apply plugin: 'java'
apply plugin: 'war'
sourceCompatibility = '1.8'
defaultTasks 'clean', 'build'
ext.libraryVersions = [
javaee : '8.0',
wildfly : '15.0.1.Final',
hibernatesearch : '5.11.1.Final',
hibernateentitymanager : '5.4.1.Final',
h2 : '1.4.198',
dom4j : '2.1.1',
junit : '4.12'
]
configurations {
wildfly
}
repositories {
mavenCentral()
}
dependencies {
wildfly "org.wildfly:wildfly-dist:${libraryVersions.wildfly}@zip"
providedCompile "javax:javaee-api:${libraryVersions.javaee}"
providedCompile "org.hibernate:hibernate-search-orm:${libraryVersions.hibernatesearch}"
testCompile "junit:junit:${libraryVersions.junit}"
testCompile "com.h2database:h2:${libraryVersions.h2}"
testCompile "org.hibernate:hibernate-entitymanager:${libraryVersions.hibernateentitymanager}"
testCompile "org.hibernate:hibernate-search-orm:${libraryVersions.hibernatesearch}"
testCompile "org.dom4j:dom4j:${libraryVersions.dom4j}"
}
**2. Создадим стандартную сущность JPA c Hibernate Search аннотациями **
src/main/java/org/.../BlogEntity.java:
package org.kostenko.example.wildfly.hibernatesearch;
import javax.persistence.*;
import org.hibernate.search.annotations.*;
@Entity
@Indexed
public class BlogEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
@Field(store = Store.YES)
private String title;
@Column
@Field(store = Store.YES)
private String body;
// getters, setters
}
Основные аннотации Hibernate Search:
Аннотация | Описание |
---|---|
@Indexed | Помечает, какие объекты должны быть индексированы; Позволяет переопределению имя индекса. Только объекты @Indexed могут быть найдены.. |
@Field | Помечает поле объекта, которое будет индексировано. Существует несколько вариантов работы с индексируемыми полями: store - перечислимый тип, указывающий, должно ли значение поля быть сохранено в индексе. По умолчанию - Store.NO (Значение поля не будет сохранено в индексе.) Хранение значений может быть полезным для использования в механизме Projections,- что позволит восстанавливать значения полей непосредственно из индекса, минуя запросы к СУБД, index - перечисление, определяющее, следует ли индексировать поле или нет. По умолчанию - Index.YES |
@DocumentId | Позволяет переопределить идентификатор документа в индексе. По-умолчанию используется поля помеченное JPA-аннотацией @Id. |
@SortableField | Помечает что индексируемое поле может быть сортируемым. |
3. Добавьте свойства hibernate-search в persistence.xml
src/test/resources/META-INF/persistence.xml :
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="myDSTest" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>org.kostenko.example.wildfly.hibernatesearch.BlogEntity</class>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.password" value=""/>
<property name="hibernate.connection.url" value="jdbc:hsqldb:mem:testdb"/>
<property name="hibernate.showSql" value="true"/>
<!-- Hibernate Search -->
<property name="hibernate.search.default.directory_provider" value="filesystem" />
<property name="hibernate.search.default.indexBase" value="/tmp/index1" />
<property name="hibernate.search.default.indexmanager" value="near-real-time" />
</properties>
</persistence-unit>
</persistence>
Как мы могли убедиться, добавить Hibernate Search в свое приложение достаточно легко, - всего пара аннотаций. Теперь Hibernate Search автоматически будет создавать индекс каждый раз, когда объект, будет изменен (создан, удален) через Hibernate ORM. Индекс может быть сохранен в ram
или filesystem
. Другие поставщики, также доступны.
Итак, пора сделать простой тест и посмотреть, как это работает с Lucene queries
src/test/java/org/.../JpaHibernateSearchTest :
public class JpaHibernateSearchTest {
private static EntityManager entityManager;
private static FullTextEntityManager fullTextEntityManager;
private static QueryBuilder queryBuilder;
@BeforeClass
public static void init() {
entityManager = Persistence.createEntityManagerFactory("myDSTest").createEntityManager();
fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(BlogEntity.class).get();
for (int i = 0; i < 1000; i++) {
BlogEntity blogEntity = new BlogEntity();
blogEntity.setTitle("Title" + i);
blogEntity.setBody("BodyBody Body" + i + " look at my horse my horse is a amazing " + i);
entityManager.getTransaction().begin();
entityManager.persist(blogEntity);
entityManager.getTransaction().commit();
}
Assert.assertEquals(1000, entityManager.createQuery("SELECT COUNT(b) FROM BlogEntity b", Number.class).getSingleResult().intValue());
}
/**
* Keyword Queries - searching for a specific word.
*/
@Test
public void shouldSearchByKeywordQuery() throws Exception {
Query query = queryBuilder.keyword().onFields("title", "body").matching("Body999").createQuery();
javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(query, BlogEntity.class); // wrap Lucene query in a javax.persistence.Query
List<BlogEntity> result = persistenceQuery.getResultList();// execute search
Assert.assertFalse(result.isEmpty());
Assert.assertEquals("Title999", result.get(0).getTitle());
}
/**
* Fuzzy Queries - we can define a limit of “fuzziness”
*/
@Test
public void shouldSearchByFuzzyQuery() throws Exception {
Query query = queryBuilder.keyword().fuzzy().withEditDistanceUpTo(2).withPrefixLength(0).onField("title").matching("TAtle999").createQuery();
javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(query, BlogEntity.class);
List<BlogEntity> result = persistenceQuery.getResultList();
Assert.assertFalse(result.isEmpty());
Assert.assertEquals("Title999", result.get(0).getTitle());
}
/**
* Wildcard Queries - queries for which a part of a word is unknown ('?' - single character, '*' - character sequence)
*/
@Test
public void shouldSearchByWildcardQuery() throws Exception {
Query query = queryBuilder.keyword().wildcard().onField("title").matching("?itle*").createQuery();
javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(query, BlogEntity.class);
List<BlogEntity> result = persistenceQuery.getResultList();
Assert.assertFalse(result.isEmpty());
Assert.assertEquals(1000, result.size());
}
/**
* Phrase Queries - search for exact or for approximate sentences
*/
@Test
public void shouldSearchByPhraseQuery() throws Exception {
Query query = queryBuilder.phrase().withSlop(10).onField("body").sentence("look amazing horse 999").createQuery();
javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(query, BlogEntity.class);
List<BlogEntity> result = persistenceQuery.getResultList();
Assert.assertFalse(result.isEmpty());
Assert.assertEquals("Title999", result.get(0).getTitle());
}
}
В тесте, мы использовали основные юзкейсы. Но даже этого достаточно, чтобы почувствовать насколько Apache Lucene search engine
неплох. И увидеть, как легко его можно интегрировать с вашим приложением. Обратитесь к официальной документации для детелей.
4. Использование Hibernate Search с Wildfly
Как я уже писал - Hibernate Search интегрирован в Wildfly начиная с версии 10. Это означает, что эта функциональность активируется автоматически в случае, если вы используете по крайней мере одну ентити с org.hibernate.search.annotations.Indexed
.
Итак, все что нам нужно, чтобы показать как это работает вместе, - это просто реализовать простой веб-сервис с похожей на юнит тест логикой и запустить его на сервере.
@Path("/")
@Stateless
public class HibernateSearchDemoEndpoint {
@PersistenceContext
EntityManager entityManager;
/**
* Persist 1000 entities and rebuild index
* @return
*/
@GET
@Path("/init")
@Transactional
public Response init() {
int count = entityManager.createQuery("SELECT COUNT(b) FROM BlogEntity b", Number.class).getSingleResult().intValue();
long time = System.currentTimeMillis();
for (int i = count; i < count + 1000; i++) {
BlogEntity blogEntity = new BlogEntity();
blogEntity.setTitle("Title" + i);
blogEntity.setBody("Body Body Body" + i + " look at my horse my horse is a amazing " + i);
entityManager.persist(blogEntity);
}
time = System.currentTimeMillis() - time;
return Response.ok().entity(String.format("1000 records persisted. Current records %s Execution time = %s ms.", count + 1000, time)).build();
}
/**
* Search by index
* @param q - query string
* @return
*/
@GET
@Path("/search")
public Response search(@QueryParam("q") String q) {
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(BlogEntity.class).get();
long time = System.currentTimeMillis();
Query query = queryBuilder.keyword().onFields("title", "body").matching(q).createQuery();
javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(query, BlogEntity.class);
List<BlogEntity> result = persistenceQuery.getResultList();
time = System.currentTimeMillis() - time;
String resultStr = result.stream().map(Object::toString).collect(Collectors.joining(","));
return Response.ok().entity( String.format("Found %s results. [%s] Execution time = %s ms.",result.size(),resultStr,time)).build();
}
}
Теперь, давайте добавим таск в наш build.gradle
для запуска Wildfly прямо из проекта
task removeWildfly(type:Delete) {
delete "build/wildfly-${libraryVersions.wildfly}"
}
task resolveWildfly(type:Copy, dependsOn:removeWildfly) {
destinationDir = buildDir
from {zipTree(configurations.wildfly.singleFile)}
}
task run() {
dependsOn 'resolveWildfly'
doLast {
copy {
from projectDir.toString() + "/build/libs/wildfly-hibernate-search-example.war"
into projectDir.toString() + "/build/wildfly-${libraryVersions.wildfly}/standalone/deployments"
}
exec {
workingDir = file(projectDir)
commandLine "./build/wildfly-${libraryVersions.wildfly}/bin/standalone.sh", "--debug", "5005"
ext.output = {
return standardOutput.toString()
}
}
}
}
run
разархивирует wildfly в каталог проекта, задеплоит приложение и запустит сервер. Итак:
gradle && gradle run
проверим вывод, чтобы убедиться, что Hiberate создал Index соглсно пути в persistence.xml
...
15:15:37,386 INFO [org.hibernate.search.store.impl.DirectoryProviderHelper] (MSC service thread 1-3) HSEARCH000041: Index directory not found, creating: '/tmp/index2/org.kostenko.example.wildfly.hibernatesearch.BlogEntity'
...
И, наконец, смотрим на результат в браузере
Исходный код проекта, доступен на GitHub
20 февраля 2019
Часто, по соображения безопасности или юзабилити, предпочтительнее использовать ssh ключи вместо паролей. Для этого нужно:
ssh-keygen -t rsa -f ~/.ssh/[KEY_FILENAME] -C [USERNAME]
cat [KEY_FILENAME].pub >> ~/.ssh/authorized_keys
chmod 400 [KEY_FILENAME]
ssh -i [KEY_FILENAME] [USERNAME]@yourhost.com
Для того, что бы запретить логиниться с паролем и под рутом, отредактируйте /etc/ssh/sshd_config
:
PasswordAuthentication no
PermitRootLogin no
Затем перезагрузите ssh сервер
sudo service ssh restart
16 февраля 2019
Моё первое видео (о том как я делал это блог) на youtube!
19 января 2019
Jbake поддерживает работу с тегами из коробки и при использовании тегов в описании будет генерироваться отдельная страница под каждый тег. Достаточно отредактировать параметр render.tags
в конфигурационном файле jbake.properties
render.tags=true
Теперь в зависимости от того, где мы хотим разместить список тегов, редактируем соответствующий темплейт. Для этого блога я использую freemarker и решил вывести теги в меню в виде выпадающего списка. Соответственно, изменив ``menu.ftl`, как показано ниже:
<ul class="dropdown-menu">
<#list tags as tag>
<li><a href="${content.rootpath}${tag.uri}">${tag.name}</a></li>
</#list>
</ul>
Готово!
К сожалению, Jbake пока не поддерживает мультиязычные блоги на уровне метаданных и для обеспечения этой функциональности мне пришлость проделать несколько нехитрых упражнений. Сперва, я просто хотел сделать полную копию с другим языком контента, но в случае любых изменений для шаблонов, скриптов, изображений - правки пришлось бы делать дважды. Что не есть хорошо.
Вместо этого я просто создал каталог content_ru
в корне проекта.
├── assets
├── content
├── content_ru
├── templates
├── jbake.properties
├── README.md
К счастью, в jbake.properies
есть возможность установить каталог контента, который будет использован при генерации, что позволяет держать несколько директорий в проекте. Теперь сгенерировать мультиязычный блог можно используя скрипт:
bakeblog.sh:
#!/bin/bash
# Helper script to bake the blog
# Author: kostenko
export PATH="/opt/jbake-2.6.3-bin/bin":$PATH
rm -R ./output
# Building en version
export JBAKE_OPTS="-Duser.language=EN"
jbake -b
# Build ru version
export JBAKE_OPTS="-Duser.language=RU"
mv jbake.properties jbake.properties.orig
cat jbake.properties.orig >> jbake.properties
echo "content.folder=content_ru" >> jbake.properties
jbake -b . output/ru
# cleanup
rm jbake.properties
mv jbake.properties.orig jbake.properties
Добавим возможность переключения языков в меню:
<!-- switch language -->
<ul class="nav navbar-nav navbar-right">
<li><a href="/">en</a></li>
<li><a href="/ru">ru</a></li>
</ul>
На этом все!
Для того что бы подключить Google analytics нужно выполнить несколько простых шагов
header.ftl
первой строкой блока <HEAD>
.<!-- Global site tag (gtag.js) - Google Analytics -->
<head>
<script async src="https://www.googletagmanager.com/gtag/js?id=<YOUR_GA_ID>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<YOUR_GA_ID>');
</script>
...
P.S. При желании к блогу можно подключить возможность оставлять комментарии, используя например Disqus.
P.P.S. Код этого блога доступен на GitHub
18 января 2019
Развернуть свой персональный блог, блог компании или лендинг проекта используя github - задача, как оказалось, несложная и не требующая особых навыков. Здесь я, максимально конструктивно, постараюсь изложить основные шаги, которые необходимо сделать для того, что бы обзавестись личным блогом на небезизвестном ресурсе.
Для этого нам потребуются аккаунт на github и базовые навыки работы с git. К слову сказать, все ниже изложенное можно проделать и для конкурирующего сервиса gitlab, - где кому больше нравится.
Далее, нужно просто создать проект, назвав его: <логин>.github.io, например kostenkoserg.github.io
В принципе, на этом вся подготовительная рабобта для хостинга статики и завершена. Теперь клонируем проект себе (пока пустой), создаём index.html и пушим в репозиторий:
$ git clone https://github.com/kostenkoserg/kostenkoserg.github.io.git
$ cd kostenkoserg.github.io
$ echo "Hello World!"
$ git commit -m "my first github page"
$ git push
Все! Страничка доступна по https://kostenkoserg.github.io
По желанию, к сайту можно подвязать уже существующий домен, если по каким-либо соображениям .github.io не подходит. Для этого нужно
A
записи в настройках DNS провайдера с GitHub ипишникамиJbake - это проект, с открытым исходным кодом, для генерации статических сайтов. Для Jbake доступна интеграция с Gradle и Maven, из коробки поддержка Bootstrap и прозрачная интеграция с другими CSS фреймворками, а так же поддержка Freemarker, Groovy, Thymeleaf и Jade в качестве шаблонов.
Для начала работы с Jbake, качаем дистрибутив с сайта проекта и распаковываем архив куда-то себе. После чего генерируем структуру своего сайта:
$ cd myblog
$ /opt/jbake/bin/jbake -i
В результате получаем следующую структуру каталогов и немного тестового контента:
├── assets
│ ├── css
│ │ ├── asciidoctor.css
│ │ ├── base.css
│ │ ├── bootstrap.min.css
│ │ └── prettify.css
│ ├── favicon.ico
│ ├── fonts
│ │ ├── ...
│ └── js
│ ├── bootstrap.min.js
│ ├── html5shiv.min.js
│ ├── jquery-1.11.1.min.js
│ └── prettify.js
├── content
│ ├── about.html
│ └── blog
│ └── 2013
│ ├── first-post.html
│ ├── second-post.md
│ └── third-post.adoc
├── jbake.properties
└── templates
├── archive.ftl
├── feed.ftl
├── footer.ftl
├── header.ftl
├── index.ftl
├── menu.ftl
├── page.ftl
├── post.ftl
├── sitemap.ftl
└── tags.ftl
Теперь генерируем сам сайт:
/opt/jbake/bin/jbake -b
В результате чего получим каталог output
с нашим статическим сайтом.
Проверить, что получилось можно по http://localhost:8820/, запустив всторенный сервер
/opt/jbake/bin/jbake -s
Всё! наш статический сайт готов! Для полноценного ведения блога, теперь достаточно отредактировать содержимое каталога content
, перегенерировать сайт и запушить полученный output
в, созданный на первом шаге, github репозиторий.
Jbake поддерживает несколько форматов контента: HTML, Markdown, AsciiDoc, - что позволяет вести свой сайт, используя любой текстовый редактор. Я использую Atom и Markdown.