Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mmmmulti match #268

Merged
merged 24 commits into from
Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b682965
Update filtering to accept multiple -m flags, with implict AND
rktjmp Nov 6, 2022
4f25192
Update NoteFindOpts for multiple match clauses
rktjmp Nov 6, 2022
e01b321
Update (dead?) Notebook.FindMatching function
rktjmp Nov 6, 2022
abba103
Generate AND queries for filter.Match []string
rktjmp Nov 6, 2022
062b352
Update tesh test's for multi-match
rktjmp Nov 6, 2022
7740bbc
Remove dead comment
rktjmp Nov 6, 2022
c3c16a5
Drop superfluous copy in note_dao
rktjmp Nov 6, 2022
dcf4224
Adjust help doc
rktjmp Nov 9, 2022
1cf117e
Mention multi-match in note-filtering guide
rktjmp Nov 9, 2022
191953c
Update match help tesh output
rktjmp Nov 9, 2022
a50582b
Redate test output
rktjmp Nov 9, 2022
cf02918
Update LSP docs
rktjmp Nov 9, 2022
36fe07e
Document matchStrategy and deprecate matchExact in LSP docs
rktjmp Nov 9, 2022
3e21193
Format table
rktjmp Nov 9, 2022
a4b3bf3
Remove dead code in notebook#FindMinimalNote
rktjmp Nov 23, 2022
a800a7e
Add assert.Regexp test helper
rktjmp Nov 23, 2022
ddb3827
Add regexp testStringMatch helpr to handlebars tests
rktjmp Nov 23, 2022
5574400
Relax handlebars date helper tests for different timezones
rktjmp Nov 23, 2022
b1aa931
Adjust match help text
rktjmp Nov 23, 2022
84e2fde
Simplify parsedFilter.Match into f.Match with spread operator
rktjmp Nov 23, 2022
edb09ff
Update cmd-graph and cmd-list tesh tests for --match help text
rktjmp Nov 23, 2022
2923218
Revert handlebars date tests to string match with local tz+offset
rktjmp Nov 24, 2022
1ed2ea0
Merge branch 'main' into mmmmulti-match
mickael-menu Nov 27, 2022
29803bc
Update changelog
mickael-menu Nov 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
Format a date returned by `get-date`:
{{date (get-date "monday") "timestamp"}}
```
* `zk list` now support multiple `--match`/`-m` flags, which allows to search for several tokens appearing in any order in the notes (contributed by [@rktjmp](https://github.com/mickael-menu/zk/pull/268)).

### Fixed

Expand Down
49 changes: 25 additions & 24 deletions docs/editors-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,30 +184,31 @@ This LSP command calls `zk list` to search a notebook. It takes two arguments:
1. A path to any file or directory in the notebook, to locate it.
2. <details><summary>A dictionary of additional options (click to expand)</summary>

| Key | Type | Required? | Description |
|------------------|--------------|-----------|-------------------------------------------------------------------------|
| `select` | string array | Yes | List of note fields to return<sup>1</sup> |
| `hrefs` | string array | No | Find notes matching the given path, including its descendants |
| `limit` | integer | No | Limit the number of notes found |
| `match` | string | No | Terms to search for in the notes |
| `exactMatch` | boolean | No | Search for exact occurrences of the `match` argument (case insensitive) |
| `excludeHrefs` | string array | No | Ignore notes matching the given path, including its descendants |
| `tags` | string array | No | Find notes tagged with the given tags |
| `mention` | string array | No | Find notes mentioning the title of the given ones |
| `mentionedBy` | string array | No | Find notes whose title is mentioned in the given ones |
| `linkTo` | string array | No | Find notes which are linking to the given ones |
| `linkedBy` | string array | No | Find notes which are linked by the given ones |
| `orphan` | boolean | No | Find notes which are not linked by any other note |
| `related` | string array | No | Find notes which might be related to the given ones |
| `maxDistance` | integer | No | Maximum distance between two linked notes |
| `recursive` | boolean | No | Follow links recursively |
| `created` | string | No | Find notes created on the given date |
| `createdBefore` | string | No | Find notes created before the given date |
| `createdAfter` | string | No | Find notes created after the given date |
| `modified` | string | No | Find notes modified on the given date |
| `modifiedBefore` | string | No | Find notes modified before the given date |
| `modifiedAfter` | string | No | Find notes modified after the given date |
| `sort` | string array | No | Order the notes by the given criterion |
| Key | Type | Required? | Description |
| ------------------ | -------------- | ----------- | ------------------------------------------------------------------------- |
| `select` | string array | Yes | List of note fields to return<sup>1</sup> |
| `hrefs` | string array | No | Find notes matching the given path, including its descendants |
| `limit` | integer | No | Limit the number of notes found |
| `match` | string array | No | Terms to search for in the notes |
| `exactMatch` | boolean | No | (deprecated: use `matchStrategy`) Search for exact occurrences of the `match` argument (case insensitive) |
| `matchStrategy` | string | No | Specify match strategy, which may be "fts" (default), "exact" or "re" |
| `excludeHrefs` | string array | No | Ignore notes matching the given path, including its descendants |
| `tags` | string array | No | Find notes tagged with the given tags |
| `mention` | string array | No | Find notes mentioning the title of the given ones |
| `mentionedBy` | string array | No | Find notes whose title is mentioned in the given ones |
| `linkTo` | string array | No | Find notes which are linking to the given ones |
| `linkedBy` | string array | No | Find notes which are linked by the given ones |
| `orphan` | boolean | No | Find notes which are not linked by any other note |
| `related` | string array | No | Find notes which might be related to the given ones |
| `maxDistance` | integer | No | Maximum distance between two linked notes |
| `recursive` | boolean | No | Follow links recursively |
| `created` | string | No | Find notes created on the given date |
| `createdBefore` | string | No | Find notes created before the given date |
| `createdAfter` | string | No | Find notes created after the given date |
| `modified` | string | No | Find notes modified on the given date |
| `modifiedBefore` | string | No | Find notes modified before the given date |
| `modifiedAfter` | string | No | Find notes modified after the given date |
| `sort` | string array | No | Order the notes by the given criterion |

1. As the output of this command might be very verbose and put a heavy load on the LSP client, you need to explicitly set which note fields you want to receive with the `select` option. The following fields are available: `filename`, `filenameStem`, `path`, `absPath`, `title`, `lead`, `body`, `snippets`, `rawContent`, `wordCount`, `tags`, `metadata`, `created`, `modified` and `checksum`.

Expand Down
15 changes: 15 additions & 0 deletions docs/note-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ Change the currently used strategy with `--match-strategy <strategy>` (or `-M`).
list = "zk list --match-strategy re $@"
```

