Files
leocrm/app/plugins/builtins/unified_search/lifecycle.py
T
Agent Zero abbe7a18fc fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner
- P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var
- P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup
- P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns
- P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs)
- P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import
- P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default
- P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed
- P2: 28 frontend TODOs (hardcoded constants, deprecated notification API)
- P3: dead code, duplicates, deprecated imports, private attr, __import__ inline
- P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n)
- ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix)
- F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String)
- Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
2026-08-16 01:17:18 +02:00

139 lines
4.1 KiB
Python

"""Derived-data lifecycle management for the Unified Search index.
Provides functions to remove, rebuild, and purge search index data
(embeddings, FTS vectors, document chunks) when entities are deleted,
restored, or corrected.
"""
from __future__ import annotations
import logging
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import text
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
# Entity type -> (table_name, tsv_column, embedding_column)
_ENTITY_MAP: dict[str, tuple[str, str, str]] = {
"contact": ("contacts", "search_tsv", "embedding"),
"mail": ("mails", "body_tsv", "embedding"),
"file": ("files", "content_tsv", "embedding"),
"event": ("calendar_entries", "search_tsv", "embedding"),
}
def _resolve_entity(entity_type: str) -> tuple[str, str, str] | None:
"""Return (table, tsv_column, embedding_column) for an entity type."""
return _ENTITY_MAP.get(entity_type)
async def remove_from_index(
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> None:
"""Remove an entity from the search index (embedding=NULL, TSV=NULL).
Does **not** delete document_chunks — use :func:`remove_chunks` for that.
"""
mapping = _resolve_entity(entity_type)
if mapping is None:
logger.warning("remove_from_index: unknown entity_type=%s", entity_type)
return
table, tsv_col, emb_col = mapping
await db.execute(
text(
f"UPDATE {table} "
f"SET {emb_col} = NULL, {tsv_col} = NULL, indexed_at = NULL "
f"WHERE id = :eid AND tenant_id = :tid"
),
{"eid": entity_id, "tid": tenant_id},
)
await db.commit()
logger.info("Removed %s/%s from search index", entity_type, entity_id)
async def remove_chunks(
db: AsyncSession,
file_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> None:
"""Remove all document_chunks for a file."""
await db.execute(
text(
"DELETE FROM document_chunks "
"WHERE file_id = :fid AND tenant_id = :tid"
),
{"fid": file_id, "tid": tenant_id},
)
await db.commit()
logger.info("Removed chunks for file %s", file_id)
async def rebuild_index(
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> bool:
"""Rebuild the search index for a single entity from the authoritative source.
Re-generates the embedding and refreshes the TSV vector.
Returns True on success, False on failure.
"""
from app.plugins.builtins.unified_search.embedding import index_entity
# Clear existing index data first
await remove_from_index(db, entity_type, entity_id, tenant_id)
# Re-generate embedding (index_entity also updates indexed_at)
try:
success = await index_entity(entity_type, entity_id, tenant_id, db)
if success and entity_type == "file":
# Re-index chunks for files
from app.plugins.builtins.unified_search.jobs import index_file_chunks
await index_file_chunks({"_lifecycle": True}, str(entity_id))
return success
except Exception:
logger.exception("rebuild_index failed for %s/%s", entity_type, entity_id)
return False
async def handle_entity_delete(
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> None:
"""Handle entity deletion: remove from index + remove chunks if applicable."""
await remove_from_index(db, entity_type, entity_id, tenant_id)
if entity_type == "file":
await remove_chunks(db, entity_id, tenant_id)
async def handle_entity_restore(
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> None:
"""Handle entity restore from trash: rebuild search index."""
await rebuild_index(db, entity_type, entity_id, tenant_id)
async def handle_entity_correction(
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> None:
"""Handle entity data correction: rebuild search index."""
await rebuild_index(db, entity_type, entity_id, tenant_id)