|
| 1 | +[[api-conventions]] |
| 2 | +== API conventions |
| 3 | + |
| 4 | +The Java client uses a very consistent code structure, using modern code |
| 5 | +patterns that make complex requests easier to write and complex responses easier |
| 6 | +to process. This page explains these so that you quickly feel at home. |
| 7 | + |
| 8 | +[discrete] |
| 9 | +=== Package structure and namespace clients |
| 10 | + |
| 11 | +The {es} API is large and is organized into feature groups, as can be seen in |
| 12 | +the {ref}/rest-apis.html[{es} API documentation]. |
| 13 | + |
| 14 | +The Java client follows this structure: feature groups are called “namespaces”, |
| 15 | +and each namespace is located in a subpackage of |
| 16 | +`co.elastic.clients.elasticsearch`. The only exceptions are the “search” and |
| 17 | +“document” APIs which are located in the `_core` subpackage. |
| 18 | + |
| 19 | +Each of the namespace clients can be accessed from the top level {es} client. |
| 20 | +The snippet below shows how to use the indices namespace client to create an |
| 21 | +index: |
| 22 | + |
| 23 | +["source","java"] |
| 24 | +-------------------------------------------------- |
| 25 | +ElasticsearchClient esClient = ... |
| 26 | +esClient.indices().create(c -> c.index("my-index")); |
| 27 | +-------------------------------------------------- |
| 28 | + |
| 29 | +Namespace clients are very lightweight objects that can be created on the fly. |
| 30 | + |
| 31 | + |
| 32 | +[discrete] |
| 33 | +=== Method naming conventions |
| 34 | + |
| 35 | +Classes in the Java API Client contain two kinds of methods and properties: |
| 36 | + |
| 37 | +* Methods and properties that are part of the API, such as |
| 38 | +`ElasticsearchClient.search()` or `SearchResponse.maxScore()`. They are derived |
| 39 | +from their respective names in the {es} JSON API using the standard Java |
| 40 | +`camelCaseName` convention. |
| 41 | + |
| 42 | +* Methods and properties that are part of the framework on which the Java API |
| 43 | +Client is built, such as `Query._type()`. These methods and properties are |
| 44 | +prefixed with an underscore to both avoid any naming conflicts with API names |
| 45 | +and ease distinguishing what identifiers belong to the API or to the framework. |
| 46 | + |
| 47 | + |
| 48 | +[discrete] |
| 49 | +=== Immutable objects, builders and builder lambdas |
| 50 | + |
| 51 | +All data types in the Java client are immutable. Object creation uses the |
| 52 | +https://www.informit.com/articles/article.aspx?p=1216151&seqNum=2[builder pattern] |
| 53 | +that was popularized in *Effective Java* in 2008. |
| 54 | + |
| 55 | +["source","java"] |
| 56 | +-------------------------------------------------- |
| 57 | +CreateResponse createResponse = client.indices().create( |
| 58 | + new CreateRequest.Builder() |
| 59 | + .index("my-index") |
| 60 | + .putAliases("foo", |
| 61 | + new Alias.Builder().isWriteIndex(true).build() |
| 62 | + ) |
| 63 | + .build() |
| 64 | +); |
| 65 | +-------------------------------------------------- |
| 66 | + |
| 67 | +Note that a builder should not be reused after its `build()` method has been |
| 68 | +called. |
| 69 | + |
| 70 | +Although this works nicely, having to instantiate builder classes and call the |
| 71 | +build() method is a bit verbose. So every builder setter in the Java client also |
| 72 | +accepts a lambda expression that takes a newly created builder as a parameter |
| 73 | +and returns a populated builder. The snippet above can be written also as: |
| 74 | + |
| 75 | +["source","java"] |
| 76 | +-------------------------------------------------- |
| 77 | +CreateResponse createResponse = client.indices() |
| 78 | + .create(createBuilder -> createBuilder |
| 79 | + .index("my-index") |
| 80 | + .putAliases("foo", aliasBuilder -> aliasBuilder |
| 81 | + .isWriteIndex(true) |
| 82 | + ) |
| 83 | + ); |
| 84 | +-------------------------------------------------- |
| 85 | + |
| 86 | +This approach allows for much more concise code, and also avoids importing |
| 87 | +classes (and even remembering their names) since types are inferred from the |
| 88 | +method parameter signature. |
| 89 | + |
| 90 | +It becomes particularly useful with complex nested queries like the one below, |
| 91 | +taken from the |
| 92 | +{ref}/query-dsl-intervals-query.html[intervals query API documentation]. |
| 93 | + |
| 94 | +This example also shows a useful naming convention for builder parameters in |
| 95 | +deeply nested structures: since we have to give them a name to comply with the |
| 96 | +Java syntax (Kotlin would accept `it` and Scala a simple `_`), we name them with |
| 97 | +an underscore followed by the depth of the item, i.e. `_0`, `_1`, and so on. |
| 98 | +This removes the need for finding names and makes reading the code easier to |
| 99 | +read by reducing the number of identifiers. |
| 100 | + |
| 101 | +["source","java"] |
| 102 | +-------------------------------------------------- |
| 103 | +client.search(_0 -> _0 |
| 104 | + .query(_1 -> _1 |
| 105 | + .intervals(_2 -> _2 |
| 106 | + .field("my_text") |
| 107 | + .allOf(_3 -> _3 |
| 108 | + .ordered(true) |
| 109 | + .addIntervals(_4 -> _4 |
| 110 | + .match(_5 -> _5 |
| 111 | + .query("my favorite food") |
| 112 | + .maxGaps(0) |
| 113 | + .ordered(true) |
| 114 | + ) |
| 115 | + ) |
| 116 | + .addIntervals(_4 -> _4 |
| 117 | + .anyOf(_5 -> _5 |
| 118 | + .addIntervals(_6 -> _6 |
| 119 | + .match(_7 -> _7 |
| 120 | + .query("hot water") |
| 121 | + ) |
| 122 | + ) |
| 123 | + .addIntervals(_6 -> _6 |
| 124 | + .match(_7 -> _7 |
| 125 | + .query("cold porridge") |
| 126 | + ) |
| 127 | + ) |
| 128 | + ) |
| 129 | + ) |
| 130 | + ) |
| 131 | + ) |
| 132 | + ), |
| 133 | + RequestTest.AppData.class |
| 134 | +); |
| 135 | +-------------------------------------------------- |
| 136 | + |
| 137 | +[discrete] |
| 138 | +=== Variant types |
| 139 | + |
| 140 | +The {es} API has a lot of variant types: queries, aggregations, field mappings, |
| 141 | +analyzers, and so on. Finding the correct class name in such large collections |
| 142 | +can be challenging. |
| 143 | + |
| 144 | +The Java client builders make this easy: the builders for variant types, such as |
| 145 | +Query, have methods for each of the available implementations. We’ve seen this |
| 146 | +in action above with `intervals` (a kind of query) and `allOf`, `match` and |
| 147 | +`anyOf` (various kinds of intervals). |
| 148 | + |
| 149 | +This is because variant objects in the Java client are implementations of a |
| 150 | +“tagged union”: they contain the identifier (or tag) of the variant they hold |
| 151 | +and the value for that variant. For example, a `Query` object can contain an |
| 152 | +`IntervalsQuery` with tag `intervals`, a `TermQuery` with tag `term`, and so on. |
| 153 | +This approach allows writing fluent code where you can let the IDE completion |
| 154 | +features guide you to build and navigate complex nested structures: |
| 155 | + |
| 156 | +* Variant builders have setter methods for every available implementation. They |
| 157 | + use the same conventions as regular properties and accept both a builder lambda |
| 158 | + expression and a ready-made object of the actual type of the variant. Here’s an |
| 159 | + example to build a term query: |
| 160 | ++ |
| 161 | +-- |
| 162 | +["source","java"] |
| 163 | +-------------------------------------------------- |
| 164 | +Query query = new Query.Builder() |
| 165 | + .term( // <1> |
| 166 | + t -> t.field("name").value("foo") // <2> |
| 167 | + ) |
| 168 | + .build(); // <3> |
| 169 | +
|
| 170 | +-------------------------------------------------- |
| 171 | +<1> Choose the `term` variant to build a term query. |
| 172 | +<2> Build the terms query with a builder lambda expression. |
| 173 | +<3> Build the `Query` that now holds a `TermQuery` object with tag `term`. |
| 174 | +-- |
| 175 | + |
| 176 | +* Variant objects have getter methods for every available implementation. These |
| 177 | + methods check that the object actually holds a variant of that type and return |
| 178 | + the value downcasted to the correct type. They throw an `IllegalStateException` |
| 179 | + otherwise. This approach allows writing fluent code to traverse variants. |
| 180 | + |
| 181 | +[discrete] |
| 182 | +=== Blocking and asynchronous clients |
| 183 | + |
| 184 | +API clients come in two flavors: blocking and asynchronous. All methods on |
| 185 | +asynchronous clients return a standard `CompletableFuture`. |
| 186 | + |
| 187 | +Both flavors can be used at the same time depending on your needs, sharing the |
| 188 | +same transport object: |
| 189 | + |
| 190 | +["source","java"] |
| 191 | +-------------------------------------------------- |
| 192 | +Transport transport = ... |
| 193 | +
|
| 194 | +ElasticsearchClient client = new ElasticsearchClient(transport); |
| 195 | +if (client.exists(b -> b.index("products").id("foo")).value()) { |
| 196 | + logger.info("product exists"); |
| 197 | +} |
| 198 | +
|
| 199 | +ElasticsearchAsyncClient asyncClient = new ElasticsearchAsyncClient(transport); |
| 200 | +asyncClient.exists(b -> b.index("products").id("foo")).thenAccept(response -> { |
| 201 | + if (response.value()) { |
| 202 | + logger.info("product exists"); |
| 203 | + } |
| 204 | +}); |
| 205 | +-------------------------------------------------- |
| 206 | + |
| 207 | +[discrete] |
| 208 | +=== Exceptions |
| 209 | + |
| 210 | +Client methods can throw two kinds of exceptions: |
| 211 | + |
| 212 | +* Requests that were received by the {es} server but that were rejected |
| 213 | +(validation error, server internal timeout exceeded, etc) will produce an |
| 214 | +`ApiException`. This exception contains details about the error provided by |
| 215 | +{es}. |
| 216 | + |
| 217 | +* Requests that fail to reach the server (network error, server unavailable, |
| 218 | +etc) will produce a subclass `IOException`. That subclass is specific to the |
| 219 | +transport used. In the case of the `RestClientTransport` it will be a |
| 220 | +`ResponseException` that contains the low level HTTP response. |
| 221 | + |
| 222 | + |
| 223 | +[discrete] |
| 224 | +=== Object life cycles |
| 225 | + |
| 226 | +There are five kinds of objects in the Java client with different life cycles: |
| 227 | + |
| 228 | + |
| 229 | +**Object mapper**:: |
| 230 | +Stateless and thread-safe, but can be costly to create. |
| 231 | +It’s usually a singleton that is created at application startup and used to |
| 232 | +create the transport. |
| 233 | + |
| 234 | +**Transport**:: |
| 235 | +Thread-safe, holds network resources through the underlying HTTP client. A |
| 236 | +transport object is associated with an {es} cluster and has to be explicitly |
| 237 | +closed to release the underlying resources such as network connections. |
| 238 | + |
| 239 | +**Clients**:: |
| 240 | +Immutable, stateless and thread-safe. |
| 241 | +These are very lightweight objects that just wrap a transport and provide API |
| 242 | +endpoints as methods. |
| 243 | + |
| 244 | +**Builders**:: |
| 245 | +Mutable, non thread-safe. |
| 246 | +Builders are transient objects that should not be reused after calling |
| 247 | +`build()`. |
| 248 | + |
| 249 | +**Requests & other API objects**:: |
| 250 | +Immutable, thread-safe. |
| 251 | +If your application uses the same request or same parts of a request over and |
| 252 | +over, these objects can be prepared in advance and reused across multiple calls |
| 253 | +over multiple clients with different transports. |
0 commit comments