fse-haystack 0.1.0__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.
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fse-haystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: fse embeddings for haystack-ai
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/oborchers/Fast_Sentence_Embeddings
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: fse>=2.0.0
|
|
11
|
+
Requires-Dist: haystack-ai>=3.4.1
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
14
|
+
Requires-Dist: pytest-socket>=0.7; extra == "test"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
16
|
+
Requires-Dist: numpy>=1.26.4; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# fse-haystack
|
|
20
|
+
|
|
21
|
+
Prepared, unpublished Haystack integration for fse 2.0. Version 0.1.0 will be published only after fse 2.0.0 and separate maintainer approval.
|
|
22
|
+
This package lives in `integrations/fse-haystack/` in the fse repository.
|
|
23
|
+
|
|
24
|
+
This package depends on fse and `haystack-ai` only. Framework dependencies never enter
|
|
25
|
+
fse core. This package has its own CI environment and release workflow.
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
After an fse 2.0 build and this package are installed:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from haystack import Document
|
|
33
|
+
from haystack_integrations.components.embedders.fse import (
|
|
34
|
+
FseDocumentEmbedder,
|
|
35
|
+
FseTextEmbedder,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
query_embedder = FseTextEmbedder(model="minishlab/potion-base-8M", workers=1)
|
|
39
|
+
query_embedder.warm_up()
|
|
40
|
+
query = query_embedder.run(text="hello world")["embedding"]
|
|
41
|
+
document_embedder = FseDocumentEmbedder(workers=1, batch_size=128)
|
|
42
|
+
document_embedder.warm_up()
|
|
43
|
+
documents = document_embedder.run(documents=[Document(content="hello world")])["documents"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For an existing local model, pass `encoder=your_encoder`, built for example with
|
|
47
|
+
`fse.Encoder.from_arrays`. No download occurs with an injected encoder.
|
|
48
|
+
|
|
49
|
+
The `@component` classes expose `embedding` and `documents` output sockets.
|
|
50
|
+
They implement `run_async` as well. Call `warm_up()` before standalone execution.
|
|
51
|
+
Haystack pipelines call warm-up themselves. Document calls use batches of at most
|
|
52
|
+
`batch_size` texts, preserving input order. Every document must have text content.
|
|
53
|
+
The document embedder returns new documents and preserves IDs and metadata.
|
|
54
|
+
It embeds content only, without metadata concatenation or implicit query prefixes.
|
|
55
|
+
|
|
56
|
+
Pipeline serialization stores model and loader options, never the loaded encoder.
|
|
57
|
+
An injected encoder cannot be serialized. Save it first, then use its local path
|
|
58
|
+
as `model`. Serialization rejects an explicit Hub token to avoid saving credentials.
|
|
59
|
+
Follow the [community integration guide](https://github.com/deepset-ai/haystack-integrations#readme)
|
|
60
|
+
for a later listing. No listing is submitted in this preparation.
|
|
61
|
+
|
|
62
|
+
## Framework baseline and guides
|
|
63
|
+
|
|
64
|
+
The supported minimum for version 0.1.0 is `haystack-ai==3.4.1`, the current stable
|
|
65
|
+
release checked on 2026-11-01 from [PyPI](https://pypi.org/project/haystack-ai/3.4.1/).
|
|
66
|
+
This is our tested minimum and makes no compatibility claim for older framework releases.
|
|
67
|
+
The runtime dependency allows later versions. Recheck compatibility before publication.
|
|
68
|
+
|
|
69
|
+
The implementation follows the [official integration guide](https://docs.haystack.deepset.ai/docs/custom-components).
|
|
70
|
+
|
|
71
|
+
## Options and behavior
|
|
72
|
+
|
|
73
|
+
`load_kwargs` passes options unchanged to `Encoder.from_pretrained`.
|
|
74
|
+
`load_kwargs={"normalize": False}` returns raw means for every supported model,
|
|
75
|
+
including fse artifacts, Model2Vec and supported sentence-transformers models.
|
|
76
|
+
`normalize="model"` (the default) preserves the model's normalization rule.
|
|
77
|
+
For example, `{"revision": "<commit>", "local_files_only": True}` selects a Hub
|
|
78
|
+
revision and offline loading after the files have been cached.
|
|
79
|
+
Hub-only options (`revision`, `token`, `cache_dir`, `local_files_only`)
|
|
80
|
+
are invalid for local paths. See the [fse loader contract](https://github.com/oborchers/Fast_Sentence_Embeddings/blob/master/docs/architecture/hub.md).
|
|
81
|
+
Do not pass `load_kwargs` with `encoder=`. Configure that encoder before injection.
|
|
82
|
+
Use the Hub credential store rather than putting tokens in application configuration.
|
|
83
|
+
|
|
84
|
+
`dtype="float32"` (default) or `"float16"` controls fse output precision.
|
|
85
|
+
`workers=None` uses fse's worker policy. `workers=1` requests one worker.
|
|
86
|
+
The adapter passes text and worker validation to fse without replacing its errors.
|
|
87
|
+
Integer embeddings are unsupported because this interface requires float vectors.
|
|
88
|
+
|
|
89
|
+
Every result converts a numpy array into Python float lists. This copies values
|
|
90
|
+
and allocates Python objects, so it takes additional time and memory.
|
|
91
|
+
It does not retain the array's compact float16 or float32 storage.
|
|
92
|
+
Empty text and unknown tokens follow the loaded encoder's tokenizer rules.
|
|
93
|
+
Async methods delegate encoding and list conversion to a thread.
|
|
94
|
+
Construct models before serving async requests. Haystack also provides
|
|
95
|
+
`warm_up_async()` so async pipelines can load outside the event loop.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
Use standard Python 3.13. From this package directory in the fse checkout:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
uv venv --python 3.13 .venv
|
|
103
|
+
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_FSE=2.0.0 \
|
|
104
|
+
uv pip install --python .venv/bin/python -e '../..' -e '.[test]'
|
|
105
|
+
.venv/bin/python -m pytest -m 'not network'
|
|
106
|
+
.venv/bin/python -m pytest -m network
|
|
107
|
+
uv build --out-dir dist
|
|
108
|
+
.venv/bin/python examples/retrieval.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The version override labels the development checkout for dependency resolution.
|
|
112
|
+
It does not create a tag, release or published package.
|
|
113
|
+
|
|
114
|
+
The declared requirement is `fse>=2.0.0`. The version override tests the local
|
|
115
|
+
unreleased checkout without weakening the published dependency.
|
|
116
|
+
The network test downloads `minishlab/potion-base-8M`. Offline tests build a tiny
|
|
117
|
+
local encoder with `Encoder.from_arrays` and compare vectors with fse directly.
|
|
118
|
+
Tests disable Python network sockets by default and enable them for the marked
|
|
119
|
+
network test. No model upload or paid service is needed.
|
|
120
|
+
|
|
121
|
+
## CI and release
|
|
122
|
+
|
|
123
|
+
The isolated CI job tests Python 3.11 with the minimum framework and Python 3.13
|
|
124
|
+
with the latest framework. Network tests and the real-model retrieval example
|
|
125
|
+
run only when requested locally. See [release setup](https://github.com/oborchers/Fast_Sentence_Embeddings/blob/master/integrations/README.md#release-setup)
|
|
126
|
+
for the package tag, approval environment and pending PyPI publisher.
|
|
127
|
+
|
|
128
|
+
## AI assistance
|
|
129
|
+
|
|
130
|
+
These package sources, tests and documentation were prepared with AI assistance
|
|
131
|
+
using Codex. Validation uses offline vector parity, framework API tests and the
|
|
132
|
+
real-model retrieval example. The maintainer remains responsible for review and maintenance.
|
|
133
|
+
Publication and framework listing submissions require a separate maintainer decision.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
fse_haystack-0.1.0.dist-info/licenses/LICENSE,sha256=bBiZDEcLSm07ahZaeojdtC7C1zEnIgKofEgUPekS7Bk,1077
|
|
2
|
+
haystack_integrations/components/embedders/fse/__init__.py,sha256=5ONU-2B71-LGo1nk0Z-eQUzBm1TMDvVV1_vsZdLInfs,4217
|
|
3
|
+
fse_haystack-0.1.0.dist-info/METADATA,sha256=BMe2rH2rLJ9dvXG8Z470A7duv9oHc5FIWMOYvKQGkWA,6513
|
|
4
|
+
fse_haystack-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
5
|
+
fse_haystack-0.1.0.dist-info/top_level.txt,sha256=xEV0YMHti-yzzXgkLgSzsILiqsuC91-4ShNpDgsKNRI,22
|
|
6
|
+
fse_haystack-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019-2026 Oliver Borchers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
haystack_integrations
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Haystack text and document embedders backed by fse."""
|
|
2
|
+
|
|
3
|
+
from asyncio import to_thread
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from haystack import Document, component, default_from_dict, default_to_dict
|
|
8
|
+
|
|
9
|
+
from fse import Encoder
|
|
10
|
+
from fse._validation import validate_arguments
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _FseEmbedder:
|
|
14
|
+
@validate_arguments
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
model: str = "minishlab/potion-base-8M",
|
|
18
|
+
*,
|
|
19
|
+
encoder: Encoder | None = None,
|
|
20
|
+
dtype: str = "float32",
|
|
21
|
+
workers: int | None = None,
|
|
22
|
+
batch_size: int = 128,
|
|
23
|
+
load_kwargs: dict | None = None,
|
|
24
|
+
):
|
|
25
|
+
if dtype not in ("float32", "float16"):
|
|
26
|
+
raise ValueError("dtype must be float32 or float16 for float-list embeddings")
|
|
27
|
+
if type(batch_size) is not int or batch_size <= 0:
|
|
28
|
+
raise ValueError("batch_size must be a positive integer")
|
|
29
|
+
if encoder is not None and load_kwargs:
|
|
30
|
+
raise ValueError("load_kwargs cannot be used with an existing encoder")
|
|
31
|
+
self.model = model
|
|
32
|
+
self.dtype = dtype
|
|
33
|
+
self.workers = workers
|
|
34
|
+
self.batch_size = batch_size
|
|
35
|
+
self.load_kwargs = dict(load_kwargs or {})
|
|
36
|
+
self._encoder = encoder
|
|
37
|
+
self._injected = encoder is not None
|
|
38
|
+
|
|
39
|
+
def warm_up(self) -> None:
|
|
40
|
+
"""Load once before running a pipeline. An injected encoder is already ready."""
|
|
41
|
+
if self._encoder is None:
|
|
42
|
+
self._encoder = Encoder.from_pretrained(self.model, **self.load_kwargs)
|
|
43
|
+
|
|
44
|
+
async def warm_up_async(self) -> None:
|
|
45
|
+
await to_thread(self.warm_up)
|
|
46
|
+
|
|
47
|
+
def _embed(self, texts: list[str]) -> list[list[float]]:
|
|
48
|
+
if self._encoder is None:
|
|
49
|
+
raise RuntimeError("Call warm_up() before run()")
|
|
50
|
+
rows = []
|
|
51
|
+
for start in range(0, len(texts), self.batch_size):
|
|
52
|
+
rows.extend(
|
|
53
|
+
self._encoder.encode(
|
|
54
|
+
texts[start : start + self.batch_size], dtype=self.dtype, workers=self.workers
|
|
55
|
+
).tolist()
|
|
56
|
+
)
|
|
57
|
+
return rows
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
"""Serialize loader options. In-memory encoders must be saved separately."""
|
|
61
|
+
if self._injected:
|
|
62
|
+
raise ValueError("Save the encoder and use its local path before serialization")
|
|
63
|
+
if "token" in self.load_kwargs:
|
|
64
|
+
raise ValueError("Do not serialize Hub tokens. Use the Hub credential store")
|
|
65
|
+
return default_to_dict(
|
|
66
|
+
self,
|
|
67
|
+
model=self.model,
|
|
68
|
+
dtype=self.dtype,
|
|
69
|
+
workers=self.workers,
|
|
70
|
+
batch_size=self.batch_size,
|
|
71
|
+
load_kwargs={
|
|
72
|
+
k: str(v) if isinstance(v, Path) else v for k, v in self.load_kwargs.items()
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: dict):
|
|
78
|
+
return default_from_dict(cls, data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@component
|
|
82
|
+
class FseTextEmbedder(_FseEmbedder):
|
|
83
|
+
"""Embed one query. Output uses Haystack's ``embedding`` socket."""
|
|
84
|
+
|
|
85
|
+
@component.output_types(embedding=list[float])
|
|
86
|
+
def run(self, text: str) -> dict:
|
|
87
|
+
return {"embedding": self._embed([text])[0]}
|
|
88
|
+
|
|
89
|
+
@component.output_types(embedding=list[float])
|
|
90
|
+
async def run_async(self, text: str) -> dict:
|
|
91
|
+
return await to_thread(self.run, text)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@component
|
|
95
|
+
class FseDocumentEmbedder(_FseEmbedder):
|
|
96
|
+
"""Embed document content, preserving order and leaving inputs unchanged."""
|
|
97
|
+
|
|
98
|
+
@component.output_types(documents=list[Document])
|
|
99
|
+
def run(self, documents: list[Document]) -> dict:
|
|
100
|
+
if not isinstance(documents, list) or any(not isinstance(d, Document) for d in documents):
|
|
101
|
+
raise TypeError("documents must be a list of Haystack Documents")
|
|
102
|
+
texts = [doc.content for doc in documents]
|
|
103
|
+
if any(text is None for text in texts):
|
|
104
|
+
raise ValueError("Every document must have text content")
|
|
105
|
+
rows = self._embed(texts)
|
|
106
|
+
return {
|
|
107
|
+
"documents": [
|
|
108
|
+
replace(doc, embedding=row) for doc, row in zip(documents, rows, strict=True)
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@component.output_types(documents=list[Document])
|
|
113
|
+
async def run_async(self, documents: list[Document]) -> dict:
|
|
114
|
+
return await to_thread(self.run, documents)
|