Commit Graph

68 Commits

Author SHA1 Message Date
kolaente cefa42da86 refactor(search): limit BM25 relevance ranking to pure-text searches
Rank ParadeDB search results by BM25 relevance only for pure-text searches
over a plain project scope. Numeric searches (the `OR index = N` branch) and
the Favorites view (the `id IN (<subquery>)` scope) keep the default ordering
(unranked, as on main): pdb.score rejects both as unsupported query shapes, and
the contortions previously needed to score them (two-arm numeric merge with
in-memory pagination, a favorites LEFT JOIN) added far more complexity than the
ranking was worth. Neither path was ranked before this PR, so leaving them at
the default order is no regression.
2026-06-21 18:49:41 +02:00
kolaente d93e98f76b fix(search): qualify the task index column to avoid ambiguity with the parent-task join 2026-06-19 23:14:51 +02:00
kolaente 116fb1e2e0 fix(search): rank exact task-index match before BM25 text relevance on ParadeDB
The BM25 relevance ranking added `pdb.score(tasks.id)` to the search SELECT
and ORDER BY. ParadeDB can only compute a score for a pure-ParadeDB query
shape, so two cases produced "pq: Unsupported query shape":

1. A numeric search (e.g. "#17") OR's the ParadeDB `|||` operators with a
   plain `"index" = N` equality in the same boolean group. Scoring that mixed
   group is unsupported.
2. When favorites are in scope, the `project_id IN (...) OR id IN (<favorites
   subquery>)` predicate is unsupported under pdb.score regardless of how the
   subquery is expressed (OR or UNION) - it just was never exercised because
   the ranking tests searched a single project with no favorites.

Both are now handled so each query ParadeDB scores is a supported shape:

- Numeric search runs as two arms: an exact `index = N` arm (no score, ranked
  first) and a text `|||` arm scored by pdb.score DESC. The arms are merged in
  Go (index matches first, deduped by task id) and paginated in memory; the
  count query keeps the combined `OR index = N` predicate (no score), which is
  a supported shape, so totalItems stays correct.
- The relevance arms reach favorites through a LEFT JOIN and scope on the
  joined column (`rank_favorites.entity_id IS NOT NULL`) instead of an
  id-IN-subquery, which ParadeDB can score.

Non-numeric (pure text) searches keep the single pdb.score-ordered query.
Non-ParadeDB databases are unchanged (no pdb.score, no ranking).

TestTaskSearchRelevanceRankingNumericIndex covers the numeric case: on
ParadeDB the exact-index task ranks first, then text matches by relevance; on
other databases it only asserts the matches are returned.

