Skip to content

feat: hybrid search — FTS5 + sqlite-vec + RRF fusion (Phase 2a)#228

Merged
aliasunder merged 7 commits into
mainfrom
phase-2
Jun 30, 2026
Merged

feat: hybrid search — FTS5 + sqlite-vec + RRF fusion (Phase 2a)#228
aliasunder merged 7 commits into
mainfrom
phase-2

Conversation

@aliasunder

Copy link
Copy Markdown
Owner

Summary

  • Hybrid searchvault_search now combines FTS5 keyword ranking with sqlite-vec vector similarity via Reciprocal Rank Fusion (RRF). Keywords stay precise on exact terms; vectors bridge vocabulary gaps. 30% of natural-language queries that failed on FTS-only now return relevant results.
  • Embedding pipeline — bge-small-en-v1.5 (~25MB ONNX) runs in-process, no external API. Content-hash gating skips unchanged chunks. Vector tables persist across restarts.
  • Progressive enhancementEMBEDDING_ENABLED=false disables the pipeline entirely. Each query falls back to FTS-only when vectors aren't available. search_mode response field tells clients which ranking was used.
  • Dockerfile Debian slim — Alpine → node:24-slim for glibc (onnxruntime-node requirement)
  • Search module decomposition — search-index.ts split into 5 focused modules (queries, helpers, FTS sanitization, RRF scoring)
  • TOCTOU fix — shared file-write-lock extracted, applied to all read-modify-write ops
  • Docs — README Hybrid Search section, ARCHITECTURE Phase 2 diagrams + decision table entries, social preview updated

PRs included

Test plan

  • 1219 tests passing
  • Deployed and live-verified on Lightsail (semantic search working)
  • FTS-only fallback verified with EMBEDDING_ENABLED=false
  • Content-hash gating verified (restart with no changes → 0 re-embeddings)

🤖 Generated with Claude Code

aliasunder and others added 7 commits June 29, 2026 20:25
- **Embedding pipeline** for Phase 2 hybrid search — notes are chunked,
embedded via bge-small-en-v1.5 (q8, 384-dim), and stored in sqlite-vec
with content-hash gating
- **New modules:** `embedder.ts` (factory-closure, lazy ONNX model
loading), `chunker.ts` (heading-aware splitting with paragraph
sub-split), `plaintext.ts` (Obsidian markdown → plain text)
- **Search index** gains vector tables (`note_chunks` + `note_vectors`),
three-pass rebuild (Pass 3 = embedding), `embedNote` for incremental
updates via file watcher
- **`EMBEDDING_ENABLED`** env var (default: `true`) — when `false`, no
model download, no vector tables, FTS5-only search

Does **not** change `vault_search` behavior — that's PR \#3 (hybrid
search with RRF fusion).

- [x] 1130 tests pass (53 new: 10 embedder, 15 chunker, 15 plaintext, 8
search-index embedding, 5 config)
- [x] TypeScript compiles clean (`tsc --noEmit`)
- [x] Docker builds successfully
- [x] Content-hash gating verified: re-embedding same content skips all
chunks
- [x] `EMBEDDING_ENABLED=false` verified: no vector tables created,
embedNote is no-op
- [x] removeNote cleans up vectors + chunks
- [x] rebuildFromVault Pass 3 embeds all notes
- [ ] Deploy to Lightsail via `phase2_deploy.yml` and verify
search/memory/reads still work

BREAKING CHANGE: Search index factory `createSearchIndex` gains an
optional second parameter (`embedder`). Existing callers are unaffected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JPDgHMKBvJ51V2YrrMYZET

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **New Features**
* Added optional embedding-based search support, with a new setting to
turn it on or off.
* Notes are now chunked and embedded to improve search quality, while
keeping basic search available as a fallback.
* Updated container support to use a Debian-based image for broader
compatibility.

