Elasticsearch是一個全文搜索引擎,專門用於處理大型數據集。根據描述,天然而然使用它來存儲和搜索應用程序日誌。與Logstash和Kibana一塊兒,它是強大的解決方案Elastic Stack的一部分,我以前的一些文章中已經對此進行了描述。node
保留應用程序日誌不是Elasticsearch的惟一使用場景。它一般用做應用程序的輔助數據庫,是一個主關係數據庫。若是您必須對大型數據集執行全文搜索或僅存儲應用程序再也不修改的許多歷史記錄,這個方法尤爲有用。固然,該方法也有優缺點。當您使用包含相同數據的兩個不一樣數據源時,您必須首先考慮同步。你有幾個選擇:根據關係數據庫供應商,您能夠利用二進制或事務日誌,其中包含SQL更新的歷史記錄。這種方法須要一些中間件來讀取日誌,而後將數據放入Elasticsearch。您始終能夠將整個職責移至數據庫端(觸發器)或Elasticsearch端(JDBC插件)。spring
不管您如何將數據導入Elasticsearch,都必須考慮另外一個問題:數據結構。關係數據庫中的數據可能分佈在幾個表之間。若是您想利用Elasticsearch,您應該將其存儲爲單一類型。它會強制您保留冗餘數據,這會致使更大的磁盤空間使用量。固然,若是Elasticsearch查詢比等效的關係數據庫中的查詢能更快,那麼這種影響是能夠接受的。docker
好的,在長時間的介紹以後繼續這個例子。 Spring Boot提供了一種經過Spring Data存儲庫與Elasticsearch進行交互的簡便方法。數據庫
按照Spring Boot的慣例,咱們沒必要在上下文中提供任何bean來啓用對Elasticsearch的支持。咱們只須要在pom.xml中添加如下依賴項:瀏覽器
<dependency>
緩存
<groupId>org.springframework.boot</groupId>
數據結構
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
app
</dependency>
elasticsearch
默認狀況下,應用程序嘗試在localhost上與Elasticsearch鏈接。若是咱們使用另外一個目標URL,咱們須要在配置設置中覆蓋它。這是咱們的application.yml文件的片斷,它覆蓋了默認的集羣名稱和地址,以及在Docker容器上啓動的Elasticsearch的地址:spring-boot
spring:
data:
elasticsearch:
cluster-name: docker-cluster
cluster-nodes: 192.168.99.100:9300
應用程序能夠經過Spring Boot Actuator health端點監測Elasticsearch鏈接的運行情況。首先,您須要添加如下Maven依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
默認狀況下啓用Healthcheck,並自動配置Elasticsearch檢查。可是,這驗證是經過Elasticsearch Rest API客戶端執行的。在這種狀況下,咱們須要覆蓋屬性spring.elasticsearch.rest.uris-負責設置REST客戶端使用的地址:
spring:
elasticsearch:
rest:
uris: http://192.168.99.100:9200
對於咱們的測試,咱們須要在開發模式下運行單節點Elasticsearch實例。像往常同樣,咱們將使用Docker容器。這是Docker容器啓動並在9200和9300端口上公開的命令。
$ docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:6.6.2
要啓用Elasticsearch存儲庫,咱們只須要使用@EnableElasticsearchRepositories註釋主類或配置類:
@SpringBootApplication
@EnableElasticsearchRepositories
public class SampleApplication { ... }
下一步是建立擴展CrudRepository的存儲庫接口。它提供了一些基本操做,如save或findById。若是您想要一些額外的find方法,您應該在跟隨Spring Data命名規範在接口內定義新方法。
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
List<Employee> findByOrganizationName(String name);
List<Employee> findByName(String name);
}
咱們的實體關係結構平鋪爲包含相關對象(組織,部門)的單個Employee對象。您能夠將此方法與在RDBMS中爲相關表組建立視圖進行比較。在Spring Data Elasticsearch命名法中,單個對象存儲爲文檔。所以,須要使用@Document註釋對象。您還應該爲Elasticsearch設置目標索引的名稱,類型和ID。可使用@Field註解配置其餘映射。
@Document(indexName = "sample", type = "employee")
public class Employee {
@Id
private Long id;
@Field(type = FieldType.Object)
private Organization organization;
@Field(type = FieldType.Object)
private Department department;
private String name;
private int age;
private String position;
// Getters and Setters ...
}
正如在前言中提到的,您可能決定使用Elasticsearch的主要緣由是須要處理大數據。所以,最好使用大量文檔填充咱們的測試Elasticsearch節點。若是您想在一步就插入許多文檔,那麼您必定要使用Bulk API。bulk API使得在單個API調用中執行許多索引/刪除操做成爲可能。這能夠大大提升索引速度。可使用Spring Data ElasticsearchTemplate bean執行批量操做。它在Spring Boot上也能夠自動配置。 Template提供了bulkIndex方法,該方法將索引查詢列表做爲輸入參數。這是在應用程序啓動時插入樣本測試數據的bean的實現:
public class SampleDataSet {
private static final Logger LOGGER = LoggerFactory.getLogger(SampleDataSet.class);
private static final String INDEX_NAME = "sample";
private static final String INDEX_TYPE = "employee";
@Autowired
EmployeeRepository repository;
@Autowired
ElasticsearchTemplate template;
@PostConstruct
public void init() {
for (int i = 0; i < 10000; i++) {
bulk(i);
}
}
public void bulk(int ii) {
try {
if (!template.indexExists(INDEX_NAME)) {
template.createIndex(INDEX_NAME);
}
ObjectMapper mapper = new ObjectMapper();
List<IndexQuery> queries = new ArrayList<>();
List<Employee> employees = employees();
for (Employee employee : employees) {
IndexQuery indexQuery = new IndexQuery();
indexQuery.setId(employee.getId().toString());
indexQuery.setSource(mapper.writeValueAsString(employee));
indexQuery.setIndexName(INDEX_NAME);
indexQuery.setType(INDEX_TYPE);
queries.add(indexQuery);
}
if (queries.size() > 0) {
template.bulkIndex(queries);
}
template.refresh(INDEX_NAME);
LOGGER.info("BulkIndex completed: {}", ii);
} catch (Exception e) {
LOGGER.error("Error bulk index", e);
}
}
// sample data set implementation ...
}
若是您不須要在啓動時插入數據,則能夠經過將屬性initial-import由enabled轉變爲false來禁用該過程。這是SampleDataSet bean的聲明:
@Bean
@ConditionalOnProperty("initial-import.enabled")
public SampleDataSet dataSet() {
return new SampleDataSet();
}
假設您已經啓動了示例應用程序,負責擴充索引的bean沒有被禁用,而且有足夠的耐心等待幾個小時,直到全部數據都插入到Elasticsearch節點中,如今它包含100M的員工類型文檔。顯示集羣有關的一些信息是值得的。您可使用Elasticsearch查詢來執行此操做,也能夠下載一個可用的GUI工具,例如ElasticHQ。碰巧的是,ElasticHQ也能夠做爲Docker容器使用。您必須執行如下命令才能啓動ElasticHQ容器:
$ docker run -d --name elastichq -p 5000:5000 elastichq/elasticsearch-hq
啓動ElasticHQ後,Web瀏覽器經過端口5000訪問GUI。它的Web控制檯提供有關集羣,索引和容許執行查詢的基本信息。您只須要輸入Elasticsearch節點地址,您將被重定向到帶有統計信息的主儀表盤。這是ElasticHQ的主儀表盤。
如您所見,咱們有一個名爲sample的索引,分爲5個分片。這是Spring Data @Document提供的默認值,可使用分片字段覆蓋它。點擊後咱們能夠導航到索引管理面板。您能夠對索引執行某些操做例如清除緩存或刷新索引等。您還能夠查看全部分片的統計信息。
出於當前的測試目的,我有大約25M(約3GB的空間)Employee類型的文檔。咱們能夠執行一些測試查詢。我已經公開了兩個用於搜索的端點:按員工姓名GET/employees/{name}和組織名稱GET/employees / organization / {organizationName}。結果並非壓倒性的。我認爲關係數據庫使用相同數量的數據也能夠得到相同的結果。
好的,咱們已經完成了開發並對大型數據集進行了一些手動測試。如今,是時候建立一些在構建時運行的集成測試了。咱們可使用容許在JUnit測試期間自動啓動數據庫的Docker容器的庫 - Testcontainers。有關此庫的更多信息,請參閱其站點https://www.testcontainers.org或我之前的一篇文章:使用Testcontainers Framework測試Spring與Vault和Postgres的集成。幸運的是,Testcontainers支持Elasticsearch。要在測試範圍內啓用它,首先須要在pom.xml中添加如下依賴項:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>elasticsearch</artifactId>
<version>1.11.1</version>
<scope>test</scope>
</dependency>
下一步是定義指向Elasticsearch容器的@ClassRule或@Rule bean。它在測試類以前或每一個依賴使用的註釋以前自動啓動。公開的端口號是自動生成的,所以您須要將其設置爲spring.data.elasticsearch.cluster-nodes屬性的值。這是咱們的JUnit集成測試的完整實現:
@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class EmployeeRepositoryTest {
@ClassRule
public static ElasticsearchContainer container = new ElasticsearchContainer();
@Autowired
EmployeeRepository repository;
@BeforeClass
public static void before() {
System.setProperty("spring.data.elasticsearch.cluster-nodes", container.getContainerIpAddress() + ":" + container.getMappedPort(9300));
}
@Test
public void testAdd() {
Employee employee = new Employee();
employee.setId(1L);
employee.setName("John Smith");
employee.setAge(33);
employee.setPosition("Developer");
employee.setDepartment(new Department(1L, "TestD"));
employee.setOrganization(new Organization(1L, "TestO", "Test Street No. 1"));
employee = repository.save(employee);
Assert.assertNotNull(employee);
}
@Test
public void testFindAll() {
Iterable<Employee> employees = repository.findAll();
Assert.assertTrue(employees.iterator().hasNext());
}
@Test
public void testFindByOrganization() {
List<Employee> employees = repository.findByOrganizationName("TestO");
Assert.assertTrue(employees.