Skip to content

ontocast.tool.vector_store.util

Backend-agnostic helpers for ontology vector storage.

EmbeddingContractMismatchError

Bases: ValueError

Embedding vectors or store metadata disagree with the active embedding config.

Source code in ontocast/tool/vector_store/util.py
class EmbeddingContractMismatchError(ValueError):
    """Embedding vectors or store metadata disagree with the active embedding config."""

atom_scope_fingerprint(store_config)

Fingerprint fragment for settings that change what gets stored per atom.

Covers both which entities become atoms and which literals become their surface forms and lexical triggers. All of these change the stored payload, so serving an index built under different values silently degrades retrieval instead of raising.

Returns None at the defaults, so collections built under them keep the fingerprint they already have and need no reindex on upgrade.

Parameters:

Name Type Description Default
store_config VectorStoreConfig

Active vector-store settings.

required

Returns:

Type Description
str | None

str | None: Compact divergence marker, or None when nothing diverges.

Source code in ontocast/tool/vector_store/util.py
def atom_scope_fingerprint(store_config: VectorStoreConfig) -> str | None:
    """Fingerprint fragment for settings that change what gets stored per atom.

    Covers both *which entities* become atoms and *which literals* become their
    surface forms and lexical triggers. All of these change the stored payload,
    so serving an index built under different values silently degrades
    retrieval instead of raising.

    Returns ``None`` at the defaults, so collections built under them keep the
    fingerprint they already have and need no reindex on upgrade.

    Args:
        store_config: Active vector-store settings.

    Returns:
        str | None: Compact divergence marker, or ``None`` when nothing diverges.
    """
    defaults = VectorStoreConfig.model_fields
    parts: list[str] = []
    if store_config.index_undescribed_iris:
        parts.append("undescribed")
    if store_config.embed_standard_vocab_iris:
        parts.append("stdvocab")
    for prefix in sorted(store_config.extra_excluded_namespace_prefixes):
        parts.append(f"x:{prefix}")

    def _diverges(name: str) -> bool:
        factory = defaults[name].default_factory
        default = factory() if factory is not None else defaults[name].default
        return getattr(store_config, name) != default

    # The surface-form and trigger settings are pushed into the atomizer and
    # decide what lands in the stored payload, so they belong in the identity of
    # the vectors. They were previously omitted, which meant changing one served
    # a stale index rather than raising EmbeddingContractMismatchError.
    for name in ("label_predicates", "symbol_predicates", "lexical_trigger_predicates"):
        if _diverges(name):
            joined = ",".join(sorted(getattr(store_config, name)))
            parts.append(f"{name}={render_text_hash(joined)[:12]}")
    for name in (
        "lexical_trigger_enabled",
        "lexical_trigger_heuristic_enabled",
        "lexical_trigger_min_len",
        "lexical_trigger_max_len",
        "lexical_trigger_heuristic_max_per_entity",
    ):
        if _diverges(name):
            parts.append(f"{name}={getattr(store_config, name)}")
    return ",".join(parts) if parts else None

embedding_fingerprint_matches(stored, embedding_config, *, minimal_label_limit=None, atom_scope=None)

Whether stored is the fingerprint the given config would produce.

Takes the same optional components as :func:embedding_model_fingerprint. Omitting them previously made this disagree with validate_embedding_contract_metadata for any non-default collection -- it would report a match the validator rejects.

Source code in ontocast/tool/vector_store/util.py
def embedding_fingerprint_matches(
    stored: str,
    embedding_config: EmbeddingConfig,
    *,
    minimal_label_limit: int | None = None,
    atom_scope: str | None = None,
) -> bool:
    """Whether ``stored`` is the fingerprint the given config would produce.

    Takes the same optional components as :func:`embedding_model_fingerprint`.
    Omitting them previously made this disagree with
    ``validate_embedding_contract_metadata`` for any non-default collection --
    it would report a match the validator rejects.
    """
    return stored == embedding_model_fingerprint(
        embedding_config,
        minimal_label_limit=minimal_label_limit,
        atom_scope=atom_scope,
    )

embedding_model_fingerprint(embedding_config, *, minimal_label_limit=None, atom_scope=None)

Identity of the vectors a config produces, stored alongside the collection.

