fse-haystack 0.1.0__tar.gz
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.
- fse_haystack-0.1.0/CHANGELOG.md +8 -0
- fse_haystack-0.1.0/LICENSE +21 -0
- fse_haystack-0.1.0/MANIFEST.in +3 -0
- fse_haystack-0.1.0/PKG-INFO +133 -0
- fse_haystack-0.1.0/README.md +115 -0
- fse_haystack-0.1.0/examples/retrieval-output.txt +4 -0
- fse_haystack-0.1.0/examples/retrieval.py +33 -0
- fse_haystack-0.1.0/pyproject.toml +30 -0
- fse_haystack-0.1.0/setup.cfg +4 -0
- fse_haystack-0.1.0/src/fse_haystack.egg-info/PKG-INFO +133 -0
- fse_haystack-0.1.0/src/fse_haystack.egg-info/SOURCES.txt +15 -0
- fse_haystack-0.1.0/src/fse_haystack.egg-info/dependency_links.txt +1 -0
- fse_haystack-0.1.0/src/fse_haystack.egg-info/requires.txt +8 -0
- fse_haystack-0.1.0/src/fse_haystack.egg-info/top_level.txt +1 -0
- fse_haystack-0.1.0/src/haystack_integrations/components/embedders/fse/__init__.py +114 -0
- fse_haystack-0.1.0/tests/conftest.py +17 -0
- fse_haystack-0.1.0/tests/test_api.py +117 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- Prepare the standalone fse integration. Unpublished.
|
|
6
|
+
- Keep the package in the fse repository with isolated CI and a separately approved release.
|
|
7
|
+
- Require fse 2.0.0 or newer for publication.
|
|
8
|
+
- Add a real POTION retrieval example for end-to-end validation.
|
|
@@ -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,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,115 @@
|
|
|
1
|
+
# fse-haystack
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
This package lives in `integrations/fse-haystack/` in the fse repository.
|
|
5
|
+
|
|
6
|
+
This package depends on fse and `haystack-ai` only. Framework dependencies never enter
|
|
7
|
+
fse core. This package has its own CI environment and release workflow.
|
|
8
|
+
|
|
9
|
+
## Use
|
|
10
|
+
|
|
11
|
+
After an fse 2.0 build and this package are installed:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from haystack import Document
|
|
15
|
+
from haystack_integrations.components.embedders.fse import (
|
|
16
|
+
FseDocumentEmbedder,
|
|
17
|
+
FseTextEmbedder,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
query_embedder = FseTextEmbedder(model="minishlab/potion-base-8M", workers=1)
|
|
21
|
+
query_embedder.warm_up()
|
|
22
|
+
query = query_embedder.run(text="hello world")["embedding"]
|
|
23
|
+
document_embedder = FseDocumentEmbedder(workers=1, batch_size=128)
|
|
24
|
+
document_embedder.warm_up()
|
|
25
|
+
documents = document_embedder.run(documents=[Document(content="hello world")])["documents"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For an existing local model, pass `encoder=your_encoder`, built for example with
|
|
29
|
+
`fse.Encoder.from_arrays`. No download occurs with an injected encoder.
|
|
30
|
+
|
|
31
|
+
The `@component` classes expose `embedding` and `documents` output sockets.
|
|
32
|
+
They implement `run_async` as well. Call `warm_up()` before standalone execution.
|
|
33
|
+
Haystack pipelines call warm-up themselves. Document calls use batches of at most
|
|
34
|
+
`batch_size` texts, preserving input order. Every document must have text content.
|
|
35
|
+
The document embedder returns new documents and preserves IDs and metadata.
|
|
36
|
+
It embeds content only, without metadata concatenation or implicit query prefixes.
|
|
37
|
+
|
|
38
|
+
Pipeline serialization stores model and loader options, never the loaded encoder.
|
|
39
|
+
An injected encoder cannot be serialized. Save it first, then use its local path
|
|
40
|
+
as `model`. Serialization rejects an explicit Hub token to avoid saving credentials.
|
|
41
|
+
Follow the [community integration guide](https://github.com/deepset-ai/haystack-integrations#readme)
|
|
42
|
+
for a later listing. No listing is submitted in this preparation.
|
|
43
|
+
|
|
44
|
+
## Framework baseline and guides
|
|
45
|
+
|
|
46
|
+
The supported minimum for version 0.1.0 is `haystack-ai==3.4.1`, the current stable
|
|
47
|
+
release checked on 2026-11-01 from [PyPI](https://pypi.org/project/haystack-ai/3.4.1/).
|
|
48
|
+
This is our tested minimum and makes no compatibility claim for older framework releases.
|
|
49
|
+
The runtime dependency allows later versions. Recheck compatibility before publication.
|
|
50
|
+
|
|
51
|
+
The implementation follows the [official integration guide](https://docs.haystack.deepset.ai/docs/custom-components).
|
|
52
|
+
|
|
53
|
+
## Options and behavior
|
|
54
|
+
|
|
55
|
+
`load_kwargs` passes options unchanged to `Encoder.from_pretrained`.
|
|
56
|
+
`load_kwargs={"normalize": False}` returns raw means for every supported model,
|
|
57
|
+
including fse artifacts, Model2Vec and supported sentence-transformers models.
|
|
58
|
+
`normalize="model"` (the default) preserves the model's normalization rule.
|
|
59
|
+
For example, `{"revision": "<commit>", "local_files_only": True}` selects a Hub
|
|
60
|
+
revision and offline loading after the files have been cached.
|
|
61
|
+
Hub-only options (`revision`, `token`, `cache_dir`, `local_files_only`)
|
|
62
|
+
are invalid for local paths. See the [fse loader contract](https://github.com/oborchers/Fast_Sentence_Embeddings/blob/master/docs/architecture/hub.md).
|
|
63
|
+
Do not pass `load_kwargs` with `encoder=`. Configure that encoder before injection.
|
|
64
|
+
Use the Hub credential store rather than putting tokens in application configuration.
|
|
65
|
+
|
|
66
|
+
`dtype="float32"` (default) or `"float16"` controls fse output precision.
|
|
67
|
+
`workers=None` uses fse's worker policy. `workers=1` requests one worker.
|
|
68
|
+
The adapter passes text and worker validation to fse without replacing its errors.
|
|
69
|
+
Integer embeddings are unsupported because this interface requires float vectors.
|
|
70
|
+
|
|
71
|
+
Every result converts a numpy array into Python float lists. This copies values
|
|
72
|
+
and allocates Python objects, so it takes additional time and memory.
|
|
73
|
+
It does not retain the array's compact float16 or float32 storage.
|
|
74
|
+
Empty text and unknown tokens follow the loaded encoder's tokenizer rules.
|
|
75
|
+
Async methods delegate encoding and list conversion to a thread.
|
|
76
|
+
Construct models before serving async requests. Haystack also provides
|
|
77
|
+
`warm_up_async()` so async pipelines can load outside the event loop.
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
Use standard Python 3.13. From this package directory in the fse checkout:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
uv venv --python 3.13 .venv
|
|
85
|
+
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_FSE=2.0.0 \
|
|
86
|
+
uv pip install --python .venv/bin/python -e '../..' -e '.[test]'
|
|
87
|
+
.venv/bin/python -m pytest -m 'not network'
|
|
88
|
+
.venv/bin/python -m pytest -m network
|
|
89
|
+
uv build --out-dir dist
|
|
90
|
+
.venv/bin/python examples/retrieval.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The version override labels the development checkout for dependency resolution.
|
|
94
|
+
It does not create a tag, release or published package.
|
|
95
|
+
|
|
96
|
+
The declared requirement is `fse>=2.0.0`. The version override tests the local
|
|
97
|
+
unreleased checkout without weakening the published dependency.
|
|
98
|
+
The network test downloads `minishlab/potion-base-8M`. Offline tests build a tiny
|
|
99
|
+
local encoder with `Encoder.from_arrays` and compare vectors with fse directly.
|
|
100
|
+
Tests disable Python network sockets by default and enable them for the marked
|
|
101
|
+
network test. No model upload or paid service is needed.
|
|
102
|
+
|
|
103
|
+
## CI and release
|
|
104
|
+
|
|
105
|
+
The isolated CI job tests Python 3.11 with the minimum framework and Python 3.13
|
|
106
|
+
with the latest framework. Network tests and the real-model retrieval example
|
|
107
|
+
run only when requested locally. See [release setup](https://github.com/oborchers/Fast_Sentence_Embeddings/blob/master/integrations/README.md#release-setup)
|
|
108
|
+
for the package tag, approval environment and pending PyPI publisher.
|
|
109
|
+
|
|
110
|
+
## AI assistance
|
|
111
|
+
|
|
112
|
+
These package sources, tests and documentation were prepared with AI assistance
|
|
113
|
+
using Codex. Validation uses offline vector parity, framework API tests and the
|
|
114
|
+
real-model retrieval example. The maintainer remains responsible for review and maintenance.
|
|
115
|
+
Publication and framework listing submissions require a separate maintainer decision.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Real POTION indexing and a connected Haystack retrieval pipeline."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
from haystack import Document, Pipeline
|
|
7
|
+
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
|
|
8
|
+
from haystack.document_stores.in_memory import InMemoryDocumentStore
|
|
9
|
+
from haystack_integrations.components.embedders.fse import FseDocumentEmbedder, FseTextEmbedder
|
|
10
|
+
|
|
11
|
+
os.environ["HAYSTACK_TELEMETRY_ENABLED"] = "false"
|
|
12
|
+
|
|
13
|
+
texts = [
|
|
14
|
+
"Cats are small pets that purr and chase mice.",
|
|
15
|
+
"Baking bread requires flour, water and yeast.",
|
|
16
|
+
"Planets orbit the sun in the solar system.",
|
|
17
|
+
]
|
|
18
|
+
embedder = FseDocumentEmbedder(model="minishlab/potion-base-8M", workers=1, batch_size=2)
|
|
19
|
+
embedder.warm_up()
|
|
20
|
+
documents = embedder.run([Document(content=text) for text in texts])["documents"]
|
|
21
|
+
assert all(len(doc.embedding) == 256 for doc in documents)
|
|
22
|
+
store = InMemoryDocumentStore(embedding_similarity_function="cosine")
|
|
23
|
+
store.write_documents(documents)
|
|
24
|
+
pipeline = Pipeline()
|
|
25
|
+
pipeline.add_component("embed", FseTextEmbedder(model="minishlab/potion-base-8M", workers=1))
|
|
26
|
+
pipeline.add_component("retrieve", InMemoryEmbeddingRetriever(document_store=store, top_k=1))
|
|
27
|
+
pipeline.connect("embed.embedding", "retrieve.query_embedding")
|
|
28
|
+
result = pipeline.run({"embed": {"text": "Which pets purr?"}})["retrieve"]["documents"]
|
|
29
|
+
assert len(result) == 1
|
|
30
|
+
assert result[0].content == texts[0]
|
|
31
|
+
print(f"haystack-ai={version('haystack-ai')}; model=minishlab/potion-base-8M")
|
|
32
|
+
print(f"documents={len(texts)}; dimensions=256; top={result[0].content}")
|
|
33
|
+
print(f"cosine={result[0].score:.6f}; retrieval passed")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fse-haystack"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "fse embeddings for haystack-ai"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = ["fse>=2.0.0", "haystack-ai>=3.4.1"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Repository = "https://github.com/oborchers/Fast_Sentence_Embeddings"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
test = ["pytest>=8", "pytest-socket>=0.7", "pytest-asyncio>=0.24", "numpy>=1.26.4"]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
addopts = "--disable-socket --allow-unix-socket"
|
|
27
|
+
asyncio_mode = "auto"
|
|
28
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
29
|
+
filterwarnings = ["error"]
|
|
30
|
+
markers = ["network: downloads a model from the Hugging Face Hub"]
|
|
@@ -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,15 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
examples/retrieval-output.txt
|
|
7
|
+
examples/retrieval.py
|
|
8
|
+
src/fse_haystack.egg-info/PKG-INFO
|
|
9
|
+
src/fse_haystack.egg-info/SOURCES.txt
|
|
10
|
+
src/fse_haystack.egg-info/dependency_links.txt
|
|
11
|
+
src/fse_haystack.egg-info/requires.txt
|
|
12
|
+
src/fse_haystack.egg-info/top_level.txt
|
|
13
|
+
src/haystack_integrations/components/embedders/fse/__init__.py
|
|
14
|
+
tests/conftest.py
|
|
15
|
+
tests/test_api.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from fse import Encoder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def encoder():
|
|
11
|
+
return Encoder.from_arrays(
|
|
12
|
+
np.array([[1, 0, 2], [0, 2, 1]], dtype=np.float32),
|
|
13
|
+
words=["hello", "world"],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
os.environ["HAYSTACK_TELEMETRY_ENABLED"] = "false"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import runpy
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from threading import get_ident
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from haystack import Document, Pipeline
|
|
7
|
+
from haystack_integrations.components.embedders.fse import FseDocumentEmbedder, FseTextEmbedder
|
|
8
|
+
|
|
9
|
+
from fse import Encoder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("dtype", ["float32", "float16"])
|
|
13
|
+
def test_local_vectors(encoder, dtype):
|
|
14
|
+
model = FseDocumentEmbedder(encoder=encoder, dtype=dtype, workers=1, batch_size=2)
|
|
15
|
+
docs = [
|
|
16
|
+
Document(content=text, meta={"label": i})
|
|
17
|
+
for i, text in enumerate(["hello world", "", "unknown", "world", "hello"])
|
|
18
|
+
]
|
|
19
|
+
rows = encoder.encode([doc.content for doc in docs], dtype=dtype, workers=1).tolist()
|
|
20
|
+
result = model.run(docs)["documents"]
|
|
21
|
+
assert [doc.embedding for doc in result] == rows
|
|
22
|
+
assert all(doc.embedding is None for doc in docs)
|
|
23
|
+
assert [doc.meta for doc in result] == [doc.meta for doc in docs]
|
|
24
|
+
assert [doc.id for doc in result] == [doc.id for doc in docs]
|
|
25
|
+
assert model.run([]) == {"documents": []}
|
|
26
|
+
assert FseTextEmbedder(encoder=encoder, dtype=dtype).run("hello")["embedding"] == rows[-1]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pipeline_and_serialization(encoder, monkeypatch, tmp_path):
|
|
30
|
+
calls = []
|
|
31
|
+
monkeypatch.setattr(
|
|
32
|
+
Encoder, "from_pretrained", lambda *a, **kw: calls.append((a, kw)) or encoder
|
|
33
|
+
)
|
|
34
|
+
pipeline = Pipeline()
|
|
35
|
+
pipeline.add_component(
|
|
36
|
+
"embed",
|
|
37
|
+
FseTextEmbedder("test/model", load_kwargs={"normalize": False, "cache_dir": tmp_path}),
|
|
38
|
+
)
|
|
39
|
+
restored = Pipeline.loads(pipeline.dumps())
|
|
40
|
+
assert (
|
|
41
|
+
restored.run({"embed": {"text": "hello"}})["embed"]["embedding"]
|
|
42
|
+
== encoder.encode(["hello"]).tolist()[0]
|
|
43
|
+
)
|
|
44
|
+
assert calls == [(("test/model",), {"normalize": False, "cache_dir": str(tmp_path)})]
|
|
45
|
+
restored.run({"embed": {"text": "world"}})
|
|
46
|
+
assert len(calls) == 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def test_async_off_loop(encoder, monkeypatch):
|
|
50
|
+
threads = []
|
|
51
|
+
loop_thread = get_ident()
|
|
52
|
+
model = FseTextEmbedder(encoder=encoder)
|
|
53
|
+
sync = model.run
|
|
54
|
+
|
|
55
|
+
def record(text):
|
|
56
|
+
threads.append(get_ident())
|
|
57
|
+
return sync(text)
|
|
58
|
+
|
|
59
|
+
monkeypatch.setattr(model, "run", record)
|
|
60
|
+
result = await model.run_async("hello")
|
|
61
|
+
assert result["embedding"] == encoder.encode(["hello"]).tolist()[0]
|
|
62
|
+
assert threads[0] != loop_thread
|
|
63
|
+
docs = [Document(content="hello")]
|
|
64
|
+
assert (await FseDocumentEmbedder(encoder=encoder).run_async(docs))["documents"][
|
|
65
|
+
0
|
|
66
|
+
].embedding == result["embedding"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_validation(encoder):
|
|
70
|
+
with pytest.raises(RuntimeError, match="warm_up"):
|
|
71
|
+
FseTextEmbedder().run("hello")
|
|
72
|
+
with pytest.raises(ValueError, match="Save the encoder"):
|
|
73
|
+
FseTextEmbedder(encoder=encoder).to_dict()
|
|
74
|
+
with pytest.raises(ValueError, match="tokens"):
|
|
75
|
+
FseTextEmbedder(load_kwargs={"token": "not-a-secret"}).to_dict()
|
|
76
|
+
with pytest.raises(ValueError, match="text content"):
|
|
77
|
+
FseDocumentEmbedder(encoder=encoder).run([Document()])
|
|
78
|
+
with pytest.raises(TypeError, match="Documents"):
|
|
79
|
+
FseDocumentEmbedder(encoder=encoder).run(["hello"])
|
|
80
|
+
for batch in [0, -1, True, 1.5]:
|
|
81
|
+
with pytest.raises(ValueError, match="batch_size"):
|
|
82
|
+
FseDocumentEmbedder(batch_size=batch)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.network
|
|
86
|
+
@pytest.mark.enable_socket
|
|
87
|
+
def test_potion_retrieval():
|
|
88
|
+
runpy.run_path(str(Path(__file__).parents[1] / "examples" / "retrieval.py"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_local_artifact(encoder, tmp_path):
|
|
92
|
+
path = tmp_path / "encoder.safetensors"
|
|
93
|
+
encoder.save(path)
|
|
94
|
+
model = FseTextEmbedder(model=str(path))
|
|
95
|
+
model.warm_up()
|
|
96
|
+
assert model.run("hello")["embedding"] == encoder.encode(["hello"]).tolist()[0]
|
|
97
|
+
pipeline = Pipeline()
|
|
98
|
+
pipeline.add_component("embed", model)
|
|
99
|
+
clone = Pipeline.loads(pipeline.dumps())
|
|
100
|
+
assert clone.run({"embed": {"text": "hello"}})["embed"] == model.run("hello")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def test_cold_async_pipeline_loads_off_loop(encoder, monkeypatch):
|
|
104
|
+
threads = []
|
|
105
|
+
loop_thread = get_ident()
|
|
106
|
+
|
|
107
|
+
def load(*_args, **_kwargs):
|
|
108
|
+
threads.append(get_ident())
|
|
109
|
+
return encoder
|
|
110
|
+
|
|
111
|
+
monkeypatch.setattr(Encoder, "from_pretrained", load)
|
|
112
|
+
pipeline = Pipeline()
|
|
113
|
+
pipeline.add_component("embed", FseTextEmbedder("local"))
|
|
114
|
+
result = await pipeline.run_async({"embed": {"text": "hello"}})
|
|
115
|
+
assert result["embed"]["embedding"] == encoder.encode(["hello"]).tolist()[0]
|
|
116
|
+
assert threads
|
|
117
|
+
assert all(thread != loop_thread for thread in threads)
|