Cache improvements
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb6866b4f0d28..d6c20956fb2ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Unreleased
 
 ### New Features
+- Added "LLAMA_INDEX_CACHE_DIR" to control cached files (#7233)
 - Default to pydantic selectors when possible (#7154, #7223)
 - Remove the need for langchain wrappers on `embed_model` in the service context (#7157)
 - Metadata extractors take an `LLM` object now, in addition to `LLMPredictor` (#7202)
diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md
index 0c2ee848843a9..fa38de4744f41 100644
--- a/docs/getting_started/installation.md
+++ b/docs/getting_started/installation.md
@@ -1,6 +1,6 @@
 # Installation and Setup
 
-### Installation from Pip
+## Installation from Pip
 
 You can simply do:
 
@@ -8,14 +8,16 @@ You can simply do:
 pip install llama-index
 ```
 
-### Installation from Source
+**NOTE:** LlamaIndex may download and store local files for various packages (NLTK, HuggingFace, ...). Use the environment variable "LLAMA_INDEX_CACHE_DIR" to control where these files are saved.
+
+## Installation from Source
 
 Git clone this repository: `git clone https://github.com/jerryjliu/llama_index.git`. Then do:
 
 - `pip install -e .` if you want to do an editable install (you can modify source files) of just the package itself.
 - `pip install -r requirements.txt` if you want to install optional dependencies + dependencies used for development (e.g. unit testing).
 
-### OpenAI Environment Setup
+## OpenAI Environment Setup
 
 By default, we use the OpenAI `gpt-3.5-turbo` model for text generation and `text-embedding-ada-002` for retrieval and embeddings. In order to use this, you must have an OPENAI_API_KEY setup.
 You can register an API key by logging into [OpenAI's page and creating a new API token](https://beta.openai.com/account/api-keys).
@@ -25,7 +27,7 @@ You can also [customize the underlying LLM](/core_modules/model_modules/llms/usa
 need additional environment keys + tokens setup depending on the LLM provider.
 ```
 
-### Local Environment Setup
+## Local Environment Setup
 
 If you don't wish to use OpenAI, the environment will automatically fallback to using `LlamaCPP` and `llama2-chat-13B` for text generation and `BAAI/bge-small-en` for retrieval and embeddings. This models will all run locally.
 
diff --git a/llama_index/embeddings/utils.py b/llama_index/embeddings/utils.py
index 681fb96c730ba..a2fa371c8f1e2 100644
--- a/llama_index/embeddings/utils.py
+++ b/llama_index/embeddings/utils.py
@@ -60,6 +60,7 @@ def resolve_embed_model(
             ) from exc
 
         cache_folder = os.path.join(get_cache_dir(), "models")
+        os.makedirs(cache_folder, exist_ok=True)
 
         embed_model = LangchainEmbedding(
             HuggingFaceBgeEmbeddings(
diff --git a/llama_index/indices/postprocessor/optimizer.py b/llama_index/indices/postprocessor/optimizer.py
index 81a0e62ad2b95..194f57b668596 100644
--- a/llama_index/indices/postprocessor/optimizer.py
+++ b/llama_index/indices/postprocessor/optimizer.py
@@ -49,11 +49,21 @@ def __init__(
 
         if tokenizer_fn is None:
             import nltk.data
+            import os
+            from llama_index.utils import get_cache_dir
+
+            cache_dir = get_cache_dir()
+            nltk_data_dir = os.environ.get("NLTK_DATA", cache_dir)
+
+            # update nltk path for nltk so that it finds the data
+            if nltk_data_dir not in nltk.data.path:
+                nltk.data.path.append(nltk_data_dir)
 
             try:
                 nltk.data.find("tokenizers/punkt")
             except LookupError:
-                nltk.download("punkt")
+                nltk.download("punkt", download_dir=nltk_data_dir)
+
             tokenizer = nltk.data.load("tokenizers/punkt/english.pickle")
             tokenizer_fn = tokenizer.tokenize
         self._tokenizer_fn = tokenizer_fn
diff --git a/llama_index/llms/llama_cpp.py b/llama_index/llms/llama_cpp.py
index 18c4a0f22ae21..d53987a36b935 100644
--- a/llama_index/llms/llama_cpp.py
+++ b/llama_index/llms/llama_cpp.py
@@ -72,6 +72,7 @@ def __init__(
             model_name = os.path.basename(model_url)
             model_path = os.path.join(cache_dir, "models", model_name)
             if not os.path.exists(model_path):
+                os.makedirs(os.path.dirname(model_path), exist_ok=True)
                 self._download_url(model_url, model_path)
                 assert os.path.exists(model_path)
 
diff --git a/llama_index/text_splitter/utils.py b/llama_index/text_splitter/utils.py
index f040e6f29c0f9..0025864df43b9 100644
--- a/llama_index/text_splitter/utils.py
+++ b/llama_index/text_splitter/utils.py
@@ -32,13 +32,19 @@ def split_by_char() -> Callable[[str], List[str]]:
 
 def split_by_sentence_tokenizer() -> Callable[[str], List[str]]:
     import nltk
+    import os
+    from llama_index.utils import get_cache_dir
+
+    cache_dir = get_cache_dir()
+    nltk_data_dir = os.environ.get("NLTK_DATA", cache_dir)
+
+    # update nltk path for nltk so that it finds the data
+    if nltk_data_dir not in nltk.data.path:
+        nltk.data.path.append(nltk_data_dir)
 
     try:
         nltk.data.find("tokenizers/punkt")
     except LookupError:
-        import os
-
-        nltk_data_dir = os.environ.get("NLTK_DATA", None)
         nltk.download("punkt", download_dir=nltk_data_dir)
 
     return nltk.sent_tokenize
diff --git a/llama_index/utils.py b/llama_index/utils.py
index 65a7de73455b2..f335e9a267761 100644
--- a/llama_index/utils.py
+++ b/llama_index/utils.py
@@ -63,10 +63,19 @@ def stopwords(self) -> List[str]:
                 raise ImportError(
                     "`nltk` package not found, please run `pip install nltk`"
                 )
+
+            from llama_index.utils import get_cache_dir
+
+            cache_dir = get_cache_dir()
+            nltk_data_dir = os.environ.get("NLTK_DATA", cache_dir)
+
+            # update nltk path for nltk so that it finds the data
+            if nltk_data_dir not in nltk.data.path:
+                nltk.data.path.append(nltk_data_dir)
+
             try:
                 nltk.data.find("corpora/stopwords")
             except LookupError:
-                nltk_data_dir = os.environ.get("NLTK_DATA", None)
                 nltk.download("stopwords", download_dir=nltk_data_dir)
             self._stopwords = stopwords.words("english")
         return self._stopwords
@@ -239,12 +248,16 @@ def get_transformer_tokenizer_fn(model_name: str) -> Callable[[str], List[str]]:
     return tokenizer.tokenize
 
 
-def get_cache_dir() -> Path:
+def get_cache_dir() -> str:
     """Locate a platform-appropriate cache directory for llama_index,
     and create it if it doesn't yet exist
     """
+    # User override
+    if "LLAMA_INDEX_CACHE_DIR" in os.environ:
+        path = Path(os.environ["LLAMA_INDEX_CACHE_DIR"])
+
     # Linux, Unix, AIX, etc.
-    if os.name == "posix" and sys.platform != "darwin":
+    elif os.name == "posix" and sys.platform != "darwin":
         # use ~/.cache if empty OR not set
         base = os.path.expanduser("~/.cache")
         path = Path(base, "llama_index")
@@ -262,7 +275,7 @@ def get_cache_dir() -> Path:
 
     if not os.path.exists(path):
         os.makedirs(path)
-    return path
+    return str(path)
 
 
 # Sample text from llama_index's readme

# Description

This PR gives users easy control over where cache files are saved across llama-index. Just set the `LLAMA_INDEX_CACHE_DIR` environment variable and you should be good to go.

Mentioned in https://github.com/jerryjliu/llama_index/issues/7231

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

- [x] Tested in terminal
- [x] I stared at the code and made sure it makes sense
