Skip to content

CNadjim/dynamic-search

Repository files navigation

Dynamic Search Library

Java Spring Boot License

Bibliothèque pour construire des recherches dynamiques avancées dans les applications Spring Boot.

Support JPA (SQL), MongoDB (NoSQL) et Elasticsearch (Search Engine)

🎯 Fonctionnalités

  • 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

📦 Structure des Modules

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

🖥️ Frontend React avec AG Grid

Frontend Screenshot

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.

Démarrage Rapide 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:5173

🚀 Quick Start

1. Ajouter la dépendance

Pour 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>

2. Annoter votre entité

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...
}

3. Créer un contrôleur

@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);
    }
}

4. Effectuer une recherche

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érateurs Disponibles

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)

📅 Gestion Intelligente des Dates

Formats supportés

La bibliothèque supporte automatiquement plusieurs formats de dates :

  • ISO 8601 : 2024-12-03T10:00:00 ou 2024-12-03
  • Format SQL : 2024-12-03 10:00:00
  • Format européen : 03-12-2024 10:00:00 ou 03/12/2024 10:00:00

Recherche par jour entier

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"
    }
  ]
}

🔎 Recherche Full-Text

La bibliothèque supporte maintenant la recherche full-text pour effectuer des recherches rapides sur tous les champs STRING searchable d'une entité.

Fonctionnement

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é)

Exemple d'utilisation

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 :

  1. Chercher "Windows" dans tous les champs STRING (name, version, kernel, etc.)
  2. ET filtrer pour garder uniquement isOpenSource = false
  3. Trier par releaseDate décroissant

Optimisation MongoDB - Index Text

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 ($text operator), une évolution future est prévue.

Intégration AG Grid

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();
  }}
/>

🎨 Détection Automatique des Types

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"
}

🏗️ Architecture Hexagonale

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     │
└─────────────────────┘        └──────────────────────┘

Avantages

  • 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.)

📚 Exemples Avancés

Recherche avec plusieurs filtres ET

{
  "filters": [
    {
      "key": "name",
      "operator": "contains",
      "value": "Windows"
    },
    {
      "key": "isOpenSource",
      "operator": "equals",
      "value": "false"
    },
    {
      "key": "marketShare",
      "operator": "greaterThan",
      "value": "50"
    }
  ]
}

Recherche avec IN

{
  "filters": [
    {
      "key": "version",
      "operator": "in",
      "values": ["10", "11", "Server 2019"]
    }
  ]
}

Recherche avec BETWEEN

{
  "filters": [
    {
      "key": "marketShare",
      "operator": "between",
      "value": "10",
      "valueTo": "90"
    }
  ]
}

Tri multiple

{
  "sorts": [
    {
      "key": "marketShare",
      "direction": "desc"
    },
    {
      "key": "name",
      "direction": "asc"
    }
  ]
}

Pagination

{
  "page": {
    "number": 2,
    "size": 20
  }
}

🛠️ Configuration

Application Properties

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=true

Pour MongoDB :

# MongoDB
spring.data.mongodb.uri=mongodb://localhost:27017/test
spring.data.mongodb.database=test

Initialisation de données (optionnel)

Vous 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=true

🧪 Tests

Lancer tous les tests

cd dynamic-search
mvn clean test

Lancer les tests d'un module spécifique

# 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-starter

📊 Performances

La 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.

🤝 Contribution

Les contributions sont les bienvenues ! N'hésitez pas à :

  1. Fork le projet
  2. Créer une branche (git checkout -b feature/AmazingFeature)
  3. Commit vos changements (git commit -m 'Add some AmazingFeature')
  4. Push vers la branche (git push origin feature/AmazingFeature)
  5. Ouvrir une Pull Request

📝 License

Ce projet est sous licence MIT - voir le fichier LICENSE pour plus de détails.

📧 Contact

Pour toute question ou suggestion : [email protected]


N'oubliez pas de mettre une étoile si vous trouvez ce projet utile !

About

Bibliothèque pour construire des recherches dynamiques dans les applications Spring Boot

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •