mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-06 03:55:10 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01421ce89a | |||
| 1dabd2601d | |||
| 88f69841b3 | |||
| 5b8fbdcec7 | |||
| beb048b94a | |||
| b33d11778a |
@@ -326,3 +326,11 @@ behind a reverse proxy may need to set
|
||||
[`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES),
|
||||
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
|
||||
or both, to avoid `403 Forbidden` errors on login.
|
||||
|
||||
## Database Migrations
|
||||
|
||||
Some integer fields have been changed to smaller types to reduce database size. If you have any `MailRule` records with a `maximum_age` greater than 32767, they will be clamped to 32767 during the migration to avoid errors during migration.
|
||||
|
||||
### Action Required
|
||||
|
||||
No user action is required. The migration will automatically clamp any `MailRule.maximum_age` values greater than 32767 to 32767 during the migration process.
|
||||
|
||||
@@ -48,6 +48,9 @@ _LANGUAGE_MAP: dict[str, str] = {
|
||||
}
|
||||
|
||||
SUPPORTED_LANGUAGES: frozenset[str] = frozenset(_LANGUAGE_MAP)
|
||||
# Document.title is max_length=128, so use 129 as the limit for
|
||||
# Tantivy's remove_long filter
|
||||
_TOKEN_REMOVE_LONG_LIMIT: Final[int] = 129
|
||||
|
||||
|
||||
def register_tokenizers(index: tantivy.Index, language: str | None) -> None:
|
||||
@@ -77,10 +80,10 @@ def register_tokenizers(index: tantivy.Index, language: str | None) -> None:
|
||||
|
||||
|
||||
def _paperless_text(language: str | None) -> tantivy.TextAnalyzer:
|
||||
"""Main full-text tokenizer for content, title, etc: simple -> remove_long(65) -> lowercase -> ascii_fold [-> stemmer]"""
|
||||
"""Main full-text tokenizer for content, title, etc: simple -> remove_long(129) -> lowercase -> ascii_fold [-> stemmer]"""
|
||||
builder = (
|
||||
tantivy.TextAnalyzerBuilder(tantivy.Tokenizer.simple())
|
||||
.filter(tantivy.Filter.remove_long(65))
|
||||
.filter(tantivy.Filter.remove_long(_TOKEN_REMOVE_LONG_LIMIT))
|
||||
.filter(tantivy.Filter.lowercase())
|
||||
.filter(tantivy.Filter.ascii_fold())
|
||||
)
|
||||
@@ -119,12 +122,12 @@ def _bigram_analyzer() -> tantivy.TextAnalyzer:
|
||||
|
||||
|
||||
def _simple_search_analyzer() -> tantivy.TextAnalyzer:
|
||||
"""Tokenizer for simple substring search fields: non-whitespace chunks -> remove_long(65) -> lowercase -> ascii_fold."""
|
||||
"""Tokenizer for simple substring search fields: non-whitespace chunks -> remove_long(129) -> lowercase -> ascii_fold."""
|
||||
return (
|
||||
tantivy.TextAnalyzerBuilder(
|
||||
tantivy.Tokenizer.regex(r"\S+"),
|
||||
)
|
||||
.filter(tantivy.Filter.remove_long(65))
|
||||
.filter(tantivy.Filter.remove_long(_TOKEN_REMOVE_LONG_LIMIT))
|
||||
.filter(tantivy.Filter.lowercase())
|
||||
.filter(tantivy.Filter.ascii_fold())
|
||||
.build()
|
||||
|
||||
@@ -163,7 +163,12 @@ def scan(query: str) -> list[Token]:
|
||||
i += 1
|
||||
continue
|
||||
token, i = matched
|
||||
_flush(buf, tokens)
|
||||
if buf and buf[-1] == ",":
|
||||
buf.pop()
|
||||
_flush(buf, tokens)
|
||||
tokens.append(Comma())
|
||||
else:
|
||||
_flush(buf, tokens)
|
||||
tokens.append(token)
|
||||
i = _maybe_comma(query, i, tokens)
|
||||
_flush(buf, tokens)
|
||||
|
||||
@@ -261,6 +261,36 @@ class TestSearch:
|
||||
== 1
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("search_mode", "query"),
|
||||
[
|
||||
pytest.param(SearchMode.TITLE, "12345", id="title_search"),
|
||||
pytest.param(SearchMode.TEXT, "12345", id="text_search"),
|
||||
pytest.param(SearchMode.QUERY, None, id="query_title_exact"),
|
||||
],
|
||||
)
|
||||
def test_search_modes_match_model_limit_title_tokens(
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
search_mode: SearchMode,
|
||||
query: str | None,
|
||||
) -> None:
|
||||
"""Search must keep filename-like title tokens up to the model limit."""
|
||||
long_title = "1234567890" * 12 + "12345678"
|
||||
doc = Document.objects.create(
|
||||
title=long_title,
|
||||
content="ordinary content",
|
||||
checksum="TXT12",
|
||||
pk=18,
|
||||
)
|
||||
backend.add_or_update(doc)
|
||||
|
||||
assert backend.search_ids(
|
||||
query or f"title:{long_title}",
|
||||
user=None,
|
||||
search_mode=search_mode,
|
||||
) == [doc.pk]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "title", "content", "hits", "misses"),
|
||||
[
|
||||
|
||||
@@ -99,6 +99,25 @@ class TestTokenizers:
|
||||
)
|
||||
assert simple_search_index.searcher().search(q, limit=5).count == 1
|
||||
|
||||
def test_simple_search_analyzer_supports_model_limit_token_substrings(
|
||||
self,
|
||||
simple_search_index: tantivy.Index,
|
||||
) -> None:
|
||||
"""Simple substring search keeps tokens up to Document.title's model limit."""
|
||||
long_token = "abcdefghij" * 12 + "abcdefgh"
|
||||
writer = simple_search_index.writer()
|
||||
doc = tantivy.Document()
|
||||
doc.add_text("simple_content", long_token)
|
||||
writer.add_document(doc)
|
||||
writer.commit()
|
||||
simple_search_index.reload()
|
||||
q = tantivy.Query.regex_query(
|
||||
simple_search_index.schema,
|
||||
"simple_content",
|
||||
".*cdefg.*",
|
||||
)
|
||||
assert simple_search_index.searcher().search(q, limit=5).count == 1
|
||||
|
||||
def test_unsupported_language_logs_warning(self, caplog: LogCaptureFixture) -> None:
|
||||
"""Unsupported language codes should log a warning and disable stemming gracefully."""
|
||||
sb = tantivy.SchemaBuilder()
|
||||
|
||||
@@ -716,6 +716,12 @@ class TestISODatetimeBounds:
|
||||
"added:[2026-05-01T00:00:00Z TO 2026-06-01T00:00:00Z]"
|
||||
)
|
||||
|
||||
def test_translate_query_text_before_comma_separated_date_clause(self) -> None:
|
||||
result = translate_query("schäfersee,created:previous year", UTC)
|
||||
assert result == (
|
||||
"schäfersee AND created:[2025-01-01T00:00:00Z TO 2026-01-01T00:00:00Z]"
|
||||
)
|
||||
|
||||
def test_invalid_iso_datetime_raises(self) -> None:
|
||||
# A token with "T" that is not valid ISO datetime -> raise.
|
||||
with pytest.raises(InvalidDateQuery) as exc_info:
|
||||
|
||||
@@ -4,6 +4,12 @@ from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
def clamp_mailrule_maximum_age(apps, schema_editor):
|
||||
# Clamp the maximum_age field of MailRule because of PositiveIntegerField --> PositiveSmallIntegerField
|
||||
MailRule = apps.get_model("paperless_mail", "MailRule")
|
||||
MailRule.objects.filter(maximum_age__gt=32767).update(maximum_age=32767)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("paperless_mail", "0001_squashed"),
|
||||
@@ -112,6 +118,10 @@ class Migration(migrations.Migration):
|
||||
verbose_name="consumption scope",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
clamp_mailrule_maximum_age,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="mailrule",
|
||||
name="maximum_age",
|
||||
|
||||
Reference in New Issue
Block a user