The `--match` option may be given multiple times, where each argument will be combined with a boolean AND.

For example,

```sh
$ zk list --tag "recipe" --match "pizza -pineapple" --match "mushrooms"
```

Is equivalent to,

```sh
$ zk list --tag "recipe" --match "(pizza -pineapple) AND (mushrooms)"
```


### Full-text search (`fts`)

The default match strategy is powered by a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database enabling near-instant results. Queries are not case-sensitive and terms are tokenized, which means that searching for `create` will also match `created` and `creating`.
Expand Down
5 changes: 3 additions & 2 deletions internal/adapter/handlebars/handlebars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,13 @@ func TestDateHelper(t *testing.T) {
testString(t, "{{date now 'timestamp'}}", context, "200911172034")
testString(t, "{{date now 'timestamp-unix'}}", context, "1258490098")
testString(t, "{{date now 'cust: %Y-%m'}}", context, "cust: 2009-11")
testString(t, "{{date now 'elapsed'}}", context, "13 years ago")
testString(t, "{{date now 'elapsed'}}", context, "14 years ago")
}

func TestGetDateHelper(t *testing.T) {
context := map[string]interface{}{"now": time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{get-date \"2009-11-17T20:34:58\"}}", context, "2009-11-17 20:34:58 +0000 UTC")
localOffsetAndTZ := time.Now().Format("-0700 MST")
testString(t, "{{get-date \"2009-11-17T20:34:58\"}}", context, "2009-11-17 20:34:58 "+localOffsetAndTZ)
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
}

func TestShellHelper(t *testing.T) {
Expand Down
25 changes: 14 additions & 11 deletions internal/adapter/sqlite/note_dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/fts5"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/paths"
strutil "github.com/mickael-menu/zk/internal/util/strings"
)
Expand Down Expand Up @@ -421,9 +420,7 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
}

