{"id":19385749,"url":"https://github.com/Kobee1203/weedow-searchy","last_synced_at":"2025-04-23T22:32:04.406Z","repository":{"id":38302653,"uuid":"260688695","full_name":"Kobee1203/weedow-searchy","owner":"Kobee1203","description":"Automatically exposes web services over HTTP to search for Entity-related data using a powerful query language","archived":false,"fork":false,"pushed_at":"2023-06-22T15:57:42.000Z","size":1324,"stargazers_count":22,"open_issues_count":12,"forks_count":3,"subscribers_count":1,"default_branch":"dev","last_synced_at":"2025-04-09T11:23:26.467Z","etag":null,"topics":["entity","java","kotlin","mongodb","query-language","querydsl","querydsl-jpa","querydsl-mongodb","repositories","repository","search","search-descriptors","spring","spring-boot","spring-boot-kotlin","spring-data","spring-data-jpa","spring-data-mongodb","spring-webmvc","springboot"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kobee1203.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-02T13:05:45.000Z","updated_at":"2023-04-26T10:34:09.000Z","dependencies_parsed_at":"2024-10-12T02:51:19.071Z","dependency_job_id":null,"html_url":"https://github.com/Kobee1203/weedow-searchy","commit_stats":{"total_commits":218,"total_committers":7,"mean_commits":"31.142857142857142","dds":"0.25688073394495414","last_synced_commit":"7a0e0bac533fc822ce4a2b58bc1f22eda53e5c72"},"previous_names":["kobee1203/spring-data-search"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kobee1203%2Fweedow-searchy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kobee1203%2Fweedow-searchy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kobee1203%2Fweedow-searchy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kobee1203%2Fweedow-searchy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kobee1203","download_url":"https://codeload.github.com/Kobee1203/weedow-searchy/tar.gz/refs/heads/dev","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250527134,"owners_count":21445311,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["entity","java","kotlin","mongodb","query-language","querydsl","querydsl-jpa","querydsl-mongodb","repositories","repository","search","search-descriptors","spring","spring-boot","spring-boot-kotlin","spring-data","spring-data-jpa","spring-data-mongodb","spring-webmvc","springboot"],"created_at":"2024-11-10T10:02:56.431Z","updated_at":"2025-04-23T22:32:04.384Z","avatar_url":"https://github.com/Kobee1203.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Weedow Searchy\n\n\u003cp align=\"center\"\u003e\n    \u003ca href=\"./docs/images/logos/logo.png\"\u003e\n        \u003cimg src=\"./docs/images/logos/logo.png\" width=\"200\" alt=\"Searchy Logo\" /\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\n## About\nSearchy is a Spring-based library that allows to automatically expose endpoints in order to search for data related to Entities, whatever the database used.\n\nSearchy provides an advanced search engine that does not require the creation of Repositories with custom methods needed to search on different fields of Entities.\n\nWe can search on any field, combine multiple criteria to refine the search, and even search on nested fields.\n\nIt supports the following data access layers:\n* [JPA](./jpa)\n* [MongoDB](./mongodb)\n\n![Query GIF](./docs/images/query.gif)\n\n## Why use Searchy?\n[Spring Data Rest](https://spring.io/projects/spring-data-rest) builds on top of the Spring Data repositories and automatically exports those as REST resources.\n\n* Each time we need to search with different criteria, we will have to add new methods (findByFirstName, findByFirstAndLastName, ...).\n* If we need to make more complex queries or handle specific fetch joins, we use the `@Query` annotation which takes as attribute a String representing the query to be executed.\nThis String is written in the language supported by the data access layer (JPQL, SQL, Mongo JSON ...).\n\nHere is an JPA example:\n```java\n@RepositoryRestResource\npublic interface PersonRepository extends Repository\u003cPerson, Long\u003e {\n    List\u003cPerson\u003e findAll();\n    List\u003cPerson\u003e findByLastName(@Param(\"name\") String name);\n    \n    @Query(\"SELECT p FROM Person \" +\n           \"LEFT JOIN FETCH p.addressEntities a \" +\n           \"WHERE p.lastName='Doe' AND a.city='Paris'\")\n    List\u003cPerson\u003e findPersonsWithAddresses();\n}\n```\n\nWe realize that we cannot be exhaustive in order to search for Person entities whatever the search criteria: a single field, nested fields, multiple fields, AND/OR conjunctions...\\\nWe also realize that each time we use `@Query`, we add a dependency to the data access layer since we write the query string in the language of the database we are querying. This can sometimes make it tedious to migrate to another type of database.\\\nAll this requires adding more code, releasing new versions ...\n\nSearchy allows to easily expose an endpoint for an Entity and thus be able to search on any fields of this entity, combine several criteria and even search on fields belonging to sub-entities.\n\nLet's say you manage Persons associated with Addresses, Vehicles and a Job.\\\nYou want to allow customers to search for them, regardless of the search criteria:\n* Search for Persons whose first name is \"John\" or \"Jane\"\n* Search for Persons whose company where they work is \"Acme\", and own a car or a motorbike \n* Search for Persons who live in London\n\nSearchy allows you to perform all these searches with a minimum configuration, without the need of a custom `Repository`.\\\nIf you want to do other different searches, you do not need to add code to do that.\nThe library provides a query language that allows to create queries on any field of the entity and sub-entities, and agnostic queries regarding the database used. \n\n## Build\n![GitHub repo size](https://img.shields.io/github/repo-size/Kobee1203/weedow-searchy)\n![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/Kobee1203/weedow-searchy)\n\n[![Build](https://img.shields.io/github/workflow/status/Kobee1203/weedow-searchy/Build%20and%20Analyze)](https://github.com/Kobee1203/weedow-searchy/actions?query=workflow%3A%22Build+and+Analyze%22)\n[![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/Kobee1203/weedow-searchy)]()\n\n[![Code Coverage](https://img.shields.io/sonar/coverage/weedow-searchy?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=weedow-searchy)\n[![Sonar Quality Gate](https://img.shields.io/sonar/quality_gate/weedow-searchy?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=weedow-searchy)\n[![Sonar Tech Debt](https://img.shields.io/sonar/tech_debt/weedow-searchy?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=weedow-searchy)\n[![Sonar Violations](https://img.shields.io/sonar/violations/weedow-searchy?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=weedow-searchy)\n\n### Built with:\n* [Kotlin](https://kotlinlang.org/)\n* [Spring Boot](https://spring.io/projects/spring-boot)\n* [Querydsl](http://www.querydsl.com/)  \n* [ANTLR](https://www.antlr.org/)\n* [Maven](https://maven.apache.org/)\n\n## Getting Started\n\n### Prerequisites\n* JDK 11 or more.\n* Spring Boot\n\n### Installation\n[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/Kobee1203/weedow-searchy?include_prereleases)](https://github.com/Kobee1203/weedow-searchy/releases)\n[![Downloads](https://img.shields.io/github/downloads/Kobee1203/weedow-searchy/total)](https://github.com/Kobee1203/weedow-searchy/releases)\n[![Maven Central](https://img.shields.io/maven-central/v/com.weedow/weedow-searchy-core)](https://search.maven.org/search?q=g:com.weedow%20AND%20a:weedow-searchy-*)\n\n* You can download the [latest release](https://github.com/Kobee1203/weedow-searchy/releases).\n* If you have a [Maven](https://maven.apache.org/) project, you can add the following dependency in your `pom.xml` file:\n  * JPA:\n  ```xml\n  \u003cdependency\u003e\n      \u003cgroupId\u003ecom.weedow\u003c/groupId\u003e\n      \u003cartifactId\u003eweedow-searchy-jpa\u003c/artifactId\u003e\n      \u003cversion\u003e0.1.0\u003c/version\u003e\n  \u003c/dependency\u003e\n  ```\n  * MongoDB:\n  ```xml\n  \u003cdependency\u003e\n      \u003cgroupId\u003ecom.weedow\u003c/groupId\u003e\n      \u003cartifactId\u003eweedow-searchy-mongodb\u003c/artifactId\u003e\n      \u003cversion\u003e0.1.0\u003c/version\u003e\n  \u003c/dependency\u003e\n  ```\n* If you have a [Gradle](https://gradle.org/) project, you can add the following dependency in your `build.gradle` file:\n  * JPA:  \n  ```groovy\n  implementation \"com.weedow:weedow-searchy-jpa:0.1.0\"\n  ```\n  * MongoDB:\n  ```groovy\n  implementation \"com.weedow:weedow-searchy-mongodb:0.1.0\"\n  ```\n\n### Getting Started in 5 minutes\n\n* Go to https://start.spring.io/\n* Generate a new Java project `sample-app-java` with the following dependencies:\n    * Spring Web\n    * Spring Data JPA\n    * H2 Database\n    ![start.spring.io](./docs/images/start.spring.io.png)\n* Update the generated project by adding the dependency of Searchy:\n    * For [Maven](https://maven.apache.org/) project, add the dependency in the `pom.xml` file: \n    ```xml\n    \u003cdependency\u003e\n      \u003cgroupId\u003ecom.weedow\u003c/groupId\u003e\n      \u003cartifactId\u003eweedow-searchy-jpa\u003c/artifactId\u003e\n      \u003cversion\u003e0.1.0\u003c/version\u003e\n    \u003c/dependency\u003e\n    ```\n    * For [Gradle](https://gradle.org/) project, add the dependency in the `build.gradle` file:\n    ```groovy\n    implementation \"com.weedow:weedow-searchy-jpa:0.1.0\"\n    ```\n* Create a new file `Person.java` to add a new JPA Entity `Person` with the following content:\n    ```java\n    import javax.persistence.*;\n    import java.time.LocalDateTime;\n    import java.util.Set;\n    \n    @Entity\n    public class Person {\n    \n        @Id\n        @GeneratedValue\n        private Long id;\n    \n        @Column(nullable = false)\n        private String firstName;\n    \n        @Column(nullable = false)\n        private String lastName;\n    \n        @Column(unique = true, length = 100)\n        private String email;\n    \n        @Column\n        private LocalDateTime birthday;\n    \n        @Column\n        private Double height;\n    \n        @Column\n        private Double weight;\n    \n        @ElementCollection(fetch = FetchType.EAGER)\n        private Set\u003cString\u003e nickNames;\n    \n        @ElementCollection\n        @CollectionTable(name = \"person_phone_numbers\", joinColumns = {@JoinColumn(name = \"person_id\")})\n        @Column(name = \"phone_number\")\n        private Set\u003cString\u003e phoneNumbers;\n    \n        public Long getId() {\n            return id;\n        }\n    \n        public Person setId(Long id) {\n            this.id = id;\n            return this;\n        }\n    \n        public String getFirstName() {\n            return firstName;\n        }\n    \n        public String getLastName() {\n            return lastName;\n        }\n    \n        public String getEmail() {\n            return email;\n        }\n    \n        public LocalDateTime getBirthday() {\n            return birthday;\n        }\n    \n        public Double getHeight() {\n            return height;\n        }\n    \n        public Double getWeight() {\n            return weight;\n        }\n    \n        public Set\u003cString\u003e getNickNames() {\n            return nickNames;\n        }\n    \n        public Person setNickNames(Set\u003cString\u003e nickNames) {\n            this.nickNames = nickNames;\n            return this;\n        }\n    \n        public Set\u003cString\u003e getPhoneNumbers() {\n            return phoneNumbers;\n        }\n    \n        public Person setPhoneNumbers(Set\u003cString\u003e phoneNumbers) {\n            this.phoneNumbers = phoneNumbers;\n            return this;\n        }\n    \n        public boolean equals(Object object) {\n            if (this == object) {\n                return true;\n            }\n            if (object == null || getClass() != object.getClass()) {\n                return false;\n            }\n            if (!super.equals(object)) {\n                return false;\n            }\n    \n            Person person = (Person) object;\n    \n            if (!firstName.equals(person.firstName)) {\n                return false;\n            }\n            if (!lastName.equals(person.lastName)) {\n                return false;\n            }\n    \n            return true;\n        }\n    \n        public int hashCode() {\n            int result = super.hashCode();\n            result = 31 * result + firstName.hashCode();\n            result = 31 * result + lastName.hashCode();\n            return result;\n        }\n    }\n    ```\n* Add the following Configuration class to add a new `SearchyDescriptor`:\n    ```java\n    import com.example.sampleappjava.entity.Person;\n    import com.weedow.searchy.config.SearchyConfigurer;\n    import com.weedow.searchy.descriptor.SearchyDescriptor;\n    import com.weedow.searchy.descriptor.SearchyDescriptorBuilder;\n    import com.weedow.searchy.descriptor.SearchyDescriptorRegistry;\n    import org.springframework.context.annotation.Configuration;\n    \n    @Configuration\n    public class SampleAppJavaConfiguration implements SearchyConfigurer {\n    \n        @Override\n        public void addSearchyDescriptors(SearchyDescriptorRegistry registry) {\n            registry.addSearchyDescriptor(personSearchyDescriptor());\n        }\n    \n        private SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor() {\n            return new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class).build();\n        }\n    }\n    ```\n* Create a new file `data.sql` in `/src/main/resources`, and add the following content:\n  ```sql\n  INSERT INTO PERSON (id, first_name, last_name, email, birthday, height, weight)\n      VALUES (1, 'John', 'Doe', 'john.doe@acme.com', '1981-03-12 10:36:00', 174.0, 70.5);\n  INSERT INTO PERSON (id, first_name, last_name, email, birthday, height, weight)\n      VALUES (2, 'Jane', 'Doe', 'jane.doe@acme.com', '1981-11-26 12:30:00', 165.0, 68.0);\n  \n  INSERT INTO PERSON_PHONE_NUMBERS (person_id, phone_number) VALUES (1, '+33612345678');\n  INSERT INTO PERSON_PHONE_NUMBERS (person_id, phone_number) VALUES (2, '+33687654321');\n  \n  INSERT INTO PERSON_NICK_NAMES (person_id, nick_names) VALUES (1, 'Johnny');\n  INSERT INTO PERSON_NICK_NAMES (person_id, nick_names) VALUES (1, 'Joe');\n  ```\n* Run the application:\n    * For Maven Project: `./mvnw spring-boot:run`\n    * For Gradle Project: `./gradlew bootRun`\n    * From your IDE: Run the Main Class `com.example.sampleappjava.SampleAppJavaApplication`\n* Open your browser and go to the URL `http://localhost:8080/search/person`\n![find-all-persons](./docs/images/find-all-persons.png)\n* You can filter the results by adding query parameters representing the Entity fields:\\\n  Here is an example where the results are filtered by the first name:\n![find-person-by-firstname](./docs/images/find-person-by-firstname.png)\n\n## Usage\n\nThe examples in this section are based on the following entity model:\n\nThe `Person.java` Entity has relationships with the `Address.java` Entity, the `Job.java` Entity and the `Vehicle.java` Entity. Here are the entities:\n```java\n@Entity\npublic class Person {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false)\n    private String firstName;\n\n    @Column(nullable = false)\n    private String lastName;\n\n    @Column\n    private LocalDateTime birthday;\n\n    @Column\n    private Double height;\n\n    @Column\n    private Double weight;\n\n    @ElementCollection(fetch = FetchType.EAGER)\n    private Set\u003cString\u003e nickNames;\n\n    @ElementCollection\n    @CollectionTable(name = \"person_phone_numbers\", joinColumns = {@JoinColumn(name = \"person_id\")})\n    @Column(name = \"phone_number\")\n    private Set\u003cString\u003e phoneNumbers;\n\n    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})\n    @JoinTable(name = \"person_address\", joinColumns = {JoinColumn(name = \"personId\")}, inverseJoinColumns = {JoinColumn(name = \"addressId\")})\n    @JsonIgnoreProperties(\"persons\")\n    private Set\u003cAddress\u003e addressEntities;\n\n    @OneToOne(mappedBy = \"person\", cascade = CascadeType.ALL, orphanRemoval = true)\n    private Job jobEntity;\n\n    @OneToMany(mappedBy = \"person\", cascade = CascadeType.ALL, orphanRemoval = true)\n    private Set\u003cVehicle\u003e vehicles;\n\n    @ElementCollection\n    @CollectionTable(\n            name = \"characteristic_mapping\",\n            joinColumns = {@JoinColumn(name = \"person_id\", referencedColumnName = \"id\")})\n    @MapKeyColumn(name = \"characteristic_name\")\n    @Column(name = \"value\")\n    private Map\u003cString, String\u003e characteristics;\n\n    @ElementCollection\n    @CollectionTable(\n            name = \"person_tasks\",\n            joinColumns = {@JoinColumn(name = \"person_id\", referencedColumnName = \"id\")})\n    @MapKeyJoinColumn(name = \"task_id\")\n    @Column(name = \"task_date\")\n    private Map\u003cTask, LocalDateTime\u003e tasks;\n\n\n  // Getters/Setters\n}\n\n@Entity\npublic class Address {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false)\n    private String street;\n\n    @Column(nullable = false)\n    private String city;\n\n    @ManyToOne(optional = false)\n    private String zipCode;\n\n    @Enumerated(EnumType.STRING)\n    private CountryCode country;\n\n    @ManyToMany(mappedBy = \"addressEntities\")\n    @JsonIgnoreProperties(\"addressEntities\")\n    private Set\u003cPerson\u003e persons;\n\n    // Getters/Setters\n}\n\n\n@Entity\npublic class Job {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false)\n    private String title;\n    \n    @Column(nullable = false)\n    private String company;\n    \n    @Column(nullable = false)\n    private Integer salary;\n    \n    @Column(nullable = false)\n    private OffsetDateTime hireDate;\n    \n    @OneToOne(optional = false)\n    private Person person;\n\n    // Getters/Setters\n\n}\n\n@Entity\npublic class Vehicle {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false)\n    @Enumerated(EnumType.STRING)\n    private VehicleType vehicleType;\n\n    @Column(nullable = false)\n    private String brand;\n\n    @Column(nullable = false)\n    private String model;\n\n    @ManyToOne(optional = false)\n    private String person;\n\n    @OneToMany(cascade = {CascadeType.ALL})\n    @JoinTable(name = \"feature_mapping\",\n            joinColumns = {@JoinColumn(name = \"vehicle_id\", referencedColumnName = \"id\")},\n            inverseJoinColumns = {@JoinColumn(name = \"feature_id\", referencedColumnName = \"id\")})\n    @MapKey(name = \"name\") // Feature name\n    private Map\u003cString, Feature\u003e features;\n\n    // Getters/Setters\n}\n\n@Entity\npublic class Task {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n      \n    @Column(nullable = false)\n    private String name;\n  \n    @Column\n    private String description;\n  \n    // Getters/Setters\n  \n    // Override toString() method to get a JSON representation used by the HTTP response\n    @Override\n    public String toString() {\n      return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);\n    }\n}\n\n@Entity\npublic class Feature {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false, unique = true)\n    private String name;\n\n    @Column(nullable = false)\n    private String description;\n\n    @ElementCollection\n    @CollectionTable(\n            name = \"metadata_mapping\",\n            joinColumns = {@JoinColumn(referencedColumnName = \"id\", name = \"feature_id\")}\n    )\n    @MapKeyColumn(name = \"metadata_name\")\n    @Column(name = \"value\")\n    private Map\u003cString, String\u003e metadata;\n\n    // Getters/Setters\n}\n\npublic enum VehicleType {\n    CAR, MOTORBIKE, SCOOTER, VAN, TRUCK\n}\n```\n\n### Query\nThere are two methods to search for entities: [Standard Query](#standard-query) and [Advanced Query](#advanced-query)\n\nAll search methods use the same operation described below.\n\nTo search for all the Database rows of an entity type, you must concatenate the [Base Path](#changing-the-base-path) (default is `/search`) and the [Search Descriptor ID](#changing-the-base-path) (default is the Entity name in lower case).\\\n_Example: Search all `Person` Entities_\\\n`/search/person`\n\nTo search on nested fields, you must concatenate the deep fields separated by the dot '`.`'.\\\n_Example: The `Person` Entity contains a property of the `Address` Entity that is named `addressEntities`, and we search for Persons who live in 'Paris'_:\\\n/search/person?`addressEntities.city=Paris`\n\nTo search on fields with a `Map` type, you have to use the special keys `key` or `value` to query the keys or values respectively.\\\n_Example 1: The `Person` Entity contains a property of type `Map` that is named `characteristics`, and we search for Persons who have 'blue eyes':_\\\n/search/person?`characteristics.key=eyes\u0026characteristics.value=blue`\n\n_Example 2: The `Person` Entity contains a property of type `Map` that is named `tasks`, and we search for Persons who have a task whose name is 'shopping':_\\\n/search/person?`tasks.key.name=shopping`\n\n_Example 3: The `Vehicle` Entity contains a property of type `Map` that is named `features`, and we search for Persons who have vehicles with the 'GPS' feature:_\\\n/search/person?`vehicles.features.value.name=gps`\n\n### Standard Query\nYou can search for entities by adding query parameters representing entity fields to the search URL.\n\nTo search on nested fields, you must concatenate the deep fields separated by the dot '`.`'.\\\n_Example: The `Person` Entity contains a property of the `Address` Entity that is named `addressEntities`, and we search for Persons who live in 'Paris'_:\\\n/search?`addressEntities.city=Paris`\n\nTo search on fields with a `Map` type, you have to use the special keys `key` or `value` to query the keys or values respectively.\n_Example: The `Person` Entity contains a property of type `Map` that is named `characteristics`, and we search for Persons who have 'blue eyes':_\\\n/search?`characteristics.key=eyes\u0026characteristics.value=blue`\n\nThis mode is limited to the use of the `AND` operator between each field criteria.\\\nEach field criteria is limited to the use of the `EQUALS` operator and the `IN` operator.\n\n| What you want to query                                                                                                   | Example                                                                                |\n| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| Persons with the firstName is _'John'_                                                                                   | `/search?firstName=John`                                                               |\n| Persons with the firstName is _'John'_ or _'Jane'_\u003cbr/\u003e_This will be result from a query with an `IN` operator_          | `/search?firstName=John\u0026firstName=Jane`                                                |\n| Persons with the firstName is _'John'_ and lastName is _'Doe'_                                                           | `/search?firstName=John\u0026lastName=Doe`                                                  |\n| Persons whose the vehicle brand is _'Renault'_                                                                           | `/search/person?vehicles.brand=Renault`                                                |\n| Persons whose the vehicle brand is _'Renault'_ and the job company is _'Acme'_                                           | `/search/person?vehicles.brand=Renault\u0026jobEntity.company=Acme`                         |\n| Persons with the firstName is _'John'_ or _'Jane'_, and the vehicle brand is _'Renault'_ and the job company is _'Acme'_ | `/search?firstName=John\u0026firstName=Jane\u0026vehicles.brand=Renault\u0026jobEntity.company=Acme`  |\n| Persons who have a vehicle with _'GPS'_\u003cbr/\u003e_This will be result from a query on the `feature` field of type `Map`_      | `/search?vehicles.features.value.name=gps`                                             |\n| Persons with the birthday is _'null'_                                                                                    | `/search?birthday=null`                                                                |\n| Persons who don't have jobs                                                                                              | `/search?jobEntity=null`                                                               |\n| Persons who have a vehicle without defined feature in database                                                           | `/search?vehicles.features=null`                                                       |\n| Persons who were born at current date                                                                                    | `/search?birthday=CURRENT_DATE`                                                        |\n| Persons who were born at current time                                                                                    | `/search?birthday=CURRENT_TIME`                                                        |\n| Persons who were born at current datetime                                                                                | `/search?birthday=CURRENT_DATE_TIME`                                                   |\n\n### Advanced Query\nYou can search for entities by using the query string `query`.\n\n`query` supports a powerful query language to perform advanced searches for the Entities.\n\nYou can combine logical operators and operators to create complex queries.\n\nThe value types are the following:\n* `String`: must be surrounded by single quotes or double quotes.\\\n  _Example: `firstName='John'`, `firstName=\"John\"`_\n* `Number`: could be an integer or a decimal number.\\\n  _Example: `height=174`, `height=175.2`_\n* `Boolean`: could be true or false and is case-insensitive\n  _Example: `active=true`, `active=FALSE`_\n* `Date`: must be surrounded by single quotes or double quotes, or use special keywords `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_DATE_TIME`.\\\n  _Example: `birthday='1981-03-12T10:36:00'`, `job.hireDate='2019-09-01T09:00:00Z'`, `birthday=CURRENT_DATE_TIME`_\n\n\u003e Note: The examples use the unencoded 'query' parameter, where **firstName = 'John'** is encoded as **firstName+%3d+%27John%27**.\n\u003e\n\u003e Remember to manage this encoding when making requests from your code.\n\n1. \u003ca name=\"equals-operator\"\u003e\u003c/a\u003e Equals operator `=`\n\n| What you want to query                                              | Example                                                                  |\n| ------------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| Persons with the first name 'John'                                  | /person?query=`firstName='John'`                                         |\n| Persons with the birthday equals to the given `LocalDateTime`       | /person?query=`birthday='1981-03-12T10:36:00'`                           |\n| Persons with the hire date equals to the given `OffsetDateTime`     | /person?query=`job.hireDate='2019-09-01T09:00:00Z'`                      |\n| Persons with the birthday equals to the current `LocalDateTime`     | /person?query=`birthday=CURRENT_DATE_TIME`                               |\n| Persons who own a car (VehicleType is an Enum)                      | /person?query=`vehicle.vehicleType='CAR'`                                |\n| Persons who are 1,74 m tall                                         | /person?query=`height=174`                                               |\n| Persons who are actively employed                                   | /person?query=`job.active=true`                                          |\n| Persons who have brown hair\u003cbr/\u003e_It uses a field of `Map` type_     | /person?query=`characteristics.key=hair AND characteristics.value=brown` |\n\n2. \u003ca name=\"not-equals-operator\"\u003e\u003c/a\u003e Not Equals operator `!=`\n\n| What you want to query                                              | Example                                               |\n| ------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons who are not named 'John'                                    | /person?query=`firstName!='John'`                     |\n| Persons with the birthday not equals to the given `LocalDateTime`   | /person?query=`birthday!='1981-03-12T10:36:00'`       |\n| Persons with the birthday not equals to the current `LocalDateTime` | /person?query=`birthday=CURRENT_DATE_TIME`            |\n| Persons with the hire date not equals to the given `OffsetDateTime` | /person?query=`job.hireDate!='2019-09-01T09:00:00Z'`  |\n| Persons who don't own a car (VehicleType is an Enum)                | /person?query=`vehicle.vehicleType!='CAR'`            |\n| Persons who are not 1,74 m tall                                     | /person?query=`height!=174`                           |\n| Persons who are not actively employed                               | /person?query=`job.active!=true`                      |\n\n3. \u003ca name=\"less-than-operator\"\u003e\u003c/a\u003e Less than operator `\u003c`\n\n| What you want to query                                              | Example                                               |\n| ------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons who were born before the given `LocalDateTime`              | /person?query=`birthday\u003c'1981-03-12T10:36:00'`        |\n| Persons who are hired before the given `OffsetDateTime`             | /person?query=`job.hireDate\u003c'2019-09-01T09:00:00Z'`   |\n| Persons who are hired before current datetime                       | /person?query=`job.hireDate\u003cCURRENT_DATE_TIME`        |\n| Persons who are smaller than 1,74 m                                 | /person?query=`height\u003c174`                            |\n\n4. \u003ca name=\"less-than-or-equals-operator\"\u003e\u003c/a\u003e Less than or equals operator `\u003c=`\n\n| What you want to query                                              | Example                                               |\n| ------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons who were born before or on the given `LocalDateTime`        | /person?query=`birthday\u003c='1981-03-12T10:36:00'`       |\n| Persons who are hired before or on the given `OffsetDateTime`       | /person?query=`job.hireDate\u003c='2019-09-01T09:00:00Z'`  |\n| Persons who are hired before or on current datetime                 | /person?query=`job.hireDate\u003c=CURRENT_DATE_TIME`       |\n| Persons who are smaller than or equal to 1,74 m                     | /person?query=`height\u003c=174`                           |\n\n5. \u003ca name=\"greater-than-operator\"\u003e\u003c/a\u003e Greater than operator `\u003e`\n\n| What you want to query                                              | Example                                               |\n| ------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons who were born after the given `LocalDateTime`               | /person?query=`birthday\u003e'1981-03-12T10:36:00'`        |\n| Persons who are hired after the given `OffsetDateTime`              | /person?query=`job.hireDate\u003e'2019-09-01T09:00:00Z'`   |\n| Persons who are hired after the current datetime                    | /person?query=`job.hireDate\u003eCURRENT_DATE_TIME`        |\n| Persons who are taller than 1,74 m                                  | /person?query=`height\u003e174`                            |\n\n6. \u003ca name=\"greater-than-or-equals-operator\"\u003e\u003c/a\u003e Greater than or equals operator `\u003e=`\n\n| What you want to query                                              | Example                                               |\n| ------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons who were born after or on the given `LocalDateTime`         | /person?query=`birthday\u003e='1981-03-12T10:36:00'`       |\n| Persons who are hired after or on the given `OffsetDateTime`        | /person?query=`job.hireDate\u003e='2019-09-01T09:00:00Z'`  |\n| Persons who are hired after or on current datetime                  | /person?query=`job.hireDate\u003e=CURRENT_DATE_TIME`       |\n| Persons who are taller than or equal to 1,74 m                      | /person?query=`height\u003e=174`                           |\n\n7. \u003ca name=\"matches-operator\"\u003e\u003c/a\u003e Matches operator `MATCHES`\n\n_Use the wildcard character `*` to match any string with zero or more characters._\n\n| What you want to query                                              | Example                                               |\n| ------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons with the first name starting with 'Jo'                      | /person?query=`firstName MATCHES 'Jo*'`               |\n| Persons with the first name ending with 'hn'                        | /person?query=`firstName MATCHES '*hn'`               |\n| Persons with the first name containing 'oh'                         | /person?query=`firstName MATCHES '*oh*'`              |\n| Persons with the first name that does not start with 'Jo'           | /person?query=`firstName NOT MATCHES 'Jo*'`           |\n\n8. \u003ca name=\"imatches-operator\"\u003e\u003c/a\u003e Case-insensitive matches operator `IMATCHES`\n\nThis operator has the same behaviour as ['MATCHES'](#matches-operator) except that it is not case-sensitive.\n\n_Use the wildcard character `*` to match any string with zero or more characters._\n\n| What you want to query                                                             | Example                                               |\n| ---------------------------------------------------------------------------------- | ----------------------------------------------------- |\n| Persons with the first name starting with 'JO', ignoring case-sensitive            | /person?query=`firstName IMATCHES 'JO*'`              |\n| Persons with the first name ending with 'HN', ignoring case-sensitive              | /person?query=`firstName IMATCHES '*HN'`              |\n| Persons with the first name containing 'OH', ignoring case-sensitive               | /person?query=`firstName IMATCHES '*OH*'`             |\n| Persons with the first name that does not start with 'JO', ignoring case-sensitive | /person?query=`firstName NOT IMATCHES 'JO*'`          |\n\n9. \u003ca name=\"in-operator\"\u003e\u003c/a\u003e `ÌN` operator\n\n| What you want to query                                                  | Example                                                              |\n| ----------------------------------------------------------------------- | -------------------------------------------------------------------- |\n| Persons who are named 'John' or 'Jane'                                  | /person?query=`firstName IN ('John', 'Jane')`                        |\n| Persons with the height is one the given values                         | /person?query=`height IN (168, 174, 185)`                            |\n| Persons who own one of the given vehicle types (VehicleType is an Enum) | /person?query=`vehicle.vehicleType IN ('CAR', 'MOTORBIKE', 'TRUCK')` |\n| Persons who are not named 'John' or 'Jane'                              | /person?query=`firstName NOT IN ('John', 'Jane')`                    |\n\n10. \u003ca name=\"date-comparison\"\u003e\u003c/a\u003e `Date` comparison\n\nThe fields representing a date, time, or datetime can be compared with a string having a valid format according to the type of the field.\n\n| What you want to query                                          | Example                                             |\n| --------------------------------------------------------------- | --------------------------------------------------- |\n| Persons with the birthday equals to the given `LocalDateTime`   | /person?query=`birthday='1981-03-12T10:36:00'`      |\n| Persons with the hire date equals to the given `OffsetDateTime` | /person?query=`job.hireDate='2019-09-01T09:00:00Z'` |\n\nAlso, the fields representing a date, time, or datetime can be compared with the following keywords:\n* `CURRENT_DATE`: keyword representing the current date\n* `CURRENT_TIME`: keyword representing the current time\n* `CURRENT_DATE_TIME`: keyword representing the current date and time\n\n| What you want to query                                      | Example                                     |\n| ----------------------------------------------------------- | ------------------------------------------- |\n| Persons with the birthday is at current datetime            | /person?query=`birthday=CURRENT_DATE_TIME`  |\n| Persons with the birthday is not at current datetime        | /person?query=`birthday!=CURRENT_DATE_TIME` |\n| Persons with the birthday is after current datetime         | /person?query=`birthday\u003eCURRENT_DATE_TIME`  |\n| Persons with the birthday is after or at current datetime   | /person?query=`birthday\u003e=CURRENT_DATE_TIME` |\n| Persons with the birthday is before current datetime        | /person?query=`birthday\u003cCURRENT_DATE_TIME`  |\n| Persons with the birthday is before or at current datetime  | /person?query=`birthday\u003c=CURRENT_DATE_TIME` |\n\n11. \u003ca name=\"null-comparison\"\u003e\u003c/a\u003e `NULL` comparison\n\n| What you want to query                                  | Example                                                                 |\n| ------------------------------------------------------- | ----------------------------------------------------------------------- |\n| Persons with the birthday is 'null'                     | /person?query=`birthday=null`\u003cbr/\u003e/person?query=`birthday IS NULL`      |\n| Persons with the birthday is not 'null'                 | /person?query=`birthday!=null`\u003cbr/\u003e/person?query=`birthday IS NOT NULL` |\n| Persons who don't have jobs                             | /person?query=`job=null`\u003cbr/\u003e/person?query=`job IS NULL`                |\n| Persons who have jobs                                   | /person?query=`job!=null`\u003cbr/\u003e/person?query=`job IS NOT NULL`           |\n\n12. \u003ca name=\"and-logical-operator\"\u003e\u003c/a\u003e `AND` logical operator\n\n| What you want to query                                                                                                                                         | Example                                                                                                                                                                  |\n| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| Persons with the first name 'John', with blue eyes, with a height greater than 1,60 m, the birthday is the given `LocalDateTime` and who are actively employed | /person?query=`firstName='John' AND characteristics.key='eyes' AND characteristics.value='blue' AND height \u003e 160 and birthday='1981-03-12T10:36:00' AND job.active=true` |\n\n13. \u003ca name=\"or-logical-operator\"\u003e\u003c/a\u003e `OR` logical operator\n\n| What you want to query                                                  | Example                                                                      |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| Persons who are named 'John' or 'Jane'                                  | /person?query=`firstName='John' OR firstName='Jane'`                         |\n| Persons with the height is 1,68 m, 1,74 m or 1,85 m                     | /person?query=`height=168 OR height=174 OR height=185`                       |\n| Persons who own a car or a motorbike (VehicleType is an Enum)           | /person?query=`vehicle.vehicleType='CAR' OR vehicle.vehicleType='MOTORBIKE'` |\n\n14. \u003ca name=\"not-operator\"\u003e\u003c/a\u003e `NOT` operator\n\n| What you want to query                                                  | Example                                                                      |\n| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| Persons with the first name is not 'John' or 'Jane'                     | /person?query=`NOT (firstName='John' OR firstName='Jane'`)                   |\n| Persons who don't live in France and is not actively employed           | /person?query=`NOT (address.country='FR' AND job.active=true`                |\n| Persons who don't own a car (VehicleType is an Enum)                    | /person?query=`NOT vehicle.vehicleType='CAR'`                                |\n\n15. \u003ca name=\"parentheses\"\u003e\u003c/a\u003e Parentheses\n\nThe precedence of operators determines the order of evaluation of terms in an expression.\n\n[AND](#and-logical-operator) operator has precedence over the [OR](#or-logical-operator) operator.\n\nTo override this order and group terms explicitly, you can use parentheses.\n\n| What you want to query                                                           | Example                                                                                                                   |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |\n| Persons who are named 'John' or 'Jane', and own a car or a motorbike             | /person?query=`(firstName='John' OR firstName='Jane') AND (vehicle.vehicleType='CAR' OR vehicle.vehicleType='MOTORBIKE')` |\n\n16. Nested fields\n\nTo search on nested fields, you must concatenate the deep fields separated by the dot '`.`'.\\\n_Example: The `Person` Entity contains a property of the `Address` Entity that is named `addressEntities`, and we search for Persons who live in 'Paris'_:\\\n/search?`addressEntities.city='Paris'`\n\n| What you want to query                                                           | Example                                                          |\n| -------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| Persons who own a car                                                            | /person?query=`vehicle.vehicleType='CAR'`                        |\n| Persons who live in 'France' or in Italy                                         | /person?query=`address.country='FR' OR address.country='IT'`     |\n| Persons who work job company is `Acme` and are actively employed                 | /person?query=`job.company='Acme' AND job.active=true`           |\n\n## Features\n\n### Javadoc\n| Module       | Javadoc                                                                                                                                                 |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Core         | [![javadoc-core](https://javadoc.io/badge2/com.weedow/weedow-searchy-core/javadoc.svg)](https://javadoc.io/doc/com.weedow/weedow-searchy-core)          |\n| JPA          | [![javadoc-jpa](https://javadoc.io/badge2/com.weedow/weedow-searchy-jpa/javadoc.svg)](https://javadoc.io/doc/com.weedow/weedow-searchy-jpa)             |\n| MongoDB      | [![javadoc-mongodb](https://javadoc.io/badge2/com.weedow/weedow-searchy-mongodb/javadoc.svg)](https://javadoc.io/doc/com.weedow/weedow-searchy-mongodb) |\n\n### Search Descriptor\nThe Search Descriptors allow exposing automatically search endpoints for Entities.\\\nThe new endpoints are mapped to `/search/{searchyDescriptorId}` where `searchyDescriptorId` is the [ID](#search-descriptor-id) defined for the `SearchyDescriptor`.\n\n_Note: You can change the default base path `/search`. See [Changing the Base Path](#changing-the-base-path)._ \n\nThe easiest way to create a Search Descriptor is to use the `com.weedow.searchy.descriptor.SearchyDescriptorBuilder` which provides every options available to configure a `SearchyDescriptor`.\n\n#### Configure a new Search Descriptor\nYou have to add the `SearchyDescriptor`s to the Searchy Configuration to expose the Entity endpoint:\n* Implement the `com.weedow.searchy.config.SearchyConfigurer` interface and override the `addSearchyDescriptors` method:\n    ```java\n    @Configuration\n    public class SearchyDescriptorConfiguration implements SearchyConfigurer {\n    \n        @Override\n        public void addSearchyDescriptors(SearchyDescriptorRegistry registry) {\n            SearchyDescriptor searchyDescriptor = new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class).build();\n            registry.addSearchyDescriptor(searchyDescriptor);\n        }\n    }\n    ```\n\n* Another solution is to add a new `@Bean`. This solution is useful when you want to create a `SearchyDescriptor` which depends on other Beans:\n    ```java\n    @Configuration\n    public class SearchyDescriptorConfiguration {\n        @Bean\n        SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor(PersonRepository personRepository) {\n            return new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class)\n                       .specificationExecutor(personRepository)\n                       .build();\n        }\n    }\n    ```\n\n#### Search Descriptor options\n##### Search Descriptor ID\nThis is the Search Descriptor Identifier. Each identifier must be unique.\\\nSearchy uses this identifier in the search endpoint URL which is mapped to `/search/{searchyDescriptorId}`: `searchyDescriptorId` is the Search Descriptor Identifier.\n\nIf the Search Descriptor ID is not set, Searchy uses the Entity Name in lowercase as Search Descriptor ID.\\\n_If the Entity is `Person.java`, the Search Descriptor ID is `person`_\n\nExample with a custom Search Descriptor ID:\n```java\n@Configuration\npublic class SearchyDescriptorConfiguration implements SearchyConfigurer {\n\n    @Override\n    public void addSearchyDescriptors(SearchyDescriptorRegistry registry) {\n        registry.addSearchyDescriptor(personSearchyDescriptor());\n    }\n    \n    SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor() {\n        return new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class)\n                        .id(\"people\")\n                        .build();\n    }\n}\n```\n\n##### Entity Class\nThis is the Class of the Entity to be searched.\\\nWhen you use `com.weedow.searchy.descriptor.SearchyDescriptorBuilder`, the Entity Class is added during instantiation:\n* In a Java project: `new SearchyDescriptorBuilder\u003c\u003e(Person.class)`\n* In a Kotlin project: `SearchyDescriptorBuilder.builder\u003cPerson\u003e().build()` or `SearchyDescriptorBuilder(Address::class.java).build()`\n\n##### DTO Mapper\nThis option allows to convert the Entity to a specific DTO before returning the HTTP response.\\\nThis can be useful when you don't want to return all data of the entity.\n\nTo do this, you need to create a class which implements the `com.weedow.searchy.dto.DtoMapper` interface:\n```java\npublic class PersonDtoMapper implements DtoMapper\u003cPerson, PersonDto\u003e {\n    @Override\n    public PersonDto map(Person source) {\n        return PersonDto.Builder()\n                .firstName(source.firstName)\n                .lastName(source.lastName)\n                .email(source.email)\n                .nickNames(source.nickNames)\n                .phoneNumbers(source.phoneNumbers)\n                .build();\n    }\n}\n```\nThen you add this DTO Mapper to the `SearchyDescriptor`:\n```java\n@Configuration\npublic class SearchyDescriptorConfiguration implements SearchyConfigurer {\n\n    @Override\n    public void addSearchyDescriptors(SearchyDescriptorRegistry registry) {\n        registry.addSearchyDescriptor(personSearchyDescriptor());\n    }\n    \n    SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor() {\n        return new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class)\n                        .dtoMapper(new PersonDtoMapper())\n                        .build();\n    }\n}\n```\n\nIf this option is not set, a default DTO Mapper is used. This default DTO Mapper may be different according to the database implementation used.\nThe `Core` provides a default DTO Mapper `com.weedow.searchy.dto.DefaultDtoMapper` that does not convert the entity, and the HTTP response returns it directly.\n\n##### Validators\nSearchy provides a validation service to validate the Field Expressions.\n\nA `Field Expression` is a representation of a query parameter which evaluates an Entity field.\\\n_Example: `/search/person?job.company=Acme` : the query parameter `job.company=Acme` is converted to a Field Expression where the `company` field from the `Job` Entity must be equals to `Acme`._\n\n**Note:** The validation service does not validate the Type of the query parameter values.\nThis is already supported when Searchy converts the query parameter values from String to the correct type expected by the related field.\n(See [Converters](#converters))\n\nThe validators is used to validate whether:\n* A value matches a specific Regular Expression,\n* A number is between a minimum and maximum value\n* There is at least one query parameter in the request\n* A query parameter for a specific field is present or absent in the request\n* ...\n\nTo do this, you need to create a new class which implements the `com.weedow.searchy.validation.SearchyValidator` interface:\n```java\npublic class EmailValidator implements SearchyValidator {\n\n    private static final String EMAIL_REGEX = \"(?:[a-z0-9!#$%\u0026'*+/=?^_`{|}~-]+(?:\\\\.[a-z0-9!#$%\u0026'*+/=?^_`{|}~-]+)*|\\\"(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21\\\\x23-\\\\x5b\\\\x5d-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])*\\\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21-\\\\x5a\\\\x53-\\\\x7f]|\\\\\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])+)\\\\])\";\n\n    @Override\n    public void validate(Collection\u003c? extends FieldExpression\u003e fieldExpressions, SearchyErrors errors) {\n        fieldExpressions\n                .stream()\n                .filter(fieldExpression -\u003e \"email\".equals(fieldExpression.getFieldInfo().getField().getName()))\n                .forEach(fieldExpression -\u003e {\n                    final Object value = fieldExpression.getValue();\n                    if (value instanceof String) {\n                        if (!value.toString().matches(EMAIL_REGEX)) {\n                            errors.reject(\"email\", \"Invalid email value\");\n                        }\n                    }\n                });\n    }\n}\n```\nThen you need to add the validators to a [Search Descriptor](#search-descriptor):\n```java\n@Configuration\npublic class SearchyDescriptorConfiguration implements SearchyConfigurer {\n\n    @Override\n    public void addSearchyDescriptors(SearchyDescriptorRegistry registry) {\n        registry.addSearchyDescriptor(personSearchyDescriptor());\n    }\n    \n    SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor() {\n        return new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class)\n                        .validators(new NotEmptyValidator(), new EmailValidator(\"email\"))\n                        .build();\n    }\n}\n```\n\nSearchy provides the following `SearchyValidator` implementations:\n* `com.weedow.searchy.validation.validator.NotEmptyValidator`: Checks if there is at least one field expression.\n* `com.weedow.searchy.validation.validator.NotNullValidator`: Checks if the field expression value is not `null`.\n* `com.weedow.searchy.validation.validator.RequiredValidator`: Checks if all specified required `fieldPaths` are present. The validator iterates over the field expressions and compare the related `fieldPath` with the required `fieldPaths`.\n* `com.weedow.searchy.validation.validator.PatternValidator`: Checks if the field expression value matches the specified `pattern`.\n* `com.weedow.searchy.validation.validator.UrlValidator`: Checks if the field expression value matches a valid `URL`.\n* `com.weedow.searchy.validation.validator.EmailValidator`: Checks if the field expression value matches the email format.\n* `com.weedow.searchy.validation.validator.MaxValidator`: Checks if the field expression value is less or equals to the specified `maxValue`.\n* `com.weedow.searchy.validation.validator.MinValidator`: Checks if the field expression value is greater or equals to the specified `minValue`.\n* `com.weedow.searchy.validation.validator.RangeValidator`: Checks if the field expression value is between the specified `minValue` and `maxValue`.\n\n##### Specification Executor\nSearchy defines the class `com.weedow.searchy.query.specification.Specification`, inspired by [Spring Data JPA Specifications](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications).\nIt is used to aggregate all expressions in query parameters and query the Entities in the Database.\n\nSearchy defines the following interface to allow execution of `Specifications`:\n```java\npublic interface SpecificationExecutor\u003cT\u003e {\n    //...//\n    List\u003cT\u003e findAll(Specification\u003cT\u003e spec);\n    //...//\n}\n```\n\nThis interface is already implemented for each Database implementation (JPA, MongoDB ...).\nThis is normally sufficient for the majority of needs, but you can set this option with your own `SpecificationExecutor` implementation if you need a specific implementation.\n\nTo ease integration with Spring Repositories, there is the `com.weedow.searchy.repository.SearchyBaseRepository` interface.\n\n* Extending an annotated `@Repository` interface with the `SearchyBaseRepository` interface\n  ```java\n  @Repository\n  public interface PersonRepository extends SearchyBaseRepository {\n  }\n  ```\n* Set the `SearchyDescriptor` with the previous interface\n  ```java\n  @Configuration\n  public class SearchyDescriptorConfiguration {\n      @Bean\n      SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor(PersonRepository personRepository) {\n          return new SearchyDescriptorBuilder\u003cPerson\u003e(Person.class)\n                     .specificationExecutor(personRepository)\n                     .build();\n      }\n  }\n  ```\n* If the annotated @Repository interface has a specific implementation, implement the `List\u003cT\u003e findAll(Specification\u003cT\u003e specification)` method\n  ```java\n  public class PersonRepositoryImpl implements PersonRepository {\n    public List\u003cPerson\u003e findAll(Specification\u003cPerson\u003e specification) {\n      // ...\n    }\n  }\n  ```\n* If the annotated @Repository interface does not have a specific implementation, it means that it uses a default Spring implementation that will not support the `List\u003cT\u003e findAll(Specification\u003cT\u003e specification)` method.\n  It is therefore necessary to override this behavior by specifying another [FactoryBean](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/FactoryBean.html) class to be used for each repository instance.\n  For example, if it's a JPA Repository, you have to specify that the `repositoryFactoryBeanClass` is `com.weedow.searchy.jpa.repository.JpaSearchyRepositoryFactoryBean`:\n  ```java\n  @SpringBootApplication\n  @EnableJpaRepositories(value = {\"com.sample.repository\"}, repositoryFactoryBeanClass = JpaSearchyRepositoryFactoryBean.class)\n  public class SampleAppJavaApplication {\n \n     public static void main(String[] args) {\n         SpringApplication.run(SampleAppJavaApplication.class, args);\n     }\n \n  }\n  ```\n\n##### Entity Join Handlers\nIt is sometimes useful to optimize the number of SQL queries by specifying the data that you want to fetch during the first SQL query with the criteria.\n\nThis option allows adding `EntityJoinHandler` implementations to specify join types for any fields having _join annotation_.\n\nYou can add several `EntityJoinHandler` implementations. The first implementation that matches from the `support(...)` method will be used to specify the join type for the given field.\n\n```java\n@Configuration\npublic class SearchyDescriptorConfiguration {\n  @Bean\n  public SearchyDescriptor\u003cPerson\u003e personSearchyDescriptor(SearchyContext searchyContext) {\n    return new SearchyDescriptorBuilder\u003c\u003e(Person.class)\n            .entityJoinHandlers(new MyEntityJoinHandler(), new JpaFetchingEagerEntityJoinHandler(searchyContext))\n            .build();\n  }\n}\n```\n\nSearchy provides the following default implementations:\n* `FetchingAllEntityJoinHandler`: This implementation allows to query an entity by fetching all data related to this entity, i.e. all fields related to another Entity recursively.\\\n  _Example:_\\\n  _`A` has a relationship with `B` and `B` has a relationship with `C`._\\\n  _When we search for `A`, we retrieve `A` with data from `B` and `C`._\n* `JpaFetchingEagerEntityJoinHandler`: This specific JPA implementation allows to query an entity by fetching all fields having a Join Annotation with the Fetch type defined as `EAGER`.\\\n  _Example:_\\\n  _`A` has a relationship with `B` using `@OneToMany` annotation and `FetchType.EAGER`, and `A` has a relationship with `C` using `@OneToMany` annotation and `FetchType.LAZY`._\\\n  _When we search for `A`, we retrieve `A` with just data from `B`, but not `C`._\n\nYou can create your own implementation to fetch the additional data you require.\\\nJust implement the `com.weedow.searchy.join.handler.EntityJoinHandler` interface:\n```java\n/**\n * Fetch all fields annotated with @ElementCollection\n */\npublic class MyEntityJoinHandler implements EntityJoinHandler {\n\n  @Override\n  public boolean supports(PropertyInfos propertyInfos) {\n    return propertyInfos.getAnnotations().stream().anyMatch(annotation -\u003e annotation instanceof ElementCollection);\n  }\n\n  @Override\n  public JoinInfo handle(PropertyInfos propertyInfos) {\n    return new JoinInfo(JoinType.LEFTJOIN, true);\n  }\n}\n```\n\nIf this option is not set, the default Searchy behavior is to create `LEFT JOIN` if needed.\n\n\n_For more details about joins handling, please read the following explanations._\n\n---\n\nIf the result contains the root Entity with the related Entities, there will be multiple SQL queries:\n* One SQL query with your criteria\n* One query by joined Entity to retrieve the related data\n\nLet's say you want to have an endpoint to search any `Person`s.\\\nThe endpoint response returns the `Person`s found with their `Job`s and `Vehicle`s.\n\nThe `Person.java` Entity has relationships with the `Job.java` Entity and the `Vehicle.java` Entity. Here are the entities :\n\n```java\n@Entity\npublic class Person {\n    //...//\n    @OneToOne(mappedBy = \"person\", cascade = CascadeType.ALL, orphanRemoval = true)\n    private Job jobEntity;\n\n    @OneToMany(mappedBy = \"person\", cascade = CascadeType.ALL, orphanRemoval = true)\n    private Set\u003cVehicle\u003e vehicles;\n    //...//\n}\n\n@Entity\npublic class Vehicle {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false)\n    private String brand;\n\n    @Column(nullable = false)\n    private String model;\n\n    @ManyToOne(optional = false)\n    private String person;\n\n    // Getters/Setters\n}\n\n@Entity\npublic class Job {\n\n    @Id\n    @GeneratedValue\n    private Long id;\n\n    @Column(nullable = false)\n    private String title;\n    \n    @Column(nullable = false)\n    private String company;\n    \n    @Column(nullable = false)\n    private Integer salary;\n    \n    @Column(nullable = false)\n    private OffsetDateTime hireDate;\n    \n    @OneToOne(optional = false)\n    private Person person;\n\n    // Getters/Setters\n}\n```\n\nYou want to search for the persons who's the vehicle brand is _Renault_, and the job company is _Acme_:\n```\n/search/person?vehicles.brand=Renault\u0026jobEntity.company=Acme\n```\nIf you have any persons who match your query, you should get an HTTP response that looks like the following:\n```json\n[\n    {\n        \"firstName\": \"John\",\n        \"lastName\": \"Doe\",\n        \"email\": \"john.doe@acme.com\",\n        \"birthday\": \"1981-03-12T10:36:00\",\n        \"jobEntity\": {\n            \"title\": \"Lab Technician\",\n            \"company\": \"Acme\",\n            \"salary\": 50000,\n            \"hireDate\": \"2019-09-01T11:00:00+02:00\",\n            \"id\": 1,\n            \"createdOn\": \"2020-03-12T11:36:00+01:00\",\n            \"updatedOn\": \"2020-04-17T14:00:00+02:00\"\n        },\n        \"vehicles\": [\n            {\n                \"vehicleType\": \"CAR\",\n                \"brand\": \"Renault\",\n                \"model\": \"Clio\",\n                \"id\": 1,\n                \"createdOn\": \"2020-03-12T11:36:00+01:00\",\n                \"updatedOn\": \"2020-04-17T14:00:00+02:00\"\n            }\n        ],\n        \"id\": 1,\n        \"createdOn\": \"2020-03-12T11:36:00+01:00\",\n        \"updatedOn\": \"2020-04-17T14:00:00+02:00\"\n    }\n]\n```\nTo get this result, there were several SQL queries:\n- The SQL query with your criteria:\n    ```sql\n    select\n        distinct p.id,\n        p.created_on,\n        p.updated_on,\n        p.birthday,\n        p.email,\n        p.first_name,\n        p.height,\n        p.last_name,\n        p.weight\n    from person p \n    left outer join vehicle v on p.id=v.person_id \n    left outer join job j on p.id=j.person_id \n    where\n        j.company='Acme' \n        and v.brand='Renault';\n    ```\n- The following SQL query executed for each Person returned by the first SQL query:\n    ```sql\n    select j.*, p.*\n    from job j \n    inner join person p on j.person_id=p.id \n    where\n        j.person_id={PERSON_ID};\n    ```\n  These SQL queries occur because the field `jobEntity` present on the `Person` Entity is annotated with the `@OneToOne` annotation whose the default fetch type is `EAGER`.\n  - The following SQL query executed for each Person returned by the first SQL query:\n    ```sql\n    select v.*\n    from vehicle v \n    where\n        v.person_id={PERSON_ID}\n    ```\n    The `vehicles` field present on the `Person` Entity is annotated with the `@OneToMany` annotation (default fetch type is `LAZY`).\\\n    However, these SQL queries occur because vehicle information must be returned in the HTTP response.\n\nIt is therefore sometimes useful to optimize the number of SQL queries by specifying the data that you want to fetch during the first SQL query with the criteria.\n\nTo do this, you can use the [EntityJoinHandlers](#entity-join-handlers) to specify the join type for each Entity field having a relationship with another Entity.\n\n### Aliases\nSearchy provides an alias management to replace any field name with another name in queries.\n\nThis can be useful when the name of a field is too technical or too long or simply to allow several possible names.\n\nLet's say you manage Persons with following Entity:\n```java\n@Entity\npublic class Person {\n    //...//\n    @OneToOne(mappedBy = \"person\", cascade = CascadeType.ALL, orphanRemoval = true)\n    private Job jobEntity;\n    //...//\n}\n```\n\nYou want to search for persons with their job company is `Acme`. The request looks like: `/search/person?jobEntity.company=Acme`\\\nHowever you don't want use the `jobEntity` string but `job` into the URL: `/search/person?job.company=Acme`\n\nTo do this, you need to create a `AliasResolver` implementation:\n```java\n/**\n * Create an alias for all fields ending with 'Entity'.\n **/\nclass MyAliasResolver implements AliasResolver {\n    private static final String SUFFIX = \"Entity\";\n\n    @Override\n    public Boolean supports(Class\u003c?\u003e entityClass, Field field) {\n        return field.name.endsWith(SUFFIX);\n    }\n\n    @Override\n    List\u003cString\u003e resolve(Class\u003c?\u003e entityClass, Field field) {\n        return Arrays.asList(StringUtils.substringBefore(fieldName, SUFFIX));        \n    }\n}\n```\nYou must then register it in the Alias Resolver Registry:\n```java\n@Configuration\npublic class SampleAppJavaConfiguration implements SearchyConfigurer {\n\n    @Override\n    public void addAliasResolvers(AliasResolverRegistry registry) {\n        registry.addAliasResolver(new MyAliasResolver());\n    }\n}\n```\n\nAnother solution is to declare your AliasResolver as `@Bean`. This solution is useful when you want to create a AliasResolver which depends on other Beans.\n\nBy default, Searchy registers the following Alias Resolvers:\n* `SearchyDefaultAliasConfigurerAutoConfiguration`: Creates an alias for all fields ending with the suffixes `Entity` or `Entities`.\n\n### Converters\nSearchy converts the query parameter values from String to the correct type expected by the related field.\n\nSearchy uses the [Spring Converter Service](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#core-convert). \\\nSpring Converter Service provides several converter implementations in the `core.convert.support` package.\n\nTo create your own converter, implement the `Converter` interface and parameterize S as the `java.lang.String` type and T as the type you are converting to.\n```java\npublic class MyConverter implements Converter\u003cString, MyObject\u003e {\n\n    @Override\n    public MyObject convert(String s) {\n        return MyObject.of(s);\n    }\n\n} \n```\nYou must then register it in the Converter registry:\n```java\n@Configuration\npublic class SampleAppJavaConfiguration implements SearchyConfigurer {\n\n    @Override\n    public void addConverters(ConverterRegistry registry) {\n        registry.addConverter(new MyConverter());\n    }\n}\n```\n\nAnother solution is to declare your Converter as `@Bean`. This solution is useful when you want to create a Converter which depends on other Beans.\n\n### Changing the Base Path\n\nBy default, Searchy defines the Base Path as `/search` and add the Search Descriptor ID. Example: `/search/person`\n\nYou can do change the Base Path by setting a single property in application.properties, as follows:\n\n````properties\nweedow.searchy.base-path=/api\n````\n\nThis changes the Base Path to `/api`. Example: `/api/person`\n\n---\n\n## Issues\n[![Issues](https://img.shields.io/github/issues/Kobee1203/weedow-searchy)](https://github.com/Kobee1203/weedow-searchy/issues)\n\n## Contributing\n\nContributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.\n\n1. Fork the Project\n2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the Branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n## How to make a new Release\nPage to describe the process of creating a new release: [Make a new release](./docs/release.md)\n\n## How to add a new module\nPage to describe the process of adding a new module: [Add a new module](./docs/new-module.md)\n\n## Contact\n\nNicolas Dos Santos - [@Kobee1203](https://twitter.com/Kobee1203)\n\nProject Link: \u003chttps://github.com/Kobee1203/weedow-searchy\u003e\n\n## Social Networks\n[![Tweets](https://img.shields.io/twitter/url?style=social\u0026url=https%3A%2F%2Fgithub.com%2FKobee1203%2Fweedow-searchy)]()\n\n[![GitHub forks](https://img.shields.io/github/forks/Kobee1203/weedow-searchy?style=social)]()\n[![GitHub stars](https://img.shields.io/github/stars/Kobee1203/weedow-searchy?style=social)]()\n[![GitHub watchers](https://img.shields.io/github/watchers/Kobee1203/weedow-searchy?style=social)]()\n\n[![Join the chat at https://gitter.im/weedow-searchy/community](https://badges.gitter.im/weedow-searchy/community.svg)](https://gitter.im/weedow-searchy/community?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n\n## License\n\n[![MIT License](https://img.shields.io/github/license/Kobee1203/weedow-searchy)](https://github.com/Kobee1203/weedow-searchy/blob/master/LICENSE.txt) \\\n_Copyright (c) 2020 Nicolas Dos Santos and other contributors_\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKobee1203%2Fweedow-searchy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FKobee1203%2Fweedow-searchy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKobee1203%2Fweedow-searchy/lists"}