Validated against the CI-pinned ParadeDB image (paradedb 0.21.12): the full
pkg/models and pkg/webtests suites pass, including
TestTaskCollection_ReadAll/search_for_task_index and the HTTP search tests.
2026-06-19 22:52:26 +02:00
kolaente 9fb0d86c1b feat(search): rank ParadeDB search results by BM25 relevance (#2690)
When ParadeDB is in use and a search is run, results now keep the current
fuzzy/OR matching but are ordered by BM25 relevance so tasks matching all
query words rank above tasks matching only some.

Details:
- ParadeDB exposes the BM25 score via pdb.score(<key_field>); Vikunja's
  key_field is id, so we order by pdb.score(tasks.id) DESC, then the
  existing order-by (ending in a stable tasks.id tiebreak).
- Gating: relevance ordering only applies when ParadeDB is available, a
  search term is present, AND the user did not pass an explicit sort_by.
  An explicit user sort still wins; relevance only replaces the default
  (id / position) sort.
- DISTINCT requires every ORDER BY expression to appear in the SELECT
  list, so pdb.score(tasks.id) is added to the selected columns too (for
  both the plain and task_positions-join query shapes). Because xorm's
  Distinct() quotes each column and corrupts the function call, the
  ranking path uses Select(rawColumns).Distinct() instead.
- ParadeDB-only by nature: pdb.score is invalid SQL on sqlite, mysql and
  plain postgres, so those paths are completely unchanged.

A test (TestTaskSearchRelevanceRanking) creates a task matching all query
words plus tasks matching only one, then searches a multi-word query. On
ParadeDB it asserts the all-words task ranks first; on other databases it
only asserts the matching tasks are returned, so it stays green across the
whole CI database matrix. The CI ParadeDB matrix entry exercises the
ranking assertion.

Follow-up (not in this change): boosting results where the words appear in
order / in close proximity above plain all-words matches.

Fixes #2690
2026-06-19 20:46:28 +02:00
kolaente e7b693c788 fix(tasks): include tasks with deleted parents in subtask-expanded queries
When expand[]=subtasks is used, the LEFT JOIN on task_relations filters
out same-project subtasks. If a parent task was deleted, the JOIN
produces NULL for parent_tasks.id/project_id, and since NULL != value
evaluates to NULL (not TRUE) in SQL, these tasks were incorrectly
excluded.

Add builder.IsNull{"parent_tasks.id"} to the OR condition so tasks
whose parent was deleted are still included in results.
2026-04-02 16:30:23 +00:00
kolaente a5b1a90c42 refactor: remove typesense support
Typesense was an optional external search backend. This commit fully
removes the integration, leaving the database searcher as the only
search implementation.

Changes:
- Delete pkg/models/typesense.go (core integration)
- Delete pkg/cmd/index.go (CLI command for indexing)
- Simplify task search to always use database searcher
- Remove Typesense event listeners for task sync
- Remove TypesenseSync model registration
- Remove Typesense config keys and defaults
- Remove Typesense doctor health check
- Remove Typesense initialization from startup
- Clean up benchmark test
- Add migration to drop typesense_sync table
- Remove golangci-lint suppression for typesense.go
- Remove typesense-go dependency
2026-02-25 12:15:28 +01:00
kolaente 1943d6993c fix: only merge range comparators in sub-table filter grouping
Restrict the AND-joined sub-table filter merging to range comparators
(>, >=, <, <=) only. Equality and negative comparators (=, !=, in,
not in) must remain as separate EXISTS/NOT EXISTS subqueries because
each matching value lives in its own row.

Merging equality filters like `labels = 4 && labels = 5` into a single
EXISTS would produce an unsatisfiable condition (no single row has
label_id=4 AND label_id=5). Merging negative filters like
`labels != 4 && labels != 5` into NOT EXISTS(label_id IN 4 AND
label_id IN 5) would be trivially true.

Also fix the join tracking to use the first filter's join type
(how the group connects to the previous element) instead of the last.
2026-02-19 12:40:29 +01:00
kolaente c034e431cb fix: merge AND-joined sub-table filters into single EXISTS subquery
When multiple AND-joined filter conditions target the same sub-table
(e.g., reminders > X && reminders < Y), they are now combined into
a single EXISTS subquery so that all conditions must be satisfied by
the same row. Previously, each condition generated a separate EXISTS
subquery that could match different rows, causing false positives.

Fixes #2245
2026-02-19 12:40:29 +01:00
Mattia Maglie 8779a28d1d
fix: prevent duplicated sql condition in filters (#1546)
Proposing the fix as in #1545

Co-authored-by: mattia.maglie <mattia.maglie@alispa.com>
Co-authored-by: kolaente <k@knt.li>
2026-02-18 17:02:25 +01:00
kolaente 3db1ddcee4 feat(tasks): add support for ParadeDB when searching tasks 2025-07-18 23:18:04 +02:00
kolaente 9b579d282c fix(tasks): do not return subtasks multiple times when they are related
to multiple tasks

Resolves https://community.vikunja.io/t/tasks-with-parent-tasks-appear-duplicated-in-list-views/3676
2025-07-02 23:16:05 +02:00
kolaente c6c18d1ca1
fix(task): ambiguous description search (#1032)
test: relocate search regression
2025-06-26 20:05:04 +00:00
kolaente 53264d350e
fix(kanban): make bucket query fixed per-view (#1007) 2025-06-25 11:38:24 +00:00
kolaente f9b31ab4bf
fix: ambiguous title column in task search (#1012) 2025-06-24 20:38:35 +00:00
kolaente 4f99bdb50c
fix(tasks): ambiguous done column in task sorting (#1011)
fix: prefix task fields in sorting
2025-06-24 20:37:25 +00:00
kolaente 49963d88a6
fix(task): add tasks table prefix for sort order (#1003)
fix: qualify task index for mysql order
2025-06-24 09:55:00 +00:00
kolaente 048e09ee12
fix(tasks): subtasks missing in list view (#1000) 2025-06-24 11:08:01 +02:00
Dominik Pschenitschni 296577a875
fix: correct license header references (#882)
See originals:
- https://www.gnu.org/licenses/agpl-3.0.txt
- https://www.gnu.org/licenses/gpl-3.0.txt
2025-06-10 12:18:38 +02:00
Vladimir c3fffefcf4
fix(db): refactor filtering with subqueries (#701)
Resolves #285
2025-05-12 10:52:48 +02:00
kolaente 7f6cb1e06e
feat: expand buckets 2025-01-24 11:03:40 +01:00
kolaente 6673adf0b5
fix(filter): do not show tasks in filter results when they are filtered out by labels
This fixes a bug where tasks which were filtered out by their label would still be shown. That was caused by the way the filter query was translated to sql under the hood.

Resolves https://github.com/go-vikunja/vikunja/issues/394
2025-01-20 14:05:42 +01:00
kolaente feb6b708e3
fix(tasks): prefix created and updated columns when sorting by them
Resolves https://github.com/go-vikunja/vikunja/issues/350
2024-11-18 12:07:29 +01:00
kolaente b9366ca9e3
fix(filter): use correct syntax for not in query in typesense
Related to https://github.com/typesense/typesense/issues/2038
2024-10-29 17:23:16 +01:00
kolaente 57c6f2cd10
feat(filters): add "not in" operator for filters
Resolves https://community.vikunja.io/t/feature-requests-regarding-view-ordering-project-reference-in-kanban-and-a-notin-operator/2728
2024-10-29 12:03:16 +01:00
kolaente 3ad5797307
fix: do not prefix tasks all the time
Fixes a regression from 29b9001078
2024-10-21 10:32:14 +02:00
kolaente d81f2db6ef
chore: replace all uses of bucket_id with the const 2024-10-02 10:43:51 +02:00
kolaente 425f6378c6
fix(kanban): make loading tasks for a bucket work
This fixes two closely-related bugs:
1. When loading tasks from a bucket of a saved filter, the saved filter query would override the user-supplied filter, which would cause to only tasks matching the saved filter query to be returned.
2. When a filter query for a bucket was specified, the function would only check if one of the top level filters was a filter for tasks in a specific bucket. That means a filter like "bucket_id = 42 && labels = foo" would return the expected result, while a filter like "labels = foo && (bucket_id = 42 && priority = 1)" would fail with an error 500 because the task_buckets table was not joined to the sql query. The fix from the first bug caused such filter queries.
2024-10-02 10:41:38 +02:00
kolaente 754d56ca82
fix(filters): increase year value when using mysql and year < 1
Mysql cannot handle year values < 1. That means filtering for a date value like 0000-01-01 won't work with mysql. Additionally, dates like 0001-01-01 could under some circumstances not work either when the date in combination with the time zone would resolve to something like 0000-12-31 - for example when the server is located (and configured) in UTC, but the user running the query is in New York. This could be observed by setting the time zone manually using the filter_timezone query parameter.

Resolves https://vikunja.sentry.io/share/issue/42bce92c15354c109eb1e6488b6a542b/
Resolves https://vikunja.sentry.io/share/issue/ef81451b0c7b43f1bff2d3a86ba393bb/
2024-09-30 08:41:24 +02:00
kolaente d22bc79d3c
chore(tasks): add more details to error message
Trying to debug https://vikunja.sentry.io/share/issue/ef81451b0c7b43f1bff2d3a86ba393bb/
2024-09-29 21:44:28 +02:00
kolaente 1fe5e9cc55
chore(tasks): add more details to error message
Trying to debug https://vikunja.sentry.io/share/issue/ef81451b0c7b43f1bff2d3a86ba393bb/
2024-09-29 21:05:55 +02:00
kolaente c54181eeda
fix(deps): update module github.com/typesense/typesense-go to v2 2024-09-19 15:30:13 +02:00
kolaente 2063da9eec
chore(web): move web handler package to Vikunja 2024-08-29 16:15:28 +02:00
kolaente 2982acc0ec
fix(tasks): limit to max 250 entries when using typesense
Resolves https://community.vikunja.io/t/typesense-250-hits-limit/2536/8
2024-07-11 10:06:39 +02:00
kolaente 2c6bc7742f
fix(tasks): explicitly add task position to select statement when looking up tasks with Typesense
Resolves https://kolaente.dev/vikunja/vikunja/issues/2546
Resolves https://community.vikunja.io/t/version-0-24-0-internal-server-error-breaking-change/2558/5
2024-07-11 09:28:22 +02:00
kolaente d3f397b036
fix(tasks): do not crash when order by id and position 2024-06-13 16:36:02 +02:00
kolaente acf4e3aa18
fix(tasks): ambiguous column name error when fetching favorite tasks 2024-06-06 21:50:30 +02:00
kolaente e56b2232bb
fix(typesense): do not try to sort by position when searching in a saved filter
This change fixes a bug where Typesense would try to sort by the project view of a saved filter. The view position is not indexed in Typesense, hence filtering fails. Because sorting by position is not a feature in saved filters, I've removed the logic for sorting saved filters with Typesense.
2024-06-05 10:24:28 +02:00
kolaente 506ce66434
fix(typesense): correctly join task position table when sorting by it
This change fixes a bug where the project view to use for joining was empty, since Typesense only supports 3 sorting parameters. When using more than that, the logic to fetch the view ID parameter would not return the correct parameter, but the logic building the order by statement would. That led to inconsistencies where the task position was included in the order by statement, but the table would not be joined, failing the query.
2024-06-05 09:54:55 +02:00
kolaente b386dfae87
fix(typesense): only return distinct tasks once 2024-06-04 18:22:05 +02:00
kolaente 5fc4ec48b7
fix(typesense): correctly incorporate existing filter when it is empty 2024-06-04 18:22:05 +02:00
kolaente 48676050d7 feat(tasks): expand subtasks (#2345)
This change adds a parameter to expand subtasks - if provided, Vikunja will ensure all subtasks are present in the results list.

Resolves https://community.vikunja.io/t/subtasks-show-on-different-pages/2292
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2345
Co-authored-by: kolaente <k@knt.li>
Co-committed-by: kolaente <k@knt.li>
2024-06-04 10:27:23 +00:00
kolaente ecbefdb921
fix(buckets): return correct task count for tasks in buckets 2024-04-14 17:21:53 +02:00
kolaente 77e95642a9
fix(tasks): make fetching tasks in buckets via typesense work 2024-04-13 17:52:47 +02:00
kolaente 3519b8b2fe
fix(tasks): index and order by task position when using typesense 2024-04-13 17:19:27 +02:00
kolaente fa137b1ffc
fix(views): include order by fields in distinct clause when sorting by task position 2024-03-19 17:05:12 +01:00
kolaente 004f1e06bb
fix(views): do not return kanban tasks multiple times 2024-03-19 00:47:50 +01:00
kolaente 398c9f1056
fix(views): return tasks in their buckets 2024-03-19 00:47:48 +01:00
kolaente ee6ea03506
feat(views): sort by position 2024-03-19 00:47:47 +01:00
kolaente 43f24661d7
feat(views): save view and position in Typesense 2024-03-19 00:47:47 +01:00
kolaente d1d07f462c
feat(views): sort tasks by their position relative to the view they're in 2024-03-19 00:47:46 +01:00