Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] Log contains InconsistentVersionWarning for sklearn using docker-compose with latest paperless docker image
### Description

I have a log entry in root_webserver_1 which reads
```
/usr/local/lib/python3.11/site-packages/sklearn/base.py:348: InconsistentVersionWarning: Trying to unpickle estimator MLPClassifier from version 1.3.0 when using version 1.3.2. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to:
https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations
  warnings.warn(
```


### Steps to reproduce

1. `podman-compose up -d`
2. `podman logs root_webserver_1`

### Webserver logs

```bash
/usr/local/lib/python3.11/site-packages/sklearn/base.py:348: InconsistentVersionWarning: Trying to unpickle estimator MLPClassifier from version 1.3.0 when using version 1.3.2. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to:
https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations
  warnings.warn(
```


### Browser logs

_No response_

### Paperless-ngx version

2.3.3

### Host OS

Debian 12

### Installation method

Docker - official image

### Browser

Firefox

### Configuration changes

PAPERLESS_TRUSTED_PROXIES=172.16.x.y PAPERLESS_USE_X_FORWARD_HOST=true PAPERLESS_ENABLE_COMPRESSION=false  PAPERLESS_OCR_MODE=redo PAPERLESS_OCR_SKIP_ARCHIVE_FILE=with_text  PAPERLESS_CONSUMER_DELETE_DUPLICATES=true PAPERLESS_CONSUMER_RECURSIVE=true PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true  PAPERLESS_OCR_ROTATE_PAGES=true PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD=10  PAPERLESS_CONSUMER_ENABLE_BARCODES=true PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=true PAPERLESS_CONSUMER_BARCODE_SCANNER=ZXING

### Other

_No response_

### Please confirm the following

- [X] I believe this issue is a bug that affects all users of Paperless-ngx, not something specific to my installation.
- [X] I have already searched for relevant existing issues and discussions before opening this report.
- [X] I have updated the title field above with a concise description.


Discussion:
Ran into this log entry after upgrading to 2.3.3.
Doesn't hinder paperless to run, but I can't tell how it affects user experience.
It doesn't really affect anything.  It appears they changed the warning text we used to capture to something else.
What version did you update from?  sklearn hasn't changed in 2 months at this point, so I'm having trouble reproducing it
From 1.17.4
If it's gone by now, maybe even better?
As soon as the classifier runs again, which is every hour (?) by default, the saved file is updated and the version warning will go away.  So it is only something ever going to be seen in some particular circumstances
Okay, so my impatient ass stroke again. 😂
Thanks for checking up so fast! 👍🏻

Can be closed, I guess.

Resolving patch:
diff --git a/src/documents/classifier.py b/src/documents/classifier.py
--- a/src/documents/classifier.py
+++ b/src/documents/classifier.py
@@ -10,6 +10,7 @@
 from typing import Optional
 
 from django.conf import settings
+from sklearn.exceptions import InconsistentVersionWarning
 
 from documents.models import Document
 from documents.models import MatchingModel
@@ -18,7 +19,9 @@
 
 
 class IncompatibleClassifierVersionError(Exception):
-    pass
+    def __init__(self, message: str, *args: object) -> None:
+        self.message = message
+        super().__init__(*args)
 
 
 class ClassifierModelCorruptError(Exception):
@@ -37,8 +40,8 @@ def load_classifier() -> Optional["DocumentClassifier"]:
     try:
         classifier.load()
 
-    except IncompatibleClassifierVersionError:
-        logger.info("Classifier version updated, will re-train")
+    except IncompatibleClassifierVersionError as e:
+        logger.info(f"Classifier version incompatible: {e.message}, will re-train")
         os.unlink(settings.MODEL_FILE)
         classifier = None
     except ClassifierModelCorruptError:
@@ -114,10 +117,12 @@ def load(self) -> None:
                 "#security-maintainability-limitations"
             )
             for warning in w:
-                if issubclass(warning.category, UserWarning):
-                    w_msg = str(warning.message)
-                    if sk_learn_warning_url in w_msg:
-                        raise IncompatibleClassifierVersionError
+                # The warning is inconsistent, the MLPClassifier is a specific warning, others have not updated yet
+                if issubclass(warning.category, InconsistentVersionWarning) or (
+                    issubclass(warning.category, UserWarning)
+                    and sk_learn_warning_url in str(warning.message)
+                ):
+                    raise IncompatibleClassifierVersionError("sklearn version update")
 
     def save(self):
         target_file: Path = settings.MODEL_FILE


Test patch:
diff --git a/src/documents/tests/data/v1.0.2.model.pickle b/src/documents/tests/data/v1.0.2.model.pickle
deleted file mode 100644
Binary files a/src/documents/tests/data/v1.0.2.model.pickle and /dev/null differ
diff --git a/src/documents/tests/data/v1.17.4.model.pickle b/src/documents/tests/data/v1.17.4.model.pickle
new file mode 100644
Binary files /dev/null and b/src/documents/tests/data/v1.17.4.model.pickle differ
diff --git a/src/documents/tests/test_classifier.py b/src/documents/tests/test_classifier.py
--- a/src/documents/tests/test_classifier.py
+++ b/src/documents/tests/test_classifier.py
@@ -1,5 +1,6 @@
 import os
 import re
+import shutil
 from pathlib import Path
 from unittest import mock
 
@@ -649,7 +650,7 @@ def test_load_classifier_incompatible_version(self, load):
         Path(settings.MODEL_FILE).touch()
         self.assertTrue(os.path.exists(settings.MODEL_FILE))
 
-        load.side_effect = IncompatibleClassifierVersionError()
+        load.side_effect = IncompatibleClassifierVersionError("Dummey Error")
         self.assertIsNone(load_classifier())
         self.assertFalse(os.path.exists(settings.MODEL_FILE))
 
@@ -661,3 +662,14 @@ def test_load_classifier_os_error(self, load):
         load.side_effect = OSError()
         self.assertIsNone(load_classifier())
         self.assertTrue(os.path.exists(settings.MODEL_FILE))
+
+    def test_load_old_classifier_version(self):
+        shutil.copy(
+            os.path.join(os.path.dirname(__file__), "data", "v1.17.4.model.pickle"),
+            self.dirs.scratch_dir,
+        )
+        with override_settings(
+            MODEL_FILE=self.dirs.scratch_dir / "v1.17.4.model.pickle",
+        ):
+            classifier = load_classifier()
+            self.assertIsNone(classifier)