Skip to content

Commit 18580ab

Browse files
authored
feat(embedding): add jina and mistral providers, fix stale install hints (#154)
Compared MFS's embedding provider set against upstream Memsearch (the project this code was originally adapted from) and found two gaps: - jina and mistral are supported upstream but missing here. Both are ported over (jina talks to Jina's embeddings REST API directly over the core httpx dep, no extra needed; mistral uses the official SDK behind a new `mistral` extra), wired into the provider registry, the setup wizard, and docs/providers.md. - openai.py, voyage.py, ollama.py, and local.py still told users to `pip install memsearch` in their module docstrings — leftover from when this code was copied over. Fixed to say mfs-server. Signed-off-by: Cheney Zhang <chen.zhang@zilliz.com>
1 parent 7ecf480 commit 18580ab

14 files changed

Lines changed: 358 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ What each section of `server.toml` configures:
638638

639639
| Section | Configures | Default | Swap in |
640640
|---|---|---|---|
641-
| `[embedding]` | embedding provider · model · dim | local ONNX (BGE-M3 int8, no key) | `openai` · `gemini` · `voyage` · `ollama` · `local` — needs that provider's key/extra |
641+
| `[embedding]` | embedding provider · model · dim | local ONNX (BGE-M3 int8, no key) | `openai` · `gemini` · `voyage` · `jina` · `mistral` · `ollama` · `local` — needs that provider's key/extra |
642642
| `[milvus]` | vector store | Milvus Lite (file under `~/.mfs/`) | self-hosted Milvus or [Zilliz Cloud](https://cloud.zilliz.com/signup?utm_source=github&utm_medium=referral&utm_campaign=mfs-readme) — set `uri` + `token` |
643643
| `[database]` | metadata + transformation-cache DB | SQLite (file) | Postgres — set `dsn` |
644644
| `[artifact_cache]` | converted-blob cache | local fs under `~/.mfs/cache` | size / eviction knobs |

docs/providers.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@ topologies, use [Deployment](deployment.md).
1616
| Hosted OpenAI embeddings | `openai` embeddings | `uv tool install mfs-server`, then `mfs-server setup --section embedding` | `OPENAI_API_KEY` |
1717
| Google Gemini embeddings | `gemini` embeddings | `uv tool install "mfs-server[gemini]"`, then `mfs-server setup --section embedding` | `GOOGLE_API_KEY`, or Vertex AI auth for the embedding SDK |
1818
| Voyage embeddings | `voyage` embeddings | `uv tool install "mfs-server[voyage]"`, then `mfs-server setup --section embedding` | `VOYAGE_API_KEY` |
19+
| Jina embeddings | `jina` embeddings | `uv tool install mfs-server`, then `mfs-server setup --section embedding` | `JINA_API_KEY` |
20+
| Mistral embeddings | `mistral` embeddings | `uv tool install "mfs-server[mistral]"`, then `mfs-server setup --section embedding` | `MISTRAL_API_KEY` |
1921
| Local Ollama embeddings | `ollama` embeddings | `uv tool install "mfs-server[ollama]"`, then `mfs-server setup --section embedding` | Running Ollama server; `OLLAMA_HOST` is optional |
2022
| Local sentence-transformers embeddings | `local` embeddings | `uv tool install "mfs-server[local]"`, then `mfs-server setup --section embedding` | None; this extra pulls the sentence-transformers stack |
2123
| Image-description and summary setup | `openai`, `anthropic`, or `gemini` LLM/VLM | `mfs-server setup --section description` (and `--section summary`) | Provider key for the selected LLM/VLM provider |
2224

2325
Provider names are exact. The supported embedding names are `openai`, `onnx`,
24-
`gemini`, `voyage`, `ollama`, and `local`.
26+
`gemini`, `voyage`, `jina`, `mistral`, `ollama`, and `local`.
2527

2628
## Install Paths
2729

28-
The base install includes the OpenAI SDK and the default ONNX embedding stack:
30+
The base install includes the OpenAI SDK and the default ONNX embedding stack.
31+
`jina` also needs no extra — it talks to Jina's REST API over the core `httpx`
32+
dependency:
2933

3034
```bash
3135
uv tool install mfs-server
@@ -36,14 +40,16 @@ Alternate provider extras are separate:
3640
```bash
3741
uv tool install "mfs-server[gemini]"
3842
uv tool install "mfs-server[voyage]"
43+
uv tool install "mfs-server[mistral]"
3944
uv tool install "mfs-server[ollama]"
4045
uv tool install "mfs-server[local]"
4146
uv tool install "mfs-server[anthropic]"
4247
uv tool install "mfs-server[all-providers]"
4348
```
4449

45-
`all-providers` installs Gemini, Voyage, Ollama, and Anthropic provider
46-
dependencies. It does not include `local`, because `local` pulls the larger
50+
`all-providers` installs Gemini, Voyage, Mistral, Ollama, and Anthropic
51+
provider dependencies (`jina` needs no extra, so it's already covered). It
52+
does not include `local`, because `local` pulls the larger
4753
sentence-transformers dependency stack.
4854

4955
## Embedding Providers
@@ -57,6 +63,8 @@ models.
5763
| `openai` | `text-embedding-3-small` | 1536 for the default model | Core | `OPENAI_API_KEY`; `OPENAI_BASE_URL` is optional | No local model download. Unknown model dimensions may require a trial embedding call. |
5864
| `gemini` | `gemini-embedding-001` | 768 for the default model | `uv tool install "mfs-server[gemini]"` or `all-providers` | `GOOGLE_API_KEY`, or Vertex AI auth with `GOOGLE_GENAI_USE_VERTEXAI=true` | Known model dimensions use a local table; unknown models require a trial embedding call. |
5965
| `voyage` | `voyage-3-lite` | 512 for the default model | `uv tool install "mfs-server[voyage]"` or `all-providers` | `VOYAGE_API_KEY` | Known model dimensions use a local table; unknown models require a trial embedding call. |
66+
| `jina` | `jina-embeddings-v4` | 2048 for the default model (Matryoshka-truncatable 256–2048) | Core (talks to the Jina REST API over `httpx`) | `JINA_API_KEY` | No local model download. Known model dimensions use a local table; unknown models fall back to 2048. |
67+
| `mistral` | `mistral-embed` | 1024 for the default model | `uv tool install "mfs-server[mistral]"` or `all-providers` | `MISTRAL_API_KEY` | Known model dimensions use a local table; unknown models require a trial embedding call. |
6068
| `ollama` | `nomic-embed-text` | Detected by a trial embed against the selected Ollama model | `uv tool install "mfs-server[ollama]"` or `all-providers` | Running Ollama server; `OLLAMA_HOST` can point at a non-default host | The selected model must be available to the Ollama server before dimension probing and embedding can succeed. |
6169
| `local` | `all-MiniLM-L6-v2` | Detected from sentence-transformers model metadata | `uv tool install "mfs-server[local]"` | No API key | `mfs-server run` and `mfs-server worker` preload the sentence-transformers model on the detected device: CUDA, MPS, then CPU. |
6270

server/python/pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,21 @@ embedding-onnx = []
8585
# mfs_server.common.embeddings).
8686
gemini = ["google-genai>=0.3"] # gemini embedding + LLM (vision-capable)
8787
voyage = ["voyageai>=0.3"] # voyage-3-lite etc.
88+
mistral = ["mistralai>=1.0"] # mistral-embed / codestral-embed
89+
# jina: no extra — talks to the REST API directly over the core httpx dep
8890
ollama = ["ollama>=0.4"] # local LLM/embedding via Ollama server
8991
local = ["sentence-transformers>=3.0", "einops>=0.8"] # CPU/GPU sentence-transformers
9092

9193
# Alternate LLM providers for summary / VLM (mfs_server.common.llm).
9294
anthropic = ["anthropic>=0.40"] # claude-sonnet-* (text + vision)
9395

9496
# Convenience bundle: every provider (text + vision LLMs and remote embeddings).
95-
# Excludes 'local' (sentence-transformers pulls torch ~2 GB).
97+
# Excludes 'local' (sentence-transformers pulls torch ~2 GB). jina needs no
98+
# extra (core httpx), so it's already covered without appearing here.
9699
all-providers = [
97100
"google-genai>=0.3",
98101
"voyageai>=0.3",
102+
"mistralai>=1.0",
99103
"ollama>=0.4",
100104
"anthropic>=0.40",
101105
]

server/python/src/mfs_server/common/embedding.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
api_calls / cache_hits for observability/tests.
66
77
Provider selection lives in `mfs_server.common.embeddings` (lazy imports).
8-
Supported names today: onnx (default), openai, google, voyage, ollama, local.
8+
Supported names today: onnx (default), openai, gemini, voyage, jina, mistral,
9+
ollama, local.
910
"""
1011

1112
from __future__ import annotations

server/python/src/mfs_server/common/embeddings/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ async def embed(self, texts: list[str]) -> list[list[float]]: ...
2424
"onnx": ("mfs_server.common.embeddings.onnx", "OnnxEmbedding"),
2525
"gemini": ("mfs_server.common.embeddings.gemini", "GeminiEmbedding"),
2626
"voyage": ("mfs_server.common.embeddings.voyage", "VoyageEmbedding"),
27+
"jina": ("mfs_server.common.embeddings.jina", "JinaEmbedding"),
28+
"mistral": ("mfs_server.common.embeddings.mistral", "MistralEmbedding"),
2729
"ollama": ("mfs_server.common.embeddings.ollama", "OllamaEmbedding"),
2830
"local": ("mfs_server.common.embeddings.local", "LocalEmbedding"),
2931
}
@@ -35,6 +37,8 @@ async def embed(self, texts: list[str]) -> list[list[float]]: ...
3537
"onnx": "gpahal/bge-m3-onnx-int8",
3638
"gemini": "gemini-embedding-001",
3739
"voyage": "voyage-3-lite",
40+
"jina": "jina-embeddings-v4",
41+
"mistral": "mistral-embed",
3842
"ollama": "nomic-embed-text",
3943
"local": "all-MiniLM-L6-v2",
4044
}
@@ -46,6 +50,8 @@ async def embed(self, texts: list[str]) -> list[list[float]]: ...
4650
"onnx": "uv sync (core dep)",
4751
"gemini": "uv sync --extra gemini",
4852
"voyage": "uv sync --extra voyage",
53+
"jina": "uv sync (core dep, uses httpx)",
54+
"mistral": "uv sync --extra mistral",
4955
"ollama": "uv sync --extra ollama",
5056
"local": "uv sync --extra local",
5157
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Jina AI embedding provider.
2+
3+
Requires: no extra — uses the core ``httpx`` dependency
4+
Environment variables:
5+
JINA_API_KEY — required
6+
7+
Jina does not publish a dedicated Python SDK for its embedding REST API, so
8+
this provider talks to ``https://api.jina.ai/v1/embeddings`` directly via
9+
httpx. The default model is the latest ``jina-embeddings-v4`` (2048-dim,
10+
Matryoshka-truncatable between 256 and 2048).
11+
12+
The ``task`` parameter activates a task-specific LoRA adapter. MFS embeds
13+
indexed chunks and search queries through the same ``embed()`` call, so we
14+
default to ``retrieval.passage`` — the common case for indexing a corpus.
15+
Override via the constructor if you need query- or code-specific behavior.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import os
21+
22+
_API_URL = "https://api.jina.ai/v1/embeddings"
23+
24+
# Native output dimensions for well-known Jina models. v4 additionally
25+
# supports Matryoshka truncation between 256 and 2048; callers can override
26+
# by passing ``dimensions=`` to the constructor.
27+
_KNOWN_DIMENSIONS: dict[str, int] = {
28+
"jina-embeddings-v4": 2048,
29+
"jina-embeddings-v3": 1024,
30+
"jina-embeddings-v2-base-en": 768,
31+
"jina-embeddings-v2-base-code": 768,
32+
}
33+
34+
35+
class JinaEmbedding:
36+
"""Jina AI embedding provider (REST)."""
37+
38+
_DEFAULT_BATCH_SIZE = 128
39+
_TIMEOUT_SECONDS = 60.0
40+
41+
def __init__(
42+
self,
43+
model: str = "jina-embeddings-v4",
44+
*,
45+
batch_size: int = 0,
46+
task: str = "retrieval.passage",
47+
dimensions: int | None = None,
48+
api_key: str | None = None,
49+
) -> None:
50+
import httpx
51+
52+
self._api_key = api_key or os.environ.get("JINA_API_KEY")
53+
if not self._api_key:
54+
raise RuntimeError("JINA_API_KEY is required for the Jina embedding provider")
55+
56+
self._model = model
57+
self._task = task
58+
self._dimensions = (
59+
dimensions if dimensions is not None else _KNOWN_DIMENSIONS.get(model, 2048)
60+
)
61+
self._batch_size = batch_size if batch_size > 0 else self._DEFAULT_BATCH_SIZE
62+
self._client = httpx.AsyncClient(timeout=self._TIMEOUT_SECONDS)
63+
64+
@property
65+
def model_name(self) -> str:
66+
return self._model
67+
68+
@property
69+
def dimension(self) -> int:
70+
return self._dimensions
71+
72+
async def embed(self, texts: list[str]) -> list[list[float]]:
73+
from .utils import batched_embed
74+
75+
return await batched_embed(texts, self._embed_batch, self._batch_size)
76+
77+
async def _embed_batch(self, texts: list[str]) -> list[list[float]]:
78+
body: dict = {
79+
"model": self._model,
80+
"input": texts,
81+
}
82+
if self._task:
83+
body["task"] = self._task
84+
if self._dimensions:
85+
body["dimensions"] = self._dimensions
86+
87+
resp = await self._client.post(
88+
_API_URL,
89+
json=body,
90+
headers={
91+
"Authorization": f"Bearer {self._api_key}",
92+
"Content-Type": "application/json",
93+
},
94+
)
95+
resp.raise_for_status()
96+
payload = resp.json()
97+
return [item["embedding"] for item in payload["data"]]

server/python/src/mfs_server/common/embeddings/local.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Local embedding via sentence-transformers (runs on CPU/GPU).
22
3-
Requires: ``pip install 'memsearch[local]'`` or ``uv add 'memsearch[local]'``
3+
Requires: ``pip install 'mfs-server[local]'`` or ``uv add 'mfs-server[local]'``
44
No API key needed.
55
"""
66

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Mistral AI embedding provider.
2+
3+
Requires: ``pip install 'mfs-server[mistral]'`` or ``uv add 'mfs-server[mistral]'``
4+
Environment variables:
5+
MISTRAL_API_KEY — required
6+
7+
Uses the official ``mistralai`` Python SDK. The default model is
8+
``mistral-embed`` — Mistral's general-purpose text embedding model
9+
(1024-dim, 8K context). Code-heavy corpora may prefer ``codestral-embed``.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import os
15+
16+
# Known output dimensions for Mistral embedding models.
17+
_KNOWN_DIMENSIONS: dict[str, int] = {
18+
"mistral-embed": 1024,
19+
"codestral-embed": 1536,
20+
"codestral-embed-2505": 1536,
21+
}
22+
23+
24+
class MistralEmbedding:
25+
"""Mistral AI embedding provider."""
26+
27+
_DEFAULT_BATCH_SIZE = 64
28+
29+
def __init__(
30+
self,
31+
model: str = "mistral-embed",
32+
*,
33+
batch_size: int = 0,
34+
api_key: str | None = None,
35+
) -> None:
36+
from mistralai.client import Mistral
37+
38+
self._api_key = api_key or os.environ.get("MISTRAL_API_KEY")
39+
if not self._api_key:
40+
raise RuntimeError("MISTRAL_API_KEY is required for the Mistral embedding provider")
41+
42+
self._client = Mistral(api_key=self._api_key)
43+
self._model = model
44+
self._dimension = _detect_dimension(self._client, model)
45+
self._batch_size = batch_size if batch_size > 0 else self._DEFAULT_BATCH_SIZE
46+
47+
@property
48+
def model_name(self) -> str:
49+
return self._model
50+
51+
@property
52+
def dimension(self) -> int:
53+
return self._dimension
54+
55+
async def embed(self, texts: list[str]) -> list[list[float]]:
56+
from .utils import batched_embed
57+
58+
return await batched_embed(texts, self._embed_batch, self._batch_size)
59+
60+
async def _embed_batch(self, texts: list[str]) -> list[list[float]]:
61+
resp = await self._client.embeddings.create_async(model=self._model, inputs=texts)
62+
return [item.embedding for item in resp.data]
63+
64+
65+
def _detect_dimension(client, model: str) -> int:
66+
"""Return the embedding dimension for *model*.
67+
68+
Uses a lookup table for well-known Mistral models. For unknown models,
69+
a sync trial embed is performed to discover the dimension.
70+
"""
71+
if model in _KNOWN_DIMENSIONS:
72+
return _KNOWN_DIMENSIONS[model]
73+
trial = client.embeddings.create(model=model, inputs=["dim"])
74+
return len(trial.data[0].embedding)

server/python/src/mfs_server/common/embeddings/ollama.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Ollama embedding provider (local models via Ollama server).
22
3-
Requires: ``pip install 'memsearch[ollama]'`` or ``uv add 'memsearch[ollama]'``
3+
Requires: ``pip install 'mfs-server[ollama]'`` or ``uv add 'mfs-server[ollama]'``
44
Environment variables:
55
OLLAMA_HOST — optional, default http://localhost:11434
66
"""

server/python/src/mfs_server/common/embeddings/openai.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""OpenAI embedding provider.
22
3-
Requires: ``pip install memsearch`` (openai is included by default)
3+
Requires: ``pip install mfs-server`` (openai is included by default)
44
Environment variables:
55
OPENAI_API_KEY — required
66
OPENAI_BASE_URL — optional, override API base URL

0 commit comments

Comments
 (0)