Le module dynamic_search fournit un moteur de recherche full-text extensible supportant plusieurs backends (Django ORM, Whoosh, Elasticsearch). Il gère l'indexation automatique, le filtrage ACL, les recherches sauvegardées, et expose une API REST complète.
Emplacement : mayan/apps/dynamic_search/
| Paramètre | Valeur |
|---|---|
| Classe | mayan.apps.dynamic_search.backends.django.DjangoSearchBackend |
| Réindexation | Non |
| Dépendances | Aucune |
Utilise le moteur ORM de Django. Adapté aux petites installations. Pas de réindexation manuelle, pas de dépendances externes. Performances limitées sur de grands jeux de données.
| Paramètre | Valeur |
|---|---|
| Classe | mayan.apps.dynamic_search.backends.whoosh.WhooshSearchBackend |
| Réindexation | Oui |
| Dépendances | whoosh |
Moteur de recherche full-text basé sur des fichiers. Rapide, sans serveur externe. Recommandé pour les déploiements de taille moyenne.
Arguments disponibles (SEARCH_BACKEND_ARGUMENTS) :
MAYAN_SEARCH_BACKEND_ARGUMENTS:
index_path: /var/media/whoosh_index # Répertoire de l'index (défaut: MEDIA_ROOT/whoosh_index)
writer_limitmb: 128 # RAM allouée à l'écriture (Mo)
writer_multisegment: false # Écriture multi-segments
writer_procs: 1 # Processus parallèles pour l'écriture
| Paramètre | Valeur |
|---|---|
| Classe | mayan.apps.dynamic_search.backends.elasticsearch.ElasticsearchSearchBackend |
| Réindexation | Oui |
| Dépendances | Cluster Elasticsearch externe |
Adapté aux déploiements à grande échelle. Supporte le Point-in-Time API pour la pagination.
Arguments disponibles :
MAYAN_SEARCH_BACKEND_ARGUMENTS:
client_kwargs:
hosts:
- http://elasticsearch:9200
basic_auth:
- elastic
- changeme
verify_certs: false
indices_namespace: mayan
search_page_size: 40
point_in_time_keep_alive: "1m"
Toutes les variables sont préfixées MAYAN_ dans Docker.
| Variable | Défaut | Description |
|---|---|---|
SEARCH_BACKEND |
...WhooshSearchBackend |
Classe du backend |
SEARCH_BACKEND_ARGUMENTS |
{} |
Arguments d'initialisation du backend |
SEARCH_DEFAULT_OPERATOR |
AND |
Opérateur par défaut (AND / OR) |
SEARCH_DISABLE_SIMPLE_SEARCH |
False |
Masquer la recherche simple dans l'interface |
SEARCH_INDEXING_CHUNK_SIZE |
25 |
Taille des lots lors de la réindexation |
SEARCH_MATCH_ALL_DEFAULT_VALUE |
True |
Valeur par défaut de "Correspondre à tous les critères" |
SEARCH_QUERY_RESULTS_LIMIT |
100000 |
Limite d'IDs retournés par le backend |
SEARCH_QUERY_RESULTS_LIMIT_ERROR |
True |
Lever une erreur si la limite est dépassée |
SEARCH_RESULTS_LIMIT |
1000 |
Nombre maximum de résultats affichés |
SEARCH_SAVED_RESULTSETS_PER_USER_LIMIT |
10 |
Recherches sauvegardées max par utilisateur |
SEARCH_SAVED_RESULTSET_RESULTS_LIMIT |
1000 |
Résultats max stockés par recherche sauvegardée |
SEARCH_SAVED_RESULTSET_TIME_TO_LIVE |
300 |
TTL initial des resultsets (secondes) |
SEARCH_SAVED_RESULTSET_TIME_TO_LIVE_INCREMENT |
60 |
Extension du TTL à chaque accès (secondes) |
SEARCH_STORE_RESULTS_DEFAULT_VALUE |
False |
Valeur par défaut de "Sauvegarder les résultats" |
SEARCH_MODEL_FIELD_DISABLE |
{} |
Désactiver des champs de recherche spécifiques |
MAYAN_SEARCH_MODEL_FIELD_DISABLE:
documents.document:
- files__checksum
- files__source_metadata__key
tags.tag:
- color
La recherche simple utilise un seul champ q qui recherche dans tous les champs indexés.
q=contrat urgent
La recherche avancée permet de cibler des champs spécifiques.
Opérateurs logiques :
| Opérateur | Comportement | Paramètre |
|---|---|---|
AND |
Tous les critères doivent correspondre | _match_all=yes |
OR |
Au moins un critère doit correspondre | _match_all=no |
Syntaxe des valeurs :
| Syntaxe | Comportement |
|---|---|
mot |
Correspondance standard (tokenisée) |
"phrase exacte" |
Correspondance de phrase exacte |
`valeur brute` |
Valeur brute sans transformation |
Exemples de requêtes avancées :
label=contrat&document_type__label=Facture&_match_all=yes
tags__label=urgent&description="accord commercial"
files__checksum=abc123
| Champ | Description |
|---|---|
label |
Libellé du document |
description |
Description |
document_type__label |
Type de document |
files__checksum |
Somme de contrôle du fichier |
files__filename |
Nom du fichier |
files__source_metadata__key |
Clé de métadonnée source |
files__source_metadata__value |
Valeur de métadonnée source |
tags__label |
Libellé des étiquettes |
tags__color |
Couleur des étiquettes |
versions__pages__content |
Contenu OCR des pages |
metadata_value |
Valeur de métadonnée (via extension) |
Chaque type d'objet recherchable est représenté par un SearchModel. Ils sont définis dans les fichiers search.py de chaque app et chargés automatiquement au démarrage.
SearchModel(
app_label='documents', # App Django
model_name='Document', # Nom du modèle
default=True, # Modèle par défaut de recherche
label=_('Document'), # Libellé affiché
list_mode=LIST_MODE_CHOICE_ITEM, # Mode d'affichage (list ou item)
manager_name='valid', # Manager utilisé (valid = non-trashed)
permission=permission_document_view, # Permission requise → filtre ACL
serializer_path='...', # Sérialiseur DRF pour l'API
)
Important : Si
permission=None, aucun filtrage ACL n'est appliqué — tous les objets correspondants sont retournés quelle que soit les permissions de l'utilisateur. Voir la section Sécurité.
| Modèle | App | Permission |
|---|---|---|
documents.document |
documents | permission_document_view |
documents.documentfile |
documents | permission_document_file_view |
documents.documentversion |
documents | permission_document_version_view |
cabinets.cabinet |
cabinets | permission_cabinet_view |
tags.tag |
tags | permission_tag_view |
metadata.metadatatype |
metadata | permission_metadata_type_view |
document_indexing.indextemplate |
document_indexing | permission_index_template_view |
document_states.workflowtemplate |
document_states | permission_workflow_template_view |
sources.source |
sources | permission_sources_view |
Les apps peuvent ajouter des champs à des SearchModels existants :
# Dans tags/search.py — étend le SearchModel document
search_model_document.add_model_field(
field='tags__label', label=_('Tag label')
)
search_model_document.add_model_field(
field='tags__color', label=_('Tag color')
)
Requête utilisateur (formulaire ou API)
↓
SearchInterpreter.init(query, search_model)
→ Sélectionne SearchInterpreterAdvanced ou Simple
↓
SearchInterpreter.do_resolve(search_backend)
→ Construit un arbre ScopedQuery
↓
Backend._search(search_field, query_type, value)
→ DjangoSearchBackend : ORM filter()
→ WhooshSearchBackend : requête Whoosh
→ ElasticsearchSearchBackend : DSL ES
↓
Retourne : ensemble d'IDs correspondants
↓
SearchBackend.search()
→ queryset.filter(pk__in=id_list)
→ Filtrage ACL (si permission définie)
→ Limite des résultats
→ Sauvegarde optionnelle du resultset
↓
Vue → rendu des résultats
Un SavedResultset stocke une liste d'IDs résultant d'une recherche. Il expire après son TTL (défaut : 5 minutes), étendu à chaque accès (+60 secondes par défaut). Utile pour paginer de grands résultats ou partager une recherche.
| Champ | Type | Description |
|---|---|---|
timestamp |
DateTimeField | Date de création |
epoch |
PositiveBigIntegerField | Timestamp Unix |
app_label |
CharField | App du modèle cherché |
model_name |
CharField | Nom du modèle cherché |
search_query_text |
TextField | Requête sérialisée (JSON) |
search_explainer_text |
TextField | Explication lisible de la requête |
result_count |
PositiveIntegerField | Nombre de résultats stockés |
time_to_live |
PositiveIntegerField | TTL en secondes |
user |
ForeignKey | Propriétaire |
SEARCH_SAVED_RESULTSET_TIME_TO_LIVE_INCREMENT secondestask_saved_resultset_expired_delete purge les resultsets expirés toutes les 5 minutesSEARCH_SAVED_RESULTSETS_PER_USER_LIMIT (défaut : 10)| Tâche | Queue | Déclenchement | Description |
|---|---|---|---|
task_index_instance |
queue_search |
Post-save signal | Indexe une instance dans le backend |
task_deindex_instance |
queue_search |
Pre-delete signal | Supprime une instance de l'index |
task_index_instances |
queue_search |
Réindexation | Indexe un lot d'instances |
task_index_related_instance_m2m |
queue_search |
Signal M2M | Réindexe les relations many-to-many |
task_saved_resultset_expired_delete |
queue_search |
Toutes les 5 min | Purge les resultsets expirés |
task_reindex_backend |
queue_search_slow |
Manuel (outil admin) | Réindexation complète du backend |
La réindexation est disponible dans Outils → Réindexer le backend de recherche (permission permission_search_tools requise).
Elle effectue :
SearchBackend.reset() — efface tous les index existantsSearchModel : indexe tous les objets par lots de SEARCH_INDEXING_CHUNK_SIZELa réindexation s'exécute dans la queue queue_search_slow (worker_d) en arrière-plan.
# Déclencher manuellement via manage.py
./manage.py search_reindex --settings=mayan.settings.development
L'indexation se fait automatiquement via des signaux Django :
task_index_instance (après chaque sauvegarde)task_deindex_instance (avant chaque suppression)task_index_related_instance_m2m (changement de relations)| Permission | Description |
|---|---|
permission_saved_resultset_delete |
Supprimer des recherches sauvegardées |
permission_saved_resultset_view |
Consulter les recherches sauvegardées |
permission_search_tools |
Exécuter les outils de recherche (réindexation) |
Ces permissions s'ajoutent aux permissions propres à chaque modèle de recherche (ex. permission_document_view pour rechercher des documents).
Base URL : /api/v4/
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/search/{model_pk}/ |
Recherche simple sur un modèle |
GET |
/search/advanced/{model_pk}/ |
Recherche avancée sur un modèle |
GET |
/search_models/ |
Liste tous les modèles de recherche |
GET |
/search_models/{model_pk}/ |
Détails d'un modèle (champs disponibles) |
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/saved_resultsets/ |
Liste les recherches sauvegardées |
POST |
/saved_resultsets/{model_pk}/ |
Crée une recherche sauvegardée |
GET |
/saved_resultsets/{id}/ |
Détails d'une recherche sauvegardée |
DELETE |
/saved_resultsets/{id}/ |
Supprime une recherche sauvegardée |
GET |
/saved_resultsets/{id}/results/ |
Résultats d'une recherche sauvegardée |
# Lister les modèles de recherche disponibles
curl -u admin:password http://mayan/api/v4/search_models/
# Recherche simple sur les documents
curl -u admin:password "http://mayan/api/v4/search/documents.document/?q=contrat"
# Recherche avancée (AND sur label + type)
curl -u admin:password \
"http://mayan/api/v4/search/advanced/documents.document/?label=contrat&document_type__label=Facture&_match_all=yes"
# Créer une recherche sauvegardée
curl -u admin:password -X POST \
"http://mayan/api/v4/saved_resultsets/documents.document/" \
-d "q=urgent"
# Récupérer les résultats d'une recherche sauvegardée
curl -u admin:password "http://mayan/api/v4/saved_resultsets/42/results/"
Fichier : search_backends.py, lignes 362–366
if search_model.permission:
queryset = AccessControlList.objects.restrict_queryset(
permission=search_model.permission,
queryset=queryset,
user=user
)
Problème : Si un SearchModel est enregistré sans permission, la condition if search_model.permission: est False et aucun filtrage ACL n'est appliqué. Tous les objets correspondants sont retournés, indépendamment des droits de l'utilisateur.
Impact : Un utilisateur peut accéder via la recherche à des objets qu'il ne devrait pas voir.
Correction recommandée :
if not search_model.permission:
return queryset.none() # ou lever une exception
queryset = AccessControlList.objects.restrict_queryset(
permission=search_model.permission,
queryset=queryset,
user=user
)
Audit : Pour vérifier qu'aucun SearchModel n'est enregistré sans permission :
# Dans un shell Django
from mayan.apps.dynamic_search.search_models import SearchModel
for sm in SearchModel.all():
if not sm.permission:
print(f"ATTENTION: {sm.full_name} n'a pas de permission définie")
| URL | Vue | Description |
|---|---|---|
/search/simple/{model}/ |
SearchSimpleView |
Formulaire de recherche simple |
/search/advanced/ |
SearchAdvancedView |
Formulaire de recherche avancée |
/search/advanced/{model}/ |
SearchAdvancedView |
Recherche avancée sur un modèle |
/search/results/ |
SearchResultView |
Résultats de recherche |
/search/backend/reindex/ |
SearchBackendReindexView |
Outil de réindexation (admin) |
/search/search_models/ |
SearchModelListView |
Liste des modèles de recherche |
/search/saved_resultsets/ |
SavedResultsetListView |
Recherches sauvegardées |
/search/saved_resultsets/{id}/results/ |
SavedResultsetResultListView |
Résultats sauvegardés |
writer_procs si le serveur a plusieurs cœurs disponiblesMEDIA_ROOT/whoosh_index)search_page_size selon la RAM disponiblepoint_in_time_keep_alive selon la durée typique de pagination# Réduire pour protéger les performances
MAYAN_SEARCH_RESULTS_LIMIT: 500
MAYAN_SEARCH_QUERY_RESULTS_LIMIT: 50000
# Augmenter les TTL si les utilisateurs paginent lentement
MAYAN_SEARCH_SAVED_RESULTSET_TIME_TO_LIVE: 600
MAYAN_SEARCH_SAVED_RESULTSET_TIME_TO_LIVE_INCREMENT: 120
La réindexation utilise queue_search_slow (worker_d). Si la queue est surchargée, surveiller avec :
docker exec mayan-app-1 celery -A mayan inspect active -d celery@worker_d
| Besoin | Action |
|---|---|
| Changer de backend | MAYAN_SEARCH_BACKEND dans config.yml |
| Réindexer | Outils → Réindexer le backend (ou manage.py search_reindex) |
| Augmenter les résultats max | MAYAN_SEARCH_RESULTS_LIMIT |
| Désactiver un champ | MAYAN_SEARCH_MODEL_FIELD_DISABLE |
| Sauvegarder une recherche | Cocher "Sauvegarder les résultats" lors de la recherche |
| Purger les recherches expirées | Automatique toutes les 5 min (tâche Celery) |
| Vérifier les permissions manquantes | Shell Django + SearchModel.all() |