From cc5aecdaf9d2a42f11d97de09b6680cd0edee246 Mon Sep 17 00:00:00 2001 From: phernandez Date: Tue, 4 Aug 2026 19:31:18 -0500 Subject: [PATCH] fix(cli): warn when embedding reindex has no entities Signed-off-by: phernandez --- docs/semantic-search.md | 13 ++++- src/basic_memory/cli/commands/db.py | 10 ++++ tests/cli/test_db_reindex.py | 87 +++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/docs/semantic-search.md b/docs/semantic-search.md index 7a2efeddd..bce2af0fc 100644 --- a/docs/semantic-search.md +++ b/docs/semantic-search.md @@ -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 @@ -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 diff --git a/src/basic_memory/cli/commands/db.py b/src/basic_memory/cli/commands/db.py index 66f438649..e5e18db64 100644 --- a/src/basic_memory/cli/commands/db.py +++ b/src/basic_memory/cli/commands/db.py @@ -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: diff --git a/tests/cli/test_db_reindex.py b/tests/cli/test_db_reindex.py index b06dc5607..d679d11f5 100644 --- a/tests/cli/test_db_reindex.py +++ b/tests/cli/test_db_reindex.py @@ -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