Query/document prefixes belong here: they change the embedded text, so an index built without them is not comparable to queries issued with them, and the mismatch would otherwise show up only as quietly degraded retrieval. The sparse surface-form cap is included for the same reason -- it decides how many of a term's aliases enter the BM25 text. It contributes only when set to a non-default value, so collections built under the default keep their existing fingerprint. atom_scope follows the same rule for settings that decide which entities are atomized at all.

The surface-form contract (sf=) is separate and always contributes: it records which literals become surface forms and which entities become atoms, both of which change the stored index even at default settings.

Parameters:

Name Type Description Default
embedding_config EmbeddingConfig

Dense/sparse model configuration.

required
minimal_label_limit int | None

Sparse surface-form cap, when it differs from the default.

None
atom_scope str | None

Atom-scope divergence from :func:atom_scope_fingerprint, if any.

None

Returns:

Name Type Description
str str

Stable fingerprint stored alongside the collection.

Source code in ontocast/tool/vector_store/util.py
def embedding_model_fingerprint(
    embedding_config: EmbeddingConfig,
    *,
    minimal_label_limit: int | None = None,
    atom_scope: str | None = None,
) -> str:
    """Identity of the vectors a config produces, stored alongside the collection.

    Query/document prefixes belong here: they change the embedded text, so an index
    built without them is not comparable to queries issued with them, and the mismatch
    would otherwise show up only as quietly degraded retrieval. The sparse surface-form
    cap is included for the same reason -- it decides how many of a term's aliases enter
    the BM25 text. It contributes only when set to a non-default value, so collections
    built under the default keep their existing fingerprint. ``atom_scope`` follows the
    same rule for settings that decide which entities are atomized at all.

    The surface-form contract (``sf=``) is separate and always contributes: it records
    *which* literals become surface forms and which entities become atoms, both of which
    change the stored index even at default settings.

    Args:
        embedding_config: Dense/sparse model configuration.
        minimal_label_limit: Sparse surface-form cap, when it differs from the default.
        atom_scope: Atom-scope divergence from :func:`atom_scope_fingerprint`, if any.

    Returns:
        str: Stable fingerprint stored alongside the collection.
    """
    ec = embedding_config
    dense_part = f"dense:{ec.provider.value}:{ec.model_name}"
    affixes = f"|q={ec.query_prefix}|d={ec.document_prefix}"
    fingerprint = (
        f"{dense_part}|bm25={ec.bm25_model_name}{affixes}|sf={_SURFACE_FORM_CONTRACT}"
    )
    if (
        minimal_label_limit is not None
        and minimal_label_limit != _DEFAULT_MINIMAL_LABEL_LIMIT
    ):
        fingerprint += f"|minlabels={minimal_label_limit}"
    if atom_scope:
        fingerprint += f"|atoms={atom_scope}"
    return fingerprint

sync_atomizer_from_store_config(atomizer, store_config)

Mirror vector-store representation settings onto the atomizer.

Source code in ontocast/tool/vector_store/util.py
def sync_atomizer_from_store_config(
    atomizer: GraphAtomizer, store_config: VectorStoreConfig
) -> None:
    """Mirror vector-store representation settings onto the atomizer."""
    atomizer.minimal_representation_label_limit = store_config.minimal_label_limit
    atomizer.label_predicates = list(store_config.label_predicates)
    atomizer.symbol_predicates = list(store_config.symbol_predicates)
    atomizer.index_undescribed_iris = store_config.index_undescribed_iris
    atomizer.embed_standard_vocab_iris = store_config.embed_standard_vocab_iris
    atomizer.extra_excluded_namespace_prefixes = list(
        store_config.extra_excluded_namespace_prefixes
    )
    atomizer.lexical_trigger_enabled = store_config.lexical_trigger_enabled
    atomizer.lexical_trigger_predicates = list(store_config.lexical_trigger_predicates)
    atomizer.lexical_trigger_heuristic_enabled = (
        store_config.lexical_trigger_heuristic_enabled
    )
    atomizer.lexical_trigger_min_len = store_config.lexical_trigger_min_len
    atomizer.lexical_trigger_max_len = store_config.lexical_trigger_max_len
    atomizer.lexical_trigger_heuristic_max_per_entity = (
        store_config.lexical_trigger_heuristic_max_per_entity
    )