diff --git a/VERSIONS_HOTLINE/149668b b/VERSIONS_HOTLINE/149668b
new file mode 100644
index 0000000000000000000000000000000000000000..29545edbd4c294ec5f238c6a318161fad470bff7
--- /dev/null
+++ b/VERSIONS_HOTLINE/149668b
@@ -0,0 +1 @@
+ - correctif #149668 : Intégrations : Amélioration des performances de l'indexation des domaines
\ No newline at end of file
diff --git a/cosmogramme/php/integration/domaines.php b/cosmogramme/php/integration/domaines.php
deleted file mode 100644
index 90f52996aed298e61cd9d604aba43c28eb069e49..0000000000000000000000000000000000000000
--- a/cosmogramme/php/integration/domaines.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
- *
- * BOKEH is free software; you can redistribute it and/or modify
- * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
- * the Free Software Foundation.
- *
- * There are special exceptions to the terms and conditions of the AGPL as it
- * is applied to this software (see README file).
- *
- * BOKEH is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
- * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
- */
-
-setVariable('traitement_phase', 'Indexation des domaines');
-
-if ($phase==16) {
-  $log->log('<h4>Indexation des domaines</h4>');
-  unset($phase_data);
-  $reprise = false;
-  $phase_data['nombre'] = 0;
-  $phase_data['nb_fic'] = 0;
-  $phase_data['timeStart'] = time();
-  $phase_data['pointeur'] = 0;
-  $phase_data['domaine'] = 0;
-
-  $phase=17;
-}
-
-if ($phase==17) {
-  if ($mode_cron) {
-    $position_domaine = $phase_data['domaine'];
-    $catalogues = array_slice(Class_Catalogue::findAllCataloguesAIndexer(),
-                              $position_domaine);
-
-    foreach ($catalogues as $catalogue) {
-      $page = $phase_data['pointeur'];
-      $log->log('Indexation du domaine : '.$catalogue->getLibelle().'<br/>');
-      $catalogue->index($page);
-
-      $position_domaine++;
-      $phase_data['domaine'] = $position_domaine;
-      $phase_data['pointeur'] = 0;
-    }
-
-    $log->log('<h4>Indexation des paniers dans les domaines</h4>');
-    Class_PanierNotice::indexAll();
-
-    $log->log('<h4>Indexation des articles dans les domaines</h4>');
-    Class_Article::indexAll();
-
-    $log->log('<h4>Indexation des sitothèques dans les domaines</h4>');
-    Class_Sitotheque::indexAll();
-  } else {
-    $log->log('<h4>Les indexations des domaines, des paniers, articles et sitothèques dans les domaines ne sont traitées qu\'en mode cron</h4>');
-  }
-}
\ No newline at end of file
diff --git a/cosmogramme/php/integre_traite_main.php b/cosmogramme/php/integre_traite_main.php
index 62371a614e12e8a7da4a32b7edf4848d2566ae65..fb7c08a8c2e852a9c0d9020bff6c77177f675fee 100644
--- a/cosmogramme/php/integre_traite_main.php
+++ b/cosmogramme/php/integre_traite_main.php
@@ -303,8 +303,9 @@ if ($phase==14 or $phase==15) {
 // ----------------------------------------------------------------
 // Indexation Domaines
 // ----------------------------------------------------------------
-if ($phase==16 or $phase==17) {
-	include("integration/domaines.php");
+if ($phase==16) {
+  startIntegrationPhase('Domains');
+  $phase = 17;
 }
 
 
@@ -368,108 +369,6 @@ $log_debug->close();
 print(BR . BR . '</body></html>');
 
 
-// ----------------------------------------------------------------
-// Ecriture logs et affichage écran
-// ----------------------------------------------------------------
-function traceTraitementNotice()
-{
-	global $log, $log_debug;
-	global $notice, $compteur, $phase_data, $nb_notices, $chrono100notices, $debug_level;
-	global $nom_bib, $libelle_type_operation, $ret;
-
-	// Recup du statut
-	$statut = $notice->getLastStatut();
-	$code_statut = $statut["statut"];
-
-	// Maj des compteurs
-	$compteur[$code_statut]++;
-	$compteur["nb_notices"]++;
-
-	// logs
-	if ($code_statut == 0)
-	{
-		//incrementeVariable("traitement_erreurs");
-		$phase_data["nb_erreurs"]++;
-		$phase_data["erreurs"][$statut["erreur"]][] = $ret["adresse"];
-		if ($debug_level == 1 or $debug_level == 2) $log->log('<span class="rouge">Notice n° ' . $nb_notices . ' - ' . $statut["erreur"] . '</span>' . BR);
-	}
-	if (count($statut["warnings"]) > 0)
-	{
-		//incrementeVariable("traitement_warnings");
-		if ($debug_level == 2) $log->log('<span class="num_notice">notice n° ' . $nb_notices . '</span>' . BR);
-		foreach ($statut["warnings"] as $warning)
-		{
-			if ($debug_level == 2) $log->log('<font color="purple">Anomalie : ' . $warning[0] . " &raquo; " . $warning[1] . '</font>' . BR);
-			if ($warning[0] == "Genre non reconnu" or $warning[0] == "Section non reconnue") continue;
-			$phase_data["nb_warnings"]++;
-			$phase_data["warnings"][$warning[0]][] = $ret["adresse"] . chr(9) . $warning[1];
-		}
-	}
-
-	// Affichage toutes les 100 notices
-	if ($nb_notices % 100 == 0)
-	{
-		$log->log("notice $nb_notices (" . $chrono100notices->tempsPasse() . " secondes)<br>");
-		$chrono100notices->start();
-	}
-	// Debug level a 1 on affiche le mode d'identificationde la notice
-	if ($debug_level > 1)
-	{
-		if ($nb_notices == 1) print(BR);
-		$log->log('<span class="num_notice">Notice n° ' . $nb_notices . '</span>' . BR);
-		$log->log('<span>Mode d\'identification ---> ' . $statut["identification"] . '</span>' . BR);
-	}
-	// Tout afficher si debug_level maxi
-	if ($debug_level > 2)
-	{
-		$detail = $notice->getNotice();
-		$log->log('<div style=width:700px;margin-left:15px;margin-bottom:10px;">');
-		$log->log('<table class="blank" cellspacing="0" cellpadding="5px">');
-		// Statut de retour
-		$log->log('<tr><td class="blank">Traitement</td>');
-		if ($statut["erreur"]) $erreur = '<span class="rouge"> - ' . $statut["erreur"] . '</span>'; else $erreur="";
-		$log->log('<td class="blank">' . $notice->libStatut[$code_statut] . $erreur . '</td></tr>');
-		// warnings
-		$log->log('<tr><td class="blank" style="vertical-align:top">Anomalies</td>');
-		if (count($statut["warnings"]) > 0)
-		{
-			$log->log('<td class="blank"><font color="purple">');
-			foreach ($statut["warnings"] as $warning)
-			{
-				$log->log($warning[0] . " &raquo; " . $warning[1] . BR);
-			}
-			$log->log('</font></td></tr>');
-		}
-		else $log->log('<td class="blank">aucune</td></tr>');
-		// Détail de l'enreg
-		foreach ($detail as $clef => $data)
-		{
-			if ($clef == "exemplaires")
-			{
-				for ($i = 0; $i < count($data); $i++)
-				{
-					$log->log('<tr><td class="blank" style="vertical-align:top">Exemplaire ' . ($i + 1) . '</td>');
-					$log->log('<td class="blank">');
-					foreach ($data[$i] as $key => $valeur) $log->log($key . " = " . $valeur . BR);
-					$log->log('</td></tr>');
-				}
-				continue;
-			} elseif ($clef == "warnings") continue;
-			elseif (gettype($data) == "array")
-			{
-				$aff = "";
-				foreach ($data as $key => $valeur) $aff.=$key . " = " . $valeur . BR;
-				$data = $aff;
-			}
-			$log->log('<tr><td class="blank" style="vertical-align:top">' . $clef . '</td>');
-			if ($data == "") $data = "&nbsp;";
-			$log->log('<td class="blank">' . $data . '</td></tr>');
-		}
-		$log->log("</table></div>");
-	}
-}
-
-
 
 // ----------------------------------------------------------------
 // Gestion du contexte pour les timeout
diff --git a/library/Class/Catalogue.php b/library/Class/Catalogue.php
index 1be530d74be49de77dbf8242c18277016f964dba..002bd88085c9b8e81566e50c9f3c33ac34e80669 100644
--- a/library/Class/Catalogue.php
+++ b/library/Class/Catalogue.php
@@ -168,29 +168,29 @@ class Class_Catalogue extends Storm_Model_Abstract {
   }
 
 
-  public function updateNoticesWithFacette($nb_par_page,$page) {
-    if (!$ids=$this->getAllNoticeIdsForDomaine($nb_par_page,$page))
+  public function updateNoticesWithFacette($nb_par_page, $page) {
+    if (!$ids = $this->getAllNoticeIdsForDomaine($nb_par_page, $page))
       return false;
 
-    $sql = Zend_Registry::get('sql');
-    $thesaurus=Class_CodifThesaurus::findThesaurusForCatalogue($this->getId());
-    if (!$thesaurus)
+    if (!$thesaurus = Class_CodifThesaurus::findThesaurusForCatalogue($this->getId())) {
       $this->getLoader()->saveThesaurus($this);
-    if ($thesaurus) {
-      if ($page==0) $this->getLoader()->deleteThesaurusInFacette($thesaurus->getIdThesaurus());
+      return false;
+    }
 
-      $sql->query('update notices set facettes=concat(facettes," H'.$thesaurus->getIdThesaurus().'") where id_notice in ('.implode(',',$ids).')');
-      return true;
+    if ($page == 0)
+      $this->getLoader()->deleteThesaurusInFacette($thesaurus);
 
-    }
-    return false;
+    Zend_Registry::get('sql')
+      ->query('update notices set facettes=concat(facettes," H'.$thesaurus->getIdThesaurus().'") where id_notice in ('.implode(',',$ids).')');
+
+    return true;
   }
 
 
   public function afterDelete() {
     if ($thesaurus = Class_CodifThesaurus::findThesaurusForCatalogue($this->getId())) {
       $thesaurus->delete();
-      $this->getLoader()->deleteThesaurusInFacette($thesaurus->getIdThesaurus());
+      $this->getLoader()->deleteThesaurusInFacette($thesaurus);
     }
   }
 
diff --git a/library/Class/Catalogue/Loader.php b/library/Class/Catalogue/Loader.php
index da11bb0b44e40dde815d114b193829604583cb6e..aa947e2171ad2ed616b361276c7d55174bdf9966 100644
--- a/library/Class/Catalogue/Loader.php
+++ b/library/Class/Catalogue/Loader.php
@@ -610,7 +610,7 @@ class Class_Catalogue_Loader extends Storm_Model_Loader {
 
   public function saveThesaurus($catalogue) {
     if ($thesaurus = Class_CodifThesaurus::findThesaurusForCatalogue($catalogue->getId())) {
-      Class_Catalogue::deleteThesaurusInFacette($thesaurus->getIdThesaurus());
+      Class_Catalogue::deleteThesaurusInFacette($thesaurus);
       $catalogue->updateThesaurusLabel($thesaurus);
     } else if (!$thesaurus = $catalogue->saveThesauriParents())
       return;
@@ -731,21 +731,41 @@ class Class_Catalogue_Loader extends Storm_Model_Loader {
   }
 
 
-  public function deleteThesaurusInFacette($id_thesaurus) : int {
-    /*
-     * Sites, articles and RSS records are first destroyed and then get their own selection of domains.
-     * So therauri facettes must not be deleted. By the way, need to find a nicer implementation ...
-     */
-    $query =
-      'update notices set facettes = clean_spaces(REGEXP_REPLACE(facettes, "\\\\bH'
-      . $id_thesaurus . '\\\\b", "")) '.
-      'where notices.type_doc not in (\''
-      . implode('\',\'',
-                [Class_TypeDoc::ARTICLE, Class_TypeDoc::RSS, Class_TypeDoc::SITE])
-      . '\') ' . 'and match(facettes) against("+H'
-      . $id_thesaurus . '" in boolean mode)';
+  public function deleteThesaurusInFacette(Class_CodifThesaurus $thesaurus) : int {
+    $where = implode(' and ',
+                     [$this->_notInPseudoRecords(),
+                      'match(facettes) against("+' . $thesaurus->getFacetCode() . '" in boolean mode)']);
 
-    return Zend_Registry::get('sql')->query($query);
+    return Class_CodifThesaurus::deleteFacetsInRecordsForThesaurus($thesaurus, $where);
   }
 
+
+  public function clearThesaurusFacets() : int {
+    $thesaurus = Class_CodifThesaurus::findFixed('Domain')->getThesaurus();
+    return Class_CodifThesaurus::deleteFacetsInRecordsForThesaurusTree($thesaurus,
+                                                                       $this->_notInPseudoRecords());
+  }
+
+
+  public function clearDomainFacets() : int {
+    $pattern = '\\\\b' . Class_Catalogue::CODE_FACETTE . '[a-zA-Z0-9]*\\\\b';
+
+    return Class_Notice::deleteFacetsInRecordsByPattern($pattern,
+                                                        $this->_notInPseudoRecords()
+                                                        . ' and match(facettes) against("+' . Class_Catalogue::CODE_FACETTE . '*" in boolean mode)');
+  }
+
+
+
+  /**
+   * Class_Sitotheque, Class_Article and Class_Rss can have their own selection of domains.
+   * Their corresponding Class_Notice will have a mix of manual selection of domains and
+   * domains matching by their criterias in their facets.
+   * So we cannot blindly delete.
+   *
+   * @see Class_Indexation_PseudoNotice::_getFacettes()
+   */
+  protected function _notInPseudoRecords() : string {
+    return 'type_doc not in (' . Class_Notice::pseudoRecordsTypes() .') and type=' . Class_Notice::TYPE_BIBLIOGRAPHIC;
+  }
 }
diff --git a/library/Class/CodifThesaurus.php b/library/Class/CodifThesaurus.php
index 114294f7892dc40c76b83cd857d89a74be802543..612e5d4983e0b9a21b8649d8acaeac17081d7c30 100644
--- a/library/Class/CodifThesaurus.php
+++ b/library/Class/CodifThesaurus.php
@@ -80,10 +80,8 @@ class CodifThesaurusLoader extends Storm_Model_Loader {
   }
 
 
-  public function ensureForModelUnderRoot($model, $definition) {
-    return ($root = Class_CodifThesaurus::findRootOfId($definition->getId(),
-                                                       $definition->getCode(),
-                                                       $definition->getLabel()))
+  public function ensureForModelUnderRoot($model, Class_CodifThesaurusFixed $definition) {
+    return ($root = $definition->getThesaurus())
       ? $root->getOrCreateChild($model->getId(), $model->getLibelle())
       : null;
   }
@@ -289,8 +287,20 @@ class CodifThesaurusLoader extends Storm_Model_Loader {
   }
 
 
-  public function deleteFacetsInRecordsForThesaurusTree(Class_CodifThesaurus $thesaurus) : int {
-    return Zend_Registry::get('sql')->query(sprintf('update notices set facettes = clean_spaces(REGEXP_REPLACE(facettes, "\\\\bH%s\\\\d*\\\\b", ""))', $thesaurus->getIdThesaurus()));
+  public function deleteFacetsInRecordsForThesaurusTree(Class_CodifThesaurus $thesaurus, string $where='') : int {
+    return $this->_deleteFacetsInRecordsByPattern('\\\\b' . $thesaurus->getFacetCode() . '\\\\d*\\\\b',
+                                                  $where);
+  }
+
+
+  public function deleteFacetsInRecordsForThesaurus(Class_CodifThesaurus $thesaurus, string $where='') : int {
+    return $this->_deleteFacetsInRecordsByPattern('\\\\b' . $thesaurus->getFacetCode() . '\\\\b',
+                                                  $where);
+  }
+
+
+  protected function _deleteFacetsInRecordsByPattern(string $pattern, string $where='') : int {
+    return Class_Notice::deleteFacetsInRecordsByPattern($pattern, $where);
   }
 
 
diff --git a/library/Class/CodifThesaurusFixed.php b/library/Class/CodifThesaurusFixed.php
index b69d58a838145db7fa11f473f6c2de7ea0f64941..4d8a26828b346bb91ef3fe416d804ea286f5195e 100644
--- a/library/Class/CodifThesaurusFixed.php
+++ b/library/Class/CodifThesaurusFixed.php
@@ -20,7 +20,7 @@
  */
 
 
-class Class_CodifThesaurusFixed extends Class_Entity {
+class Class_CodifThesaurusFixed {
   const
     CODE_DOMAIN_FACET = 'DOMAIN',
     CODE_UNIMARC_FACET = 'UNIMARC';
@@ -39,7 +39,12 @@ class Class_CodifThesaurusFixed extends Class_Entity {
     ];
 
 
-  public static function getAll() {
+  protected string $_id;
+  protected string $_code;
+  protected string $_label;
+
+
+  public static function getAll() : array {
     $all = [];
     foreach(static::$_known as $k => $v)
       $all[$k] = static::newWith($v);
@@ -48,7 +53,7 @@ class Class_CodifThesaurusFixed extends Class_Entity {
   }
 
 
-  public static function isFixedValue($value) {
+  public static function isFixedValue($value) : bool {
     if (!$value
         || Class_CodifThesaurus::CODE_FACETTE !== substr($value, 0, 1)
         || 1 !== (strlen($value) % Class_CodifThesaurus::ID_KEY_LENGTH))
@@ -75,10 +80,34 @@ class Class_CodifThesaurusFixed extends Class_Entity {
   }
 
 
-  public static function newWith($params) {
-    return (new Class_CodifThesaurusFixed())
-      ->setId($params[0])
-      ->setCode($params[1])
-      ->setLabel($params[2]);
+  public static function newWith(array $params) : self {
+    return new static($params[0], $params[1], $params[2]);
+  }
+
+
+  public function __construct(string $id, string $code, string $label) {
+    $this->_id = $id;
+    $this->_code = $code;
+    $this->_label = $label;
+  }
+
+
+  public function getId() : string {
+    return $this->_id;
+  }
+
+
+  public function getCode() : string {
+    return $this->_code;
+  }
+
+
+  public function getLabel() : string {
+    return $this->_label;
+  }
+
+
+  public function getThesaurus() : Class_CodifThesaurus {
+    return Class_CodifThesaurus::findRootOfId($this->_id, $this->_code, $this->_label);
   }
 }
diff --git a/library/Class/Cosmogramme/Integration/PhaseAbstract.php b/library/Class/Cosmogramme/Integration/PhaseAbstract.php
index 28ce7718c7ea760006fd2d53c207cc96096a8ac8..0928e7c2a1dcae65221e44d461f224a29ce88d2a 100644
--- a/library/Class/Cosmogramme/Integration/PhaseAbstract.php
+++ b/library/Class/Cosmogramme/Integration/PhaseAbstract.php
@@ -191,25 +191,29 @@ abstract class Class_Cosmogramme_Integration_PhaseAbstract {
   }
 
 
-  protected function _logMessage($message) {
-    if ($this->_log)
-      $this->_log->log($message);
+  protected function _logMessage(string $message) : self {
+    return $this->_withLogDo(fn($log) => $log->log($message));
+  }
 
-    return $this;
+
+  protected function _logInfo(string $message) : self {
+    return $this->_withLogDo(fn($log) => $log->info($message));
   }
 
 
-  protected function _logSuccess($message) {
-    if ($this->_log)
-      $this->_log->success($message);
+  protected function _logSuccess(string $message) : self {
+    return $this->_withLogDo(fn($log) => $log->success($message));
+  }
 
-    return $this;
+
+  protected function _logError(string $message) : self {
+    return $this->_withLogDo(fn($log) => $log->error($message));
   }
 
 
-  protected function _logError($message) {
+  protected function _withLogDo(Closure $callback) : self {
     if ($this->_log)
-      $this->_log->error($message);
+      $callback($this->_log);
 
     return $this;
   }
diff --git a/library/Class/Cosmogramme/Integration/PhaseDomains.php b/library/Class/Cosmogramme/Integration/PhaseDomains.php
new file mode 100644
index 0000000000000000000000000000000000000000..d04b64cb2649166b8aa9a2d5cde053e2bf606432
--- /dev/null
+++ b/library/Class/Cosmogramme/Integration/PhaseDomains.php
@@ -0,0 +1,232 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Cosmogramme_Integration_PhaseDomains
+  extends Class_Cosmogramme_Integration_PhaseAbstract {
+  const MY_ID = 17;
+
+  protected int $_indexed_count;
+
+  public function __construct($phase, $log, $chrono) {
+    parent::__construct($phase, $log, $chrono);
+    $this->_label = $this->_('Indexation des domaines');
+  }
+
+
+  protected function _init($phase) {
+    $phase->resetDatas();
+    return $this;
+  }
+
+
+  protected function _execute() {
+    if (!$this->_phase->isCron()) {
+      $this->_logInfo($this->_('Ce traitement ne se lance qu\'en mode cron.'));
+      return;
+    }
+
+    $this
+      ->_handleDomains()
+      ->_handlePaniers();
+  }
+
+
+  protected function _handleDomains() : self {
+    $this->_chrono->startOnFile();
+    $clear_count = Class_Catalogue::clearThesaurusFacets();
+    $this->_logInfo($this->_plural($clear_count,
+                                   'Aucune notices à désindexer',
+                                   '1 notice désindexée',
+                                   '%d notices désindexées',
+                                   $clear_count));
+
+    $this->_indexed_count = 0;
+    foreach (Class_Catalogue::findAllCataloguesAIndexer() as $domain)
+      $this->_runOneDomain($domain);
+
+    $this->_summarize();
+
+    return $this;
+  }
+
+
+  protected function _runOneDomain(Class_Catalogue $domain) : self {
+    $this->_logInfo($this->_('Indexation du domaine : %s', $domain->getLibelle()));
+
+    if ((!$thesaurus = Class_CodifThesaurus::findThesaurusForCatalogue($domain->getId()))
+        && (!$thesaurus = Class_Catalogue::saveThesaurus($domain))) {
+      $this->_logError($this->_('Impossible de créer le thesaurus pour ce domaine'));
+      return $this;
+    }
+
+    if (!$where = Class_Catalogue::getQueryConditionsForDomain($domain)) {
+      $this->_logInfo($this->_('Impossible d\'indexer un domaine sans critère'));
+      return $this;
+    }
+
+    $indexed_count = Zend_Registry::get('sql')
+      ->query(sprintf('update notices set facettes=concat(facettes, " %s") %s',
+                      $thesaurus->getFacetCode(),
+                      trim($where)));
+
+    $this->_logInfo($this->_plural($indexed_count,
+                                   'Aucune notices à indexer',
+                                   '1 notice indexée',
+                                   '%d notices indexées',
+                                   $indexed_count));
+    $this->_indexed_count += $indexed_count;
+
+    return $this;
+  }
+
+
+  protected function _summarize() : self {
+    $this->_logSuccess($this->_plural($this->_indexed_count,
+                                      'Aucune notices traitée',
+                                      '1 notice traitée',
+                                      '%d notices traitées',
+                                      $this->_indexed_count));
+    if ($this->_indexed_count)
+      $this->_logSuccess($this->_('Temps de traitement %s (%s)',
+                                  $this->_chrono->endFile(),
+                                  $this->_chrono->meanOnFile($this->_indexed_count,
+                                                             $this->_('notices'))));
+
+    return $this;
+  }
+
+
+  protected function _handlePaniers() : self {
+    $this->_chrono->startOnFile();
+
+    $this->_logMessage('<h4>' . $this->_('Indexation des paniers dans les domaines') . '</h4>');
+
+    $clear_count = Class_NoticeDomain::clearNotPseudoRecordsWithPanier();
+    $this->_logInfo($this->_plural($clear_count,
+                                   'Aucun lien notice/domaine à supprimer',
+                                   '1 lien notice/domaine supprimé',
+                                   '%d liens notice/domaine supprimés',
+                                   $clear_count));
+
+    $clear_count = Class_Catalogue::clearDomainFacets();
+    $this->_logInfo($this->_plural($clear_count,
+                                   'Aucune notices à désindexer',
+                                   '1 notice désindexée',
+                                   '%d notices désindexées',
+                                   $clear_count));
+
+    $this->_indexed_count = 0;
+    foreach (Class_PanierNotice::findAllWithCatalogue() as $panier)
+      $this->_runOnePanier($panier);
+
+    $this->_summarize();
+
+    return $this;
+  }
+
+
+  protected function _runOnePanier(Class_PanierNotice $panier) : self {
+    $this->_logInfo($this->_('Indexation du panier : %s', $panier->getLibelle()));
+
+    if (!$linked_domains = $panier->rawLinkedDomains()) {
+      $this->_logError($this->_('Impossible d\'indexer un panier qui n\'est pas rattaché à au moins un domaine'));
+      return $this;
+    }
+
+    if (!$keys = $panier->getClesNotices()) {
+      $this->_logInfo($this->_('Inutile d\'indexer un panier vide'));
+      return $this;
+    }
+
+    $domains_id = array_map(fn($row) => $row['id_catalogue'],
+                            $linked_domains);
+
+    return $this
+      ->_runOnePanierNoticeDomainLinks($panier, $domains_id, $keys)
+      ->_runOnePanierDomainFacets($linked_domains, $keys);
+  }
+
+
+  protected function _runOnePanierNoticeDomainLinks(Class_PanierNotice $panier, array $domains_id, array $keys) : self {
+    $inserted_count = Class_NoticeDomain::createAllForPanier($panier, $domains_id, $keys);
+
+    $this->_logInfo($this->_plural($inserted_count,
+                                   'Aucun lien notice/domaine créé',
+                                   '1 lien notice/domaine créé',
+                                   '%d liens notice/domaine créés',
+                                   $inserted_count));
+
+    return $this;
+  }
+
+
+  protected function _runOnePanierDomainFacets(array $linked_domains, array $keys) : self {
+    $facets = new Storm_Collection(array_map(fn($row) => Class_Catalogue::CODE_FACETTE.$row['id_catalogue'],
+                                             $linked_domains));
+
+    $facets = $this->_addDomainThesaurusFacetTo($linked_domains, $facets);
+
+    $where = 'clef_alpha in ('
+      . implode(', ',
+                array_map(fn($key) => '"' . $key . '"',
+                          $keys))
+      . ')';
+
+    $indexed_count = Zend_Registry::get('sql')
+      ->query(sprintf('update notices set facettes=concat(facettes, " %s") where %s',
+                      implode(' ', $facets->getArrayCopy()),
+                      $where));
+
+    $this->_logInfo($this->_plural($indexed_count,
+                                   'Aucune notices à indexer',
+                                   '1 notice indexée',
+                                   '%d notices indexées',
+                                   $indexed_count));
+
+    $this->_indexed_count += $indexed_count;
+
+    return $this;
+  }
+
+
+  protected function _addDomainThesaurusFacetTo(array $linked_domains, Storm_Collection $facets) : Storm_Collection {
+    $with_thesaurus = array_map(fn($row) => $row['id_catalogue'],
+                                array_filter($linked_domains,
+                                             fn($row) => 1 == $row['indexer']));
+    if (!$with_thesaurus)
+      return $facets;
+
+    $query = 'select id_thesaurus from codif_thesaurus where id_thesaurus like "CCCC%" and length(id_thesaurus)=8 and id_origine in ('
+      . implode(', ',
+                array_map(fn($id) => '"' . $id . '"',
+                          $with_thesaurus))
+      . ')';
+
+    if (!$ids = Zend_Registry::get('sql')->fetchAllByColumn($query))
+      return $facets;
+
+    $facets->addAll(array_map(fn($id) => Class_CodifThesaurus::CODE_FACETTE.$id,
+                              $ids));
+
+    return $facets;
+  }
+}
diff --git a/library/Class/Indexation/Model/WithManyDomains.php b/library/Class/Indexation/Model/WithManyDomains.php
index 3f8079f8eda1dbdb15e00b45e934985b34b305a4..d1dbe203443aeda7eb341ad36ada0127f7b4cb2c 100644
--- a/library/Class/Indexation/Model/WithManyDomains.php
+++ b/library/Class/Indexation/Model/WithManyDomains.php
@@ -27,54 +27,74 @@ class Class_Indexation_Model_WithManyDomains {
     $this->_model = $model;
   }
 
-  public function index() {
-    if (!$alpha_key = $this->_model->getAlphaKey())
-      return $this;
 
-    $domains_ids = Class_Catalogue::getIds($this->_model->getDomaines());
-    $existing = Class_NoticeDomain::findAllBy(['record_alpha_key' => $alpha_key]);
-    $existing_domains = array_filter(array_map(function ($item) {
-                                                                   return $item->getDomain();
-                                                                 }, $existing));
+  public function index() : self {
+    if (!$alpha_key = $this->_getAlphaKey())
+      return $this;
 
-    $existing_ids = array_map(function ($item) { return $item->getId();}, $existing_domains);
+    $selected_domains = new Storm_Model_Collection($this->_model->getDomaines());
+    $selected_domain_ids = $selected_domains->collect('id')->getArrayCopy();
 
-    if ($to_delete = array_diff($existing_ids, $domains_ids))
-      Class_NoticeDomain::deleteBy(['record_alpha_key' => $alpha_key,
-                                    'domain_id' => $to_delete]);
+    $existings = Class_NoticeDomain::findAllBy(['record_alpha_key' => $alpha_key,
+                                                'panier_id' => 0]);
+    $existings = new Storm_Model_Collection($existings);
 
-    $to_create = array_diff($domains_ids, $existing_ids);
+    // delete unselected
+    $existings
+      ->select(fn($existing) => !in_array($existing->getDomainId(), $selected_domain_ids))
+      ->eachDo(fn($existing) => $existing->delete());
 
-    foreach($to_create as $domain_id) {
-      $notice_domain = Class_NoticeDomain::newInstance(['domain_id' => $domain_id,
-                                                        'record_alpha_key' => $alpha_key]);
-      $notice_domain->updateFacette();
-      $notice_domain->save();
-    }
+    /// create newly selected
+    $existing_domain_ids = $existings->collect('domain_id')->getArrayCopy();
+    foreach(array_diff($selected_domain_ids, $existing_domain_ids) as $domain_id)
+      Class_NoticeDomain::newInstance(['domain_id' => $domain_id,
+                                       'record_alpha_key' => $alpha_key])
+        ->save();
 
-    foreach(Class_NoticeDomain::findAllBy(['record_alpha_key' => $alpha_key]) as $notice_domain)
-      $notice_domain->updateFacette();
+    $this->_synchronizeRecordFacets($selected_domains, $alpha_key);
 
     $this->_model
-      ->setDomaineIds($domains_ids)
+      ->setDomaineIds($selected_domain_ids)
       ->save();
 
     return $this;
   }
 
 
-  public function unindex() {
-    if (!$alpha_key = $this->getAlphaKey())
+  protected function _synchronizeRecordFacets(Storm_Collection $selected_domains, string $alpha_key) : self {
+    $record = $this->_model->getNotice();
+    $linked_by_selection = Class_NoticeDomain::findAllBy(['record_alpha_key' => $alpha_key,
+                                                          'panier_id not' => 0]);
+    $linked_by_selection = (new Storm_Model_Collection($linked_by_selection))
+      ->collect('domain')
+      ->select(fn($domain) => null !== $domain);
+
+    $facets = array_filter($record->getFacetCodes(),
+                           fn($facet) => $facet && Class_Catalogue::CODE_FACETTE !== substr($facet, 0, 1));
+    $facets = array_unique([...$facets,
+                            ...$selected_domains->collect('facet_code')->getArrayCopy(),
+                            ...$linked_by_selection->collect('facet_code')->getArrayCopy()]);
+
+    $record->setFacettes($facets);
+
+    return $this;
+  }
+
+
+  public function unindex() : self {
+    if (!$alpha_key = $this->_getAlphaKey())
       return $this;
 
-    Class_NoticeDomain::deleteBy(['record_alpha_key' => $alpha_key,
-                                  'panier_id' => 0]);
+    Class_NoticeDomain::basicDeleteBy(['record_alpha_key' => $alpha_key,
+                                       'panier_id' => 0]);
 
     return $this;
   }
 
 
-  public function getAlphaKey() {
-    return $this->_model->getAlphaKey();
+  protected function _getAlphaKey() : string {
+    return isset($this->_model)
+      ? $this->_model->getAlphaKey()
+      : '';
   }
 }
\ No newline at end of file
diff --git a/library/Class/Notice.php b/library/Class/Notice.php
index 0a5837bc1c9964c5c6482da83b2ecd07598665f3..0fc3f982f57749ec596cb9bc195c0699fcbc2575 100644
--- a/library/Class/Notice.php
+++ b/library/Class/Notice.php
@@ -19,190 +19,6 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class NoticeLoader extends Storm_Model_Loader {
-  use Trait_MemoryCleaner;
-
-  public function getNoticesFromPreferences($preferences) {
-    $requetes = Class_Catalogue::getRequetes($preferences);
-
-    if (!isset($requetes['req_liste']))
-      return [];
-
-    $notices = $this->findAll($requetes["req_liste"]);
-
-    // Tirage aleatoire
-    if (isset($preferences['aleatoire'])
-        && $preferences["aleatoire"] == 1) {
-      shuffle($notices);
-      $notices = array_slice($notices, 0, $preferences["nb_notices"]);
-    }
-
-    return $notices;
-  }
-
-
-  public function findFirstNoticeForClefChapeau($clef) {
-    $parts = explode('-', $clef);
-    $params = ['clef_chapeau' => $parts[0]];
-    if (isset($parts[1]))
-      $params['type_doc'] = $parts[1];
-
-    return $this->findFirstBy($params);
-  }
-
-
-  public function getNoticeIdsByRequeteRecherche($req, $clear_cache = false) {
-    if (!$req)
-      return [];
-
-    $cache = (new Storm_Cache())->useImplodeExplodeSerialization();
-    $cache_key = [$req, __CLASS__, __FUNCTION__];
-
-    if ($clear_cache)
-      $cache->remove($cache_key);
-
-    return $cache->memoize($cache_key,
-                           function() use ($req)
-                           {
-                             return Zend_Registry::get('sql')->fetchAllByColumn($req);
-                           });
-  }
-
-
-  public function findAllByRequeteRecherche($req, $nb_par_page, $page_no=1 ) {
-    return $this->findAllByIds($this->getNoticeIdsByRequeteRecherche($req),
-                               $nb_par_page,
-                               $page_no);
-  }
-
-
-  public function findAllByIds($ids, $nb_par_page, $page_no) {
-    $page_no = (int)$page_no;
-    $nb_par_page = (int)$nb_par_page;
-
-    if ($nb_par_page) {
-      $offset = ($page_no ? $page_no - 1 : 0) * $nb_par_page;
-      $ids = array_slice($ids, $offset, $nb_par_page);
-    }
-
-    if (empty($ids))
-      return [];
-
-    return Class_Notice::getLoader()->findAllBy(['id_notice' => $ids,
-                                                 'order' => 'FIELD(id_notice, '.implode(',', $ids).')']);
-  }
-
-
-  public function countBySQLSelect($req) {
-    return fetchOne($req);
-  }
-
-
-  public function getNoticeByOAIIdentifier($identifier) {
-    $parts = explode(':', $identifier);
-    return Class_Notice::getLoader()->getNoticeByClefAlpha(end($parts));
-  }
-
-
-  public function getNoticeByClefAlpha($clef) {
-    return Class_Notice::findFirstBy(['clef_alpha' => $clef]);
-  }
-
-
-  public function getAllNoticesByClefChapeau($clef, $conditions) {
-    return Class_Notice::findAllBy(array_merge($conditions,
-                                               ['clef_chapeau' => $clef,
-                                                'order' => 'tome_alpha desc']));
-  }
-
-
-  public function getAllNoticesByClefAlpha($clef) {
-    return Class_Notice::findAllBy(['clef_alpha' => $clef]);
-  }
-
-
-  public function findAllByCatalogue($catalogue) {
-    return Class_Catalogue::getLoader()->loadNoticesFor($catalogue);
-  }
-
-  public function findByUrl($url) {
-    $frbr_link = new Class_FRBR_Link();
-    if (!$clef_alpha = $frbr_link->extractKeyFromUrl($url))
-      return;
-
-    return Class_Notice::getNoticeByClefAlpha($clef_alpha);
-  }
-
-
-  public function getEarliestNotice() {
-    $result = $this->findAllBy(['limit' => 1,
-                                'created_at not' => null,
-                                'order' => 'created_at asc']);
-    if (0 == count($result))
-      return null;
-
-    return $result[0];
-  }
-
-
-  public function findAllAfter($record_id, $update_date) {
-    $where = "id_notice > " . $record_id . " and date_maj >='" . $update_date . "'";
-    return Class_Notice::findAllBy(['where' => $where,
-                                    'order' => 'id_notice',
-                                    'limit' => '100']);
-  }
-
-
-  public function indexNoveltyFacets() {
-    $facets = ['HNNNN*', 'HNANA*'];
-    if ($novelty_thesaurus = Class_CodifThesaurus::recordNoveltyFor(true))
-      $facets[] = $novelty_thesaurus->getFacetCode();
-    $facets = implode(' ', $facets);
-
-    $record_id = 0;
-    while ($records = $this->_findNoveltyRecordsFrom($facets, $record_id))
-      $record_id = $this->_updateNovelty($records);
-  }
-
-
-  protected function _findNoveltyRecordsFrom($facets, $record_id) {
-    if (null === $record_id)
-      return [];
-
-    $where = sprintf('match(facettes) against("+(%s)" in boolean mode) and id_notice > %d',
-                     $facets, $record_id);
-
-    return Class_Notice::findAllBy(['where' => $where,
-                                    'order' => 'id_notice',
-                                    'limit' => 100]);
-  }
-
-
-  protected function _updateNovelty($records) {
-    $record_id = null;
-    foreach($records as $record) {
-      $record->updateNoveltyFacets();
-      $record_id = $record->getId();
-    }
-    $this->_cleanMemory();
-
-    return $record_id;
-  }
-
-
-  public function basicDeleteAllFromSigb() {
-    // @see Class_TypeDoc::isSigb()
-    $params = ['cast(type_doc as UNSIGNED) > 0',
-               'type_doc < '.Class_TypeDoc::LIVRE_NUM,
-               'type_doc not in (' . implode(',', [Class_TypeDoc::ARTICLE,
-                                                   Class_TypeDoc::RSS,
-                                                   Class_TypeDoc::SITE]) . ')'];
-
-    return Class_Notice::basicDeleteBy(['where' => implode(' and ', $params)]);
-  }
-}
-
-
 
 class Class_Notice extends Storm_Model_Abstract {
   use Trait_TimeSource, Trait_Translator;
@@ -214,7 +30,7 @@ class Class_Notice extends Storm_Model_Abstract {
     TYPE_AUTHORITY_PARTIAL = 4;
 
   protected
-    $_loader_class = 'NoticeLoader',
+    $_loader_class = Class_Notice_Loader::class,
     $_table_name = 'notices',
     $_table_primary = 'id_notice',
     $_notice_unimarc,
diff --git a/library/Class/Notice/Loader.php b/library/Class/Notice/Loader.php
new file mode 100644
index 0000000000000000000000000000000000000000..e547a615a9c8b93fae636537a5031acf3f8af0fd
--- /dev/null
+++ b/library/Class/Notice/Loader.php
@@ -0,0 +1,223 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Notice_Loader extends Storm_Model_Loader {
+  use Trait_MemoryCleaner;
+
+  public function getNoticesFromPreferences($preferences) {
+    $requetes = Class_Catalogue::getRequetes($preferences);
+
+    if (!isset($requetes['req_liste']))
+      return [];
+
+    $notices = $this->findAll($requetes["req_liste"]);
+
+    // Tirage aleatoire
+    if (isset($preferences['aleatoire'])
+        && $preferences["aleatoire"] == 1) {
+      shuffle($notices);
+      $notices = array_slice($notices, 0, $preferences["nb_notices"]);
+    }
+
+    return $notices;
+  }
+
+
+  public function findFirstNoticeForClefChapeau($clef) {
+    $parts = explode('-', $clef);
+    $params = ['clef_chapeau' => $parts[0]];
+    if (isset($parts[1]))
+      $params['type_doc'] = $parts[1];
+
+    return $this->findFirstBy($params);
+  }
+
+
+  public function getNoticeIdsByRequeteRecherche($req, $clear_cache = false) {
+    if (!$req)
+      return [];
+
+    $cache = (new Storm_Cache())->useImplodeExplodeSerialization();
+    $cache_key = [$req, __CLASS__, __FUNCTION__];
+
+    if ($clear_cache)
+      $cache->remove($cache_key);
+
+    return $cache->memoize($cache_key,
+                           function() use ($req)
+                           {
+                             return Zend_Registry::get('sql')->fetchAllByColumn($req);
+                           });
+  }
+
+
+  public function findAllByRequeteRecherche($req, $nb_par_page, $page_no=1 ) {
+    return $this->findAllByIds($this->getNoticeIdsByRequeteRecherche($req),
+                               $nb_par_page,
+                               $page_no);
+  }
+
+
+  public function findAllByIds($ids, $nb_par_page, $page_no) {
+    $page_no = (int)$page_no;
+    $nb_par_page = (int)$nb_par_page;
+
+    if ($nb_par_page) {
+      $offset = ($page_no ? $page_no - 1 : 0) * $nb_par_page;
+      $ids = array_slice($ids, $offset, $nb_par_page);
+    }
+
+    if (empty($ids))
+      return [];
+
+    return Class_Notice::getLoader()->findAllBy(['id_notice' => $ids,
+                                                 'order' => 'FIELD(id_notice, '.implode(',', $ids).')']);
+  }
+
+
+  public function countBySQLSelect($req) {
+    return fetchOne($req);
+  }
+
+
+  public function getNoticeByOAIIdentifier($identifier) {
+    $parts = explode(':', $identifier);
+    return Class_Notice::getLoader()->getNoticeByClefAlpha(end($parts));
+  }
+
+
+  public function getNoticeByClefAlpha($clef) {
+    return Class_Notice::findFirstBy(['clef_alpha' => $clef]);
+  }
+
+
+  public function getAllNoticesByClefChapeau($clef, $conditions) {
+    return Class_Notice::findAllBy(array_merge($conditions,
+                                               ['clef_chapeau' => $clef,
+                                                'order' => 'tome_alpha desc']));
+  }
+
+
+  public function getAllNoticesByClefAlpha($clef) {
+    return Class_Notice::findAllBy(['clef_alpha' => $clef]);
+  }
+
+
+  public function findAllByCatalogue($catalogue) {
+    return Class_Catalogue::getLoader()->loadNoticesFor($catalogue);
+  }
+
+  public function findByUrl($url) {
+    $frbr_link = new Class_FRBR_Link();
+    if (!$clef_alpha = $frbr_link->extractKeyFromUrl($url))
+      return;
+
+    return Class_Notice::getNoticeByClefAlpha($clef_alpha);
+  }
+
+
+  public function getEarliestNotice() {
+    $result = $this->findAllBy(['limit' => 1,
+                                'created_at not' => null,
+                                'order' => 'created_at asc']);
+    if (0 == count($result))
+      return null;
+
+    return $result[0];
+  }
+
+
+  public function findAllAfter($record_id, $update_date) {
+    $where = "id_notice > " . $record_id . " and date_maj >='" . $update_date . "'";
+    return Class_Notice::findAllBy(['where' => $where,
+                                    'order' => 'id_notice',
+                                    'limit' => '100']);
+  }
+
+
+  public function indexNoveltyFacets() {
+    $facets = ['HNNNN*', 'HNANA*'];
+    if ($novelty_thesaurus = Class_CodifThesaurus::recordNoveltyFor(true))
+      $facets[] = $novelty_thesaurus->getFacetCode();
+    $facets = implode(' ', $facets);
+
+    $record_id = 0;
+    while ($records = $this->_findNoveltyRecordsFrom($facets, $record_id))
+      $record_id = $this->_updateNovelty($records);
+  }
+
+
+  protected function _findNoveltyRecordsFrom($facets, $record_id) {
+    if (null === $record_id)
+      return [];
+
+    $where = sprintf('match(facettes) against("+(%s)" in boolean mode) and id_notice > %d',
+                     $facets, $record_id);
+
+    return Class_Notice::findAllBy(['where' => $where,
+                                    'order' => 'id_notice',
+                                    'limit' => 100]);
+  }
+
+
+  protected function _updateNovelty($records) {
+    $record_id = null;
+    foreach($records as $record) {
+      $record->updateNoveltyFacets();
+      $record_id = $record->getId();
+    }
+    $this->_cleanMemory();
+
+    return $record_id;
+  }
+
+
+  public function basicDeleteAllFromSigb() {
+    // @see Class_TypeDoc::isSigb()
+    $params = ['cast(type_doc as UNSIGNED) > 0',
+               'type_doc < '.Class_TypeDoc::LIVRE_NUM,
+               'type_doc not in (' . $this->pseudoRecordsTypes() . ')'];
+
+    return Class_Notice::basicDeleteBy(['where' => implode(' and ', $params)]);
+  }
+
+
+  public function deleteFacetsInRecordsByPattern(string $pattern, string $where) : int {
+    $query = 'update notices set facettes = clean_spaces(REGEXP_REPLACE(facettes, "'. $pattern .'", ""))';
+    if ($where)
+      $query .= ' where ' . $where;
+
+    return Zend_Registry::get('sql')->query($query);
+  }
+
+
+  public function pseudoRecordsTypes(?Closure $callback=null) : string {
+    if (null === $callback)
+      $callback = fn($type) => '"' . $type. '"';
+
+    return implode(', ',
+                   array_map($callback,
+                             [Class_TypeDoc::ARTICLE,
+                              Class_TypeDoc::RSS,
+                              Class_TypeDoc::SITE]));
+  }
+}
diff --git a/library/Class/NoticeDomain.php b/library/Class/NoticeDomain.php
index c3c3da5ed79ce16f8f23ed0698db2b632a698cea..df240abedc3247f0dc2e2e9618584a3ae376c499 100644
--- a/library/Class/NoticeDomain.php
+++ b/library/Class/NoticeDomain.php
@@ -100,6 +100,55 @@ class NoticeDomainLoader extends Storm_Model_Loader {
       $notice_domain->delete();
     }
   }
+
+
+  /**
+   * Class_Sitotheque, Class_Article and Class_Rss can have their own selection of domains.
+   * Their corresponding Class_Notice will have a mix of manual selection of domains and
+   * domains matching by their criterias in their facets.
+   * So we cannot blindly delete.
+   *
+   * @see Class_Indexation_PseudoNotice::_getFacettes()
+   */
+  public function clearNotPseudoRecordsWithPanier() : int {
+    $pseudo_records_key_ends = Class_Notice::pseudoRecordsTypes(fn($type) => '"-'. $type .'"');
+    $where =
+      'panier_id != 0'
+      . ' and substring(record_alpha_key, -2) not in ('. $pseudo_records_key_ends .')';
+
+    return Zend_Registry::get('sql')->query('delete from notice_domain where ' . $where);
+  }
+
+
+  /**
+   * @param $panier Class_PanierNotice
+   * @param $domains array<int>
+   * @param $clefs_alpha array<string>
+   */
+  public function createAllForPanier(Class_PanierNotice $panier, array $domains, array $clefs_alpha) : int {
+    if (!$panier_id = $panier->getId())
+      return 0;
+
+    $values = new Storm_Collection;
+
+    foreach($domains as $domain)
+      $values->addAll($this->_insertValuesForPanierAndDomain($panier_id, $domain, $clefs_alpha));
+
+    return Zend_Registry::get('sql')
+      ->query('insert into notice_domain (domain_id, panier_id, record_alpha_key) values ' . implode(', ', $values->getArrayCopy()));
+  }
+
+
+  protected function _insertValuesForPanierAndDomain(int $panier_id, int $domain_id, array $clefs_alpha) : array {
+    return
+      array_map(fn($alpha_key) => '(' . $domain_id . ', ' . $panier_id . ', "' . $alpha_key . '")',
+                $clefs_alpha);
+  }
+
+
+  protected function _insertValuesFor(int $domain_id, int $panier_id, string $record_alpha_key) : string {
+    return '(' . $domain_id . ', ' . $panier_id . ', "' . $record_alpha_key . '")';
+  }
 }
 
 
diff --git a/library/Class/PanierNotice.php b/library/Class/PanierNotice.php
index 3f0fa9fa735869f70265be19fad8eb4066c078f5..148d5d0fee3b2a616bf2ef6a97c89cbc52bdc0dd 100644
--- a/library/Class/PanierNotice.php
+++ b/library/Class/PanierNotice.php
@@ -139,6 +139,16 @@ class PanierNoticeLoader extends Storm_Model_Loader {
       Class_PanierNotice::findFirstBy(['libelle' => $label,
                                        'id_user' => $user->getId()]);
   }
+
+
+  public function rawLinkedDomainsOf(Class_PanierNotice $panier) : array {
+    $query = 'select c.id_catalogue, c.indexer'
+      .' from catalogue c'
+      .' inner join notices_paniers_catalogues npc'
+      .' on (c.id_catalogue=npc.id_catalogue and npc.id_panier=' . $panier->getId() . ')';
+
+    return Zend_Registry::get('sql')->fetchAll($query);
+  }
 }
 
 
@@ -582,4 +592,23 @@ class Class_PanierNotice extends Storm_Model_Abstract {
   public function isEmpty() : bool {
     return ! $this->getClesNotices();
   }
+
+
+  public function rawLinkedDomains() : array {
+    return static::getLoader()->rawLinkedDomainsOf($this);
+  }
+
+
+  public function getClesNoticesNotPseudoRecords() : array {
+    if (!$keys = $this->getClesNotices())
+      return [];
+
+    $pseudo_ends = array_map(fn($type) => '-' . $type,
+                             [Class_TypeDoc::ARTICLE,
+                              Class_TypeDoc::RSS,
+                              Class_TypeDoc::SITE]);
+
+    return array_filter($keys,
+                        fn($key) => !in_array(substr($key, -2), $pseudo_ends));
+  }
 }
\ No newline at end of file
diff --git a/library/Trait/HasManyDomaines.php b/library/Trait/HasManyDomaines.php
index 452c8bc969b3e861e913c0827d6bc83ed03d6651..4a6939c5b427a22d9e5f0d581389e04c1baeefd0 100644
--- a/library/Trait/HasManyDomaines.php
+++ b/library/Trait/HasManyDomaines.php
@@ -23,19 +23,19 @@
 trait Trait_HasManyDomaines {
   public function setDomaineIds($domaines) {
     if (is_array($domaines))
-      $domaines=implode(';', array_unique($domaines));
-    $domaines=str_replace('-',';',$domaines);
+      $domaines = implode(';', array_unique($domaines));
+    $domaines = str_replace('-',';',$domaines);
     return $this->_set('domaine_ids',$domaines);
   }
 
 
-  public function getDomaineIdsAsArray() {
+  public function getDomaineIdsAsArray() : array {
     $domaines = $this->_get('domaine_ids') ;
     return array_filter(explode(';', $domaines));
   }
 
 
-  public function getDomaines() {
+  public function getDomaines() : array {
     $domains = $this->getDomaineIdsAsArray();
     return empty($domains)
       ? []
@@ -43,18 +43,14 @@ trait Trait_HasManyDomaines {
   }
 
 
-  public function getDomaineLibelles() {
-    return array_map(function($model) {return $model->getLibelle();},
+  public function getDomaineLibelles() : array {
+    return array_map(fn($model) => $model->getLibelle(),
                      $this->getDomaines());
   }
 
 
   public function setDomaines($domaines) {
-    $ids = [];
-    foreach($domaines as $domaine)
-      $ids []= $domaine->getId();
-    return $this->setDomaineIds($ids);
+    return $this->setDomaineIds(array_map(fn($domaine) => $domaine->getId(),
+                                          $domaines));
   }
 }
-
-?>
\ No newline at end of file
diff --git a/tests/library/Class/Cosmogramme/Integration/PhaseDomainsTest.php b/tests/library/Class/Cosmogramme/Integration/PhaseDomainsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b5401f09aecbff5ecf4fbdf4dd24dba76abd1df
--- /dev/null
+++ b/tests/library/Class/Cosmogramme/Integration/PhaseDomainsTest.php
@@ -0,0 +1,189 @@
+<?php
+/**
+ * Copyright (c) 2012-2022, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+abstract class PhaseDomainsTestCase extends Class_Cosmogramme_Integration_PhaseTestCase {
+  protected function _getPreviousPhase() {
+    return new Class_Cosmogramme_Integration_Phase(16);
+  }
+
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->_phase = $this->_buildPhase('Domains')
+                         ->setMemoryCleaner(function() {})
+                         ->run();
+  }
+
+
+  /** @test */
+  public function logShouldNotContainsError() {
+    $this->assertNotError();
+  }
+
+
+  /** @test */
+  public function logShouldContainsIndexationDesDomaines() {
+    $this->assertLogContains('Indexation des domaines');
+  }
+}
+
+
+
+
+class PhaseDomainsNotCronTest extends PhaseDomainsTestCase {
+  /** @test */
+  public function logShouldContainsQuEnModeCron() {
+    $this->assertLogContains('Ce traitement ne se lance qu\'en mode cron.');
+  }
+}
+
+
+
+
+class PhaseDomainsCronTest extends PhaseDomainsTestCase {
+  protected function _getPreviousPhase() {
+    return parent::_getPreviousPhase()->beCron();
+  }
+
+
+  protected function _prepareFixtures() {
+    $sql = $this->mock()
+
+                ->whenCalled('query')
+                ->with('update notices set facettes = clean_spaces(REGEXP_REPLACE(facettes, "\\\\bHCCCC\\\\d*\\\\b", "")) where type_doc not in ("8", "9", "10") and type=1')
+                ->answers(12)
+
+                ->whenCalled('query')
+                ->with('update notices set facettes=concat(facettes, " HCCCC0001") Where (notices.type_doc=\'2\') and type=1')
+                ->answers(1)
+
+                ->whenCalled('query')
+                ->with('delete from notice_domain where panier_id != 0 and substring(record_alpha_key, -2) not in ("-8", "-9", "-10")')
+                ->answers(8)
+
+                ->whenCalled('query')
+                ->with('update notices set facettes = clean_spaces(REGEXP_REPLACE(facettes, "\\\\bQ[a-zA-Z0-9]*\\\\b", "")) where type_doc not in ("8", "9", "10") and type=1 and match(facettes) against("+Q*" in boolean mode)')
+                ->answers(5)
+
+                ->whenCalled('fetchAll')
+                ->with('select c.id_catalogue, c.indexer from catalogue c inner join notices_paniers_catalogues npc on (c.id_catalogue=npc.id_catalogue and npc.id_panier=348)')
+                ->answers([
+                           ['id_catalogue' => 33, 'indexer' => 1],
+                           ['id_catalogue' => 34, 'indexer' => 0]
+                           ])
+
+                ->whenCalled('query')
+                ->with('insert into notice_domain (domain_id, panier_id, record_alpha_key) values (33, 348, "MYSUPER-KEY----1"), (33, 348, "MYOTHER-KEY-----1"), (34, 348, "MYSUPER-KEY----1"), (34, 348, "MYOTHER-KEY-----1")')
+                ->answers(2)
+
+                ->whenCalled('query')
+                ->with('update notices set facettes=concat(facettes, " Q33 Q34 HCCCC007Z") where clef_alpha in ("MYSUPER-KEY----1", "MYOTHER-KEY-----1")')
+                ->answers(2)
+
+                ->whenCalled('fetchAllByColumn')
+                ->with('select id_thesaurus from codif_thesaurus where id_thesaurus like "CCCC%" and length(id_thesaurus)=8 and id_origine in ("33")')
+                ->answers(['CCCC007Z'])
+      ;
+
+    Zend_Registry::set('sql', $sql);
+
+    $serials = $this->fixture(Class_Catalogue::class,
+                              ['id' => 33,
+                               'libelle' => 'My Domain of serials',
+                               'indexer' => 1,
+                               'type_doc' => Class_TypeDoc::PERIODIQUE]);
+
+    $not_indexed = $this->fixture(Class_Catalogue::class,
+                                  ['id' => 34,
+                                   'libelle' => 'Shy',
+                                   'indexer' => 0,
+                                   'type_doc' => Class_TypeDoc::LIVRE]);
+
+    $this->fixture(Class_PanierNotice::class,
+                   ['id' => 348,
+                    'libelle' => 'My selection',
+                    'notices' => 'MYSUPER-KEY----1;MYOTHER-KEY-----1',
+                    'catalogues' => [$serials, $not_indexed]]);
+  }
+
+
+  /** @test */
+  public function logShouldContains12NoticesDésindexées() {
+    $this->assertLogContains('12 notices désindexées');
+  }
+
+
+  /** @test */
+  public function logShouldContainsMyDomainOfSerials() {
+    $this->assertLogContains('Indexation du domaine : My Domain of serials');
+  }
+
+
+  /** @test */
+  public function logShouldContainsUneNoticeIndexée() {
+    $this->assertLogContains('1 notice indexé');
+  }
+
+
+  /** @test */
+  public function logShouldContainsSummary() {
+    $this->assertLogContains('1 notice traitée');
+    $this->assertLogContains('Temps de traitement');
+  }
+
+
+  /** @test */
+  public function logShouldContainsIndexationDesPaniers() {
+    $this->assertLogContains('Indexation des paniers dans les domaines');
+  }
+
+
+  /** @test */
+  public function logShouldContains8LiensNoticeDomaineSupprimés() {
+    $this->assertLogContains('8 liens notice/domaine supprimés');
+  }
+
+
+  /** @test */
+  public function logShouldContains5NoticesDésindexées() {
+    $this->assertLogContains('5 notices désindexées');
+  }
+
+
+  /** @test */
+  public function logShouldContainsIndexationDuPanierMySelection() {
+    $this->assertLogContains('Indexation du panier : My selection');
+  }
+
+
+  /** @test */
+  public function logShouldContains2LiensNoticeDomaineCréés() {
+    $this->assertLogContains('2 liens notice/domaine créés');
+  }
+
+
+  /** @test */
+  public function logShouldContains2NoticesIndexées() {
+    $this->assertLogContains('2 notices indexées');
+  }
+}
diff --git a/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php b/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php
index ad039b1d1a16b70fdae162fc963cfe4a39898b4c..f57e0de504809c99ca33a0ccca3d97e556a27c3b 100644
--- a/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php
+++ b/tests/library/Class/Cosmogramme/Integration/PhasePrepareIntegrationsTest.php
@@ -855,7 +855,7 @@ class PhasePrepareIntegrationsFlushBeforeFullAndTotalRecordsTest
 
   /** @test */
   public function shouldDeleteAllNoticeFromSigb() {
-    $this->assertEquals(['where' => 'cast(type_doc as UNSIGNED) > 0 and type_doc < 100 and type_doc not in (8,9,10)'],
+    $this->assertEquals(['where' => 'cast(type_doc as UNSIGNED) > 0 and type_doc < 100 and type_doc not in ("8", "9", "10")'],
                         Class_Notice::getFirstAttributeForLastCallOn('basicDeleteBy'));
   }