Skip to content

feat(search): answer filter-only queries and match repo: by name - #22

Merged
hsusul merged 1 commit into
mainfrom
search-filter-only
Oct 26, 2026
Merged

hsusul merged 1 commit into
mainfrom
search-filter-only

Conversation

@hsusul

@hsusul hsusul commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Why

The rebuilt UI puts the query syntax front and center. Two gaps made the search line feel broken.

  • A query of filters alone, like has:error or agent:codex, returned an empty page.
  • repo: matched only an internal repository id. Typing repo:lore matched nothing.

What changed

  • A query with filters but no plain terms now returns one SearchHit per matching session, newest first.
    • It uses the same keyset pagination, and next_cursor appears only on a full page.
    • SortOrder::Relevance falls back to newest. Oldest is honoured. The doc comment on search_page says so.
    • A query with neither terms nor filters still returns an empty page. So do filters that parse to nothing, such as agent: or git-source:bogus.
  • Filter-only hit shape, with the DTOs in lore-ipc unchanged:
    • The hit describes the session by its first redacted search_document. That is the native title if there is one (session / title), otherwise the opening message (message_part).
    • snippet is that redacted text, whitespace-collapsed and cut to 160 characters, with no highlight markers.
    • rank is 0. A session with no projected text gets session / title and an empty snippet.
    • The cursor is keyed on the session id, so it has its own shape. A cursor from the other kind of query decodes to nothing and gives the first page.
  • repo: now accepts a repository id, a display name, or the last component of primary_path.
    • Names match case-insensitively. A name shared by several repositories matches all of them.
    • An unknown name matches nothing. The literal value is always kept as an id candidate, so the id form works as before.
    • Resolution is plain string equality in Rust, not LIKE. The candidate ids go to SQL as one JSON array parameter.
  • The filter SQL is now one shared function used by both paths. The term path builds the same SQL as before.

Scope

  • crates/lore-core/src/search.rs
  • crates/lore-core/tests/search.rs
  • No change to lore-ipc, query.rs, ingest.rs, migrations, adapters, src/, or src-tauri/.

Blast Radius

  • The UI search line, lorectl search, and the lorectl mcp search tool all call search_page. A filter-only query now lists sessions in each of them instead of returning nothing.
  • Term queries are unchanged in behavior, except that repo:<name> now resolves names.
  • The security properties are unchanged.
    • Every filter value is a bound parameter.
    • FTS input is still quoted phrases.
    • Filter-only snippets come from search_document.redacted_text, never from raw message text.
  • repo: reads the whole repository table once per query that uses it. That table has one row per repository.

Verification

  • cargo fmt --all -- --check: passed.
  • cargo clippy -p lore-core -p lore-ipc -p lorectl --all-targets --all-features -- -D warnings: passed.
  • cargo test -p lore-core --test search: 40 passed, 0 failed. 14 of the 40 are new. Four new unit tests cover the helpers.
  • cargo test --workspace: exit 0, no failures.
  • Mutation check: with filter-only disabled, 12 of the new tests fail. With name resolution disabled, 5 fail.
  • End to end on a synthetic archive (scripts/synth-archive.sh /tmp/sf-homes 30 20 7, then lorectl scan):
    • agent:codex lists the 20 Codex sessions. agent:claude-code fills the CLI's 20-row page.
    • has:error reports no matches, because the synthetic archive has no failed tool calls. The tests cover it.
    • The synthetic archive has no repositories, so repo: was checked on a copy with one repository row and its link rows added by SQL. repo:repo_demo, repo:loreapp and repo:DEMO-display each returned the same 7 sessions. repo:nope and repo:* returned none.
    • Real ~/.claude, ~/.codex and the default archive were not touched.
  • Not run: the desktop UI and lorectl mcp end to end.

A query with filters but no plain terms used to return an empty page, so
`has:error` or `agent:codex` alone showed "no results". It now returns one
hit per matching session, newest first, under the same keyset pagination.
Relevance falls back to newest; Oldest is honoured. A query with neither
terms nor filters still returns nothing.

`repo:` previously matched only an internal repository id. It now also
matches a repository's display name and the last component of its root
path, case-insensitively. A name shared by several repositories matches all
of them, and an unknown name matches nothing.

Filter-only hits describe the session from its first redacted projection
document, never from raw session text. All filter values stay bound
parameters; the repo: candidates travel as one JSON array parameter.
@hsusul
hsusul merged commit 0176b21 into main Oct 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant