Le module document_indexing permet d'organiser automatiquement les documents dans une arborescence dynamique calculée à partir de leurs métadonnées, dates, tags ou tout autre attribut. Contrairement aux cabinets (organisation manuelle), les index se peuplent automatiquement dès qu'un document est créé ou modifié.
Emplacement : mayan/apps/document_indexing/
Un index de documents est un arbre dont chaque niveau est défini par une expression Django template. Quand Mayan indexe un document, il évalue ces expressions pour déterminer dans quelle branche de l'arbre le document doit apparaître.
IndexTemplate "Documents par date"
└── Nœud expression: {{ document.datetime_created|date:"Y" }}
└── Nœud expression: {{ document.datetime_created|date:"m" }}
└── [link_documents=True — les documents apparaissent ici]
→ Résultat (IndexInstance) :
2024
├── 01 [doc1, doc2, doc3]
├── 03 [doc4]
└── 11 [doc5, doc6]
2025
├── 01 [doc7]
└── 05 [doc8, doc9]
Si l'expression d'un nœud retourne une chaîne vide, le document n'apparaît pas dans ce niveau — il est silencieusement ignoré pour ce nœud.
L'indexation repose sur deux paires de modèles :
IndexTemplate → définition (structure, expressions)
IndexTemplateNode → nœuds de la définition (expression, enabled, link_documents)
IndexInstance → état évalué (proxy d'IndexTemplate)
IndexInstanceNode → nœuds évalués (valeur calculée, documents liés)
| Champ | Type | Description |
|---|---|---|
label |
CharField(128) | Nom affiché de l'index |
slug |
SlugField(128) | Identifiant unique (URL-safe) |
enabled |
BooleanField | Active/désactive l'index |
document_types |
ManyToMany → DocumentType | Types de documents couverts par cet index |
Un nœud racine (root node) est créé automatiquement à la création du template. Il est invisible à l'utilisateur et sert de point d'ancrage pour tous les nœuds enfants.
| Champ | Type | Description |
|---|---|---|
parent |
TreeForeignKey | Nœud parent (null = nœud racine) |
index |
ForeignKey → IndexTemplate | Index auquel appartient ce nœud |
expression |
TextField | Expression Django template à évaluer |
enabled |
BooleanField | Active/désactive ce nœud |
link_documents |
BooleanField | Si True : les documents sont attachés à ce nœud |
link_documentsest le paramètre le plus important. Un nœud aveclink_documents=Falseest un nœud intermédiaire (dossier). Aveclink_documents=Truec'est un nœud feuille (contient des documents).
| Champ | Type | Description |
|---|---|---|
value |
CharField(255) | Résultat calculé de l'expression du nœud template |
index_template_node |
ForeignKey | Nœud template correspondant |
parent |
TreeForeignKey | Nœud parent dans l'arbre évalué |
documents |
ManyToMany → Document | Documents liés (si link_documents=True) |
Contrainte d'unicité : (index_template_node, parent, value) — deux documents avec la même valeur partagent le même nœud.
Définit quels événements déclenchent une réindexation automatique. Peuplé automatiquement avec tous les événements Document lors de la création du template.
Document.save()
→ signal post_save
→ handler_index_document()
→ task_index_instance_document_add(document_id) [queue: indexing / worker_b]
→ Pour chaque IndexInstance couvrant ce type de document :
→ Verrouillage (index + document)
→ Suppression des nœuds précédents du document
→ _document_add() récursif :
→ Évalue expression du nœud courant
→ Si résultat non vide :
→ get_or_create(IndexInstanceNode, value=résultat)
→ Si link_documents : node.documents.add(document)
→ Récursion sur les enfants du nœud
→ Nettoyage des nœuds vides
Quand un événement Document se produit (métadonnée modifiée, tag ajouté, etc.) :
Action.save()
→ signal post_save (activity stream)
→ handler_event_trigger()
→ Cherche IndexTemplates avec :
- Ce type de document dans document_types
- Un EventTrigger pour ce type d'événement
→ task_index_instance_document_add() pour chaque template
Document.delete()
→ signal pre_delete
→ handler_remove_document()
→ task_index_instance_document_remove(document_id) [queue: indexing]
→ Supprime le document de tous les IndexInstanceNodes
→ Supprime les nœuds devenus vides
Fichier : models/index_instance_model_mixins.py:27-31
template = Template(
template_string=index_template_node.expression
)
result = template.render(
context={'document': document}
)
L'expression est rendue via le moteur de templates Django standard. Le seul contexte injecté est document.
Le contexte de rendu contient uniquement document. Tous les attributs et relations du modèle Document sont accessibles.
| Expression | Résultat exemple |
|---|---|
{{ document.label }} |
Contrat Dupont 2026 |
{{ document.description }} |
Description du document |
{{ document.uuid }} |
a1b2c3d4-... |
{{ document.document_type }} |
Facture |
{{ document.document_type.label }} |
Facture |
{{ document.datetime_created }} |
2026-05-01 14:32:00 |
| Expression | Résultat |
|---|---|
{{ document.datetime_created\|date:"Y" }} |
2026 |
{{ document.datetime_created\|date:"m" }} |
05 |
{{ document.datetime_created\|date:"Y-m" }} |
2026-05 |
{{ document.datetime_created\|date:"d" }} |
01 |
{{ document.datetime_created\|date:"l" }} |
Thursday |
{{ document.metadata_value_of.nom_du_type }}
Remplacer nom_du_type par le name (pas le label) du MetadataType.
| Expression | Résultat |
|---|---|
{{ document.metadata_value_of.client }} |
Dupont SA |
{{ document.metadata_value_of.departement }} |
Ressources Humaines |
{{ document.metadata_value_of.date_facture\|date:"Y" }} |
2026 |
{{ document.metadata_value_of.statut }} |
Approuvé |
Si la métadonnée n'existe pas sur le document, l'expression retourne une chaîne vide — le document n'est pas indexé dans ce nœud.
{{ document.source_metadata_value_of.nom_de_cle }}
{{ document.file_metadata_value_of.nom_de_cle }}
Tous les filtres Django standard sont disponibles dans les expressions :
| Filtre | Usage | Exemple |
|---|---|---|
\|date:"format" |
Formater une date | \|date:"Y-m" |
\|lower |
Minuscules | Facture → facture |
\|upper |
Majuscules | facture → FACTURE |
\|title |
Capitaliser | facture dupont → Facture Dupont |
\|slugify |
Format slug | Dupont SA → dupont-sa |
\|truncatechars:n |
Tronquer | \|truncatechars:20 |
\|default:"Inconnu" |
Valeur par défaut | Si vide → Inconnu |
{% if document.metadata_value_of.priorite %}{{ document.metadata_value_of.priorite }}{% else %}Non définie{% endif %}
link_documents pour le nœud feuilleBASE="https://mayan.example.com/api/v4"
TOKEN="votre_token"
# 1. Créer le template
TEMPLATE=$(curl -s -X POST $BASE/index_templates/ \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"label": "Factures par client", "slug": "factures_client", "enabled": true}')
TEMPLATE_ID=$(echo $TEMPLATE | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# Récupérer l'ID du nœud racine
ROOT_NODE_ID=$(echo $TEMPLATE | python3 -c "import sys,json; print(json.load(sys.stdin)['index_template_root_node_id'])")
# 2. Créer le nœud client (niveau 1, pas de documents)
NODE_CLIENT=$(curl -s -X POST $BASE/index_templates/$TEMPLATE_ID/nodes/ \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"parent\": $ROOT_NODE_ID, \"expression\": \"{{ document.metadata_value_of.client }}\", \"enabled\": true, \"link_documents\": false}")
NODE_CLIENT_ID=$(echo $NODE_CLIENT | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# 3. Créer le nœud année (niveau 2, contient les documents)
curl -X POST $BASE/index_templates/$TEMPLATE_ID/nodes/ \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"parent\": $NODE_CLIENT_ID, \"expression\": \"{{ document.datetime_created|date:\\\"Y\\\" }}\", \"enabled\": true, \"link_documents\": true}"
# 4. Associer un type de document
curl -X POST $BASE/index_templates/$TEMPLATE_ID/document_types/add/ \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"document_type": 1}'
# 5. Reconstruire
curl -X POST $BASE/index_templates/$TEMPLATE_ID/rebuild/ \
-H "Authorization: Token $TOKEN"
Niveau 1 : {{ document.datetime_created|date:"Y" }} [link_documents=false]
Niveau 2 : {{ document.datetime_created|date:"m" }} [link_documents=true]
→ 2026 / 05 / [documents de mai 2026]
Niveau 1 : {{ document.metadata_value_of.departement }} [link_documents=false]
Niveau 2 : {{ document.metadata_value_of.statut }} [link_documents=true]
→ Ressources Humaines / Approuvé / [documents approuvés de RH]
→ Ressources Humaines / En attente / [...]
→ Finance / Approuvé / [...]
Niveau 1 : {{ document.document_type.label }} [link_documents=false]
Niveau 2 : {{ document.datetime_created|date:"Y" }} [link_documents=true]
→ Facture / 2025 / [toutes les factures 2025]
→ Contrat / 2026 / [tous les contrats 2026]
Niveau 1 : {{ document.metadata_value_of.client }} [link_documents=false]
Niveau 2 : {{ document.metadata_value_of.projet }} [link_documents=false]
Niveau 3 : {{ document.metadata_value_of.responsable }} [link_documents=true]
→ Dupont SA / Projet Alpha / Martin / [documents de Martin sur Projet Alpha]
Niveau 1 : {{ document.metadata_value_of.region|default:"Sans région" }} [link_documents=false]
Niveau 2 : {{ document.datetime_created|date:"Y-m" }} [link_documents=true]
→ Sans région / 2026-05 / [documents sans région assignée]
→ Québec / 2026-05 / [...]
Aller dans Documents → Indexes (menu de navigation)
L'arbre s'affiche avec pour chaque nœud :
En cliquant sur un nœud feuille, la liste des documents s'affiche avec les colonnes et filtres habituels.
Dans la vue détail d'un document, l'onglet "Indexes" liste toutes les branches d'index qui contiennent ce document, avec le chemin complet (ex: Factures par client / Dupont SA / 2026).
# Lister tous les index (instances)
curl -H "Authorization: Token $TOKEN" $BASE/index_instances/
# Naviguer dans l'arbre d'un index
curl -H "Authorization: Token $TOKEN" $BASE/index_instances/1/nodes/
# Voir les enfants d'un nœud
curl -H "Authorization: Token $TOKEN" $BASE/index_instances/1/nodes/5/nodes/
# Voir les documents dans un nœud
curl -H "Authorization: Token $TOKEN" $BASE/index_instances/1/nodes/5/documents/
# Voir dans quels nœuds se trouve un document
curl -H "Authorization: Token $TOKEN" $BASE/documents/42/indexes/
import requests
HEADERS = {"Authorization": "Token votre_token"}
BASE = "https://mayan.example.com/api/v4"
# Lister les index disponibles
indexes = requests.get(f"{BASE}/index_instances/", headers=HEADERS).json()
# Naviguer dans l'arbre
nodes = requests.get(f"{BASE}/index_instances/1/nodes/", headers=HEADERS).json()
for node in nodes["results"]:
print(f"{' ' * node['level']}{node['value']} ({node['node_count']} sous-nœuds)")
# Récupérer les documents d'un nœud
docs = requests.get(f"{BASE}/index_instances/1/nodes/5/documents/", headers=HEADERS).json()
for doc in docs["results"]:
print(doc["id"], doc["label"])
# Voir dans quels index se trouve un document
indexes_for_doc = requests.get(f"{BASE}/documents/42/indexes/", headers=HEADERS).json()
for node in indexes_for_doc["results"]:
print(node["index"]["label"], "→", node["value"])
Supprime tous les nœuds évalués existants et réindexe tous les documents depuis zéro. Nécessaire après :
# Via API
curl -X POST $BASE/index_templates/1/rebuild/ \
-H "Authorization: Token $TOKEN"
# Tous les index d'un coup (via l'interface)
# Administration → Document Indexing → Rebuild all
La reconstruction est asynchrone (queue indexing_slow / worker_c). Sur de grands volumes, elle peut prendre plusieurs minutes.
Vide l'index sans reconstruire. L'arbre est vide jusqu'au prochain rebuild ou au prochain événement de document.
curl -X POST $BASE/index_templates/1/reset/ \
-H "Authorization: Token $TOKEN"
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/v4/index_templates/ |
Lister tous les templates |
POST |
/api/v4/index_templates/ |
Créer un template |
GET |
/api/v4/index_templates/{id}/ |
Détail d'un template |
PATCH |
/api/v4/index_templates/{id}/ |
Modifier label/slug/enabled |
DELETE |
/api/v4/index_templates/{id}/ |
Supprimer |
GET |
/api/v4/index_templates/{id}/nodes/ |
Lister les nœuds |
POST |
/api/v4/index_templates/{id}/nodes/ |
Créer un nœud |
GET |
/api/v4/index_templates/{id}/nodes/{node_id}/ |
Détail d'un nœud |
PATCH |
/api/v4/index_templates/{id}/nodes/{node_id}/ |
Modifier expression/enabled/link_documents |
DELETE |
/api/v4/index_templates/{id}/nodes/{node_id}/ |
Supprimer un nœud |
GET |
/api/v4/index_templates/{id}/document_types/ |
Types de documents liés |
POST |
/api/v4/index_templates/{id}/document_types/add/ |
Associer un type |
POST |
/api/v4/index_templates/{id}/document_types/remove/ |
Désassocier un type |
POST |
/api/v4/index_templates/{id}/rebuild/ |
Déclencher la reconstruction |
POST |
/api/v4/index_templates/{id}/reset/ |
Réinitialiser |
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/v4/index_instances/ |
Lister tous les index évalués |
GET |
/api/v4/index_instances/{id}/ |
Détail (profondeur, nb nœuds) |
GET |
/api/v4/index_instances/{id}/nodes/ |
Nœuds racine de l'instance |
GET |
/api/v4/index_instances/{id}/nodes/{node_id}/ |
Détail d'un nœud évalué |
GET |
/api/v4/index_instances/{id}/nodes/{node_id}/nodes/ |
Enfants d'un nœud |
GET |
/api/v4/index_instances/{id}/nodes/{node_id}/documents/ |
Documents dans un nœud |
GET |
/api/v4/documents/{doc_id}/indexes/ |
Index contenant un document |
{
"id": 5,
"value": "Dupont SA",
"level": 1,
"depth": 2,
"node_count": 3,
"parent_id": 2,
"parent_url": "...",
"index_url": "...",
"children_url": "...",
"documents_url": "...",
"url": "..."
}
| Permission | Description |
|---|---|
permission_index_instance_view |
Naviguer dans les index (voir les nœuds et les documents) |
permission_index_template_view |
Voir les templates d'index |
permission_index_template_create |
Créer de nouveaux templates |
permission_index_template_edit |
Modifier templates et nœuds (expressions) |
permission_index_template_delete |
Supprimer des templates |
permission_index_template_rebuild |
Déclencher une reconstruction |
Note de sécurité :
permission_index_template_editpermet de modifier les expressions des nœuds, qui sont rendues via le moteur de templates Django. Réserver ce droit aux administrateurs de confiance uniquement (voir VULN-4 dans le rapport de sécurité).
| Tâche | Queue | Worker | Description |
|---|---|---|---|
task_index_instance_document_add |
indexing |
worker_b | Indexe un document dans un ou tous les index |
task_index_instance_document_remove |
indexing |
worker_b | Retire un document de tous les nœuds |
task_index_template_rebuild |
indexing_slow |
worker_c | Reconstruction complète d'un index |
Toutes les tâches utilisent des verrous distribués pour éviter les conflits d'indexation concurrente. En cas de conflit de verrou (LockError), la tâche est automatiquement réessayée avec backoff exponentiel.
Si l'expression d'un nœud retourne une chaîne vide (métadonnée absente, condition non satisfaite), le document n'apparaît pas dans ce nœud et ses enfants. Ce comportement est intentionnel — il permet de créer des index qui ne contiennent que les documents avec les données nécessaires.
Deux documents ayant la même valeur calculée partagent le même nœud. Par exemple, tous les documents du client "Dupont SA" pointent vers le même IndexInstanceNode avec value="Dupont SA". Ce nœud est supprimé automatiquement quand il ne contient plus aucun document.
Un IndexTemplate avec enabled=False n'est plus mis à jour lors des événements de document. Les données existantes restent en base mais ne sont plus synchronisées.
La re-indexation est déclenchée par les événements Document enregistrés dans IndexTemplateEventTrigger. Par défaut, tous les événements Document déclenchent une re-indexation (création de fichier, modification de métadonnée, ajout de tag, etc.).
| Besoin | Action |
|---|---|
| Créer un index | Administration → Index Templates → Create |
| Définir les nœuds | Template → Nodes → Create child node |
| Accéder à la valeur d'une métadonnée | {{ document.metadata_value_of.nom_type }} |
| Grouper par année | {{ document.datetime_created\|date:"Y" }} |
| Documents visibles dans un nœud | Cocher link_documents=True sur le nœud feuille |
| Mettre à jour après changement de structure | Rebuild (bouton ou API POST) |
| Naviguer dans un index | Documents → Indexes |
| Voir les index d'un document | Vue document → onglet Indexes |
| Re-indexation automatique | Automatique via événements (aucune config requise) |