Skip to content

Commit 6bd036f

Browse files
committed
feat: implement BM25 lexical search and hybrid retrieval capabilities
- Add a segmentation-free tokenizer for mixed Chinese/English text to support BM25 indexing. - Introduce vector math helpers for dot product, norm, and cosine similarity calculations. - Extend IVectorStore interface to include optional lexical search functionality. - Implement LocalVectorStore to support BM25 indexing and hybrid retrieval. - Update settings to include options for hybrid retrieval and MMR (Maximal Marginal Relevance) configurations. - Add tests for BM25, tokenization, vector math, and hybrid recall mechanisms. - Introduce a Vitest configuration for running tests in a Node environment with stubs for Obsidian.
1 parent fc04a4f commit 6bd036f

30 files changed

Lines changed: 2784 additions & 134 deletions

CLAUDE.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
99
| `npm run dev` | Watch mode build via esbuild; outputs `main.js` |
1010
| `npm run build` | Type-check (`tsc --noEmit`) + production esbuild bundle (minified) |
1111
| `npm run lint` | Run ESLint (typescript-eslint + eslint-plugin-obsidianmd) |
12+
| `npm test` | Run the vitest unit suite (`vitest run`) |
13+
| `npm run test:watch` | Run vitest in watch mode |
1214
| `npm version <patch/minor/major>` | Bump version in `manifest.json` and `versions.json` |
1315

14-
There are no unit tests. Testing is manual: build, copy `main.js` + `manifest.json` + `styles.css` to `<Vault>/.obsidian/plugins/obsidian-copilot/`, reload Obsidian, and enable the plugin.
16+
Unit tests live in `tests/` and run under vitest. They cover the dependency-free
17+
RAG primitives (tokenizer, BM25, RRF fusion, MMR, cosine math, chunk utilities)
18+
plus a synthetic `recall@k` benchmark (`tests/hybridRecall.test.ts`) that
19+
demonstrates hybrid + MMR retrieval beating pure top-K cosine. Tests only import
20+
modules that do **not** depend on `obsidian`, so they need no DOM/electron env.
21+
22+
End-to-end testing is still manual: build, copy `main.js` + `manifest.json` +
23+
`styles.css` to `<Vault>/.obsidian/plugins/simple-rag-plugin/` (the `id` in
24+
`manifest.json`), reload Obsidian, and enable the plugin.
1525

1626
## High-Level Architecture
1727

@@ -25,13 +35,16 @@ This is an Obsidian community plugin that adds an AI chat sidebar with RAG (Retr
2535
- Providers support a "thinking depth" (reasoning budget tokens) mapped via `ThinkingDepth`.
2636

2737
2. **RAG Layer** (`src/rag/`)
28-
- **Chunking**: Pluggable strategies — `DelimiterChunker`, `LengthChunker`, and `AIChunker` (uses an LLM call to semantically split text).
29-
- **Embedding**: `EmbeddingService` calls an OpenAI-compatible `/embeddings` endpoint with batching.
38+
- **Chunking**: Pluggable strategies resolved in `ChunkingResolver` `DelimiterChunker`, `ParagraphChunker`, `LengthChunker`, `MarkdownHeadingChunker`, `MarkdownQAChunker`. `AIChunker` (LLM-based semantic split) exists but is **not** currently wired into the resolver — see the dead-code note below.
39+
- **Embedding**: `EmbeddingService` calls an OpenAI-compatible `/embeddings` endpoint with batching, bounded concurrency, and retry/backoff (honoring `Retry-After`).
3040
- **Vector Store**: `IVectorStore` interface with two implementations:
31-
- `LocalVectorStore` — persists to `.copilot/vectors.json` inside the vault.
32-
- `RemoteVectorStore` — connects to Pinecone, Qdrant, or Weaviate.
41+
- `LocalVectorStore` — persists to `.copilot/vectors.json` inside the vault; also serves BM25 lexical search (`lexicalSearch`) for hybrid retrieval.
42+
- `PgvectorStore` — Postgres + the `pgvector` extension (cosine distance, HNSW index when available). Vector-only; no lexical search yet, so hybrid degrades gracefully to vector + MMR.
43+
- **Retrieval primitives** (`src/rag/retrieval/`): dependency-free `tokenize` (CJK-bigram + Latin), `BM25`, `reciprocalRankFusion` (RRF), `mmrSelect` (MMR), and shared `cosineSimilarity`/vector math.
3344
- **Reranking**: `RerankService` supports Jina, Cohere, or custom rerank APIs.
34-
- **Pipeline**: `RAGPipeline` orchestrates the flow: embed query → vector search → optional rerank → build context string.
45+
- **Pipeline**: `RAGPipeline` orchestrates: embed query → vector over-fetch (threshold-filtered) → optional BM25 lexical search → RRF fusion → optional cross-encoder rerank → optional MMR diversification → neighbor-chunk expansion → build context string. Tunables live in `RAGConfig` (`hybrid`, `mmrEnabled`, `mmrLambda`, `neighborRadius`, `candidateMultiplier`).
46+
47+
> **Dead code**: `AIChunker` is implemented but unreferenced — it needs async chunking plumbing and an `AIService` dependency threaded through `createChunker`. Decide to wire it (as an `ai` strategy) or delete it; don't leave it dangling.
3548
3649
3. **Indexing** (`src/indexer/VaultIndexer.ts`)
3750
- Incremental full-vault indexing that skips unchanged files by mtime.
@@ -43,16 +56,16 @@ This is an Obsidian community plugin that adds an AI chat sidebar with RAG (Retr
4356
- Two chat modes:
4457
- **Vault mode**: Uses `RAGPipeline.retrieve()` to inject relevant note chunks as context.
4558
- **Note mode**: Lets the user attach specific files or URLs; no vault search.
46-
- Settings are split across four tabs: General, Models, Embedding, VectorDB.
59+
- Settings are split across five tabs: General (incl. RAG + advanced retrieval), Models, Embedding, VectorDB, and LightRAG.
4760

4861
### Plugin Lifecycle (`src/main.ts`)
4962

5063
`main.ts` should stay minimal. It:
5164
1. Loads persisted data (`settings` + `conversations`) via `loadData()`.
52-
2. Wires services together in `initServices()`: `AIService``EmbeddingService` + `RerankService` + `LocalVectorStore` + `IVectorStore``RAGPipeline``VaultIndexer`.
65+
2. Wires services together in `initServices()`: `AIService``EmbeddingService` + `RerankService` + (`LocalVectorStore` | `PgvectorStore`) as `IVectorStore``RAGPipeline``VaultIndexer`, plus `LightRAGClient` + `LightRAGSyncService`.
5366
3. Registers the `ChatView`, commands, and settings tab.
5467
4. On settings update, calls `reinitServices()`; if the vector store backend changed, it rebuilds the pipeline and indexer from scratch.
55-
5. On `onunload`, saves the local vector store and detaches the chat view.
68+
5. On `onunload`, saves the local vector store, stops LightRAG sync, closes the pgvector pool, and detaches the chat view.
5669

5770
### Key Patterns
5871

eslint.config.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,9 @@ export default tseslint.config(
5959
"version-bump.mjs",
6060
"versions.json",
6161
"main.js",
62+
// Tests run under vitest (esbuild transpile), outside the src tsconfig
63+
// project, so they're excluded from the typed lint pass.
64+
"tests",
65+
"vitest.config.mts",
6266
]),
6367
);

0 commit comments

Comments
 (0)