mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-13 19:47:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68b5d4be70 | ||
|
|
c86c822bc7 |
No files matched your search
@@ -76,11 +76,12 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Reaching here with the honeypot means AI usage was not disclosed in the form, so that
|
||||
// message can name the cause.
|
||||
// A missing template label only tells us the issue did not come from the form, which
|
||||
// is reason enough to close it, but not proof of how it was written.
|
||||
const body = honeypot
|
||||
// is reason enough to close it, but not proof of how it was written. It takes priority:
|
||||
// a hand-built body can carry the honeypot *and* a disclosure, and the form message is
|
||||
// the only one that is accurate for both. Reaching the honeypot message means the issue
|
||||
// came from the form without disclosing AI usage, so that message can name the cause.
|
||||
const body = honeypot && !bypassedTemplate
|
||||
? 'This issue was automatically closed because it contains a marker that is only visible to ' +
|
||||
'automated tools, which indicates it was generated by an AI agent, but the "AI usage" field does not ' +
|
||||
'disclose this.\n\n' +
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ dependencies = [
|
||||
"drf-spectacular~=0.30",
|
||||
"drf-spectacular-sidecar>=2026.7.1,<2026.10",
|
||||
"drf-writable-nested~=0.7.1",
|
||||
"filelock>=3.32,<4.1",
|
||||
"filelock~=3.32.0",
|
||||
"flower>=2.0.1,<2.3",
|
||||
"gotenberg-client[httpx]~=1.0",
|
||||
"httpx-oauth~=0.17",
|
||||
|
||||
@@ -926,6 +926,7 @@ class TantivyBackend:
|
||||
sort_field: str | None = None,
|
||||
sort_reverse: bool = False,
|
||||
search_mode: SearchMode = SearchMode.QUERY,
|
||||
filter_query: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> list[int]:
|
||||
"""
|
||||
@@ -941,6 +942,7 @@ class TantivyBackend:
|
||||
sort_field: Field to sort by (None for relevance ranking)
|
||||
sort_reverse: Whether to reverse the sort order
|
||||
search_mode: Query parsing mode (QUERY, TEXT, or TITLE)
|
||||
filter_query: Optional advanced query results must also match
|
||||
limit: Maximum number of IDs to return (None = all matching docs)
|
||||
|
||||
Returns:
|
||||
@@ -948,6 +950,13 @@ class TantivyBackend:
|
||||
"""
|
||||
self._ensure_open()
|
||||
user_query = self._parse_query(query, search_mode)
|
||||
if filter_query is not None:
|
||||
user_query = user_query.and_must_match(
|
||||
tantivy.Query.const_score_query(
|
||||
self._parse_query(filter_query, SearchMode.QUERY),
|
||||
0.0, # zero so doesn't affect relevance
|
||||
),
|
||||
)
|
||||
final_query = self._apply_permission_filter(user_query, user)
|
||||
|
||||
searcher = self._index.searcher()
|
||||
|
||||
+34
-22
@@ -289,6 +289,7 @@ logger = logging.getLogger("paperless.api")
|
||||
# clauses efficiently, so this threshold mainly protects SQLite users.
|
||||
_TANTIVY_INTERSECT_THRESHOLD = 5_000
|
||||
_TANTIVY_SEARCH_PARAM_NAMES = ("text", "title_search", "query", "more_like_id")
|
||||
_TANTIVY_COMBINABLE_SEARCH_PARAMS = ({"text", "query"}, {"title_search", "query"})
|
||||
|
||||
# whoosh-compat's fieldname tagger (used only for SearchMode.QUERY, via the
|
||||
# whoosh grammar in parse_user_query) is O(n^2) in plain word characters:
|
||||
@@ -308,6 +309,18 @@ _TANTIVY_SEARCH_PARAM_NAMES = ("text", "title_search", "query", "more_like_id")
|
||||
_MAX_QUERY_LENGTH: Final[int] = 4096
|
||||
|
||||
|
||||
def _validate_tantivy_search_params(active: list[str]) -> None:
|
||||
# may also have a filter query alongside a simple search e.g. relative dates
|
||||
if len(active) > 1 and set(active) not in _TANTIVY_COMBINABLE_SEARCH_PARAMS:
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _(
|
||||
"Specify only one of text, title_search, query, or more_like_id.",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _get_tantivy_query_and_mode(params):
|
||||
from documents.search import QueryTooLongError
|
||||
from documents.search import SearchMode
|
||||
@@ -321,9 +334,15 @@ def _get_tantivy_query_and_mode(params):
|
||||
else:
|
||||
return None # pragma: no cover
|
||||
|
||||
if len(raw) > _MAX_QUERY_LENGTH:
|
||||
raise QueryTooLongError(len(raw), _MAX_QUERY_LENGTH)
|
||||
return raw, mode
|
||||
filter_query = (
|
||||
str(params["query"])
|
||||
if mode is not SearchMode.QUERY and "query" in params
|
||||
else None
|
||||
)
|
||||
for value in (raw, filter_query):
|
||||
if value is not None and len(value) > _MAX_QUERY_LENGTH:
|
||||
raise QueryTooLongError(len(value), _MAX_QUERY_LENGTH)
|
||||
return raw, mode, filter_query
|
||||
|
||||
|
||||
def _get_more_like_id(query_params: dict[str, Any], user: User | None) -> int:
|
||||
@@ -2566,15 +2585,9 @@ class UnifiedSearchViewSet(DocumentViewSet):
|
||||
|
||||
def parse_search_params() -> SearchParams:
|
||||
"""Extract query string, search mode, and ordering from request."""
|
||||
active = self._get_active_search_params(request)
|
||||
if len(active) > 1:
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _(
|
||||
"Specify only one of text, title_search, query, or more_like_id.",
|
||||
),
|
||||
},
|
||||
)
|
||||
_validate_tantivy_search_params(
|
||||
self._get_active_search_params(request),
|
||||
)
|
||||
|
||||
ordering_param = request.query_params.get("ordering", "")
|
||||
sort_reverse = ordering_param.startswith("-")
|
||||
@@ -2636,7 +2649,9 @@ class UnifiedSearchViewSet(DocumentViewSet):
|
||||
filtered_qs: QuerySet[Document],
|
||||
) -> SearchResultPage:
|
||||
"""Handle text/title/query search: IDs, ORM intersection, page highlights."""
|
||||
query_str, search_mode = _get_tantivy_query_and_mode(request.query_params)
|
||||
query_str, search_mode, filter_query = _get_tantivy_query_and_mode(
|
||||
request.query_params,
|
||||
)
|
||||
|
||||
# "score" is not a real Tantivy sort field — it means relevance order,
|
||||
# which is Tantivy's default when no sort field is specified.
|
||||
@@ -2649,6 +2664,7 @@ class UnifiedSearchViewSet(DocumentViewSet):
|
||||
),
|
||||
sort_reverse=sort_reverse,
|
||||
search_mode=search_mode,
|
||||
filter_query=filter_query,
|
||||
)
|
||||
ordered_ids = intersect_and_order(
|
||||
all_ids,
|
||||
@@ -2883,14 +2899,7 @@ class DocumentSelectionMixin:
|
||||
]
|
||||
if not search_filters:
|
||||
return None
|
||||
if len(search_filters) > 1:
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _(
|
||||
"Specify only one of text, title_search, query, or more_like_id.",
|
||||
),
|
||||
},
|
||||
)
|
||||
_validate_tantivy_search_params(search_filters)
|
||||
|
||||
from documents.search import SearchQueryError
|
||||
from documents.search import get_backend
|
||||
@@ -2909,11 +2918,14 @@ class DocumentSelectionMixin:
|
||||
user=search_user,
|
||||
)
|
||||
else:
|
||||
query_str, search_mode = _get_tantivy_query_and_mode(filters)
|
||||
query_str, search_mode, filter_query = _get_tantivy_query_and_mode(
|
||||
filters,
|
||||
)
|
||||
search_ids = backend.search_ids(
|
||||
query_str,
|
||||
user=search_user,
|
||||
search_mode=search_mode,
|
||||
filter_query=filter_query,
|
||||
)
|
||||
except SearchQueryError as e:
|
||||
# Same user-fixable-query mapping as the search list endpoint:
|
||||
|
||||
@@ -1272,11 +1272,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "4.0.12"
|
||||
version = "3.32.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/e4/34efcb869715cf299e47d1ac7b2624d2bcb6f2d3dffc2f0abe8417f65ab2/filelock-4.0.12.tar.gz", hash = "sha256:cf42711a7ac791818b299fab0332a088c65aeeefa36290de98db92c434303b0c", size = 253880, upload-time = "2026-10-05T13:17:43.677Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/46/126b1831dca12060d4a8296bf9c4fe5c93c4f22197fa239cb0cc82042bba/filelock-3.32.6.tar.gz", hash = "sha256:a3f55a18af3652a94d8f47d6055df434f254ca1d02ef2524850c6d249ca2512c", size = 225172, upload-time = "2026-09-08T22:57:11.528Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a1/b047e1c486a2c66283633b7dbb73872ca16ca1ce6a426f70d4d037fc8e1c/filelock-4.0.12-py3-none-any.whl", hash = "sha256:5f17ee83ecee8a6f3e389c75822fb70a1c2f0506b99438a6dffa1d56793588c8", size = 111732, upload-time = "2026-10-05T13:17:42.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/06/4f138f618dbea66803291274f228f01daf29f306fe8b96bc30dab765df75/filelock-3.32.6-py3-none-any.whl", hash = "sha256:3f16ecd0117feae0dfc147e8c62eb5daeccd8bd800378c3ddf416de9b4feb6b1", size = 100189, upload-time = "2026-09-08T22:57:10.182Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3184,7 +3184,7 @@ requires-dist = [
|
||||
{ name = "drf-spectacular", specifier = "~=0.30" },
|
||||
{ name = "drf-spectacular-sidecar", specifier = ">=2026.7.1,<2026.10" },
|
||||
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
|
||||
{ name = "filelock", specifier = ">=3.32,<4.1" },
|
||||
{ name = "filelock", specifier = "~=3.32.0" },
|
||||
{ name = "flower", specifier = ">=2.0.1,<2.3" },
|
||||
{ name = "gotenberg-client", extras = ["httpx"], specifier = "~=1.0" },
|
||||
{ name = "granian", extras = ["uvloop"], marker = "extra == 'webserver'", specifier = ">=2.7,<2.9" },
|
||||
|
||||
Reference in new issue
Block a user