How to use Elasticsearch’s range types with Spring Data Elasticsearch

Elasticsearch allows the data, that is stored in a document, to be not only of elementary types, but also of a range of types, see the documentation. With a short example I will explain this range type and how to use it in Spring Data Elasticsearch (the current version being 4.0.3).

For this example we want be able to answer the question: “Who was president of the United States of America in the year X?”. We will store in Elasticsearch a document describing a president with the name and his term, defined be a range of years, defined by a from and to value. We will then query the index for documents where this term range contains a given value

The first thing we need to define is our entity. I named it President:

@Document(indexName = "presidents")
public class President {
    @Id
    private String id;

    @Field(type = FieldType.Text)
    private String name;

    @Field(type = FieldType.Integer_Range)
    private Term term;

    static President of(String name, Integer from, Integer to) {
        return new President(name, new Term(from, to));
    }

    public President() {
    }

    public President(String name, Term term) {
        this(UUID.randomUUID().toString(), name, term);
    }

    public President(String id, String name, Term term) {
        this.id = id;
        this.name = name;
        this.term = term;
    }

    // getter/setter

    static class Term {
        @Field(name = "gte")
        private Integer from;
        @Field(name = "lte")
        private Integer to;

        public Term() {
        }

        public Term(Integer from, Integer to) {
            this.from = from;
            this.to = to;
        }

        // getter/setter
    }
}

There are the standard annotations for a Spring Data Elasticsearch entity like @Document and @Id, but in addition there is the property term that is annotated with @Field(type = FieldType.Integer_Range) (line 9). This marks it as an integer range property. The Term class is defined as inner class at line 31 (not to be confused with the Elasticsearch Term), it defines the term of a president with the two value from and to. Elasticsearch needs for a range the fields to be named gte and lte, this we achieve by defining these names with the @Field annotations in lines 32 and 34.

The rest is just a basic repository:

public interface PresidentRepository extends ElasticsearchRepository<President, String> {
    SearchHits<President> searchByTerm(Integer year);
}

Here we use a single Integer as value because Elasticsearch does the magic by finding the corresponding entries where the searched value is in the range of the stored documents.

And of yourse we have some Controller using it. This Controller has one endpoint that loads the presidents since World War II into Elasticsearch, and a second one returns the desired results:

@RequestMapping("presidents")
public class PresidentController {

    private final PresidentRepository repository;

    public PresidentController(PresidentRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/load")
    public void load() {
        repository.saveAll(Arrays.asList(
                President.of("Dwight D Eisenhower", 1953, 1961),
                President.of("Lyndon B Johnson", 1963, 1969),
                President.of("Richard Nixon", 1969, 1974),
                President.of("Gerald Ford", 1974, 1977),
                President.of("Jimmy Carter", 1977, 1981),
                President.of("Ronald Reagen", 1981, 1989),
                President.of("George Bush", 1989, 1993),
                President.of("Bill Clinton", 1993, 2001),
                President.of("George W Bush", 2001, 2009),
                President.of("Barack Obama", 2009, 2017),
                President.of("Donald Trump", 2017, 2021)));
    }

    @GetMapping("/term/{year}")
    public SearchHits<President> searchByTerm(@PathVariable Integer year) {
        return repository.searchByTerm(year);
    }
}

See it in action (I am using HTTPie), my application is listening on port 9090:

$ http -b :9090/presidents/term/2009
{
    "aggregations": null,
    "empty": false,
    "maxScore": 1.0,
    "scrollId": null,
    "searchHits": [
        {
            "content": {
                "id": "c3a3a0d0-d835-4a02-a2e8-20cc1c0e9285",
                "name": "George W Bush",
                "term": {
                    "from": 2001,
                    "to": 2009
                }
            },
            "highlightFields": {},
            "id": "c3a3a0d0-d835-4a02-a2e8-20cc1c0e9285",
            "score": 1.0,
            "sortValues": []
        },
        {
            "content": {
                "id": "36416746-ff11-4243-a4f3-a6bb0cff9a93",
                "name": "Barack Obama",
                "term": {
                    "from": 2009,
                    "to": 2017
                }
            },
            "highlightFields": {},
            "id": "36416746-ff11-4243-a4f3-a6bb0cff9a93",
            "score": 1.0,
            "sortValues": []
        }
    ],
    "totalHits": 2,
    "totalHitsRelation": "EQUAL_TO"
}

$http -b :9090/presidents/term/2000
{
    "aggregations": null,
    "empty": false,
    "maxScore": 1.0,
    "scrollId": null,
    "searchHits": [
        {
            "content": {
                "id": "984fdf87-a7d8-4dc2-b2e8-5dd948065147",
                "name": "Bill Clinton",
                "term": {
                    "from": 1993,
                    "to": 2001
                }
            },
            "highlightFields": {},
            "id": "984fdf87-a7d8-4dc2-b2e8-5dd948065147",
            "score": 1.0,
            "sortValues": []
        }
    ],
    "totalHits": 1,
    "totalHitsRelation": "EQUAL_TO"
}

So just with putting the right types and names into our @Field annotations we are able to use the range types of Elasticsearch in our Spring Data Elasticsearch application.

Search entities within a geographic distance with Spring Data Elasticsearch 4

A couple of months ago I published the post Using geo-distance sort in Spring Data Elasticsearch 4. In the comments there came up the question “What about searching within a distance?”

Well, this is not supported by query derivation from the method name, but it can easily be done with a custom repository implementation (see the documentation for more information about that).

I updated the example – which is available on GitHub – and will explain what is needed for this implementation. I won’t describe the entity and setup, please check the original post for that.

The custom repository interface

First we need to define a new repository interface that defines the method we want to provide:

public interface FoodPOIRepositoryCustom {

