isotrieve 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. isotrieve/__init__.py +22 -0
  2. isotrieve/adapters/__init__.py +6 -0
  3. isotrieve/adapters/base.py +127 -0
  4. isotrieve/adapters/chroma.py +281 -0
  5. isotrieve/adapters/langchain.py +117 -0
  6. isotrieve/adapters/llamaindex_store.py +208 -0
  7. isotrieve/adapters/pinecone.py +180 -0
  8. isotrieve/adapters/qdrant.py +173 -0
  9. isotrieve/calibration/__init__.py +17 -0
  10. isotrieve/calibration/calib_v1.py +150 -0
  11. isotrieve/calibration/corpus.py +81 -0
  12. isotrieve/calibration/planner.py +87 -0
  13. isotrieve/cli.py +323 -0
  14. isotrieve/cli_doctor.py +183 -0
  15. isotrieve/cli_gate.py +205 -0
  16. isotrieve/cli_report.py +52 -0
  17. isotrieve/mapping/__init__.py +29 -0
  18. isotrieve/mapping/base.py +331 -0
  19. isotrieve/mapping/linear.py +547 -0
  20. isotrieve/mapping/mlp.py +316 -0
  21. isotrieve/mapping/registry.py +165 -0
  22. isotrieve/migrate.py +185 -0
  23. isotrieve/providers/__init__.py +17 -0
  24. isotrieve/providers/base.py +33 -0
  25. isotrieve/providers/cached.py +121 -0
  26. isotrieve/providers/cohere.py +68 -0
  27. isotrieve/providers/factory.py +92 -0
  28. isotrieve/providers/gemini.py +64 -0
  29. isotrieve/providers/openai.py +84 -0
  30. isotrieve/providers/sentence_transformers.py +52 -0
  31. isotrieve/providers/voyage.py +61 -0
  32. isotrieve/py.typed +0 -0
  33. isotrieve/quality/__init__.py +19 -0
  34. isotrieve/quality/gate.py +283 -0
  35. isotrieve/quality/gate_model_v1.json +52 -0
  36. isotrieve/quality/metrics.py +198 -0
  37. isotrieve/quality/thresholds.json +11 -0
  38. isotrieve/quality/thresholds.yaml +16 -0
  39. isotrieve/recalibration.py +238 -0
  40. isotrieve/reporting/__init__.py +1 -0
  41. isotrieve/reporting/html_report.py +62 -0
  42. isotrieve/reranking.py +217 -0
  43. isotrieve/serve.py +208 -0
  44. isotrieve/stores/__init__.py +6 -0
  45. isotrieve/stores/base.py +41 -0
  46. isotrieve/stores/numpy_files.py +168 -0
  47. isotrieve/stores/qdrant_store.py +181 -0
  48. isotrieve/wrappers/__init__.py +31 -0
  49. isotrieve/wrappers/llamaindex.py +161 -0
  50. isotrieve/wrappers/openai_shim.py +118 -0
  51. isotrieve/wrappers/telemetry.py +79 -0
  52. isotrieve-0.2.1.dist-info/METADATA +283 -0
  53. isotrieve-0.2.1.dist-info/RECORD +56 -0
  54. isotrieve-0.2.1.dist-info/WHEEL +4 -0
  55. isotrieve-0.2.1.dist-info/entry_points.txt +2 -0
  56. isotrieve-0.2.1.dist-info/licenses/LICENSE +17 -0
