Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/semantic-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ pip install basic-memory
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
```

3. Build vector embeddings for your existing content:
3. Index your project files and build vector embeddings:

```bash
bm reindex --embeddings
bm reindex
```

Use `bm reindex --embeddings` only after the notes have already been indexed. That flag rebuilds
derived vectors from database entities; it does not discover new files on disk.

4. Search using semantic modes:

```python
Expand Down Expand Up @@ -450,11 +453,15 @@ not change stored embeddings, so it does not require `bm reindex --embeddings`.

The `bm reindex` command rebuilds search indexes without dropping the database.

Plain `bm reindex` runs the project index before embeddings, so use it for a new project or for
files written directly to disk. `bm reindex --embeddings` intentionally skips file discovery and
only rebuilds vectors for entities already present in the database.

```bash
# Rebuild everything (FTS + embeddings if semantic is enabled)
bm reindex

# Only rebuild vector embeddings
# Only rebuild vector embeddings (notes must already be indexed)
bm reindex --embeddings

# Only rebuild the full-text search index
Expand Down
10 changes: 10 additions & 0 deletions src/basic_memory/cli/commands/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,16 @@ def on_progress(entity_id, index, total):
f"{stats['skipped']} skipped, "
f"{stats['errors']} errors"
)
if stats["total_entities"] == 0 and not search:
# Trigger: embeddings-only mode found no database entities.
# Why: this mode rebuilds derived vectors; it does not discover files.
# Outcome: explain the prerequisite instead of reporting a silent no-op.
console.print(
" [yellow]No indexed entities found.[/yellow] "
"[cyan]--embeddings[/cyan] only processes notes already in the "
"database. Run [green]bm reindex[/green] to index project files first, "
"or start the MCP server and retry after its initial index completes."
)

console.print("\n[green]Reindex complete![/green]")
finally:
Expand Down
87 changes: 87 additions & 0 deletions tests/cli/test_db_reindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,93 @@ def update(self, task_id, **kwargs):
assert any("full rebuild" in line for line in printed_lines)


@pytest.mark.asyncio
async def test_reindex_embeddings_only_warns_when_project_has_no_indexed_entities(
monkeypatch,
session_maker,
):
"""Embeddings-only mode explains that it cannot discover project files."""
app_config = _stub_app_config()
project = SimpleNamespace(id=1, name="foo", path="/tmp/foo")
printed_lines: list[str] = []

class StubProjectRepository:
async def get_active_projects(self, session):
return [project]

class StubSearchService:
def __init__(self, search_repository, entity_repository, file_service, *, session_maker):
pass

async def reindex_vectors(self, *, progress_callback=None, force_full: bool = False):
return {"total_entities": 0, "embedded": 0, "skipped": 0, "errors": 0}

class SilentProgress:
def __init__(self, *args, **kwargs):
pass

def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
return False

def add_task(self, description, total=None):
return 1

def update(self, task_id, **kwargs):
pass

monkeypatch.setattr(
"basic_memory.services.initialization.reconcile_projects_with_config", AsyncMock()
)
monkeypatch.setattr(
"basic_memory.db.get_or_create_db",
AsyncMock(return_value=(None, session_maker)),
)
monkeypatch.setattr("basic_memory.db.shutdown_db", AsyncMock())
monkeypatch.setattr("basic_memory.repository.ProjectRepository", StubProjectRepository)
monkeypatch.setattr(
"basic_memory.repository.search_repository.create_search_repository",
lambda *args, **kwargs: object(),
)
monkeypatch.setattr(
"basic_memory.repository.EntityRepository", lambda *args, **kwargs: object()
)
monkeypatch.setattr(
"basic_memory.markdown.entity_parser.EntityParser",
lambda *args, **kwargs: object(),
)
monkeypatch.setattr(
"basic_memory.markdown.markdown_processor.MarkdownProcessor",
lambda *args, **kwargs: object(),
)
monkeypatch.setattr(
"basic_memory.services.file_service.FileService", lambda *args, **kwargs: object()
)
monkeypatch.setattr("basic_memory.services.search_service.SearchService", StubSearchService)
monkeypatch.setattr(db_cmd, "Progress", SilentProgress)
monkeypatch.setattr(
db_cmd.console,
"print",
lambda message="", *args, **kwargs: printed_lines.append(str(message)),
)

await db_cmd._reindex(
app_config,
search=False,
embeddings=True,
full=False,
project="foo",
)

output = "\n".join(printed_lines)
assert "0 entities embedded" in output
assert "No indexed entities found" in output
assert "--embeddings" in output
assert "bm reindex" in output


@pytest.mark.asyncio
async def test_reindex_recovers_stuck_materializations_before_scan(monkeypatch, session_maker):
"""The scan reconciles deletes against the filesystem, so a note whose accepted
Expand Down
Loading