* **Documentation**
* Updated setup and deployment guidance to explain the new search
configuration option.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ately (#218)

- **Server blocked for ~5 min on first startup** while embedding every
note — FTS was ready but the server didn't accept requests until Pass 3
finished
- `rebuildFromVault` now returns after FTS indexing (Passes 1+2) and
fires embedding (Pass 3) in the background
- Return type changes from `number` to `{ count, embedding }` — server
ignores the promise, tests can await it

- [x] 1131 tests pass
- [x] TypeScript compiles clean
- [x] Embedding test awaits the background promise to verify embeddings
complete
- [ ] Deploy and verify server responds to health check within seconds
of startup (not minutes)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JPDgHMKBvJ51V2YrrMYZET

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **New Features**
* Added `EMBEDDING_ENABLED` feature flag (default: `true`) to disable
the embedding pipeline and run search using FTS5 only.
* Added `MEMORY_ENABLED` feature flag (default: `true`) to the
local/remote compose setups.
* **Bug Fixes**
* Improved incremental indexing so unchanged content isn’t re-embedded
and rebuilds preserve existing vector data.
* Fixed missed re-embeds when an earlier embedding attempt fails,
ensuring subsequent updates still proceed.
* **Documentation / Tests**
* Updated indexing documentation and environment-variable references;
expanded search/indexing test coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- Add hybrid search (FTS5 + sqlite-vec vector + RRF fusion) to
`vault_search` tool
- Transparent upgrade: uses hybrid ranking when embeddings are
available, falls back to FTS-only when they are not
- Three new functions: `computeRrfScores` (pure RRF), `vectorSearch`
(KNN), `hybridSearch` (orchestrator)
- Updated `vault_search` tool description to reflect hybrid search
capabilities

## Spike result
8/9 previously-failed queries fixed, 0 regressions, ~8ms latency
(RRF-only mode).

## Test plan
- [x] 9 `computeRrfScores` unit tests (table-driven: single/both
systems, bonuses, custom k, sort order)
- [x] 12 `hybridSearch` integration tests (fallback, hybrid ranking,
filters, limits, dedup, callouts)
- [x] All 1177 tests pass
- [x] Build and lint clean
- [ ] Deploy to Lightsail and run 9-query eval against live vault

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018ZbaHwP28ASYTpQnZk6MqM

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Search now uses a hybrid ranking approach that blends keyword and
semantic matching for more relevant results.
* Results can now fall back to keyword-only search when embeddings
aren’t available.
* Search snippets and result metadata are more complete for semantic
matches, with improved relevance ordering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- ARCHITECTURE.md: hybrid search flow, RRF algorithm, fallback behavior,
`search_mode`, Mermaid diagrams
- README.md: value prop → "Hybrid search", Phase 2 status → "In
progress", "AI agent" terminology
- AGENTS.md: search module description updated in structure tree
- .devin/wiki.json: hybrid search functions and conditional description
documented
- DEPLOY.md: `EMBEDDING_ENABLED` row clarified
- 3x .env.example: "for hybrid search" instead of "for vector search"

## Test plan
- [x] Build and tests pass (1182)
- [x] Docs-only changes — no code modified

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Search now supports hybrid ranking, combining keyword and semantic
results for more relevant matches.
* Search responses now include the search mode used, making results
clearer.

* **Bug Fixes**
* Search falls back cleanly to keyword-only matching when embeddings are
unavailable or disabled.
  * Timestamps in search results are now handled more reliably.

* **Documentation**
* Updated setup and product docs to reflect hybrid search behavior and
embedding configuration options.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What does this PR do?

Updates README.md and ARCHITECTURE.md to reflect Phase 2a (hybrid
search) as shipped — the final doc pass before merging `phase-2` into
`main`.

**README.md:**
- Intro paragraph: `full-text search` → `hybrid search` in the bold
callout
- Roadmap table: split Phase 2 into Phase 2a (hybrid search, Complete)
and Phase 2b (reranker, In progress)
- How It Works mermaid diagram: `SQLite FTS5` → `SQLite FTS5 +
sqlite-vec`, edge label updated to `FTS5 + vector`
- Prose below diagram: accurately describes both persistence strategies
(FTS rebuilds on startup, vectors persist with content-hash gating) and
RRF fusion

**ARCHITECTURE.md:**
- Phasing section: `Phase 2 is in progress` → `Phase 2a` as shipped,
with forward reference to Phase 2b (reranker)
- Component diagram: moved sqlite-vec from a separate "Phase 2" subgraph
with dotted arrows into the Docker Compose subgraph with solid arrows
- Key Decisions table: two new rows — "Hybrid search over LightRAG"
(grounded in the actual decision journey: LightRAG too heavy, qmd showed
lightweight approach) and "RRF fusion (k=60)" (rank-based over
score-based, validation results)
- Search module decomposition: 5-module table (search-index,
search-queries, search-helpers, fts-query, rrf) added before MCP Prompts
section

## Type of change

- [x] Documentation

## Checklist

- [x] `npm test` passes
- [x] `npm run lint` passes
- [x] `npm run prettier:check` passes
- [x] `npm run build` succeeds
- [x] README or ARCHITECTURE.md updated (if user-facing behavior
changed)

> `npm test`, `npm run lint`, and `npm run build` require Node 24 (not
available in this environment). Changes are docs-only — no code
modified.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01NP25qUqi8Phdro6EeMJjVo)_

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Updated the product and architecture documentation to reflect the
current search experience and roadmap.
* Clarified that search now uses a hybrid approach, with improved
description of how results are ranked and refreshed.
* Revised roadmap details to split the upcoming search work into two
stages, including a later reranking phase.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsqlite-vec@​0.1.9971006487100
Added@​huggingface/​transformers@​4.2.08810010091100

View full report

@socket-security

socket-security Bot commented Jun 30, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • @huggingface/transformers@4.2.0

View full report

Comment thread src/vault-mcp/search/search-helpers.ts
Comment thread src/vault-mcp/search/embedder.ts
Comment thread src/vault-mcp/search/file-watcher.ts
Comment thread src/vault-mcp/search/search-queries.ts
Comment thread src/vault-mcp/search/search-queries.ts
Comment thread src/vault-mcp/search/search-index.ts
Comment thread src/vault-mcp/search/search-queries.ts
@aliasunder

Copy link
Copy Markdown
Owner Author

@SocketSecurity ignore npm/@huggingface/transformers@4.2.0

@aliasunder
aliasunder merged commit 2d5894b into main Jun 30, 2026
15 checks passed
@aliasunder
aliasunder deleted the phase-2 branch June 30, 2026 17:51
aliasunder added a commit that referenced this pull request Jun 30, 2026
## Summary

- Folder inputs with trailing slashes (e.g. `"Projects/"`) produced a
LIKE pattern with `//` (`Projects//%`) that silently returned zero
results
- Added `stripTrailingSlashes` helper in search-helpers.ts, applied at
all 5 folder filter sites in search-queries.ts
- 5 unit tests for the new helper

Found by qodo in PR #228 review.

## Test plan

- [x] `stripTrailingSlashes` unit tests (5 cases: single slash,
multiple, no slash, internal slashes, bare name)
- [x] Full search test suite passes (298 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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