Skip to content

Commit 2d836d6

Browse files
rbs333Copilot
andauthored
Drop nltk (#645)
# Drop NLTK dependency; serve stopwords statically ## Why RedisVL only used NLTK for one thing: default stopword lists in full-text query building. Pulling in the whole `nltk` package for that has cost us repeatedly — packaging/support friction and a recurring source of test flakiness (parallel pytest-xdist workers racing on `nltk.download()`, most recently patched in #644). The stopword lists are small, static, and change rarely. This PR bundles them directly and removes `nltk` entirely. ## What changed - **New static data** — `redisvl/utils/stopwords.json` (33 languages, ~11k words, ~120KB), extracted once from NLTK's `stopwords` corpus so the lists are byte-identical to what NLTK returned. **No behavior change** for any supported language. - **New loader** — `redisvl/utils/stopwords.py` exposes `get_stopwords(language)`, which lazily reads and caches the JSON via `importlib.resources`. - **Rewired consumers** — `full_text_query_helper.py` and `query.py` now call `get_stopwords()` instead of lazily importing NLTK and downloading the corpus. The download/race/retry blocks are gone. - **Removed vestigial imports** — `aggregate.py` had unused `nltk` lazy-imports (it already routes through the helper); deleted. - **Dropped the dependency** — removed the `nltk` extra and its entry in the `all` extra from `pyproject.toml`. - **Simplified tests** — deleted the `ensure_nltk_stopwords` session fixture from `tests/conftest.py`. The xdist download race it guarded against no longer exists: there is no download. - **Spellcheck** — added `stopwords.json` to codespell's `--skip` list (it's a multi-language word-list data file, not prose). ## Backward compatibility The public API is unchanged. All existing usages still work: - `stopwords="english"` (default), `"german"`, `"french"`, etc. — any of the 33 bundled languages - `stopwords={"custom", "set"}` / list / tuple - `stopwords=None` (no filtering) Error behavior is preserved: an unknown language string raises `ValueError` (now with the list of available languages), and a non-string/non-collection raises `TypeError`. The only user-visible change: `pip install redisvl[nltk]` no longer resolves (the extra was removed). Worth a changelog note. ## Verification - All stopword / query / aggregation / hybrid unit tests pass (945 passed); `german` still yields `der`/`die`/`und`, invalid language → `ValueError`, invalid type → `TypeError`. - Confirmed `stopwords.json` ships in the built wheel (`redisvl/utils/stopwords.json` present in `redisvl-*.whl`). - pre-commit (format, isort, mypy, codespell) passes. ## Downstream follow-up (separate, not in this PR) `redis-ai-resources` has two notebooks (`python-recipes/vector-search/01_redisvl.ipynb`, `02_hybrid_search.ipynb`) that carry `nltk` in their `%pip install` lines and one stale markdown reference. Nothing breaks (they never `import nltk`), but the `nltk` install is now dead weight. That cleanup should land **after** this release and bump the notebooks' `redisvl` floor to the version that bundles stopwords. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches default full-text tokenization for all string `stopwords` language codes; lists are meant to match NLTK but any drift or packaging omission of `stopwords.json` would change search behavior. Removing the `nltk` extra is a breaking install change for users who relied on it. > > **Overview** > **Removes the `nltk` optional dependency** and ships default full-text stopword lists as bundled `redisvl/utils/stopwords.json` (~33 languages), loaded via new `get_stopwords()` in `redisvl/utils/stopwords.py`. > > **Full-text query paths** (`query.py`, `full_text_query_helper.py`) now call `get_stopwords()` instead of lazy NLTK imports and runtime `nltk.download()`; unused NLTK imports in `aggregate.py` are removed. > > **Packaging and CI:** `nltk` is dropped from `pyproject.toml` extras/`all` and `uv.lock`; codespell skips `stopwords.json`. The pytest session fixture that pre-downloaded NLTK stopwords for xdist is deleted from `tests/conftest.py`. > > Public stopword API behavior for supported languages and custom sets is intended to stay the same; `pip install redisvl[nltk]` no longer applies. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 23a8c335ea20a879319cdccababa91b0cabe5be6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 36f7612 commit 2d836d6

9 files changed

Lines changed: 11205 additions & 105 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ repos:
1313
name: Check spelling
1414
args:
1515
- --write-changes
16-
- --skip=*.pyc,*.pyo,*.lock,*.git,*.mypy_cache,__pycache__,*.egg-info,.pytest_cache,docs/_build,env,venv,.venv
16+
- --skip=*.pyc,*.pyo,*.lock,*.git,*.mypy_cache,__pycache__,*.egg-info,.pytest_cache,docs/_build,env,venv,.venv,redisvl/utils/stopwords.json
1717
- --ignore-words-list=enginee

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ mcp = [
4040
]
4141
mistralai = ["mistralai>=1.0.0,<2"]
4242
openai = ["openai>=1.1.0"]
43-
nltk = ["nltk>=3.8.1,<4"]
4443
cohere = ["cohere>=4.44"]
4544
voyageai = ["voyageai>=0.2.2"]
4645
sentence-transformers = ["sentence-transformers>=3.4.0,<4"]
@@ -62,7 +61,6 @@ sql-redis = [
6261
all = [
6362
"mistralai>=1.0.0,<2",
6463
"openai>=1.1.0",
65-
"nltk>=3.8.1,<4",
6664
"cohere>=4.44",
6765
"voyageai>=0.2.2",
6866
"sentence-transformers>=3.4.0,<4",

redisvl/query/aggregate.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
from redisvl.redis.utils import array_to_buffer
99
from redisvl.schema.fields import VectorDataType
1010
from redisvl.utils.full_text_query_helper import FullTextQueryHelper
11-
from redisvl.utils.utils import lazy_import
12-
13-
nltk = lazy_import("nltk")
14-
nltk_stopwords = lazy_import("nltk.corpus.stopwords")
1511

1612

1713
class Vector(BaseModel):

redisvl/query/query.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
from redisvl.query.filter import FilterExpression
77
from redisvl.redis.utils import array_to_buffer
88
from redisvl.utils.log import get_logger
9+
from redisvl.utils.stopwords import get_stopwords
910
from redisvl.utils.token_escaper import TokenEscaper
10-
from redisvl.utils.utils import denorm_cosine_distance, lazy_import
11+
from redisvl.utils.utils import denorm_cosine_distance
1112

1213
logger = get_logger(__name__)
1314

14-
nltk = lazy_import("nltk")
15-
nltk_stopwords = lazy_import("nltk.corpus.stopwords")
16-
1715
# Type alias for sort specification
1816
# Can be:
1917
# - str: single field name (ASC by default)
@@ -1406,21 +1404,7 @@ def _set_stopwords(self, stopwords: str | set[str] | None = "english"):
14061404
if not stopwords:
14071405
self._stopwords = set()
14081406
elif isinstance(stopwords, str):
1409-
try:
1410-
# Try loading first; only download if not already present.
1411-
# This avoids race conditions when parallel workers (e.g.
1412-
# pytest-xdist) call nltk.download() concurrently.
1413-
try:
1414-
self._stopwords = set(nltk_stopwords.words(stopwords))
1415-
except LookupError:
1416-
nltk.download("stopwords", quiet=True)
1417-
self._stopwords = set(nltk_stopwords.words(stopwords))
1418-
except ImportError:
1419-
raise ValueError(
1420-
f"Loading stopwords for {stopwords} failed: nltk is not installed."
1421-
)
1422-
except Exception as e:
1423-
raise ValueError(f"Error trying to load {stopwords} from nltk. {e}")
1407+
self._stopwords = get_stopwords(stopwords)
14241408
elif isinstance(stopwords, (set, list, tuple)) and all(
14251409
isinstance(word, str) for word in stopwords
14261410
):

redisvl/utils/full_text_query_helper.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
from redisvl.query.filter import FilterExpression
2+
from redisvl.utils.stopwords import get_stopwords
23
from redisvl.utils.token_escaper import TokenEscaper
3-
from redisvl.utils.utils import lazy_import
4-
5-
nltk = lazy_import("nltk")
6-
nltk_stopwords = lazy_import("nltk.corpus.stopwords")
74

85

96
def _parse_text_weights(weights: dict[str, float] | None) -> dict[str, float]:
@@ -88,21 +85,7 @@ def _get_stopwords(self, stopwords: str | set[str] | None = "english") -> set[st
8885
if not stopwords:
8986
return set()
9087
elif isinstance(stopwords, str):
91-
try:
92-
# Try loading first; only download if not already present.
93-
# This avoids race conditions when parallel workers (e.g.
94-
# pytest-xdist) call nltk.download() concurrently.
95-
try:
96-
return set(nltk_stopwords.words(stopwords))
97-
except LookupError:
98-
nltk.download("stopwords", quiet=True)
99-
return set(nltk_stopwords.words(stopwords))
100-
except ImportError:
101-
raise ValueError(
102-
f"Loading stopwords for {stopwords} failed: nltk is not installed."
103-
)
104-
except Exception as e:
105-
raise ValueError(f"Error trying to load {stopwords} from nltk. {e}")
88+
return get_stopwords(stopwords)
10689
elif isinstance(stopwords, (set, list, tuple)) and all(
10790
isinstance(word, str) for word in stopwords
10891
):

0 commit comments

Comments
 (0)