Repository navigation
new: support pluggable stemmer and inline stopwords for Bm25 #655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| from collections import defaultdict | ||
| from multiprocessing import get_all_start_methods | ||
| from pathlib import Path | ||
| from typing import Any, Iterable, Type | ||
| from typing import Any, Iterable, Protocol, Type | ||
|
|
||
| import mmh3 | ||
| import numpy as np | ||
|
|
@@ -22,6 +22,16 @@ | |
| from fastembed.common.model_description import SparseModelDescription, ModelSource | ||
|
|
||
|
|
||
| class Stemmer(Protocol): | ||
| """Protocol for custom stemmers pluggable into Bm25. | ||
|
|
||
| Any object exposing a `stem_word` method, e.g. `py_rust_stemmers.SnowballStemmer`, | ||
| satisfies this protocol. | ||
| """ | ||
|
|
||
| def stem_word(self, word: str) -> str: ... | ||
|
|
||
|
|
||
| supported_languages = [ | ||
| "arabic", | ||
| "danish", | ||
|
|
@@ -82,10 +92,23 @@ class Bm25(SparseTextEmbeddingBase): | |
| b (float, optional): The b parameter in the BM25 formula. Defines the importance of the document length. | ||
| Defaults to 0.75. | ||
| avg_len (float, optional): The average length of the documents in the corpus. Defaults to 256.0. | ||
| language (str): Specifies the language for the stemmer. | ||
| disable_stemmer (bool): Disable the stemmer. | ||
| language (str): Selects the default Snowball stemmer and bundled stopwords. | ||
| Unsupported languages require a custom stemmer or disable_stemmer=True, | ||
| and have no bundled stopwords. | ||
| disable_stemmer (bool): Disable stemming and bundled stopwords. Explicit inline | ||
| stopwords still apply. Cannot be combined with a custom stemmer. Lowercasing, | ||
| tokenization, punctuation removal and token_max_length filtering still apply. | ||
| stemmer (Stemmer, optional): A custom stemmer to use instead of the default SnowballStemmer. | ||
| Its stem_word method receives lowercase tokens after stopword and length filtering, | ||
| and returns a stem or an empty string to discard the token. The same stemmer is used | ||
| for documents and queries. For parallel embedding it must be deepcopyable and | ||
| pickleable, with its class defined in an importable module. Defaults to None. | ||
| stopwords (set[str], optional): Inline stopwords to use instead of the stopwords file | ||
| shipped with the model for the chosen language. Words are lowercased and matched | ||
| before stemming. None uses the default stopwords; an empty set disables filtering. | ||
| Raises: | ||
| ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en. | ||
| ValueError: If a default stemmer is unavailable for the language, or stemmer is supplied with | ||
| disable_stemmer=True. | ||
| """ | ||
|
|
||
| def __init__( | ||
|
|
@@ -99,14 +122,22 @@ def __init__( | |
| token_max_length: int = 40, | ||
| disable_stemmer: bool = False, | ||
| specific_model_path: str | None = None, | ||
| stemmer: Stemmer | None = None, | ||
| stopwords: set[str] | None = None, | ||
| **kwargs: Any, | ||
| ): | ||
| super().__init__(model_name, cache_dir, **kwargs) | ||
|
|
||
| if language not in supported_languages: | ||
| if disable_stemmer and stemmer is not None: | ||
| raise ValueError("stemmer cannot be supplied with disable_stemmer=True") | ||
| if not disable_stemmer and stemmer is None and language not in supported_languages: | ||
| raise ValueError(f"{language} language is not supported") | ||
| else: | ||
| self.language = language | ||
| self.language = language | ||
|
|
||
| self._custom_stemmer = stemmer | ||
| self._custom_stopwords = ( | ||
| {word.lower() for word in stopwords} if stopwords is not None else None | ||
| ) | ||
|
|
||
| self.k = k | ||
| self.b = b | ||
|
|
@@ -127,11 +158,18 @@ def __init__( | |
| self.punctuation = set(get_all_punctuation()) | ||
| self.disable_stemmer = disable_stemmer | ||
|
|
||
| if disable_stemmer: | ||
| self.stopwords: set[str] = set() | ||
| self.stemmer = None | ||
| if self._custom_stopwords is not None: | ||
| self.stopwords = self._custom_stopwords | ||
|
Comment on lines
+161
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Inspect whether the model download fetches additional_files unconditionally.
rg -n -C 12 'def download_model\(|additional_files' fastembedRepository: qdrant/fastembed Length of output: 36393 Skip bundled stopword files when inline stopwords are provided.
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is no include or exclude api There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
rg -n -C 5 'def download_model\(|additional_files' fastembed/sparse fastembed/common | head -220
fd -a 'sparse_embedding_base.py|model_description.py|model_management.py' fastembedLength of output: 6242 🏁 Script executed: #!/bin/bash
set -e
ast-grep outline fastembed/common/model_management.py --match 'download_model|_download' --view expanded
ast-grep outline fastembed/common/model_description.py --view expanded
sed -n '370,485p' fastembed/common/model_management.py
sed -n '1,65p' fastembed/common/model_description.pyLength of output: 7191
The underlying concern still applies. ✏️ Learnings added
You are interacting with an AI system. |
||
| elif disable_stemmer: | ||
| self.stopwords = set() | ||
| else: | ||
| self.stopwords = set(self._load_stopwords(self._model_dir, self.language)) | ||
|
|
||
| if stemmer is not None: | ||
| self.stemmer = stemmer | ||
| elif disable_stemmer: | ||
| self.stemmer = None | ||
| else: | ||
| self.stemmer = SnowballStemmer(language) | ||
|
|
||
| self.tokenizer = SimpleTokenizer | ||
|
|
@@ -147,6 +185,9 @@ def _list_supported_models(cls) -> list[SparseModelDescription]: | |
|
|
||
| @classmethod | ||
| def _load_stopwords(cls, model_dir: Path, language: str) -> list[str]: | ||
| if language not in supported_languages: | ||
| return [] | ||
|
|
||
| stopwords_path = model_dir / f"{language}.txt" | ||
| if not stopwords_path.exists(): | ||
| return [] | ||
|
|
@@ -191,6 +232,8 @@ def _embed_documents( | |
| "language": self.language, | ||
| "token_max_length": self.token_max_length, | ||
| "disable_stemmer": self.disable_stemmer, | ||
| "stemmer": self._custom_stemmer, | ||
| "stopwords": self._custom_stopwords, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| "local_files_only": local_files_only, | ||
| "specific_model_path": specific_model_path, | ||
| } | ||
|
|
@@ -249,7 +292,9 @@ def _stem(self, tokens: list[str]) -> list[str]: | |
| if len(token) > self.token_max_length: | ||
| continue | ||
|
|
||
| stemmed_token = self.stemmer.stem_word(lower_token) if self.stemmer else lower_token | ||
| stemmed_token = ( | ||
| self.stemmer.stem_word(lower_token) if self.stemmer is not None else lower_token | ||
| ) | ||
|
|
||
| if stemmed_token: | ||
| stemmed_tokens.append(stemmed_token) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.