    /**
     * search all {@link FoodPOI} that are within a given distance of a point
     *
     * @param geoPoint
     *     the center point
     * @param distance
     *     the distance
     * @param unit
     *     the distance unit
     * @return the found entities
     */
    List<SearchHit<FoodPOI>> searchWithin(GeoPoint geoPoint, Double distance, String unit);
}

The custom repository implementation

Next we need to provide an implementation, important here is that this is named like the interface with the suffix “Impl”:

public class FoodPOIRepositoryCustomImpl implements FoodPOIRepositoryCustom {

    private final ElasticsearchOperations operations;

    public FoodPOIRepositoryCustomImpl(ElasticsearchOperations operations) {
        this.operations = operations;
    }

    @Override
    public List<SearchHit<FoodPOI>> searchWithin(GeoPoint geoPoint, Double distance, String unit) {

        Query query = new CriteriaQuery(
          new Criteria("location").within(geoPoint, distance.toString() + unit)
        );

        // add a sort to get the actual distance back in the sort value
        Sort sort = Sort.by(new GeoDistanceOrder("location", geoPoint).withUnit(unit));
        query.addSort(sort);

        return operations.search(query, FoodPOI.class).getSearchHits();
    }
}

In this implementation we have an ElasticsearchOperations instance injected by Spring. In the method implementation we build a NativeSearchQuery that specifies the distance query we want. In addition to that we add a GeoDistanceSort to have the actual distance of the found entities in the output. We pass this query to the ElasticsearchOperations instance and return the search result.

Adapt the repository

We need to add the new interface to our FoodPOIRepository definition, which otherwise is unchanged:

public interface FoodPOIRepository extends ElasticsearchRepository<FoodPOI, String>, FoodPOIRepositoryCustom {

    List<SearchHit<FoodPOI>> searchTop3By(Sort sort);

    List<SearchHit<FoodPOI>> searchTop3ByName(String name, Sort sort);
}

Use it in the controller

In the rest controller, there is a new method that uses the distance search:

@PostMapping("/within")
List<ResultData> withinDistance(@RequestBody RequestData requestData) {

    GeoPoint location = new GeoPoint(requestData.getLat(), requestData.getLon());

    List<SearchHit<FoodPOI>> searchHits
        = repository.searchWithin(location, requestData.distance, requestData.unit);

    return toResultData(searchHits);
}

private List<ResultData> toResultData(List<SearchHit<FoodPOI>> searchHits) {
    return searchHits.stream()
        .map(searchHit -> {
            Double distance = (Double) searchHit.getSortValues().get(0);
            FoodPOI foodPOI = searchHit.getContent();
            return new ResultData(foodPOI.getName(), foodPOI.getLocation(), distance);
        }).collect(Collectors.toList());
}

We extract the needed parameters from the requestData that came in, call our repository method and convert the results to our output format.

And that’s it

So with a small custom repository implementation we were able to add the desired functionality to our repository

mapjfx display problem on Windows 10 seems solved

For some time now there was a bug that the map was not displaying properly on some Windows systems, see

It seems this was because of a bug in the WebView from JavaFX https://bugs.openjdk.java.net/browse/JDK-8234471. Thanks to https://github.com/vewert and https://github.com/Abu-Abdullah investigating into this.

This issue was fixed with JavaFX15, I tried this on a virtual machine with Windows10 and could not reproduce the error anymore.

There is no need to update mapjfx to JavaFX15 (as macOS and *nix are not hit by this bug). If you are on Windows10 you need to add the following dependency to your application:

<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-web</artifactId>
    <version>16-ea+1</version>
</dependency>

I tried 16-ea+1 and 15-ea+8, the version should be the same that is used for the whole application.