Skip to content

feat(search)!: hybrid search_notes — lexical leg, mode/effort axes#53

Merged
AlexMost merged 18 commits into
mainfrom
worktree-hybrid-search-notes
Jul 6, 2026
Merged

feat(search)!: hybrid search_notes — lexical leg, mode/effort axes#53
AlexMost merged 18 commits into
mainfrom
worktree-hybrid-search-notes

Conversation

@AlexMost

@AlexMost AlexMost commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Makes search_notes hybrid: it now runs a semantic leg (embedding similarity) and a new lexical (exact-match) leg over note titles, headings, and body — curing the tool's worst failure mode (silent semantic noise on exact terms/names/codes, or a cold/absent corpus) without the agent having to re-query.

OpenSpec change: hybrid-search-notes (archived at openspec/changes/archive/2026-07-05-hybrid-search-notes/; specs synced into openspec/specs/hybrid-search/ + mcp-tool-surface/).

⚠️ Breaking changes (ships as a major)

  • Response key resultssemantic_matches; new sibling lexical_matches (grouped per note).
  • Input axis split: old mode: "quick" | "deep" is rejected (no alias). Now two orthogonal axes:
    • mode: "hybrid" | "lexical" (default hybrid) — which legs run.
    • effort: "quick" | "deep" (default quick) — result volume (absorbs the old mode values).
  • MCP parameter dictionary gains effort, redefines mode (ADR-0005 satisfied by the major bump).

What's inside

  • New lexical stack under src/lib/obsidian/lexical/: normalize (case/NFKD/apostrophe-unification incl. Ukrainian) → blocks (mdast AST blocks with line positions) → match (AND-substring + phrase) → rank (six deterministic tiers → density → backlink_count → path) → snippet (grapheme-safe window), fronted by a per-vault LexicalIndex with an mtime cache.
  • Lexical leg is fully embeddings-independent: mode: "lexical" never touches the corpus loader; hybrid on a cold/absent corpus returns lexical matches instead of throwing; an available corpus that errors still surfaces DEPENDENCY_ERROR.
  • filter, multi-query merge (matched_queries), and multi-vault fan-out all bind both legs identically.
  • New dependency: mdast-util-from-markdown (MIT) + @types/mdast (dev) — first markdown parser in the repo.
  • Docs restructured by intent (finding-notes.md / reading-and-modifying.md), routing.md rewritten around one search entry point, new docs/architecture/lexical-search.md, parameter dictionary updated.

Verification

  • npm test — 758 passing (706 → 758, +52), npm run lint clean, npx tsc --noEmit clean, npm run build OK.
  • openspec validate --all — all valid.
  • Executed via subagent-driven-development: 11 TDD tasks, per-task code review, a final whole-branch review (0 Critical/Important), verify (PASS) + retrospective + archive all included in this branch.

Release

Do not release from this branch — after merge, run npm run release on main (major version bump; breaking notes ride the feat(search)!: commit).

🤖 Generated with Claude Code

AlexMost added 18 commits July 5, 2026 23:04
…_notes

BREAKING CHANGE: search_notes now takes mode: 'hybrid'|'lexical' (retrieval
channel) and effort: 'quick'|'deep' (replacing the old mode: 'quick'|'deep');
old mode values are rejected. Response results key renamed to
semantic_matches, with a new lexical_matches placeholder (always [] until
the lexical leg is wired up).
Add a real-vault (FsVaultReader + LexicalIndex) e2e test covering
intersection between legs, Ukrainian apostrophe+case folding, filter
binding both legs, and lexical-only mode on a cold corpus. Extract
makeLexicalVault/makeMockEngine into a shared _hybrid-helpers module
(rather than importing across .test.ts files) and extend it with
sources/engine/listMatchingPaths overrides. Close gaps found during
the spec self-check: hybrid-mode effort->lexical-cap sizing,
mode:lexical never touching an available corpus, inflected-substring
matching, hard-wrapped phrase and code-fence-heading matching through
the full rank pipeline, and reproducible ordering across repeated
searches.
@AlexMost
AlexMost force-pushed the worktree-hybrid-search-notes branch from 13c9f4d to 41cb146 Compare July 6, 2026 09:41
@AlexMost
AlexMost merged commit 11a1878 into main Jul 6, 2026
2 checks passed
@AlexMost
AlexMost deleted the worktree-hybrid-search-notes branch July 6, 2026 09:48
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