Bibliothèque pour construire des recherches dynamiques avancées dans les applications Spring Boot.
Support JPA (SQL), MongoDB (NoSQL) et Elasticsearch (Search Engine)
- ✅ Filtrage dynamique - 13 opérateurs (EQUALS, CONTAINS, BETWEEN, etc.)
- ✅ Recherche full-text - Recherche rapide sur tous les champs STRING searchable
- ✅ Tri dynamique - ASC/DESC sur n'importe quel champ
- ✅ Pagination - Page et taille configurables
- ✅ Détection automatique des types - Plus besoin de spécifier le fieldType
- ✅ Support multi-formats de dates - ISO, formats européens, américains
- ✅ Architecture hexagonale - Découplage domaine/infrastructure
- ✅ Support JPA, MongoDB et Elasticsearch - Même API pour tous
- ✅ Spring Boot Auto-configuration - Configuration automatique
- ✅ Type-safe - Utilisation d'enums pour les opérateurs
dynamic-search/
├── dynamic-search-domain # Couche domaine (logique métier)
│ ├── model/ # Modèles du domaine
│ ├── port/in/ # Ports entrants (use cases)
│ ├── port/out/ # Ports sortants (repositories)
│ └── service/ # Services du domaine
├── dynamic-search-spring-boot-starter # Starter Spring Boot commun
│ ├── gateway/ # SearchGateway (façade)
│ ├── mapper/ # Mappers REST ↔ Domaine
│ ├── request/ # DTOs de requête
│ └── response/ # DTOs de réponse
├── dynamic-search-spring-boot-jpa-starter # Implémentation JPA
│ ├── adapter/ # Adaptateurs JPA
│ ├── specification/ # Specifications JPA
│ └── factory/ # Factory pour JPA
├── dynamic-search-spring-boot-mongo-starter # Implémentation MongoDB
│ ├── adapter/ # Adaptateurs MongoDB
│ ├── criteria/ # Criteria MongoDB
│ └── factory/ # Factory pour MongoDB
├── dynamic-search-spring-boot-elasticsearch-starter # Implémentation Elasticsearch
│ ├── adapter/ # Adaptateurs Elasticsearch
│ ├── criteria/ # Criteria Elasticsearch
│ └── factory/ # Factory pour Elasticsearch
└── dynamic-search-spring-boot-jpa-example # Exemple JPA + React Frontend
├── src/main/java # Backend Spring Boot
└── src/main/resources/webapp # Frontend React + AG Grid
L'exemple JPA inclut une interface web moderne avec :
- ✅ AG Grid Infinite Row Model - Pagination/tri/filtrage côté serveur
- ✅ Génération dynamique des colonnes - Basée sur les métadonnées de l'API
- ✅ Filtres natifs AG Grid - Mappés automatiquement vers l'API
- Texte : contains, equals, startsWith, endsWith, blank, notBlank
- Nombre : equals, lessThan, greaterThan, between
- Date : equals, lessThan, greaterThan, between (formatage automatique YYYY-MM-DD)
- Booléen : equals via selection
- ✅ Formatage automatique des dates - AG Grid → API (suppression timestamp)
- ✅ Single JAR deployment - Frontend compilé dans le JAR Spring Boot
👉 Voir README.md pour la documentation complète du frontend.
# Méthode 1 : Tout-en-un (Maven build + frontend)
cd dynamic-search-spring-boot-jpa-example
mvn clean package
java -jar target/dynamic-search-spring-boot-jpa-example-0.0.1-SNAPSHOT.jar
# Accès : http://localhost:8080
# Méthode 2 : Développement avec hot reload
# Terminal 1 - Backend
mvn spring-boot:run
# Terminal 2 - Frontend
cd src/main/resources/webapp
npm run dev
# Accès : http://localhost:5173Pour JPA (SQL) :
<dependency>
<groupId>io.github.cnadjim</groupId>
<artifactId>dynamic-search-spring-boot-jpa-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>Pour MongoDB :
<dependency>
<groupId>io.github.cnadjim</groupId>
<artifactId>dynamic-search-spring-boot-mongo-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>Pour Elasticsearch :
<dependency>
<groupId>io.github.cnadjim</groupId>
<artifactId>dynamic-search-spring-boot-elasticsearch-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>JPA :
@Entity
@Table(name = "operating_systems")
@EnableSearchable // Active la recherche dynamique
public class OperatingSystem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String version;
private LocalDateTime releaseDate;
private Integer marketShare;
private Boolean isOpenSource;
// Getters/Setters...
}MongoDB :
@Document(collection = "operating_systems")
@EnableSearchable // Active la recherche dynamique
public class OperatingSystem {
@Id
private String id;
private String name;
private String version;
private LocalDateTime releaseDate;
private Integer marketShare;
private Boolean isOpenSource;
// Getters/Setters...
}Elasticsearch :
@Document(indexName = "operating_systems")
@EnableSearchable // Active la recherche dynamique
public class OperatingSystem {
@Id
private String id;
private String name;
private String version;
private LocalDateTime releaseDate;
private Integer marketShare;
private Boolean isOpenSource;
// Getters/Setters...
}@RestController
@RequestMapping("/api/operating-systems")
@RequiredArgsConstructor
public class OperatingSystemController {
private final SearchGateway searchGateway;
@GetMapping("/filters")
public List<FilterDescriptorResponse> getAvailableFilters() {
return searchGateway.getAvailableFilters(OperatingSystem.class);
}
@PostMapping("/search")
public SearchResult<OperatingSystem> search(@RequestBody SearchRequest request) {
return searchGateway.search(request, OperatingSystem.class);
}
}Requête HTTP POST /api/operating-systems/search :
{
"filters": [
{
"key": "name",
"operator": "contains",
"value": "Windows"
},
{
"key": "releaseDate",
"operator": "between",
"value": "2015-01-01",
"valueTo": "2020-12-31"
},
{
"key": "marketShare",
"operator": "greaterThan",
"value": "10"
},
{
"key": "isOpenSource",
"operator": "equals",
"value": "false"
}
],
"sorts": [
{
"key": "releaseDate",
"direction": "desc"
}
],
"page": {
"number": 0,
"size": 10
}
}Réponse :
{
"content": [
{
"id": 1,
"name": "Windows 10",
"version": "21H2",
"releaseDate": "2015-07-29T00:00:00",
"marketShare": 69,
"isOpenSource": false
}
],
"totalElements": 1,
"totalPages": 1,
"currentPage": 0,
"pageSize": 10
}| Opérateur | Description | Types supportés | Exemple |
|---|---|---|---|
equals |
Égalité stricte | Tous | "value": "Windows" |
notEquals |
Différent de | Tous | "value": "Linux" |
contains |
Contient (case insensitive) | STRING | "value": "Win" |
notContains |
Ne contient pas (case insensitive) | STRING | "value": "Mac" |
startsWith |
Commence par (case insensitive) | STRING | "value": "Win" |
endsWith |
Se termine par (case insensitive) | STRING | "value": "10" |
in |
Valeur dans une liste | Tous | "values": ["Windows", "Linux"] |
notIn |
Valeur pas dans une liste | Tous | "values": ["macOS", "iOS"] |
lessThan |
Inférieur à | NUMBER, DATE | "value": "100" |
greaterThan |
Supérieur à | NUMBER, DATE | "value": "50" |
between |
Entre deux valeurs (inclusif) | NUMBER, DATE | "value": "10", "valueTo": "100" |
blank |
Null ou vide | STRING | (pas de valeur) |
notBlank |
Non null et non vide | STRING | (pas de valeur) |
La bibliothèque supporte automatiquement plusieurs formats de dates :
- ISO 8601 :
2024-12-03T10:00:00ou2024-12-03 - Format SQL :
2024-12-03 10:00:00 - Format européen :
03-12-2024 10:00:00ou03/12/2024 10:00:00
Lorsque vous utilisez l'opérateur equals avec une date sans heure (format yyyy-MM-dd), la bibliothèque convertit automatiquement la recherche en BETWEEN pour matcher toute la journée :
Requête :
{
"filters": [
{
"key": "releaseDate",
"operator": "equals",
"value": "2019-11-05"
}
]
}Équivalent automatique :
{
"filters": [
{
"key": "releaseDate",
"operator": "between",
"value": "2019-11-05T00:00:00",
"valueTo": "2019-11-05T23:59:59.999999999"
}
]
}La bibliothèque supporte maintenant la recherche full-text pour effectuer des recherches rapides sur tous les champs STRING searchable d'une entité.
La recherche full-text :
- 🔍 Cherche automatiquement dans tous les champs de type STRING annotés ou auto-détectés
- 🔤 Utilise une recherche case-insensitive (CONTAINS)
- ➕ Se combine avec les filtres existants via un AND logique
- ⚡ Optimisée pour MongoDB avec index text (recommandé)
Requête HTTP POST /api/operating-systems/search :
{
"fullText": {
"query": "Windows"
},
"filters": [
{
"key": "isOpenSource",
"operator": "equals",
"value": "false"
}
],
"sorts": [
{
"key": "releaseDate",
"direction": "desc"
}
],
"page": {
"number": 0,
"size": 10
}
}Cette requête va :
- Chercher "Windows" dans tous les champs STRING (
name,version,kernel, etc.) - ET filtrer pour garder uniquement
isOpenSource = false - Trier par
releaseDatedécroissant
Pour optimiser les performances des recherches full-text sur MongoDB, il est fortement recommandé de créer des index text sur vos collections.
Option 1 : Index text sur tous les champs STRING (recommandé)
// MongoDB Shell
db.operating_systems.createIndex(
{
"name": "text",
"version": "text",
"kernel": "text"
},
{
name: "fulltext_search_idx",
weights: {
name: 10, // Plus de poids sur le nom
version: 5, // Poids moyen sur la version
kernel: 1 // Poids faible sur le kernel
},
default_language: "french" // ou "english", "none"
}
);Option 2 : Index text avec wildcard (tous les champs STRING automatiquement)
// MongoDB Shell - Index automatique sur tous les champs texte
db.operating_systems.createIndex(
{ "$**": "text" },
{
name: "fulltext_search_wildcard_idx",
default_language: "french"
}
);Vérifier les index existants :
db.operating_systems.getIndexes();Supprimer un index :
db.operating_systems.dropIndex("fulltext_search_idx");Note : Actuellement, l'implémentation utilise des REGEX MongoDB. Pour utiliser les index text natifs de MongoDB (
$textoperator), une évolution future est prévue.
Pour intégrer la recherche full-text avec AG Grid, ajoutez un champ de saisie personnalisé dans votre interface :
const [fullTextQuery, setFullTextQuery] = useState('');
const onGridReady = (params) => {
const datasource = {
getRows: async (params) => {
const searchRequest = {
filters: convertAGGridFiltersToAPI(params.filterModel),
sorts: convertAGGridSortsToAPI(params.sortModel),
fullText: fullTextQuery ? { query: fullTextQuery } : null,
page: {
number: Math.floor(params.startRow / pageSize),
size: pageSize
}
};
const response = await fetch('/api/operating-systems/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(searchRequest)
});
const data = await response.json();
params.successCallback(data.content, data.totalElements);
}
};
params.api.setGridOption('datasource', datasource);
};
// Dans votre JSX
<input
type="text"
placeholder="Recherche globale..."
value={fullTextQuery}
onChange={(e) => {
setFullTextQuery(e.target.value);
// Optionnel : debounce pour éviter trop de requêtes
gridApi.refreshInfiniteCache();
}}
/>Vous n'avez plus besoin de spécifier le fieldType dans vos requêtes ! La bibliothèque le déduit automatiquement depuis les métadonnées de l'entité.
Avant :
{
"key": "releaseDate",
"operator": "equals",
"fieldType": "date",
"value": "2019-11-05"
}Maintenant :
{
"key": "releaseDate",
"operator": "equals",
"value": "2019-11-05"
}La bibliothèque suit les principes de l'architecture hexagonale (Ports & Adapters) :
┌──────────────────────────────────────────────────────────────┐
│ APPLICATION │
│ (Controllers REST) │
└─────────────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ SEARCH GATEWAY │
│ (Façade pour simplifier l'API) │
└─────────────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ COUCHE DOMAINE │
│ ┌────────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Use Cases │ │ Models │ │ Services │ │
│ │ (Ports In) │ │ (Entities) │ │ (Business) │ │
│ └────────────────┘ └──────────────┘ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Repositories (Ports Out) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────┬────────────────────────────────────┘
│
┌────────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ JPA ADAPTER │ │ MONGODB ADAPTER │
│ - Specifications │ │ - Criteria Builder │
│ - JPA Repository │ │ - MongoTemplate │
└─────────────────────┘ └──────────────────────┘
- Découplage - Le domaine ne dépend pas de l'infrastructure
- Testabilité - Tests unitaires du domaine sans DB
- Flexibilité - Changement facile d'implémentation (JPA ↔ MongoDB)
- Évolutivité - Ajout facile de nouveaux adaptateurs (Elasticsearch, etc.)
{
"filters": [
{
"key": "name",
"operator": "contains",
"value": "Windows"
},
{
"key": "isOpenSource",
"operator": "equals",
"value": "false"
},
{
"key": "marketShare",
"operator": "greaterThan",
"value": "50"
}
]
}{
"filters": [
{
"key": "version",
"operator": "in",
"values": ["10", "11", "Server 2019"]
}
]
}{
"filters": [
{
"key": "marketShare",
"operator": "between",
"value": "10",
"valueTo": "90"
}
]
}{
"sorts": [
{
"key": "marketShare",
"direction": "desc"
},
{
"key": "name",
"direction": "asc"
}
]
}{
"page": {
"number": 2,
"size": 20
}
}Pour JPA :
# DataSource
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
# JPA/Hibernate
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=truePour MongoDB :
# MongoDB
spring.data.mongodb.uri=mongodb://localhost:27017/test
spring.data.mongodb.database=testVous pouvez activer l'initialisation automatique de données de test :
# Générer 1 million d'enregistrements au démarrage (pour tests de performance)
app.data.init.enabled=truecd dynamic-search
mvn clean test# Tests du domaine
mvn clean test -pl dynamic-search-domain
# Tests JPA
mvn clean test -pl dynamic-search-spring-boot-jpa-starter
# Tests MongoDB
mvn clean test -pl dynamic-search-spring-boot-mongo-starterLa bibliothèque a été testée avec 1 million d'enregistrements :
- Recherche simple : ~50-100ms
- Recherche avec filtres multiples : ~100-200ms
- Recherche avec tri : ~150-250ms
- Recherche avec pagination : ~50-100ms
Les performances dépendent de la configuration de votre base de données et des index définis.
Les contributions sont les bienvenues ! N'hésitez pas à :
- Fork le projet
- Créer une branche (
git checkout -b feature/AmazingFeature) - Commit vos changements (
git commit -m 'Add some AmazingFeature') - Push vers la branche (
git push origin feature/AmazingFeature) - Ouvrir une Pull Request
Ce projet est sous licence MIT - voir le fichier LICENSE pour plus de détails.
Pour toute question ou suggestion : [email protected]
⭐ N'oubliez pas de mettre une étoile si vous trouvez ce projet utile ! ⭐
