Skip to content

Commit ae1524e

Browse files
authored
Merge pull request #3339 from obsidian-tasks-group/cache-user-search-data
spike: Experimentally cache data from query.allTasks calculations
2 parents dbd8bda + ad98209 commit ae1524e

File tree

6 files changed

+168
-4
lines changed

6 files changed

+168
-4
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Custom Filters With Complex Caching
2+
3+
> [!error] WARNING
4+
> These searches use a `query.searchCache` mechanism that is experimental, and may be changed or removed at any time: it is not recommended for use by users at this stage.
5+
6+
This is too complex an example for a general demonstration, or for the documentation.
7+
8+
But it was useful for confirming my (Clare's) understanding of the behaviour.
9+
10+
## Find tasks with duplicate descriptions - counts in group names differ from number of displayed
11+
12+
Show Tasks with more than one instance of the same description.
13+
14+
The counting is done across all tasks in the vault, including ones that do not match the Global Query, so some of the tasks counts in the groups are obviously wrong.
15+
16+
```tasks
17+
not done
18+
19+
filter by function { \
20+
const cacheKey = 'descriptionCountsAllTasks'; \
21+
const getDescription = (t) => t.descriptionWithoutTags; \
22+
if (!query.searchCache[cacheKey]) { \
23+
console.log('Computing and caching description counts...'); \
24+
const taskCounts = new Map(); \
25+
query.allTasks.forEach(t => { \
26+
const group = getDescription(t); \
27+
taskCounts.set(group, (taskCounts.get(group) || 0) + 1); \
28+
}); \
29+
query.searchCache[cacheKey] = taskCounts; \
30+
} \
31+
const taskCounts = query.searchCache[cacheKey]; \
32+
const group = getDescription(task); \
33+
const counts = taskCounts.get(group); \
34+
return counts > 1; \
35+
}
36+
37+
group by function { \
38+
const cacheKey = 'descriptionCountsAllTasks'; \
39+
const getDescription = (t) => t.descriptionWithoutTags; \
40+
const group = getDescription(task); \
41+
const counts = query.searchCache[cacheKey].get(group); \
42+
return `%%${1000000 - counts}%%` + group + " (" + (counts || 0) + " tasks)"; \
43+
}
44+
```
45+
46+
## Find tasks with duplicate descriptions - counts in group names match the number displayed
47+
48+
Show Tasks with more than one instance of the same description.
49+
50+
The counting is done only on tasks that match the query, by splitting it across the final two filter instructions in the search.
51+
52+
```tasks
53+
not done
54+
55+
# Count the number of instances of descriptions that match this query.
56+
# This must be the second-from-last filter in the query, to ensure the
57+
# counts are accurate.
58+
filter by function { \
59+
const cacheKey = 'descriptionCountsForMatchingTasks'; \
60+
if (!query.searchCache[cacheKey]) { \
61+
console.log('Initialising description counts map...'); \
62+
const taskCounts = new Map(); \
63+
query.searchCache[cacheKey] = taskCounts; \
64+
} \
65+
const group = task.descriptionWithoutTags; \
66+
taskCounts = query.searchCache[cacheKey]; \
67+
taskCounts.set(group, (taskCounts.get(group) || 0) + 1); \
68+
return true; \
69+
}
70+
71+
# Filter out the instances with fewer than 2 actual matches...
72+
# This must be the last filter in the query.
73+
filter by function { \
74+
const cacheKey = 'descriptionCountsForMatchingTasks'; \
75+
const group = task.descriptionWithoutTags; \
76+
const count = query.searchCache[cacheKey].get(group); \
77+
return count > 1; \
78+
}
79+
80+
group by function { \
81+
const cacheKey = 'descriptionCountsForMatchingTasks'; \
82+
const group = task.descriptionWithoutTags; \
83+
const count = query.searchCache[cacheKey].get(group); \
84+
return `%%${1000000 - count}%%` + group + " (" + (count || 0) + " tasks)"; \
85+
}
86+
```
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Custom Filters With Simple Caching
2+
3+
> [!error] WARNING
4+
> These searches use a `query.searchCache` mechanism that is experimental, and may be changed or removed at any time: it is not recommended for use by users at this stage.
5+
6+
## Find tasks with duplicate non-empty IDs - slow on many tasks
7+
8+
```tasks
9+
filter by function { \
10+
const thisValue = task.id; \
11+
if (thisValue === '') return false; \
12+
let count = 0; \
13+
query.allTasks.forEach(t => { \
14+
if (t.id === thisValue) count += 1; \
15+
}); \
16+
return (count > 1); \
17+
}
18+
19+
group by id
20+
```
21+
22+
## Find tasks with duplicate non-empty IDs - fast on many tasks
23+
24+
```tasks
25+
filter by function { \
26+
const cacheKey = 'idCounts'; \
27+
const getValue = (task) => task.id; \
28+
if (!query.searchCache[cacheKey]) { \
29+
const taskCounts = new Map(); \
30+
query.allTasks.forEach(t => { \
31+
const group = getValue(t); \
32+
taskCounts.set(group, (taskCounts.get(group) || 0) + 1); \
33+
}); \
34+
query.searchCache[cacheKey] = taskCounts; \
35+
} \
36+
const taskCounts = query.searchCache[cacheKey]; \
37+
const value = getValue(task); \
38+
if (value === '') return false; \
39+
const count = taskCounts.get(value); \
40+
return count > 1; \
41+
}
42+
43+
group by id
44+
```

src/Query/SearchInfo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ export class SearchInfo {
1414
*/
1515
public readonly allTasks: Readonly<Task[]>;
1616
public readonly tasksFile: OptionalTasksFile;
17+
private readonly _queryContext: QueryContext | undefined;
1718

1819
public constructor(tasksFile: OptionalTasksFile, allTasks: Task[]) {
1920
this.tasksFile = tasksFile;
2021
this.allTasks = [...allTasks];
22+
this._queryContext = this.tasksFile ? makeQueryContextWithTasks(this.tasksFile, this.allTasks) : undefined;
2123
}
2224

2325
public static fromAllTasks(tasks: Task[]): SearchInfo {
@@ -35,6 +37,6 @@ export class SearchInfo {
3537
* @return A QueryContext, or undefined if the path to the query file is unknown.
3638
*/
3739
public queryContext(): QueryContext | undefined {
38-
return this.tasksFile ? makeQueryContextWithTasks(this.tasksFile, this.allTasks) : undefined;
40+
return this._queryContext;
3941
}
4042
}

src/Scripting/QueryContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface QueryContext {
2222
query: {
2323
file: TasksFile;
2424
allTasks: Readonly<Task[]>;
25+
searchCache: Record<string, any>; // Added caching capability
2526
};
2627
}
2728

@@ -54,6 +55,7 @@ export function makeQueryContextWithTasks(tasksFile: TasksFile, allTasks: Readon
5455
query: {
5556
file: tasksFile,
5657
allTasks: allTasks,
58+
searchCache: {}, // Added for caching
5759
},
5860
};
5961
}

tests/Query/SearchInfo.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { TaskBuilder } from '../TestingTools/TaskBuilder';
33
import { TasksFile } from '../../src/Scripting/TasksFile';
44

55
describe('SearchInfo', () => {
6+
const path = 'a/b/c.md';
7+
const tasksFile = new TasksFile(path);
8+
69
it('should not be able to modify SearchInfo.allTasks directly', () => {
710
const tasks = [new TaskBuilder().build()];
811
const searchInfo = SearchInfo.fromAllTasks(tasks);
@@ -25,16 +28,13 @@ describe('SearchInfo', () => {
2528
});
2629

2730
it('should provide access to query search path', () => {
28-
const path = 'a/b/c.md';
29-
const tasksFile = new TasksFile(path);
3031
const searchInfo = new SearchInfo(tasksFile, []);
3132

3233
expect(searchInfo.tasksFile).toEqual(tasksFile);
3334
expect(searchInfo.tasksFile?.path).toEqual(path);
3435
});
3536

3637
it('should create a QueryContext from a known path', () => {
37-
const tasksFile = new TasksFile('a/b/c.md');
3838
const searchInfo = new SearchInfo(tasksFile, []);
3939

4040
const queryContext = searchInfo.queryContext();
@@ -50,4 +50,12 @@ describe('SearchInfo', () => {
5050

5151
expect(queryContext).toBeUndefined();
5252
});
53+
54+
it('should give the same QueryContext on successive calls, for caching data', () => {
55+
const searchInfo = new SearchInfo(tasksFile, [new TaskBuilder().build()]);
56+
57+
searchInfo.queryContext()!.query.searchCache['saved'] = 1;
58+
59+
expect(searchInfo.queryContext()!.query.searchCache['saved']).toBe(1);
60+
});
5361
});

tests/Scripting/QueryContext.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,27 @@ describe('QueryContext', () => {
5555
// Assert
5656
expect(group).toEqual(['1']);
5757
});
58+
59+
it('query.searchCache should be empty initially', () => {
60+
// Arrange
61+
const searchInfo = new SearchInfo(tasksFile, [task]);
62+
const queryContext = searchInfo.queryContext();
63+
64+
expect(queryContext?.query?.searchCache).toEqual({});
65+
});
66+
67+
it('query.searchCache should cache a value', () => {
68+
// Arrange
69+
const searchInfo = new SearchInfo(tasksFile, [task]);
70+
const queryContext = searchInfo.queryContext();
71+
expect(queryContext).not.toBeNull();
72+
const cacheKey = 'function1';
73+
74+
// Act
75+
queryContext!.query.searchCache[cacheKey] = 1;
76+
77+
// Assert
78+
expect(queryContext!.query.searchCache[cacheKey]).toEqual(1);
79+
});
5880
});
5981
});

0 commit comments

Comments
 (0)