Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Features

- **Config-driven local embedding models: `generic-onnx` backend + `e5-small` preset.** Local embedders were fixed choices (minilm, embeddinggemma) and anything else required standing up an `openai-compat` endpoint. `embedding_model: "generic-onnx"` now runs any HuggingFace-hosted ONNX encoder selected entirely from configuration — repo, ONNX file, pooling, instruction prefixes and the persisted EF identity come from `embedding_onnx_*` keys in `config.json` (each overridable via the matching `MEMPALACE_EMBEDDING_ONNX_*` env var). `embedding_model: "e5-small"` is a bundled preset for `intfloat/multilingual-e5-small`: 384-dim like the default collections, ~120 MB vs embeddinggemma's ~300 MB, and measured on a real 200k-drawer RU-heavy palace at MRR 0.754 vs 0.536 for minilm. Same lazy deps as embeddinggemma — no new requirements; switching models on an existing palace still requires `mempalace repair rebuild-index`. (#1563, #1261, #1663)

### Bug Fixes

- **`sweep` books a failed `stat` as a failure again.** The non-regular-file gate added in 3.7.1 reads `stat.S_ISREG(f.stat().st_mode)` inside a `try`, and its `except OSError` printed `SKIP` and moved on. A dangling symlink, a symlink loop and a file unlinked between `rglob` and the gate all raise there, and before the gate existed every one of them reached `sweep()` and was booked in `failures` — so `sweep` went from reporting a transcript it could not read to reporting success. A failed probe is now an error, not a benign file type: it is logged, printed as `WARNING`, and appended to `failures`, while a probe that succeeds and says "not regular" still skips silently. (#2221)
Expand Down
138 changes: 134 additions & 4 deletions mempalace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,10 +748,14 @@ def embedding_model(self):

Values: ``"minilm"`` (ChromaDB's all-MiniLM-L6-v2 — English-only),
``"embeddinggemma"`` (multilingual, 100+ languages, default for
new installs since onboarding writes the choice), or
``"openai-compat"`` (embeddings served by an OpenAI-compatible
``/v1/embeddings`` endpoint — see ``embedding_api_url`` /
``embedding_api_model`` / ``embedding_api_key``). Read from env
new installs since onboarding writes the choice),
``"e5-small"`` (bundled ``intfloat/multilingual-e5-small`` preset —
see :mod:`mempalace.generic_onnx_embedding`), ``"generic-onnx"``
(any HuggingFace ONNX encoder, driven by the ``embedding_onnx_*``
settings), or ``"openai-compat"`` (embeddings served by an
OpenAI-compatible ``/v1/embeddings`` endpoint — see
``embedding_api_url`` / ``embedding_api_model`` /
``embedding_api_key``). Read from env
``MEMPALACE_EMBEDDING_MODEL`` first, then ``embedding_model`` in
``config.json``, then ``"minilm"`` as a back-compat fallback for
palaces created before onboarding asked the question.
Expand Down Expand Up @@ -881,6 +885,132 @@ def embedding_api_key(self):
"""
return self._resolve_str_setting("MEMPALACE_EMBEDDING_API_KEY", "embedding_api_key")

def _resolve_prefix_setting(self, env_var: str, config_key: str) -> str:
"""Resolve a prefix setting: env var > ``config.json`` > ``""``.

Unlike :meth:`_resolve_str_setting` the value is **not** stripped —
instruction prefixes for e5-style models end with a significant
space (``"query: "``). A whitespace-only env var still counts as
unset so it cannot silently mask the config-file value.
"""
env_val = os.environ.get(env_var)
if env_val is not None and env_val.strip():
return env_val
cfg_val = self._file_config.get(config_key)
if isinstance(cfg_val, str):
return cfg_val
return ""

@property
def embedding_onnx_repo(self):
"""HuggingFace repo id of the ``generic-onnx`` embedding model.

Required when ``embedding_model == "generic-onnx"`` (e.g.
``intfloat/multilingual-e5-small``). Resolved from env
``MEMPALACE_EMBEDDING_ONNX_REPO`` first, then ``embedding_onnx_repo``
in ``config.json``; ``None`` when unset.
"""
return self._resolve_str_setting("MEMPALACE_EMBEDDING_ONNX_REPO", "embedding_onnx_repo")

@property
def embedding_onnx_file(self):
"""ONNX graph filename inside the model repo (default ``model.onnx``).

Pick the fp32 export unless you have measured otherwise — quantized
and fp16 exports are not universally faster on CPU execution
providers. Env ``MEMPALACE_EMBEDDING_ONNX_FILE`` overrides.
"""
return (
self._resolve_str_setting("MEMPALACE_EMBEDDING_ONNX_FILE", "embedding_onnx_file")
or "model.onnx"
)

@property
def embedding_onnx_subfolder(self):
"""Repo subfolder holding the ONNX file (default ``onnx``).

Set to ``"."`` for repos that keep the graph at the repo root.
Env ``MEMPALACE_EMBEDDING_ONNX_SUBFOLDER`` overrides.
"""
return (
self._resolve_str_setting(
"MEMPALACE_EMBEDDING_ONNX_SUBFOLDER", "embedding_onnx_subfolder"
)
or "onnx"
)

@property
def embedding_onnx_pooling(self):
"""Pooling strategy for the ``generic-onnx`` model: ``mean`` or ``cls``.

``mean`` (default) is the attention-masked token average most
sentence encoders expect; ``cls`` takes the first token. Values are
validated at EF construction time. Env
``MEMPALACE_EMBEDDING_ONNX_POOLING`` overrides.
"""
val = self._resolve_str_setting(
"MEMPALACE_EMBEDDING_ONNX_POOLING", "embedding_onnx_pooling"
)
return val.lower() if val else "mean"

@property
def embedding_onnx_max_len(self) -> int:
"""Tokenizer truncation length for the ``generic-onnx`` model.

Defaults to ``512`` (the ceiling for most BERT-lineage encoders).
Env ``MEMPALACE_EMBEDDING_ONNX_MAX_LEN`` overrides; non-numeric or
non-positive values fall back to the default.
"""
raw = os.environ.get("MEMPALACE_EMBEDDING_ONNX_MAX_LEN")
if raw is None:
raw = self._file_config.get("embedding_onnx_max_len")
try:
val = int(str(raw).strip())
except (TypeError, ValueError):
return 512
return val if val > 0 else 512

@property
def embedding_onnx_doc_prefix(self):
"""Prefix prepended on the indexing path (default ``""``).

Instruction-tuned retrieval models expect a role marker — e5 uses
``"passage: "`` for documents. The trailing space is significant and
preserved; see :meth:`_resolve_prefix_setting`. Env
``MEMPALACE_EMBEDDING_ONNX_DOC_PREFIX`` overrides.
"""
return self._resolve_prefix_setting(
"MEMPALACE_EMBEDDING_ONNX_DOC_PREFIX", "embedding_onnx_doc_prefix"
)

@property
def embedding_onnx_query_prefix(self):
"""Prefix prepended on the ``embed_query`` path (default ``""``).

Only effective for call sites that use ``embed_query``; the current
embedding wrapper routes everything through ``__call__``, which
applies ``embedding_onnx_doc_prefix`` symmetrically. Env
``MEMPALACE_EMBEDDING_ONNX_QUERY_PREFIX`` overrides.
"""
return self._resolve_prefix_setting(
"MEMPALACE_EMBEDDING_ONNX_QUERY_PREFIX", "embedding_onnx_query_prefix"
)

@property
def embedding_onnx_ef_name(self):
"""Identity name persisted on the collection for ``generic-onnx``.

Defaults to a name derived from repo + ONNX file, so pointing the
config at a different model trips the embedder-identity check
instead of silently mixing vector spaces. Set it explicitly when
changing vector-affecting knobs that the derived name cannot see
(pooling, max_len, prefixes). Env
``MEMPALACE_EMBEDDING_ONNX_EF_NAME`` overrides; ``None`` when unset.
"""
return self._resolve_str_setting(
"MEMPALACE_EMBEDDING_ONNX_EF_NAME", "embedding_onnx_ef_name"
)

@property
def topic_tunnel_min_count(self):
"""Minimum number of overlapping confirmed topics required to create
Expand Down
19 changes: 18 additions & 1 deletion mempalace/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
bound to a user-selected ONNX Runtime execution provider, or an
OpenAI-compatible HTTP ``/v1/embeddings`` endpoint.

Three embedding-model options are available, selected via
Five embedding-model options are available, selected via
``MEMPALACE_EMBEDDING_MODEL`` or ``embedding_model`` in
``~/.mempalace/config.json``:

Expand All @@ -17,6 +17,17 @@
model is lazy-downloaded from HuggingFace on first use. Switching models
on an existing palace requires ``mempalace repair rebuild-index``
(different vector space).
* ``e5-small`` — ``intfloat/multilingual-e5-small`` (fp32), 384-dim,
multilingual. A lighter alternative to embeddinggemma (~120 MB vs ~300 MB)
with strong non-English retrieval; see
:func:`mempalace.generic_onnx_embedding.e5_small_ef` for measured numbers.
Same rebuild-index caveat when switching an existing palace.
* ``generic-onnx`` — any HuggingFace-hosted ONNX encoder, fully
config-driven (#1563, #1261): repo, ONNX file, pooling, prefixes and the
persisted EF name come from ``embedding_onnx_*`` settings in
``config.json`` (each overridable via the matching
``MEMPALACE_EMBEDDING_ONNX_*`` env var). See
:mod:`mempalace.generic_onnx_embedding`.
* ``openai-compat`` — embeddings served by any OpenAI-compatible
``/v1/embeddings`` endpoint (LM Studio, llama.cpp, vLLM, Ollama's OpenAI
shim, or a self-hosted server) instead of a local ONNX model. Useful for
Expand Down Expand Up @@ -686,6 +697,12 @@ def get_embedding_function(device: Optional[str] = None, model: Optional[str] =
threads = _resolve_intra_op_threads()
if model == "embeddinggemma":
ef = EmbeddinggemmaONNX(preferred_providers=providers, intra_op_num_threads=threads)
elif model in ("e5-small", "generic-onnx"):
from .generic_onnx_embedding import build_generic_onnx_ef

ef = build_generic_onnx_ef(
model, preferred_providers=providers, intra_op_num_threads=threads
)
else:
# Default: minilm (or anything we don't recognize — back-compat win).
ef_cls = _build_ef_class()
Expand Down
Loading