isotrieve/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Isotrieve — Embedding migration without re-embedding.
2
+
3
+ Learn a linear (or optional shallow non-linear) mapping between source and
4
+ target embedding spaces from a small calibration sample, then transform stored
5
+ vectors in place instead of re-embedding an entire corpus.
6
+
7
+ This package does **not** claim algorithmic novelty. It productizes known
8
+ linear-mapping techniques with a quality gate, providers, store adapters, and
9
+ a reproducible benchmark harness. See README "Prior Art & Research Basis".
10
+ """
11
+
12
+ from isotrieve.mapping.base import Mapping, ValidationReport
13
+ from isotrieve.mapping.linear import OrthogonalProcrustesMapping, RidgeMapping
14
+
15
+ __version__ = "0.2.1"
16
+ __all__ = [
17
+ "Mapping",
18
+ "RidgeMapping",
19
+ "OrthogonalProcrustesMapping",
20
+ "ValidationReport",
21
+ "__version__",
22
+ ]
@@ -0,0 +1,6 @@
1
+ """Vector database adapters for Isotrieve.
2
+
3
+ Provides thin wrappers that apply Isotrieve mappings to popular vector databases.
4
+ Core mapping/gate/recalibration logic stays in isotrieve.adapters.base; DB-specific
5
+ code is in per-DB modules.
6
+ """
@@ -0,0 +1,127 @@
1
+ """Core adapter abstractions.
2
+
3
+ ``EmbeddingAdapter`` wraps any embedding model.
4
+ ``VectorStoreAdapter`` wraps any vector DB with serve + offline modes.
5
+ ``MigrationReport`` captures what happened during offline migration.
6
+
7
+ All mapping/gate/recalibration logic is imported from isotrieve core;
8
+ DB-specific code stays thin.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Literal
16
+
17
+ import numpy as np
18
+
19
+ from isotrieve.mapping.base import Mapping, l2_normalize
20
+
21
+
22
+ @dataclass
23
+ class MigrationReport:
24
+ """Result of an offline migration pass."""
25
+
26
+ rows_processed: int = 0
27
+ elapsed_seconds: float = 0.0
28
+ sampled_recall_at_10: float | None = None
29
+ mapping_checksum: str = ""
30
+ source_collection: str = ""
31
+ target_collection: str = ""
32
+ errors: list[str] = field(default_factory=list)
33
+ idempotent: bool = True # False if double-migration detected
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ return {
37
+ "rows_processed": self.rows_processed,
38
+ "elapsed_seconds": self.elapsed_seconds,
39
+ "sampled_recall_at_10": self.sampled_recall_at_10,
40
+ "mapping_checksum": self.mapping_checksum,
41
+ "source_collection": self.source_collection,
42
+ "target_collection": self.target_collection,
43
+ "errors": self.errors,
44
+ "idempotent": self.idempotent,
45
+ }
46
+
47
+
48
+ class EmbeddingAdapter(ABC):
49
+ """Wraps any embedding model; source of truth for model identity."""
50
+
51
+ model_id: str
52
+ dim: int
53
+
54
+ @abstractmethod
55
+ def embed(self, texts: list[str]) -> np.ndarray:
56
+ """Embed a list of texts. Returns (n, dim) array."""
57
+
58
+
59
+ class VectorStoreAdapter(ABC):
60
+ """Thin per-DB shell. All mapping logic lives in isotrieve core.
61
+
62
+ Parameters
63
+ ----------
64
+ mapping:
65
+ A fitted Isotrieve Mapping (with inverse + optional recalibrator).
66
+ mode:
67
+ ``"serve"`` = transform queries on-the-fly, corpus untouched.
68
+ ``"migrated"`` = corpus already transformed, no query mapping needed.
69
+ """
70
+
71
+ def __init__(
72
+ self, mapping: Mapping, mode: Literal["serve", "migrated"] = "serve"
73
+ ) -> None:
74
+ self._mapping = mapping
75
+ self._mode = mode
76
+
77
+ @property
78
+ def has_recalibrator(self) -> bool:
79
+ return self._mapping.has_recalibrator
80
+
81
+ def recalibrate_scores(self, scores: np.ndarray) -> np.ndarray:
82
+ """Apply score recalibration (WS-A) to raw similarity scores."""
83
+ return self._mapping.recalibrate_scores(scores)
84
+
85
+ def preflight(self, sample_vectors: np.ndarray, target_vectors: np.ndarray) -> Any:
86
+ """Run quality gate on a sample before serve/migrate.
87
+
88
+ Returns a GateReport. Caller should check verdict before proceeding.
89
+ """
90
+ from isotrieve.quality.gate import QualityGate
91
+
92
+ gate = QualityGate()
93
+ return gate.evaluate(self._mapping, sample_vectors, target_vectors)
94
+
95
+ def _map_query(self, vec: np.ndarray) -> np.ndarray:
96
+ """Map a single query vector from new-model space to legacy space."""
97
+ return l2_normalize(self._mapping.inverse_transform(vec.reshape(1, -1)).ravel())
98
+
99
+ def _map_queries(self, vecs: np.ndarray) -> np.ndarray:
100
+ """Batch map query vectors."""
101
+ return l2_normalize(self._mapping.inverse_transform(vecs))
102
+
103
+ @abstractmethod
104
+ def query(
105
+ self,
106
+ query_vectors: np.ndarray,
107
+ k: int = 10,
108
+ **kwargs: Any,
109
+ ) -> list[list[dict[str, Any]]]:
110
+ """Serve-mode query: map queries, search, return results.
111
+
112
+ Returns a list (per query) of lists of result dicts with keys
113
+ ``id``, ``score``, ``metadata``.
114
+ """
115
+
116
+ @abstractmethod
117
+ def migrate(
118
+ self,
119
+ batch_size: int = 1000,
120
+ dry_run: bool = False,
121
+ new_collection: str | None = None,
122
+ ) -> MigrationReport:
123
+ """Offline migration: transform corpus vectors and write to new collection.
124
+
125
+ Never modifies the source collection. If ``new_collection`` is None,
126
+ appends ``_migrated`` to the source name.
127
+ """
@@ -0,0 +1,281 @@
1
+ """ChromaDB adapter for Isotrieve.
2
+
3
+ Two modes:
4
+ - **Serve mode**: ``IsotrieveChromaFunction`` — a Chroma ``EmbeddingFunction`` that
5
+ transparently maps new-model queries into legacy space before searching.
6
+ - **Offline migration**: ``migrate_collection()`` — transforms stored vectors
7
+ into the new model's space and writes to a new collection.
8
+
9
+ Requires: ``pip install isotrieve[chroma]`` or ``pip install chromadb``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import time
16
+ from typing import Any
17
+
18
+ import numpy as np
19
+
20
+ from isotrieve.adapters.base import MigrationReport
21
+ from isotrieve.mapping.base import Mapping, l2_normalize
22
+
23
+
24
+ def _require_chroma():
25
+ try:
26
+ import chromadb
27
+ from chromadb import Documents, EmbeddingFunction, Embeddings
28
+
29
+ return chromadb, Documents, EmbeddingFunction, Embeddings
30
+ except ImportError:
31
+ raise ImportError(
32
+ "ChromaDB adapter requires chromadb. Install with: pip install chromadb"
33
+ )
34
+
35
+
36
+ class IsotrieveChromaFunction:
37
+ """Chroma ``EmbeddingFunction`` that applies an Isotrieve mapping.
38
+
39
+ In serve mode, ``__call__`` embeds with the new model then maps into
40
+ legacy space, so the legacy index is searched without modification.
41
+
42
+ Usage::
43
+
44
+ from isotrieve.adapters.chroma import IsotrieveChromaFunction
45
+ from isotrieve.mapping.base import Mapping
46
+
47
+ mapping = Mapping.load("ada002_to_te3.isotrieve")
48
+ ef = IsotrieveChromaFunction(mapping, new_model_embedder=my_embed_fn)
49
+ col = client.get_collection("docs", embedding_function=ef)
50
+ results = col.query(query_texts=["..."], n_results=10)
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ mapping: Mapping,
56
+ new_model_embedder: Any = None,
57
+ *,
58
+ recalibrate: bool = True,
59
+ ) -> None:
60
+ """
61
+ Parameters
62
+ ----------
63
+ mapping:
64
+ Fitted Isotrieve mapping with inverse direction.
65
+ new_model_embedder:
66
+ Callable that takes ``list[str]`` and returns ``np.ndarray``
67
+ of shape ``(n, d_new)``. If None, the EF embeds with the
68
+ new model (caller must provide this).
69
+ recalibrate:
70
+ If True and the mapping has a recalibrator, post-process
71
+ query scores.
72
+ """
73
+ self._mapping = mapping
74
+ self._embedder = new_model_embedder
75
+ self._recalibrate = recalibrate
76
+
77
+ def __call__(self, input: list[str]) -> list[list[float]]:
78
+ """Embed texts into legacy space (for querying legacy index)."""
79
+ if self._embedder is None:
80
+ raise RuntimeError(
81
+ "No new_model_embedder provided. Pass a callable that "
82
+ "embeds text with the new model."
83
+ )
84
+ # Embed with new model
85
+ new_vecs = np.asarray(self._embedder(input), dtype=np.float64)
86
+ # Map to legacy space
87
+ legacy_vecs = l2_normalize(self._mapping.inverse_transform(new_vecs))
88
+ return legacy_vecs.tolist()
89
+
90
+ def embed_query(self, input: str) -> list[float]:
91
+ """Embed a single query into legacy space."""
92
+ return self.__call__([input])[0]
93
+
94
+ @property
95
+ def has_recalibrator(self) -> bool:
96
+ return self._mapping.has_recalibrator
97
+
98
+ def default_space(self) -> str:
99
+ return "cosine"
100
+
101
+ def name(self) -> str:
102
+ return "isotrieve_chroma"
103
+
104
+ def get_config(self) -> dict[str, Any]:
105
+ return {
106
+ "mapping_type": self._mapping.mapping_type,
107
+ "d_src": self._mapping._d_src,
108
+ "d_tgt": self._mapping._d_tgt,
109
+ }
110
+
111
+
112
+ def _hash_mapping(mapping: Mapping) -> str:
113
+ """Short checksum of the mapping matrix for metadata."""
114
+ if mapping._W is None:
115
+ return "unfitted"
116
+ h = hashlib.sha256(mapping._W.tobytes()).hexdigest()[:12]
117
+ return h
118
+
119
+
120
+ def migrate_collection(
121
+ client: Any,
122
+ collection_name: str,
123
+ mapping: Mapping,
124
+ *,
125
+ new_collection: str | None = None,
126
+ batch_size: int = 1000,
127
+ dry_run: bool = False,
128
+ new_model_embedder: Any = None,
129
+ ) -> MigrationReport:
130
+ """Migrate a Chroma collection from legacy to new model space.
131
+
132
+ Reads all vectors from ``collection_name``, applies the forward
133
+ mapping (legacy → new), and writes to ``new_collection``.
134
+ Source collection is never modified.
135
+
136
+ Parameters
137
+ ----------
138
+ client:
139
+ ``chromadb.Client`` instance.
140
+ collection_name:
141
+ Source collection (legacy embeddings).
142
+ mapping:
143
+ Fitted Isotrieve mapping (forward direction: legacy → new).
144
+ new_collection:
145
+ Target name. Default: ``{collection_name}_migrated``.
146
+ batch_size:
147
+ Rows per read/write batch.
148
+ dry_run:
149
+ If True, read and transform but don't write.
150
+ new_model_embedder:
151
+ If provided, re-embed texts with new model instead of mapping
152
+ (for comparison / ground-truth baseline).
153
+
154
+ Returns
155
+ -------
156
+ MigrationReport with rows processed, timing, and sampled recall.
157
+ """
158
+ chromadb, _, _, _ = _require_chroma()
159
+
160
+ t0 = time.perf_counter()
161
+ target_name = new_collection or f"{collection_name}_migrated"
162
+ report = MigrationReport(
163
+ source_collection=collection_name,
164
+ target_collection=target_name,
165
+ mapping_checksum=_hash_mapping(mapping),
166
+ )
167
+
168
+ # Open source collection
169
+ src = client.get_collection(collection_name)
170
+ total = src.count()
171
+
172
+ if dry_run:
173
+ # Just read a batch and report dimensions
174
+ sample = src.get(
175
+ limit=min(batch_size, total),
176
+ include=["embeddings", "metadatas", "documents"],
177
+ )
178
+ if sample["embeddings"]:
179
+ dims = len(sample["embeddings"][0])
180
+ report.rows_processed = len(sample["embeddings"])
181
+ report.elapsed_seconds = time.perf_counter() - t0
182
+ report.errors.append(
183
+ f"DRY RUN: would migrate {total} vectors of dim {dims}"
184
+ )
185
+ return report
186
+
187
+ # Check for double-migration: does the source already have isotrieve metadata?
188
+ sample_meta = src.get(limit=1, include=["metadatas"])
189
+ if sample_meta["metadatas"] and "isotrieve_mapping_id" in (
190
+ sample_meta["metadatas"][0] or {}
191
+ ):
192
+ report.idempotent = False
193
+ report.errors.append(
194
+ "Source collection already has isotrieve_mapping_id metadata. "
195
+ "This may be a double migration. Proceeding anyway."
196
+ )
197
+
198
+ # Create target collection with correct dimensions
199
+ # Read first batch to determine dims
200
+ first_batch = src.get(
201
+ limit=batch_size, include=["embeddings", "metadatas", "documents"]
202
+ )
203
+ if not first_batch["embeddings"]:
204
+ report.errors.append("Source collection is empty")
205
+ return report
206
+
207
+ # Create target
208
+ try:
209
+ client.delete_collection(target_name)
210
+ except Exception:
211
+ pass
212
+ target = client.create_collection(
213
+ name=target_name,
214
+ metadata={"hnsw:space": "cosine"},
215
+ )
216
+
217
+ # Process in batches
218
+ offset = 0
219
+ rows_written = 0
220
+ sampled_correct = 0
221
+ sampled_total = 0
222
+
223
+ while offset < total:
224
+ batch = src.get(
225
+ offset=offset,
226
+ limit=batch_size,
227
+ include=["embeddings", "metadatas", "documents", "ids"],
228
+ )
229
+ if not batch["ids"]:
230
+ break
231
+
232
+ ids = batch["ids"]
233
+ metadatas = batch["metadatas"] or [{} for _ in ids]
234
+ documents = batch["documents"] or [None for _ in ids]
235
+ embeddings = np.array(batch["embeddings"], dtype=np.float64)
236
+
237
+ # Transform: legacy → new space
238
+ mapped = mapping.transform(embeddings)
239
+ mapped_list = mapped.tolist()
240
+
241
+ # Add Isotrieve metadata to each row
242
+ for i, meta in enumerate(metadatas):
243
+ if meta is None:
244
+ meta = {}
245
+ metadatas[i] = meta
246
+ meta["isotrieve_mapping_id"] = _hash_mapping(mapping)
247
+ meta["isotrieve_format_version"] = 1
248
+ meta["isotrieve_source_collection"] = collection_name
249
+
250
+ # Write to target
251
+ target.add(
252
+ ids=ids,
253
+ embeddings=mapped_list,
254
+ metadatas=metadatas,
255
+ documents=documents,
256
+ )
257
+ rows_written += len(ids)
258
+
259
+ # Sample recall check (every 10th batch)
260
+ if rows_written % (batch_size * 10) == 0 or rows_written >= total:
261
+ sample_n = min(50, len(ids))
262
+ src_sample = embeddings[:sample_n]
263
+ tgt_sample = mapped[:sample_n]
264
+ # Recall@10: for each mapped vector, how many of its top-10
265
+ # nearest in mapped space match its top-10 in source space
266
+ src_sims = l2_normalize(src_sample) @ l2_normalize(src_sample).T
267
+ tgt_sims = l2_normalize(tgt_sample) @ l2_normalize(tgt_sample).T
268
+ for i in range(sample_n):
269
+ src_top10 = set(np.argsort(-src_sims[i])[:10])
270
+ tgt_top10 = set(np.argsort(-tgt_sims[i])[:10])
271
+ sampled_correct += len(src_top10 & tgt_top10)
272
+ sampled_total += 10
273
+
274
+ offset += batch_size
275
+
276
+ report.rows_processed = rows_written
277
+ report.elapsed_seconds = time.perf_counter() - t0
278
+ if sampled_total > 0:
279
+ report.sampled_recall_at_10 = sampled_correct / sampled_total
280
+
281
+ return report
@@ -0,0 +1,117 @@
1
+ """LangChain embeddings adapter for Isotrieve.
2
+
3
+ ``IsotrieveEmbeddings`` is a drop-in ``langchain_core.embeddings.Embeddings``
4
+ shim that transparently applies an Isotrieve mapping. No index migration
5
+ needed — query vectors are mapped on-the-fly.
6
+
7
+ Requires: ``pip install isotrieve[langchain]`` or ``pip install langchain-core``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+
16
+ from isotrieve.mapping.base import Mapping, l2_normalize
17
+
18
+
19
+ def _require_langchain():
20
+ try:
21
+ from langchain_core.embeddings import Embeddings
22
+
23
+ return Embeddings
24
+ except ImportError:
25
+ raise ImportError(
26
+ "LangChain adapter requires langchain-core. "
27
+ "Install with: pip install langchain-core"
28
+ )
29
+
30
+
31
+ class IsotrieveEmbeddings:
32
+ """Drop-in ``Embeddings`` that applies an Isotrieve mapping.
33
+
34
+ Usage::
35
+
36
+ from langchain_chroma import Chroma
37
+ from isotrieve.adapters.langchain import IsotrieveEmbeddings
38
+ from isotrieve.mapping.base import Mapping
39
+
40
+ mapping = Mapping.load("ada002_to_te3.isotrieve")
41
+ base_embed = OpenAIEmbeddings(model="text-embedding-3-small")
42
+ ae = IsotrieveEmbeddings(mapping, base_embed)
43
+
44
+ # Works with any LangChain vector store
45
+ db = Chroma.from_documents(docs, embedding=ae)
46
+ results = db.similarity_search("query", k=10)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ mapping: Mapping,
52
+ base_embeddings: Any,
53
+ ) -> None:
54
+ """
55
+ Parameters
56
+ ----------
57
+ mapping:
58
+ Fitted Isotrieve mapping. Forward direction should be
59
+ ``legacy → new`` so that ``mapping.inverse_transform()``
60
+ maps new-model vectors back to legacy space.
61
+ base_embeddings:
62
+ A ``langchain_core.embeddings.Embeddings`` instance for the
63
+ new model (e.g. ``OpenAIEmbeddings``).
64
+ """
65
+ _require_langchain() # side effect: raises ImportError if langchain missing
66
+ # Validate that base_embeddings satisfies the interface
67
+ if not hasattr(base_embeddings, "embed_documents"):
68
+ raise TypeError("base_embeddings must implement embed_documents()")
69
+ self._mapping = mapping
70
+ self._base = base_embeddings
71
+
72
+ def _map_vectors(self, vecs: np.ndarray) -> np.ndarray:
73
+ """Map new-model vectors to legacy space."""
74
+ return l2_normalize(self._mapping.inverse_transform(vecs))
75
+
76
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
77
+ """Embed documents into legacy space (for legacy index)."""
78
+ new_vecs = np.asarray(self._base.embed_documents(texts), dtype=np.float64)
79
+ legacy_vecs = self._map_vectors(new_vecs)
80
+ return legacy_vecs.tolist()
81
+
82
+ def embed_query(self, text: str) -> list[float]:
83
+ """Embed a single query into legacy space."""
84
+ new_vec = np.asarray(self._base.embed_query(text), dtype=np.float64)
85
+ legacy_vec = self._map_vectors(new_vec.reshape(1, -1)).ravel()
86
+ return legacy_vec.tolist()
87
+
88
+ # Async variants — delegate to base + map
89
+ async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
90
+ if hasattr(self._base, "aembed_documents"):
91
+ new_vecs = np.asarray(
92
+ await self._base.aembed_documents(texts), dtype=np.float64
93
+ )
94
+ else:
95
+ new_vecs = np.asarray(self._base.embed_documents(texts), dtype=np.float64)
96
+ legacy_vecs = self._map_vectors(new_vecs)
97
+ return legacy_vecs.tolist()
98
+
99
+ async def aembed_query(self, text: str) -> list[float]:
100
+ if hasattr(self._base, "aembed_query"):
101
+ new_vec = np.asarray(await self._base.aembed_query(text), dtype=np.float64)
102
+ else:
103
+ new_vec = np.asarray(self._base.embed_query(text), dtype=np.float64)
104
+ legacy_vec = self._map_vectors(new_vec.reshape(1, -1)).ravel()
105
+ return legacy_vec.tolist()
106
+
107
+ @property
108
+ def has_recalibrator(self) -> bool:
109
+ return self._mapping.has_recalibrator
110
+
111
+ def __repr__(self) -> str:
112
+ base_cls = type(self._base).__name__
113
+ return (
114
+ f"IsotrieveEmbeddings(base={base_cls}, "
115
+ f"mapping={self._mapping.mapping_type}, "
116
+ f"d_src={self._mapping._d_src}, d_tgt={self._mapping._d_tgt})"
117
+ )