From 02373831a0ad61b7d939b0ae10902b2783878002 Mon Sep 17 00:00:00 2001 From: Rohit Behera <126186063+r0h1tb@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:32:51 +0530 Subject: [PATCH] perf(cli): stop importing sentence_transformers for every command Importing ast_rag.cli took 3.65s, of which 3.05s was sentence_transformers (and transitively torch and transformers), reached via services/__init__ -> embedding_manager. Only semantic search needs it. Every other command -- goto, callers, refs, sig, blocks, cache-stats -- paid the full cost and never used the model. Moves the import into _get_model(), where it is actually needed, with TYPE_CHECKING for the annotations. import ast_rag.cli 3.65s -> 0.61s Measured end to end over 15 CLI scenarios against a live index: median command latency 3.068s -> 0.558s fastest command 2.864s -> 0.487s Semantic search is unaffected; the model loads on first use and 'query' still returns its full result set. --- ast_rag/services/embedding_manager.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/ast_rag/services/embedding_manager.py b/ast_rag/services/embedding_manager.py index 6e213ae..6c98513 100644 --- a/ast_rag/services/embedding_manager.py +++ b/ast_rag/services/embedding_manager.py @@ -21,7 +21,7 @@ import logging import uuid -from typing import Callable, Optional +from typing import TYPE_CHECKING, Callable, Optional import numpy as np from neo4j import Driver @@ -35,7 +35,9 @@ FieldCondition, MatchValue, ) -from sentence_transformers import SentenceTransformer + +if TYPE_CHECKING: # pragma: no cover - typing only + from sentence_transformers import SentenceTransformer from ast_rag.models import ASTNode, SearchResult, QdrantConfig, EmbeddingConfig, NodeKind @@ -95,7 +97,7 @@ def __init__( self._embed_config = embed_config self._neo4j_driver = neo4j_driver self._client: Optional[QdrantClient] = None - self._model: Optional[SentenceTransformer] = None + self._model: Optional["SentenceTransformer"] = None # Validate hybrid weights if hybrid search is enabled if self._embed_config.hybrid_search: total = self._embed_config.vector_weight + self._embed_config.keyword_weight @@ -110,8 +112,16 @@ def __init__( # Lazy initialisation # ------------------------------------------------------------------ - def _get_model(self) -> SentenceTransformer: - """Return local SentenceTransformer. Not called when remote_url is set.""" + def _get_model(self) -> "SentenceTransformer": + """Return local SentenceTransformer. Not called when remote_url is set. + + sentence_transformers pulls in torch and transformers and costs ~3s to + import. Only semantic search needs it, so it is imported here rather + than at module scope -- otherwise every CLI command, including pure + graph lookups like `goto` and `callers`, pays that cost. + """ + from sentence_transformers import SentenceTransformer + if self._model is None: logger.info( "Loading embedding model locally: %s (device=%s)",