fastembed-gpu 0.5.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.
- fastembed/__init__.py +20 -0
- fastembed/common/__init__.py +3 -0
- fastembed/common/model_management.py +301 -0
- fastembed/common/onnx_model.py +132 -0
- fastembed/common/preprocessor_utils.py +82 -0
- fastembed/common/types.py +16 -0
- fastembed/common/utils.py +55 -0
- fastembed/embedding.py +24 -0
- fastembed/image/__init__.py +3 -0
- fastembed/image/image_embedding.py +97 -0
- fastembed/image/image_embedding_base.py +44 -0
- fastembed/image/onnx_embedding.py +211 -0
- fastembed/image/onnx_image_model.py +131 -0
- fastembed/image/transform/functional.py +150 -0
- fastembed/image/transform/operators.py +268 -0
- fastembed/late_interaction/__init__.py +5 -0
- fastembed/late_interaction/colbert.py +256 -0
- fastembed/late_interaction/jina_colbert.py +62 -0
- fastembed/late_interaction/late_interaction_embedding_base.py +62 -0
- fastembed/late_interaction/late_interaction_text_embedding.py +114 -0
- fastembed/parallel_processor.py +252 -0
- fastembed/rerank/cross_encoder/__init__.py +3 -0
- fastembed/rerank/cross_encoder/onnx_text_cross_encoder.py +224 -0
- fastembed/rerank/cross_encoder/onnx_text_model.py +150 -0
- fastembed/rerank/cross_encoder/text_cross_encoder.py +120 -0
- fastembed/rerank/cross_encoder/text_cross_encoder_base.py +58 -0
- fastembed/sparse/__init__.py +4 -0
- fastembed/sparse/bm25.py +347 -0
- fastembed/sparse/bm42.py +340 -0
- fastembed/sparse/sparse_embedding_base.py +83 -0
- fastembed/sparse/sparse_text_embedding.py +121 -0
- fastembed/sparse/splade_pp.py +180 -0
- fastembed/sparse/utils/tokenizer.py +120 -0
- fastembed/text/__init__.py +3 -0
- fastembed/text/clip_embedding.py +54 -0
- fastembed/text/e5_onnx_embedding.py +72 -0
- fastembed/text/onnx_embedding.py +333 -0
- fastembed/text/onnx_text_model.py +145 -0
- fastembed/text/pooled_embedding.py +92 -0
- fastembed/text/pooled_normalized_embedding.py +125 -0
- fastembed/text/text_embedding.py +107 -0
- fastembed/text/text_embedding_base.py +62 -0
- fastembed_gpu-0.5.1.dist-info/LICENSE +201 -0
- fastembed_gpu-0.5.1.dist-info/METADATA +262 -0
- fastembed_gpu-0.5.1.dist-info/NOTICE +14 -0
- fastembed_gpu-0.5.1.dist-info/RECORD +47 -0
- fastembed_gpu-0.5.1.dist-info/WHEEL +4 -0
fastembed/sparse/bm42.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import string
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
|
5
|
+
|
|
6
|
+
import mmh3
|
|
7
|
+
import numpy as np
|
|
8
|
+
from py_rust_stemmers import SnowballStemmer
|
|
9
|
+
|
|
10
|
+
from fastembed.common import OnnxProvider
|
|
11
|
+
from fastembed.common.onnx_model import OnnxOutputContext
|
|
12
|
+
from fastembed.common.utils import define_cache_dir
|
|
13
|
+
from fastembed.sparse.sparse_embedding_base import (
|
|
14
|
+
SparseEmbedding,
|
|
15
|
+
SparseTextEmbeddingBase,
|
|
16
|
+
)
|
|
17
|
+
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
|
|
18
|
+
|
|
19
|
+
supported_bm42_models = [
|
|
20
|
+
{
|
|
21
|
+
"model": "Qdrant/bm42-all-minilm-l6-v2-attentions",
|
|
22
|
+
"vocab_size": 30522,
|
|
23
|
+
"description": "Light sparse embedding model, which assigns an importance score to each token in the text",
|
|
24
|
+
"license": "apache-2.0",
|
|
25
|
+
"size_in_GB": 0.09,
|
|
26
|
+
"sources": {
|
|
27
|
+
"hf": "Qdrant/all_miniLM_L6_v2_with_attentions",
|
|
28
|
+
},
|
|
29
|
+
"model_file": "model.onnx",
|
|
30
|
+
"additional_files": ["stopwords.txt"],
|
|
31
|
+
"requires_idf": True,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
MODEL_TO_LANGUAGE = {
|
|
36
|
+
"Qdrant/bm42-all-minilm-l6-v2-attentions": "english",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
|
|
41
|
+
"""
|
|
42
|
+
Bm42 is an extension of BM25, which tries to better evaluate importance of tokens in the documents,
|
|
43
|
+
by extracting attention weights from the transformer model.
|
|
44
|
+
|
|
45
|
+
Traditional BM25 uses a count of tokens in the document to evaluate the importance of the token,
|
|
46
|
+
but this approach doesn't work well with short documents or chunks of text, as almost all tokens
|
|
47
|
+
there are unique.
|
|
48
|
+
|
|
49
|
+
BM42 addresses this issue by replacing the token count with the attention weights from the transformer model.
|
|
50
|
+
This allows sparse embeddings to work well with short documents, handle rare tokens and leverage traditional NLP
|
|
51
|
+
techniques like stemming and stopwords.
|
|
52
|
+
|
|
53
|
+
WARNING: This model is expected to be used with `modifier="idf"` in the sparse vector index of Qdrant.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
ONNX_OUTPUT_NAMES = ["attention_6"]
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
model_name: str,
|
|
61
|
+
cache_dir: Optional[str] = None,
|
|
62
|
+
threads: Optional[int] = None,
|
|
63
|
+
providers: Optional[Sequence[OnnxProvider]] = None,
|
|
64
|
+
alpha: float = 0.5,
|
|
65
|
+
cuda: bool = False,
|
|
66
|
+
device_ids: Optional[list[int]] = None,
|
|
67
|
+
lazy_load: bool = False,
|
|
68
|
+
device_id: Optional[int] = None,
|
|
69
|
+
**kwargs,
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Args:
|
|
73
|
+
model_name (str): The name of the model to use.
|
|
74
|
+
cache_dir (str, optional): The path to the cache directory.
|
|
75
|
+
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
|
76
|
+
Defaults to `fastembed_cache` in the system's temp directory.
|
|
77
|
+
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
|
78
|
+
providers (Optional[Sequence[OnnxProvider]], optional): The providers to use for onnxruntime.
|
|
79
|
+
alpha (float, optional): Parameter, that defines the importance of the token weight in the document
|
|
80
|
+
versus the importance of the token frequency in the corpus. Defaults to 0.5, based on empirical testing.
|
|
81
|
+
It is recommended to only change this parameter based on training data for a specific dataset.
|
|
82
|
+
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
|
83
|
+
Defaults to False.
|
|
84
|
+
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
|
85
|
+
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
|
86
|
+
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
|
87
|
+
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
|
88
|
+
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
super().__init__(model_name, cache_dir, threads, **kwargs)
|
|
95
|
+
self.providers = providers
|
|
96
|
+
self.lazy_load = lazy_load
|
|
97
|
+
|
|
98
|
+
# List of device ids, that can be used for data parallel processing in workers
|
|
99
|
+
self.device_ids = device_ids
|
|
100
|
+
self.cuda = cuda
|
|
101
|
+
|
|
102
|
+
# This device_id will be used if we need to load model in current process
|
|
103
|
+
if device_id is not None:
|
|
104
|
+
self.device_id = device_id
|
|
105
|
+
elif self.device_ids is not None:
|
|
106
|
+
self.device_id = self.device_ids[0]
|
|
107
|
+
else:
|
|
108
|
+
self.device_id = None
|
|
109
|
+
|
|
110
|
+
self.model_description = self._get_model_description(model_name)
|
|
111
|
+
self.cache_dir = define_cache_dir(cache_dir)
|
|
112
|
+
|
|
113
|
+
self._model_dir = self.download_model(
|
|
114
|
+
self.model_description, self.cache_dir, local_files_only=self._local_files_only
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.invert_vocab = {}
|
|
118
|
+
|
|
119
|
+
self.special_tokens = set()
|
|
120
|
+
self.special_tokens_ids = set()
|
|
121
|
+
self.punctuation = set(string.punctuation)
|
|
122
|
+
self.stopwords = set(self._load_stopwords(self._model_dir))
|
|
123
|
+
self.stemmer = SnowballStemmer(MODEL_TO_LANGUAGE[model_name])
|
|
124
|
+
self.alpha = alpha
|
|
125
|
+
|
|
126
|
+
if not self.lazy_load:
|
|
127
|
+
self.load_onnx_model()
|
|
128
|
+
|
|
129
|
+
def load_onnx_model(self) -> None:
|
|
130
|
+
self._load_onnx_model(
|
|
131
|
+
model_dir=self._model_dir,
|
|
132
|
+
model_file=self.model_description["model_file"],
|
|
133
|
+
threads=self.threads,
|
|
134
|
+
providers=self.providers,
|
|
135
|
+
cuda=self.cuda,
|
|
136
|
+
device_id=self.device_id,
|
|
137
|
+
)
|
|
138
|
+
for token, idx in self.tokenizer.get_vocab().items():
|
|
139
|
+
self.invert_vocab[idx] = token
|
|
140
|
+
self.special_tokens = set(self.special_token_to_id.keys())
|
|
141
|
+
self.special_tokens_ids = set(self.special_token_to_id.values())
|
|
142
|
+
self.stopwords = set(self._load_stopwords(self._model_dir))
|
|
143
|
+
|
|
144
|
+
def _filter_pair_tokens(self, tokens: list[tuple[str, Any]]) -> list[tuple[str, Any]]:
|
|
145
|
+
result = []
|
|
146
|
+
for token, value in tokens:
|
|
147
|
+
if token in self.stopwords or token in self.punctuation:
|
|
148
|
+
continue
|
|
149
|
+
result.append((token, value))
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
def _stem_pair_tokens(self, tokens: list[tuple[str, Any]]) -> list[tuple[str, Any]]:
|
|
153
|
+
result = []
|
|
154
|
+
for token, value in tokens:
|
|
155
|
+
processed_token = self.stemmer.stem_word(token)
|
|
156
|
+
result.append((processed_token, value))
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def _aggregate_weights(
|
|
161
|
+
cls, tokens: list[tuple[str, list[int]]], weights: list[float]
|
|
162
|
+
) -> list[tuple[str, float]]:
|
|
163
|
+
result = []
|
|
164
|
+
for token, idxs in tokens:
|
|
165
|
+
sum_weight = sum(weights[idx] for idx in idxs)
|
|
166
|
+
result.append((token, sum_weight))
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def _reconstruct_bpe(
|
|
170
|
+
self, bpe_tokens: Iterable[tuple[int, str]]
|
|
171
|
+
) -> list[tuple[str, list[int]]]:
|
|
172
|
+
result = []
|
|
173
|
+
acc = ""
|
|
174
|
+
acc_idx = []
|
|
175
|
+
|
|
176
|
+
continuing_subword_prefix = self.tokenizer.model.continuing_subword_prefix
|
|
177
|
+
continuing_subword_prefix_len = len(continuing_subword_prefix)
|
|
178
|
+
|
|
179
|
+
for idx, token in bpe_tokens:
|
|
180
|
+
if token in self.special_tokens:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if token.startswith(continuing_subword_prefix):
|
|
184
|
+
acc += token[continuing_subword_prefix_len:]
|
|
185
|
+
acc_idx.append(idx)
|
|
186
|
+
else:
|
|
187
|
+
if acc:
|
|
188
|
+
result.append((acc, acc_idx))
|
|
189
|
+
acc_idx = []
|
|
190
|
+
acc = token
|
|
191
|
+
acc_idx.append(idx)
|
|
192
|
+
|
|
193
|
+
if acc:
|
|
194
|
+
result.append((acc, acc_idx))
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def _rescore_vector(self, vector: dict[str, float]) -> dict[int, float]:
|
|
199
|
+
"""
|
|
200
|
+
Orders all tokens in the vector by their importance and generates a new score based on the importance order.
|
|
201
|
+
So that the scoring doesn't depend on absolute values assigned by the model, but on the relative importance.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
new_vector = {}
|
|
205
|
+
|
|
206
|
+
for token, value in vector.items():
|
|
207
|
+
token_id = abs(mmh3.hash(token))
|
|
208
|
+
# Examples:
|
|
209
|
+
# Num 0: Log(1/1 + 1) = 0.6931471805599453
|
|
210
|
+
# Num 1: Log(1/2 + 1) = 0.4054651081081644
|
|
211
|
+
# Num 2: Log(1/3 + 1) = 0.28768207245178085
|
|
212
|
+
new_vector[token_id] = math.log(1.0 + value) ** self.alpha # value
|
|
213
|
+
|
|
214
|
+
return new_vector
|
|
215
|
+
|
|
216
|
+
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[SparseEmbedding]:
|
|
217
|
+
if output.input_ids is None:
|
|
218
|
+
raise ValueError("input_ids must be provided for document post-processing")
|
|
219
|
+
|
|
220
|
+
token_ids_batch = output.input_ids
|
|
221
|
+
|
|
222
|
+
# attention_value shape: (batch_size, num_heads, num_tokens, num_tokens)
|
|
223
|
+
pooled_attention = np.mean(output.model_output[:, :, 0], axis=1) * output.attention_mask
|
|
224
|
+
|
|
225
|
+
for document_token_ids, attention_value in zip(token_ids_batch, pooled_attention):
|
|
226
|
+
document_tokens_with_ids = (
|
|
227
|
+
(idx, self.invert_vocab[token_id])
|
|
228
|
+
for idx, token_id in enumerate(document_token_ids)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
reconstructed = self._reconstruct_bpe(document_tokens_with_ids)
|
|
232
|
+
|
|
233
|
+
filtered = self._filter_pair_tokens(reconstructed)
|
|
234
|
+
|
|
235
|
+
stemmed = self._stem_pair_tokens(filtered)
|
|
236
|
+
|
|
237
|
+
weighted = self._aggregate_weights(stemmed, attention_value)
|
|
238
|
+
|
|
239
|
+
max_token_weight = {}
|
|
240
|
+
|
|
241
|
+
for token, weight in weighted:
|
|
242
|
+
max_token_weight[token] = max(max_token_weight.get(token, 0), weight)
|
|
243
|
+
|
|
244
|
+
rescored = self._rescore_vector(max_token_weight)
|
|
245
|
+
|
|
246
|
+
yield SparseEmbedding.from_dict(rescored)
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def list_supported_models(cls) -> list[dict[str, Any]]:
|
|
250
|
+
"""Lists the supported models.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
|
254
|
+
"""
|
|
255
|
+
return supported_bm42_models
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def _load_stopwords(cls, model_dir: Path) -> list[str]:
|
|
259
|
+
stopwords_path = model_dir / "stopwords.txt"
|
|
260
|
+
if not stopwords_path.exists():
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
with open(stopwords_path, "r") as f:
|
|
264
|
+
return f.read().splitlines()
|
|
265
|
+
|
|
266
|
+
def embed(
|
|
267
|
+
self,
|
|
268
|
+
documents: Union[str, Iterable[str]],
|
|
269
|
+
batch_size: int = 256,
|
|
270
|
+
parallel: Optional[int] = None,
|
|
271
|
+
**kwargs,
|
|
272
|
+
) -> Iterable[SparseEmbedding]:
|
|
273
|
+
"""
|
|
274
|
+
Encode a list of documents into list of embeddings.
|
|
275
|
+
We use mean pooling with attention so that the model can handle variable-length inputs.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
documents: Iterator of documents or single document to embed
|
|
279
|
+
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
|
280
|
+
parallel:
|
|
281
|
+
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
|
282
|
+
If 0, use all available cores.
|
|
283
|
+
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of embeddings, one per document
|
|
287
|
+
"""
|
|
288
|
+
yield from self._embed_documents(
|
|
289
|
+
model_name=self.model_name,
|
|
290
|
+
cache_dir=str(self.cache_dir),
|
|
291
|
+
documents=documents,
|
|
292
|
+
batch_size=batch_size,
|
|
293
|
+
parallel=parallel,
|
|
294
|
+
providers=self.providers,
|
|
295
|
+
cuda=self.cuda,
|
|
296
|
+
device_ids=self.device_ids,
|
|
297
|
+
alpha=self.alpha,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def _query_rehash(cls, tokens: Iterable[str]) -> dict[int, float]:
|
|
302
|
+
result = {}
|
|
303
|
+
for token in tokens:
|
|
304
|
+
token_id = abs(mmh3.hash(token))
|
|
305
|
+
result[token_id] = 1.0
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[SparseEmbedding]:
|
|
309
|
+
"""
|
|
310
|
+
To emulate BM25 behaviour, we don't need to use smart weights in the query, and
|
|
311
|
+
it's enough to just hash the tokens and assign a weight of 1.0 to them.
|
|
312
|
+
It is also faster, as we don't need to run the model for the query.
|
|
313
|
+
"""
|
|
314
|
+
if isinstance(query, str):
|
|
315
|
+
query = [query]
|
|
316
|
+
|
|
317
|
+
if not hasattr(self, "model") or self.model is None:
|
|
318
|
+
self.load_onnx_model()
|
|
319
|
+
|
|
320
|
+
for text in query:
|
|
321
|
+
encoded = self.tokenizer.encode(text)
|
|
322
|
+
document_tokens_with_ids = enumerate(encoded.tokens)
|
|
323
|
+
reconstructed = self._reconstruct_bpe(document_tokens_with_ids)
|
|
324
|
+
filtered = self._filter_pair_tokens(reconstructed)
|
|
325
|
+
stemmed = self._stem_pair_tokens(filtered)
|
|
326
|
+
|
|
327
|
+
yield SparseEmbedding.from_dict(self._query_rehash(token for token, _ in stemmed))
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
|
|
331
|
+
return Bm42TextEmbeddingWorker
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class Bm42TextEmbeddingWorker(TextEmbeddingWorker):
|
|
335
|
+
def init_embedding(self, model_name: str, cache_dir: str, **kwargs) -> Bm42:
|
|
336
|
+
return Bm42(
|
|
337
|
+
model_name=model_name,
|
|
338
|
+
cache_dir=cache_dir,
|
|
339
|
+
**kwargs,
|
|
340
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Iterable, Optional, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from fastembed.common.model_management import ModelManagement
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SparseEmbedding:
|
|
11
|
+
values: np.ndarray
|
|
12
|
+
indices: np.ndarray
|
|
13
|
+
|
|
14
|
+
def as_object(self) -> dict[str, np.ndarray]:
|
|
15
|
+
return {
|
|
16
|
+
"values": self.values,
|
|
17
|
+
"indices": self.indices,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def as_dict(self) -> dict[int, float]:
|
|
21
|
+
return {i: v for i, v in zip(self.indices, self.values)}
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_dict(cls, data: dict[int, float]) -> "SparseEmbedding":
|
|
25
|
+
if len(data) == 0:
|
|
26
|
+
return cls(values=np.array([]), indices=np.array([]))
|
|
27
|
+
indices, values = zip(*data.items())
|
|
28
|
+
return cls(values=np.array(values), indices=np.array(indices))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SparseTextEmbeddingBase(ModelManagement):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
model_name: str,
|
|
35
|
+
cache_dir: Optional[str] = None,
|
|
36
|
+
threads: Optional[int] = None,
|
|
37
|
+
**kwargs,
|
|
38
|
+
):
|
|
39
|
+
self.model_name = model_name
|
|
40
|
+
self.cache_dir = cache_dir
|
|
41
|
+
self.threads = threads
|
|
42
|
+
self._local_files_only = kwargs.pop("local_files_only", False)
|
|
43
|
+
|
|
44
|
+
def embed(
|
|
45
|
+
self,
|
|
46
|
+
documents: Union[str, Iterable[str]],
|
|
47
|
+
batch_size: int = 256,
|
|
48
|
+
parallel: Optional[int] = None,
|
|
49
|
+
**kwargs,
|
|
50
|
+
) -> Iterable[SparseEmbedding]:
|
|
51
|
+
raise NotImplementedError()
|
|
52
|
+
|
|
53
|
+
def passage_embed(self, texts: Iterable[str], **kwargs) -> Iterable[SparseEmbedding]:
|
|
54
|
+
"""
|
|
55
|
+
Embeds a list of text passages into a list of embeddings.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
texts (Iterable[str]): The list of texts to embed.
|
|
59
|
+
**kwargs: Additional keyword argument to pass to the embed method.
|
|
60
|
+
|
|
61
|
+
Yields:
|
|
62
|
+
Iterable[SparseEmbedding]: The sparse embeddings.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# This is model-specific, so that different models can have specialized implementations
|
|
66
|
+
yield from self.embed(texts, **kwargs)
|
|
67
|
+
|
|
68
|
+
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[SparseEmbedding]:
|
|
69
|
+
"""
|
|
70
|
+
Embeds queries
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Iterable[SparseEmbedding]: The sparse embeddings.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# This is model-specific, so that different models can have specialized implementations
|
|
80
|
+
if isinstance(query, str):
|
|
81
|
+
yield from self.embed([query], **kwargs)
|
|
82
|
+
if isinstance(query, Iterable):
|
|
83
|
+
yield from self.embed(query, **kwargs)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
|
2
|
+
|
|
3
|
+
from fastembed.common import OnnxProvider
|
|
4
|
+
from fastembed.sparse.bm25 import Bm25
|
|
5
|
+
from fastembed.sparse.bm42 import Bm42
|
|
6
|
+
from fastembed.sparse.sparse_embedding_base import (
|
|
7
|
+
SparseEmbedding,
|
|
8
|
+
SparseTextEmbeddingBase,
|
|
9
|
+
)
|
|
10
|
+
from fastembed.sparse.splade_pp import SpladePP
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SparseTextEmbedding(SparseTextEmbeddingBase):
|
|
15
|
+
EMBEDDINGS_REGISTRY: list[Type[SparseTextEmbeddingBase]] = [SpladePP, Bm42, Bm25]
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def list_supported_models(cls) -> list[dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
Lists the supported models.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```
|
|
27
|
+
[
|
|
28
|
+
{
|
|
29
|
+
"model": "prithvida/SPLADE_PP_en_v1",
|
|
30
|
+
"vocab_size": 30522,
|
|
31
|
+
"description": "Independent Implementation of SPLADE++ Model for English",
|
|
32
|
+
"license": "apache-2.0",
|
|
33
|
+
"size_in_GB": 0.532,
|
|
34
|
+
"sources": {
|
|
35
|
+
"hf": "qdrant/SPLADE_PP_en_v1",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
result = []
|
|
42
|
+
for embedding in cls.EMBEDDINGS_REGISTRY:
|
|
43
|
+
result.extend(embedding.list_supported_models())
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
model_name: str,
|
|
49
|
+
cache_dir: Optional[str] = None,
|
|
50
|
+
threads: Optional[int] = None,
|
|
51
|
+
providers: Optional[Sequence[OnnxProvider]] = None,
|
|
52
|
+
cuda: bool = False,
|
|
53
|
+
device_ids: Optional[list[int]] = None,
|
|
54
|
+
lazy_load: bool = False,
|
|
55
|
+
**kwargs,
|
|
56
|
+
):
|
|
57
|
+
super().__init__(model_name, cache_dir, threads, **kwargs)
|
|
58
|
+
if model_name == "prithvida/Splade_PP_en_v1":
|
|
59
|
+
warnings.warn(
|
|
60
|
+
"The right spelling is prithivida/Splade_PP_en_v1. "
|
|
61
|
+
"Support of this name will be removed soon, please fix the model_name",
|
|
62
|
+
DeprecationWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
model_name = "prithivida/Splade_PP_en_v1"
|
|
66
|
+
|
|
67
|
+
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
|
|
68
|
+
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
|
|
69
|
+
if any(model_name.lower() == model["model"].lower() for model in supported_models):
|
|
70
|
+
self.model = EMBEDDING_MODEL_TYPE(
|
|
71
|
+
model_name,
|
|
72
|
+
cache_dir,
|
|
73
|
+
threads=threads,
|
|
74
|
+
providers=providers,
|
|
75
|
+
cuda=cuda,
|
|
76
|
+
device_ids=device_ids,
|
|
77
|
+
lazy_load=lazy_load,
|
|
78
|
+
**kwargs,
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Model {model_name} is not supported in SparseTextEmbedding."
|
|
84
|
+
"Please check the supported models using `SparseTextEmbedding.list_supported_models()`"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def embed(
|
|
88
|
+
self,
|
|
89
|
+
documents: Union[str, Iterable[str]],
|
|
90
|
+
batch_size: int = 256,
|
|
91
|
+
parallel: Optional[int] = None,
|
|
92
|
+
**kwargs,
|
|
93
|
+
) -> Iterable[SparseEmbedding]:
|
|
94
|
+
"""
|
|
95
|
+
Encode a list of documents into list of embeddings.
|
|
96
|
+
We use mean pooling with attention so that the model can handle variable-length inputs.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
documents: Iterator of documents or single document to embed
|
|
100
|
+
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
|
101
|
+
parallel:
|
|
102
|
+
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
|
103
|
+
If 0, use all available cores.
|
|
104
|
+
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of embeddings, one per document
|
|
108
|
+
"""
|
|
109
|
+
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
|
|
110
|
+
|
|
111
|
+
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[SparseEmbedding]:
|
|
112
|
+
"""
|
|
113
|
+
Embeds queries
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Iterable[SparseEmbedding]: The sparse embeddings.
|
|
120
|
+
"""
|
|
121
|
+
yield from self.model.query_embed(query, **kwargs)
|