fix(core): clean up delete vectors and cloud sync (#733)

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
Paul Hernandez
2026-04-09 21:23:45 -05:00
committed by GitHub
parent 7945c1e2f7
commit 093c94fea5
8 changed files with 173 additions and 87 deletions
@@ -124,22 +124,6 @@ def sync_project_command(
if success:
console.print(f"[green]{name} synced successfully[/green]")
# Trigger database sync if not a dry run
if not dry_run:
async def _trigger_db_sync():
async with get_client(project_name=name) as client:
return await ProjectClient(client).sync(
project_data.external_id, force_full=False
)
try:
with force_routing(cloud=True):
result = run_with_cleanup(_trigger_db_sync())
console.print(f"[dim]Database sync initiated: {result.get('message')}[/dim]")
except Exception as e:
console.print(f"[yellow]Warning: Could not trigger database sync: {e}[/yellow]")
else:
console.print(f"[red]{name} sync failed[/red]")
raise typer.Exit(1)
@@ -202,22 +186,6 @@ def bisync_project_command(
sync_entry.last_sync = datetime.now()
sync_entry.bisync_initialized = True
ConfigManager().save_config(config)
# Trigger database sync if not a dry run
if not dry_run:
async def _trigger_db_sync():
async with get_client(project_name=name) as client:
return await ProjectClient(client).sync(
project_data.external_id, force_full=False
)
try:
with force_routing(cloud=True):
result = run_with_cleanup(_trigger_db_sync())
console.print(f"[dim]Database sync initiated: {result.get('message')}[/dim]")
except Exception as e:
console.print(f"[yellow]Warning: Could not trigger database sync: {e}[/yellow]")
else:
console.print(f"[red]{name} bisync failed[/red]")
raise typer.Exit(1)
@@ -70,6 +70,10 @@ class SearchRepository(Protocol):
"""Sync semantic vector chunks for an entity."""
...
async def delete_entity_vector_rows(self, entity_id: int) -> None:
"""Delete semantic vector chunks and embeddings for one entity."""
...
async def sync_entity_vectors_batch(
self,
entity_ids: list[int],
@@ -454,6 +454,15 @@ class SearchRepositoryBase(ABC):
logger.debug(f"Query executed successfully in {elapsed_time:.2f}s.")
return result
async def delete_entity_vector_rows(self, entity_id: int) -> None:
"""Delete one entity's derived vector rows using the backend's cleanup path."""
await self._ensure_vector_tables()
async with db.scoped_session(self.session_maker) as session:
await self._prepare_vector_session(session)
await self._delete_entity_chunks(session, entity_id)
await session.commit()
# ------------------------------------------------------------------
# Shared semantic search: guard, text processing, chunking
# ------------------------------------------------------------------
@@ -565,21 +565,6 @@ class SQLiteSearchRepository(SearchRepositoryBase):
stale_params,
)
async def delete_entity_vector_rows(self, entity_id: int) -> None:
"""Delete one entity's vec rows on a sqlite-vec-enabled connection."""
await self._ensure_vector_tables()
async with db.scoped_session(self.session_maker) as session:
await self._ensure_sqlite_vec_loaded(session)
# Constraint: sqlite-vec virtual tables are only visible after vec0 is
# loaded on this exact connection.
# Why: generic repository sessions can reach search_vector_chunks but still
# fail with "no such module: vec0" when touching embeddings.
# Outcome: service-level cleanup routes vec-table deletes through this helper.
await self._delete_entity_chunks(session, entity_id)
await session.commit()
async def delete_project_vector_rows(self) -> None:
"""Delete all vector rows for this project on a sqlite-vec-enabled connection."""
await self._ensure_vector_tables()
+7 -13
View File
@@ -660,7 +660,6 @@ class SearchService:
async def _clear_entity_vectors(self, entity_id: int) -> None:
"""Delete derived vector rows for one entity."""
from basic_memory.repository.search_repository_base import SearchRepositoryBase
from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository
# Trigger: semantic indexing is disabled for this repository instance.
# Why: repositories only create vector tables when semantic search is enabled.
@@ -671,17 +670,7 @@ class SearchService:
):
return
params = {"project_id": self.repository.project_id, "entity_id": entity_id}
if isinstance(self.repository, SQLiteSearchRepository):
await self.repository.delete_entity_vector_rows(entity_id)
else:
await self.repository.execute_query(
text(
"DELETE FROM search_vector_chunks "
"WHERE project_id = :project_id AND entity_id = :entity_id"
),
params,
)
await self.repository.delete_entity_vector_rows(entity_id)
async def index_entity_file(
self,
@@ -889,7 +878,7 @@ class SearchService:
await self.repository.delete_by_entity_id(entity_id)
async def handle_delete(self, entity: Entity):
"""Handle complete entity deletion from search index including observations and relations.
"""Handle complete entity deletion from search and semantic index state.
This replicates the logic from sync_service.handle_delete() to properly clean up
all search index entries for an entity and its related data.
@@ -916,3 +905,8 @@ class SearchService:
await self.delete_by_permalink(permalink)
else:
await self.delete_by_entity_id(entity.id)
# Trigger: entity deletion removes the source rows for this note.
# Why: semantic chunks/embeddings are stored separately from search_index rows.
# Outcome: deleting an entity clears both full-text and vector-derived search state.
await self._clear_entity_vectors(entity.id)