// Expand the mention queries in the match predicate.
match := opts.Match.String()
match += " " + strings.Join(mentionQueries, " OR ")
opts.Match = opt.NewString(match)
opts.Match = append(opts.Match, " ("+strings.Join(mentionQueries, " OR ")+") ")
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved

return opts, nil
}
Expand Down Expand Up @@ -519,20 +516,26 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
return nil
}

if !opts.Match.IsNull() {
if 0 < len(opts.Match) {
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
switch opts.MatchStrategy {
case core.MatchStrategyExact:
whereExprs = append(whereExprs, `n.raw_content LIKE '%' || ? || '%' ESCAPE '\'`)
args = append(args, escapeLikeTerm(opts.Match.String(), '\\'))
for _, match := range opts.Match {
whereExprs = append(whereExprs, `n.raw_content LIKE '%' || ? || '%' ESCAPE '\'`)
args = append(args, escapeLikeTerm(match, '\\'))
}
case core.MatchStrategyFts:
snippetCol = `snippet(fts_match.notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20)`
joinClauses = append(joinClauses, "JOIN notes_fts fts_match ON n.id = fts_match.rowid")
additionalOrderTerms = append(additionalOrderTerms, `bm25(fts_match.notes_fts, 1000.0, 500.0, 1.0)`)
whereExprs = append(whereExprs, "fts_match.notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(opts.Match.String()))
for _, match := range opts.Match {
whereExprs = append(whereExprs, "fts_match.notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(match))
}
case core.MatchStrategyRe:
whereExprs = append(whereExprs, "n.raw_content REGEXP ?")
args = append(args, opts.Match.String())
for _, match := range opts.Match {
whereExprs = append(whereExprs, "n.raw_content REGEXP ?")
args = append(args, match)
}
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
break
}
}
Expand Down
23 changes: 19 additions & 4 deletions internal/adapter/sqlite/note_dao_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ func TestNoteDAOFindMinimalAll(t *testing.T) {
func TestNoteDAOFindMinimalWithFilter(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{
Match: opt.NewString("daily | index"),
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{{Field: core.NoteSortWordCount, Ascending: true}},
Limit: 3,
Expand Down Expand Up @@ -368,7 +368,7 @@ func TestNoteDAOFindTag(t *testing.T) {
func TestNoteDAOFindMatch(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{
Match: opt.NewString("daily | index"),
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
},
[]core.ContextualNote{
Expand Down Expand Up @@ -452,10 +452,25 @@ func TestNoteDAOFindMatch(t *testing.T) {
)
}

func TestNoteDAOFindMatchWithMultiMatch(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: []string{"daily | index", "second"},
MatchStrategy: core.MatchStrategyFts,
rktjmp marked this conversation as resolved.
Show resolved Hide resolved
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
},
},
[]string{
"log/2021-01-04.md",
},
)
}

func TestNoteDAOFindMatchWithSort(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: opt.NewString("daily | index"),
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
Expand All @@ -474,7 +489,7 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test := func(match string, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: opt.NewString(match),
Match: []string{match},
MatchStrategy: core.MatchStrategyExact,
},
expected,
Expand Down
12 changes: 4 additions & 8 deletions internal/cli/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/mickael-menu/zk/internal/core"
dateutil "github.com/mickael-menu/zk/internal/util/date"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/strings"
)

Expand All @@ -19,7 +18,7 @@ type Filtering struct {

Interactive bool `kong:"group='filter',short='i',help='Select notes interactively with fzf.'" json:"-"`
Limit int `kong:"group='filter',short='n',placeholder='COUNT',help='Limit the number of notes found.'" json:"limit"`
Match string `kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match"`
rktjmp marked this conversation as resolved.
Show resolved Hide resolved
Match []string `kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match"`
MatchStrategy string `kong:"group='filter',short='M',default='fts',placeholder='STRATEGY',help='Text matching strategy among: fts, re, exact.'" json:"matchStrategy"`
Exclude []string `kong:"group='filter',short='x',placeholder='PATH',help='Ignore notes matching the given path, including its descendants.'" json:"excludeHrefs"`
Tag []string `kong:"group='filter',short='t',help='Find notes tagged with the given tags.'" json:"tags"`
Expand Down Expand Up @@ -117,11 +116,7 @@ func (f Filtering) ExpandNamedFilters(filters map[string]string, expandedFilters
f.ModifiedAfter = parsedFilter.ModifiedAfter
}

if f.Match == "" {
f.Match = parsedFilter.Match
} else if parsedFilter.Match != "" {
f.Match = fmt.Sprintf("(%s) AND (%s)", f.Match, parsedFilter.Match)
}
f.Match = append(f.Match, parsedFilter.Match...)
if f.MatchStrategy == "" {
f.MatchStrategy = parsedFilter.MatchStrategy
}
Expand All @@ -148,7 +143,8 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
return opts, fmt.Errorf("the --exact-match (-e) option is deprecated, use --match-strategy=exact (-Me) instead")
}

opts.Match = opt.NewNotEmptyString(f.Match)
opts.Match = make([]string, len(f.Match))
copy(opts.Match, f.Match)
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
opts.MatchStrategy, err = core.MatchStrategyFromString(f.MatchStrategy)
if err != nil {
return opts, err
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/filtering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestExpandNamedFiltersNone(t *testing.T) {
Path: []string{"path1"},
Limit: 10,
Interactive: true,
Match: "match query",
Match: []string{"match query"},
Exclude: []string{"excl-path1", "excl-path2"},
Tag: []string{"tag1", "tag2"},
Mention: []string{"mention1", "mention2"},
Expand Down Expand Up @@ -156,7 +156,7 @@ func TestExpandNamedFiltersJoinLitterals(t *testing.T) {
func TestExpandNamedFiltersJoinMatch(t *testing.T) {
f := Filtering{
Path: []string{"f1", "f2"},
Match: "(chocolate OR caramel)",
Match: []string{"(chocolate OR caramel)"},
}

res, err := f.ExpandNamedFilters(
Expand All @@ -168,7 +168,7 @@ func TestExpandNamedFiltersJoinMatch(t *testing.T) {
)

assert.Nil(t, err)
assert.Equal(t, res.Match, "(((chocolate OR caramel)) AND (banana)) AND (apple)")
assert.Equal(t, res.Match, []string{"(chocolate OR caramel)", "banana", "apple"})
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
}

func TestExpandNamedFiltersExpandsRecursively(t *testing.T) {
Expand Down
4 changes: 1 addition & 3 deletions internal/core/note_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import (
"strings"
"time"
"unicode/utf8"

"github.com/mickael-menu/zk/internal/util/opt"
)

// NoteFindOpts holds a set of filtering options used to find notes.
type NoteFindOpts struct {
// Filter used to match the notes with the given MatchStrategy.
Match opt.String
Match []string
rktjmp marked this conversation as resolved.
Show resolved Hide resolved
// Text matching strategy used with Match.
MatchStrategy MatchStrategy
// Filter by note hrefs.
Expand Down
7 changes: 0 additions & 7 deletions internal/core/notebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,6 @@ func (n *Notebook) FindByHref(href string, allowPartialHref bool) (*MinimalNote,
})
}

// FindMatching retrieves the first note matching the given search terms.
func (n *Notebook) FindMatching(terms string) (*MinimalNote, error) {
return n.FindMinimalNote(NoteFindOpts{
Match: opt.NewNotEmptyString(terms),
})
}

// FindLinksBetweenNotes retrieves the links between the given notes.
func (n *Notebook) FindLinksBetweenNotes(ids []NoteID) ([]ResolvedLink, error) {
return n.index.FindLinksBetweenNotes(ids)
Expand Down
2 changes: 1 addition & 1 deletion tests/cmd-graph.tesh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ $ zk graph --help
>Filtering
> -i, --interactive Select notes interactively with fzf.
> -n, --limit=COUNT Limit the number of notes found.
> -m, --match=QUERY Terms to search for in the notes.
> -m, --match=QUERY,... Terms to search for in the notes.
> -M, --match-strategy=STRATEGY Text matching strategy among: fts, re, exact.
> -x, --exclude=PATH,... Ignore notes matching the given path,
> including its descendants.
Expand Down
Loading