From 73bf3dd9882b47e2705812ba7b0c00255034ff4d Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Mon, 10 Aug 2020 21:58:57 +0200 Subject: [PATCH] DATAES-899 - Add documentation for join-type. original PR: #501 --- .../reference/elasticsearch-misc.adoc | 204 ++++++++++++++++++ .../elasticsearch/core/join/JoinField.java | 12 -- .../core/ElasticsearchTemplateTests.java | 15 +- 3 files changed, 214 insertions(+), 17 deletions(-) diff --git a/src/main/asciidoc/reference/elasticsearch-misc.adoc b/src/main/asciidoc/reference/elasticsearch-misc.adoc index 112d750c5..15b25ef85 100644 --- a/src/main/asciidoc/reference/elasticsearch-misc.adoc +++ b/src/main/asciidoc/reference/elasticsearch-misc.adoc @@ -99,4 +99,208 @@ If the class to be retrieved has a `GeoPoint` property named _location_, the fol Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247))) ---- +[[elasticsearch.misc.jointype]] +== Join-Type implementation + +Spring Data Elasticsearch supports the https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html[Join data type] for creating the corresponding index mappings and for storing the relevant information. + +=== Setting up the data + +For an entity to be used in a parent child join relationship, it must have a property of type `JoinField` which must be annotated. +Let's assume a `Statement` entity where a statement may be a _question_, an _answer_, a _comment_ or a _vote_ (a _Builder_ is also shown in this example, it's not necessary, but later used in the sample code): + +==== +[source,java] +---- +@Document(indexName = "statements") +public class Statement { + @Id + private String id; + + @Field(type = FieldType.Text) + private String text; + + @JoinTypeRelations( + relations = + { + @JoinTypeRelation(parent = "question", children = {"answer", "comment"}), <1> + @JoinTypeRelation(parent = "answer", children = "vote") <2> + } + ) + private JoinField relation; <3> + + private Statement() { + } + + public static StatementBuilder builder() { + return new StatementBuilder(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public JoinField getRelation() { + return relation; + } + + public void setRelation(JoinField relation) { + this.relation = relation; + } + + public static final class StatementBuilder { + private String id; + private String text; + private JoinField relation; + + private StatementBuilder() { + } + + public StatementBuilder withId(String id) { + this.id = id; + return this; + } + + public StatementBuilder withText(String text) { + this.text = text; + return this; + } + + public StatementBuilder withRelation(JoinField relation) { + this.relation = relation; + return this; + } + + public Statement build() { + Statement statement = new Statement(); + statement.setId(id); + statement.setText(text); + statement.setRelation(relation); + return statement; + } + } +} +---- +<1> a question can have answers and comments +<2> an answer can have votes +<3> the `JoinField` property is used to combine the name (_question_, _answer_, _comment_ or _vote_) of the relation with the parent id. The generic type must be the same as the `@Id` annotated property. +==== + +Spring Data Elasticsearch will build the following mapping for this class: + +==== +[source,json] +---- +{ + "statements": { + "mappings": { + "properties": { + "_class": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "relation": { + "type": "join", + "eager_global_ordinals": true, + "relations": { + "question": [ + "answer", + "comment" + ], + "answer": "vote" + } + }, + "text": { + "type": "text" + } + } + } + } +} +---- +==== + +=== Storing data + +Given a repository for this class the following code inserts a question, two answers, a comment and a vote: + +==== +[source,java] +---- +void init() { + repository.deleteAll(); + + Statement savedWeather = repository.save( + Statement.builder() + .withText("How is the weather?") + .withRelation(new JoinField<>("question")) <1> + .build()); + + Statement sunnyAnswer = repository.save( + Statement.builder() + .withText("sunny") + .withRelation(new JoinField<>("answer", savedWeather.getId())) <2> + .build()); + + repository.save( + Statement.builder() + .withText("rainy") + .withRelation(new JoinField<>("answer", savedWeather.getId())) <3> + .build()); + + repository.save( + Statement.builder() + .withText("I don't like the rain") + .withRelation(new JoinField<>("comment", savedWeather.getId())) <4> + .build()); + + repository.save( + Statement.builder() + .withText("+1 for the sun") + .withRelation(new JoinField<>("vote", sunnyAnswer.getId())) <5> + .build()); +} +---- +<1> create a question statement +<2> the first answer to the question +<3> the second answer +<4> a comment to the question +<5> a vote for the first answer +==== + +=== Retrieving data + +Currently native search queries must be used to query the data, so there is no support from standard repository methods. <> can be used instead. + +The following code shows as an example how to retrieve all entries that have a _vote_ (which must be _answers_, because only answers can have a vote) using an `ElasticsearchOperations` instance: + +==== +[source,java] +---- +SearchHits hasVotes() { + NativeSearchQuery query = new NativeSearchQueryBuilder() + .withQuery(hasChildQuery("vote", matchAllQuery(), ScoreMode.None)) + .build(); + + return operations.search(query, Statement.class); +} +---- +==== diff --git a/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java b/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java index 9a08dbadd..70ce58ce3 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/join/JoinField.java @@ -15,10 +15,6 @@ */ package org.springframework.data.elasticsearch.core.join; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import org.springframework.lang.Nullable; /** @@ -56,12 +52,4 @@ public class JoinField { public String getName() { return name; } - - public Map getAsMap() { - Map joinMap = new HashMap<>(); - joinMap.put("name", getName()); - joinMap.put("parent", getParent()); - - return Collections.unmodifiableMap(joinMap); - } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java index 7137b3b4f..7ffda4062 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -3379,7 +3379,7 @@ public abstract class ElasticsearchTemplateTests { void shouldUpdateEntityWithJoinFields(String qId1, String qId2, String aId1, String aId2) throws Exception { org.springframework.data.elasticsearch.core.document.Document document = org.springframework.data.elasticsearch.core.document.Document .create(); - document.put("myJoinField", new JoinField<>("answer", qId2).getAsMap()); + document.put("myJoinField", toDocument(new JoinField<>("answer", qId2))); UpdateQuery updateQuery = UpdateQuery.builder(aId2) // .withDocument(document) // .withRouting(qId2).build(); @@ -3389,8 +3389,7 @@ public abstract class ElasticsearchTemplateTests { // when operations.bulkUpdate(queries, IndexCoordinates.of(INDEX_NAME_JOIN_SAMPLE_ENTITY)); - indexOperations.refresh(); - Thread.sleep(5000); + operations.indexOps(IndexCoordinates.of(INDEX_NAME_JOIN_SAMPLE_ENTITY)).refresh(); SearchHits updatedHits = operations.search( new NativeSearchQueryBuilder().withQuery(new ParentIdQueryBuilder("answer", qId2)).build(), @@ -3423,8 +3422,7 @@ public abstract class ElasticsearchTemplateTests { Query query = new NativeSearchQueryBuilder().withQuery(new ParentIdQueryBuilder("answer", qId2)).withRoute(qId2) .build(); operations.delete(query, SampleJoinEntity.class, IndexCoordinates.of(INDEX_NAME_JOIN_SAMPLE_ENTITY)); - indexOperations.refresh(); - Thread.sleep(5000); + operations.indexOps(IndexCoordinates.of(INDEX_NAME_JOIN_SAMPLE_ENTITY)).refresh(); SearchHits deletedHits = operations.search( new NativeSearchQueryBuilder().withQuery(new ParentIdQueryBuilder("answer", qId2)).build(), @@ -3439,6 +3437,13 @@ public abstract class ElasticsearchTemplateTests { assertThat(hitIds.size()).isEqualTo(0); } + private org.springframework.data.elasticsearch.core.document.Document toDocument(JoinField joinField) { + org.springframework.data.elasticsearch.core.document.Document document = create(); + document.put("name", joinField.getName()); + document.put("parent", joinField.getParent()); + return document; + } + protected RequestFactory getRequestFactory() { return ((AbstractElasticsearchTemplate) operations).getRequestFactory(); }