Skip to content

Commit 306e562

Browse files
authored
chore: Make semantic deps default, auto-backfill embeddings, and default search to semantic (#586)
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent ed82b0c commit 306e562

24 files changed

Lines changed: 490 additions & 100 deletions

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ jobs:
254254
255255
- name: Install dependencies
256256
run: |
257-
uv pip install -e ".[dev,semantic]"
257+
uv pip install -e ".[dev]"
258258
259259
- name: Run tests (Semantic)
260260
run: |
@@ -296,7 +296,7 @@ jobs:
296296
297297
- name: Install dependencies
298298
run: |
299-
uv pip install -e ".[dev,semantic]"
299+
uv pip install -e ".[dev]"
300300
301301
- name: Run combined coverage (SQLite + Postgres)
302302
run: |

docs/post-v0.18.0-test-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ These are the most important post-`v0.18.0` feature modules currently under-cove
7979
### Acceptance criteria
8080

8181
- `search_type=text|vector|hybrid` returns expected ranked results on canonical semantic corpus.
82-
- Missing semantic extras fail fast with actionable install guidance.
82+
- Missing semantic dependencies fail fast with actionable install guidance.
8383
- Reindex and provider/model changes produce valid vectors without dimension mismatch.
8484
- SQLite and Postgres produce equivalent behavior for semantic modes on the same dataset.
8585
- Generated-column migration path is valid on SQLite environments in use.

docs/semantic-search.md

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
# Semantic Search
22

3-
This guide covers Basic Memory's optional semantic (vector) search feature, which adds meaning-based retrieval alongside the existing full-text search.
3+
This guide covers Basic Memory's semantic (vector) search feature, which adds meaning-based retrieval alongside the existing full-text search.
44

55
## Overview
66

7-
Basic Memory's default search uses full-text search (FTS) — keyword matching with boolean operators. Semantic search adds vector embeddings that capture the *meaning* of your content, enabling:
7+
Basic Memory's search supports both full-text search (FTS) and semantic retrieval. Semantic search adds vector embeddings that capture the *meaning* of your content, enabling:
88

99
- **Paraphrase matching**: Find "authentication flow" when searching for "login process"
1010
- **Conceptual queries**: Search for "ways to improve performance" and find notes about caching, indexing, and optimization
1111
- **Hybrid retrieval**: Combine the precision of keyword search with the recall of semantic similarity
1212

13-
Semantic search is **opt-in** — existing behavior is completely unchanged unless you enable it. It works on both SQLite (local) and Postgres (cloud) backends.
13+
Semantic search is enabled by default when semantic dependencies are available at runtime. It works on both SQLite (local) and Postgres (cloud) backends.
1414

1515
## Installation
1616

17-
Semantic search dependencies (fastembed, sqlite-vec, openai) are **optional extras** — they are not installed with the base `basic-memory` package. Install them with:
17+
Semantic search dependencies (fastembed, sqlite-vec, openai) are included in the default `basic-memory` install.
1818

1919
```bash
20-
pip install 'basic-memory[semantic]'
20+
pip install basic-memory
2121
```
2222

23-
This keeps the base install lightweight and avoids platform-specific issues with ONNX Runtime wheels.
23+
You can always override with `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true|false`.
2424

2525
### Platform Compatibility
2626

@@ -34,36 +34,40 @@ This keeps the base install lightweight and avoids platform-specific issues with
3434

3535
#### Intel Mac Workaround
3636

37-
The default FastEmbed provider uses ONNX Runtime, which dropped Intel Mac (x86_64) wheels starting in v1.24. Intel Mac users have two options:
37+
The default install includes FastEmbed, which depends on ONNX Runtime. ONNX Runtime dropped Intel Mac (x86_64) wheels starting in v1.24, so install with a compatible ONNX Runtime pin first:
3838

39-
**Option 1: Use OpenAI embeddings (recommended)**
39+
```bash
40+
pip install basic-memory 'onnxruntime<1.24'
41+
```
4042

41-
Install only the OpenAI dependency manually — no ONNX Runtime or FastEmbed needed:
43+
After installation, Intel Mac users have two runtime options:
44+
45+
**Option 1: Use OpenAI embeddings (recommended)**
4246

4347
```bash
44-
pip install openai sqlite-vec
4548
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
4649
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=openai
4750
export OPENAI_API_KEY=sk-...
4851
```
4952

50-
**Option 2: Pin an older ONNX Runtime**
53+
**Option 2: Use FastEmbed locally**
5154

52-
FastEmbed's ONNX Runtime dependency is unpinned, so you can constrain it to an older version that still ships Intel Mac wheels by passing both requirements in the same install command:
55+
Keep the same pinned installation and use FastEmbed (default provider):
5356

5457
```bash
55-
pip install 'basic-memory[semantic]' 'onnxruntime<1.24'
58+
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
59+
export BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER=fastembed
5660
```
5761

5862
## Quick Start
5963

60-
1. Install semantic extras:
64+
1. Install Basic Memory:
6165

6266
```bash
63-
pip install 'basic-memory[semantic]'
67+
pip install basic-memory
6468
```
6569

66-
2. Enable semantic search:
70+
2. (Optional) Explicitly enable semantic search:
6771

6872
```bash
6973
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
@@ -84,7 +88,7 @@ search_notes("login process", search_type="vector")
8488
# Hybrid: combines FTS precision with vector recall (recommended)
8589
search_notes("login process", search_type="hybrid")
8690

87-
# Traditional full-text search (still the default)
91+
# Explicit full-text search
8892
search_notes("login process", search_type="text")
8993
```
9094

@@ -94,7 +98,7 @@ All settings are fields on `BasicMemoryConfig` and can be set via environment va
9498

9599
| Config Field | Env Var | Default | Description |
96100
|---|---|---|---|
97-
| `semantic_search_enabled` | `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED` | `false` | Enable semantic search. Required before vector/hybrid modes work. |
101+
| `semantic_search_enabled` | `BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED` | Auto (`true` when semantic deps are available) | Enable semantic search. Required before vector/hybrid modes work. |
98102
| `semantic_embedding_provider` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_PROVIDER` | `"fastembed"` | Embedding provider: `"fastembed"` (local) or `"openai"` (API). |
99103
| `semantic_embedding_model` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_MODEL` | `"bge-small-en-v1.5"` | Model identifier. Auto-adjusted per provider if left at default. |
100104
| `semantic_embedding_dimensions` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DIMENSIONS` | Auto-detected | Vector dimensions. 384 for FastEmbed, 1536 for OpenAI. Override only if using a non-default model. |
@@ -112,8 +116,8 @@ FastEmbed runs entirely locally using ONNX models — no API key, no network cal
112116
- **Tradeoff**: Smaller model, fast inference, good quality for most use cases
113117

114118
```bash
115-
# Install semantic extras and enable
116-
pip install 'basic-memory[semantic]'
119+
# Install basic-memory and enable semantic search
120+
pip install basic-memory
117121
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
118122
```
119123

@@ -197,7 +201,8 @@ bm reindex -p my-project
197201

198202
### When You Need to Reindex
199203

200-
- **First enable**: After turning on `semantic_search_enabled` for the first time
204+
- **Upgrade note**: Migration now performs a one-time automatic embedding backfill on upgrade.
205+
- **Manual enable case**: If you explicitly had `semantic_search_enabled=false` and then turn it on
201206
- **Provider change**: After switching between `fastembed` and `openai`
202207
- **Model change**: After changing `semantic_embedding_model`
203208
- **Dimension change**: After changing `semantic_embedding_dimensions`

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Install dependencies
44
install:
5-
uv sync --extra semantic
5+
uv sync
66
@echo ""
77
@echo "💡 Remember to activate the virtual environment by running: source .venv/bin/activate"
88

pyproject.toml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ dependencies = [
4444
"sniffio>=1.3.1",
4545
"anyio>=4.10.0",
4646
"httpx>=0.28.0",
47-
]
48-
49-
[project.optional-dependencies]
50-
semantic = [
5147
"fastembed>=0.7.4",
5248
"sqlite-vec>=0.1.6",
5349
"openai>=1.100.2",
@@ -78,7 +74,7 @@ markers = [
7874
"postgres: Tests that run against Postgres backend (deselect with '-m \"not postgres\"')",
7975
"windows: Windows-specific tests (deselect with '-m \"not windows\"')",
8076
"smoke: Fast end-to-end smoke tests for MCP flows",
81-
"semantic: Tests requiring [semantic] extras (fastembed, sqlite-vec, openai)",
77+
"semantic: Tests requiring semantic dependencies (fastembed, sqlite-vec, openai)",
8278
]
8379

8480
[tool.ruff]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Trigger automatic semantic embedding backfill during migration.
2+
3+
Revision ID: i2c3d4e5f6g7
4+
Revises: h1b2c3d4e5f6
5+
Create Date: 2026-02-19 00:00:00.000000
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
# revision identifiers, used by Alembic.
12+
revision: str = "i2c3d4e5f6g7"
13+
down_revision: Union[str, None] = "h1b2c3d4e5f6"
14+
branch_labels: Union[str, Sequence[str], None] = None
15+
depends_on: Union[str, Sequence[str], None] = None
16+
17+
18+
def upgrade() -> None:
19+
"""No schema change.
20+
21+
Trigger: this revision is newly applied.
22+
Why: db.run_migrations() detects this revision transition and runs the existing
23+
sync_entity_vectors() pipeline to backfill semantic embeddings automatically.
24+
Outcome: users no longer need to run `bm reindex --embeddings` after upgrading.
25+
"""
26+
27+
28+
def downgrade() -> None:
29+
"""No-op downgrade."""

src/basic_memory/cli/commands/tool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -847,8 +847,8 @@ def search_notes(
847847
if not metadata_filters:
848848
metadata_filters = None
849849

850-
# set search type
851-
search_type = "text"
850+
# set search type (None delegates to MCP tool default selection)
851+
search_type: str | None = None
852852
if permalink:
853853
search_type = "permalink"
854854
if query and "*" in query:

src/basic_memory/config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ class DatabaseBackend(str, Enum):
4040

4141

4242
def _default_semantic_search_enabled() -> bool:
43-
"""Enable semantic search by default when semantic extras are installed."""
44-
return importlib.util.find_spec("fastembed") is not None
43+
"""Enable semantic search by default when required local semantic dependencies exist."""
44+
required_modules = ("fastembed", "sqlite_vec")
45+
return all(importlib.util.find_spec(module_name) is not None for module_name in required_modules)
4546

4647

4748
@dataclass
@@ -145,7 +146,7 @@ class BasicMemoryConfig(BaseSettings):
145146
# Semantic search configuration
146147
semantic_search_enabled: bool = Field(
147148
default_factory=_default_semantic_search_enabled,
148-
description="Enable semantic search (vector/hybrid retrieval). Works on both SQLite and Postgres backends. Requires semantic extras.",
149+
description="Enable semantic search (vector/hybrid retrieval). Works on both SQLite and Postgres backends. Requires semantic dependencies (included by default).",
149150
)
150151
semantic_embedding_provider: str = Field(
151152
default="fastembed",

src/basic_memory/db.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,99 @@
4343
_engine: Optional[AsyncEngine] = None
4444
_session_maker: Optional[async_sessionmaker[AsyncSession]] = None
4545

46+
# Alembic revision that enables one-time automatic embedding backfill.
47+
SEMANTIC_EMBEDDING_BACKFILL_REVISION = "i2c3d4e5f6g7"
48+
49+
50+
async def _load_applied_alembic_revisions(
51+
session_maker: async_sessionmaker[AsyncSession],
52+
) -> set[str]:
53+
"""Load applied Alembic revisions from alembic_version.
54+
55+
Returns an empty set when the version table does not exist yet
56+
(fresh database before first migration).
57+
"""
58+
try:
59+
async with scoped_session(session_maker) as session:
60+
result = await session.execute(text("SELECT version_num FROM alembic_version"))
61+
return {str(row[0]) for row in result.fetchall() if row[0]}
62+
except Exception as exc:
63+
error_message = str(exc).lower()
64+
if "alembic_version" in error_message and (
65+
"no such table" in error_message or "does not exist" in error_message
66+
):
67+
return set()
68+
raise
69+
70+
71+
def _should_run_semantic_embedding_backfill(
72+
revisions_before_upgrade: set[str],
73+
revisions_after_upgrade: set[str],
74+
) -> bool:
75+
"""Check if this migration run newly applied the backfill-trigger revision."""
76+
return (
77+
SEMANTIC_EMBEDDING_BACKFILL_REVISION in revisions_after_upgrade
78+
and SEMANTIC_EMBEDDING_BACKFILL_REVISION not in revisions_before_upgrade
79+
)
80+
81+
82+
async def _run_semantic_embedding_backfill(
83+
app_config: BasicMemoryConfig,
84+
session_maker: async_sessionmaker[AsyncSession],
85+
) -> None:
86+
"""Backfill semantic embeddings for all active projects/entities."""
87+
if not app_config.semantic_search_enabled:
88+
logger.info("Skipping automatic semantic embedding backfill: semantic search is disabled.")
89+
return
90+
91+
async with scoped_session(session_maker) as session:
92+
project_result = await session.execute(
93+
text("SELECT id, name FROM project WHERE is_active = :is_active ORDER BY id"),
94+
{"is_active": True},
95+
)
96+
projects = [(int(row[0]), str(row[1])) for row in project_result.fetchall()]
97+
98+
if not projects:
99+
logger.info("Skipping automatic semantic embedding backfill: no active projects found.")
100+
return
101+
102+
repository_class = (
103+
PostgresSearchRepository
104+
if app_config.database_backend == DatabaseBackend.POSTGRES
105+
else SQLiteSearchRepository
106+
)
107+
108+
total_entities = 0
109+
for project_id, project_name in projects:
110+
async with scoped_session(session_maker) as session:
111+
entity_result = await session.execute(
112+
text("SELECT id FROM entity WHERE project_id = :project_id ORDER BY id"),
113+
{"project_id": project_id},
114+
)
115+
entity_ids = [int(row[0]) for row in entity_result.fetchall()]
116+
117+
if not entity_ids:
118+
continue
119+
120+
total_entities += len(entity_ids)
121+
logger.info(
122+
"Automatic semantic embedding backfill: "
123+
f"project={project_name}, entities={len(entity_ids)}"
124+
)
125+
126+
search_repository = repository_class(
127+
session_maker,
128+
project_id=project_id,
129+
app_config=app_config,
130+
)
131+
for entity_id in entity_ids:
132+
await search_repository.sync_entity_vectors(entity_id)
133+
134+
logger.info(
135+
"Automatic semantic embedding backfill complete: "
136+
f"projects={len(projects)}, entities={total_entities}"
137+
)
138+
46139

47140
class DatabaseType(Enum):
48141
"""Types of supported databases."""
@@ -384,6 +477,23 @@ async def run_migrations(
384477
"""
385478
logger.info("Running database migrations...")
386479
try:
480+
revisions_before_upgrade: set[str] = set()
481+
# Trigger: run_migrations() can be invoked before module-level session maker is set.
482+
# Why: we still need reliable before/after revision detection for one-time backfill.
483+
# Outcome: create a short-lived session maker when needed, then dispose it immediately.
484+
if _session_maker is None:
485+
temp_engine, temp_session_maker = _create_engine_and_session(
486+
app_config.database_path,
487+
database_type,
488+
app_config,
489+
)
490+
try:
491+
revisions_before_upgrade = await _load_applied_alembic_revisions(temp_session_maker)
492+
finally:
493+
await temp_engine.dispose()
494+
else:
495+
revisions_before_upgrade = await _load_applied_alembic_revisions(_session_maker)
496+
387497
# Get the absolute path to the alembic directory relative to this file
388498
alembic_dir = Path(__file__).parent / "alembic"
389499
config = Config()
@@ -422,6 +532,13 @@ async def run_migrations(
422532
await PostgresSearchRepository(session_maker, 1).init_search_index()
423533
else:
424534
await SQLiteSearchRepository(session_maker, 1).init_search_index()
535+
536+
revisions_after_upgrade = await _load_applied_alembic_revisions(session_maker)
537+
if _should_run_semantic_embedding_backfill(
538+
revisions_before_upgrade,
539+
revisions_after_upgrade,
540+
):
541+
await _run_semantic_embedding_backfill(app_config, session_maker)
425542
except Exception as e: # pragma: no cover
426543
logger.error(f"Error running migrations: {e}")
427544
raise

src/basic_memory/mcp/tools/chatgpt_tools.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ async def search(
120120
project=default_project, # Use default project for ChatGPT
121121
page=1,
122122
page_size=10, # Reasonable default for ChatGPT consumption
123-
search_type="text", # Default to full-text search
124123
output_format="json",
125124
context=context,
126125
)

0 commit comments

Comments
 (0)