Source code for aeat.application.corpus_search._query_embed
"""Runtime query embedder (the live-query half of the R3 semantic stack).The corpus vectors are precomputed at build time and ship as data(:mod:`~application.corpus_search._embed_build`); the model is needed atruntime ONLY to embed the operator's live query into the same vector space so acosine search can run.This module owns that one live use of ``model2vec``, behind the capability-gated ``aeat-cli[search]`` extra: absent the extra, :func:`search_extra_available`reports ``False`` and the retrieval layer degrades to lexical-only, whileconstructing/using a :class:`QueryEmbedder` refuses with the install hint.The model download is app-controlled: the cache directory is rooted under theSettings ``aeat_local_storage_root`` (``<root>/search-models``) rather than theuser's global Hugging Face cache, so a bundled Desktop Extension keeps its modelstate inside the one app state root. The model is loaded lazily on first embedand cached for the embedder's lifetime, so repeated queries pay the load once."""from__future__importannotationsimportimportlib.utilfrompathlibimportPathfromtypingimportTYPE_CHECKING,Anyfrom...core.configimportSettings,load_settingsfrom._embed_buildimportPOTION_MODEL_ID,POTION_MODEL_REVISIONfrom._errorsimportCorpusSearchInputErrorfrom._model_loaderimportload_static_modelifTYPE_CHECKING:importnumpyasnp_SEARCH_MODEL_CACHE_SUBDIR="search-models"
[docs]defsearch_extra_available()->bool:"""Return whether the semantic ``search`` extra (``model2vec``) is importable. The retrieval layer calls this to decide between hybrid and lexical-only degraded mode WITHOUT triggering a model load or download. """returnimportlib.util.find_spec("model2vec")isnotNone
[docs]defsearch_model_cache_dir(settings:Settings|None=None)->Path:"""Return the app-controlled model cache directory under the storage root."""resolved=settingsorload_settings()returnresolved.aeat_local_storage_root/_SEARCH_MODEL_CACHE_SUBDIR
[docs]classQueryEmbedder:"""Embed live queries with the pinned potion static model. Construction records the model id, pinned revision, and app-controlled cache directory but does NOT load the model; the first :meth:`embed_query` loads it (refusing with the install hint when the extra is absent) and caches it for reuse. """def__init__(self,*,model_id:str=POTION_MODEL_ID,revision:str=POTION_MODEL_REVISION,cache_dir:Path|None=None,settings:Settings|None=None,)->None:self._model_id=model_idself._revision=revisionself._cache_dir=cache_dirifcache_dirisnotNoneelsesearch_model_cache_dir(settings)self._model:Any|None=None@propertydefmodel_id(self)->str:returnself._model_id@propertydefrevision(self)->str:returnself._revision@propertydefcache_dir(self)->Path:returnself._cache_dirdef_ensure_model(self)->Any:ifself._modelisNone:self._cache_dir.mkdir(parents=True,exist_ok=True)self._model=load_static_model(self._model_id,revision=self._revision,cache_dir=self._cache_dir)returnself._model
[docs]defembed_query(self,text:str)->np.ndarray:"""Embed one query string into a 1-D float32 vector. Args: text: The free-text query. Returns: A 1-D float32 numpy vector in the corpus embedding space. Raises: CorpusSearchInputError: If ``text`` is blank. CorpusSearchDependencyError: If the ``search`` extra is absent. """importnumpyasnpcleaned=text.strip()ifnotcleaned:raiseCorpusSearchInputError("query embedding requires non-empty text",context={"query":text})model=self._ensure_model()raw=model.encode([cleaned])vector=np.asarray(raw,dtype=np.float32).reshape(-1)returnvector