qunwang commited on
Commit
4af5c8c
·
1 Parent(s): d5f4a5c

方案 A: 预构建索引 Dataset,启动时直接下载免 embedding

Browse files

- 新增 build_and_upload_index.py 本地构建并上传索引
- 优先从 INDEX_DATASET_ID 下载预构建索引
- 默认 INDEX_DATASET_ID=claudqunwang/genai-courses-index

Files changed (4) hide show
  1. .gitignore +4 -0
  2. README.md +26 -1
  3. app.py +45 -0
  4. build_and_upload_index.py +124 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ local_index_build/
README.md CHANGED
@@ -28,10 +28,35 @@ Space 在启动时从 Dataset 下载课程目录后再构建/加载索引。
28
  - **可选**:`GENAI_COURSES_DATASET_ID`(默认:`claudqunwang/genai-courses-data`)
29
  - **可选**:`GENAI_COURSES_DATASET_SUBDIR`(默认:`GENAI COURSES`)
30
  - **可选**:`HF_EMBEDDING_MODEL`(免费方案时生效,默认:`sentence-transformers/all-MiniLM-L6-v2`)
 
31
 
32
  ## 使用方式
33
 
34
- 打开 Space 页面后直接提问即可;首次启动或勾选“强制重建索引”会花更久(因为需要做 Embedding)使用免费 embedding 时首次会下载模型,可能稍慢。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  ## Clare 方案三:外部调用 retrieve 接口
37
 
 
28
  - **可选**:`GENAI_COURSES_DATASET_ID`(默认:`claudqunwang/genai-courses-data`)
29
  - **可选**:`GENAI_COURSES_DATASET_SUBDIR`(默认:`GENAI COURSES`)
30
  - **可选**:`HF_EMBEDDING_MODEL`(免费方案时生效,默认:`sentence-transformers/all-MiniLM-L6-v2`)
31
+ - **可选(方案 A)**:`INDEX_DATASET_ID`(预构建索引 Dataset,设置后启动时直接下载,无需 embedding)
32
 
33
  ## 使用方式
34
 
35
+ 打开 Space 页面后直接提问即可。
36
+
37
+ ### 方案 A:预构建索引(启动快,无需每次 embedding)
38
+
39
+ 若配置了预构建索引,Space 启动时**直接下载索引**,几秒内就绪,不再做 embedding。
40
+
41
+ **步骤:**
42
+
43
+ 1. **创建索引 Dataset**:在 [huggingface.co/datasets](https://huggingface.co/datasets) 点击 **Create new dataset**,名称如 `genai-courses-index`。
44
+
45
+ 2. **本地构建并上传**(需有 `GENAI COURSES` 目录):
46
+ ```bash
47
+ cd hf_space/GenAICoursesDB_space
48
+ # 设置与 Space 一致(推荐用免费 embedding)
49
+ export EMBEDDING_PROVIDER=huggingface
50
+ # 若用默认 Dataset,可设置 INDEX_DATASET_ID=你的用户名/genai-courses-index
51
+ python build_and_upload_index.py
52
+ ```
53
+
54
+ 3. **Space 配置**:在 GenAICoursesDB Space → **Settings → Variables** 添加:
55
+ - `INDEX_DATASET_ID` = `你的用户名/genai-courses-index`
56
+
57
+ 4. **重启 Space**:下次启动将从 Dataset 下载索引,无需重新 embedding。
58
+
59
+ **未配置预构建时**:首次启动或勾选“强制重建索引”会从课程 Dataset 下载并构建,耗时较长(需做 Embedding)。
60
 
61
  ## Clare 方案三:外部调用 retrieve 接口
62
 
app.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  from pathlib import Path
 
3
 
4
  import gradio as gr
5
  from dotenv import load_dotenv
@@ -20,6 +21,9 @@ load_dotenv()
20
  DATASET_ID = (os.getenv("GENAI_COURSES_DATASET_ID") or "claudqunwang/genai-courses-data").strip()
21
  DATASET_SUBDIR = (os.getenv("GENAI_COURSES_DATASET_SUBDIR") or "GENAI COURSES").strip()
22
 
 
 
 
23
  # Hugging Face Spaces 没有持久化磁盘时,每次重启可能需要重建索引
24
  PERSIST_DIR = Path(os.getenv("GENAI_INDEX_DIR") or "/tmp/genai_courses_index").resolve()
25
 
@@ -133,6 +137,31 @@ def _get_courses_dir() -> Path:
133
  return courses_dir
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def get_index(force_rebuild: bool = False) -> VectorStoreIndex:
137
  _ensure_openai_key()
138
 
@@ -144,6 +173,19 @@ def get_index(force_rebuild: bool = False) -> VectorStoreIndex:
144
  except Exception as e:
145
  print(f"[index] load failed, rebuilding: {repr(e)}")
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  courses_dir = _get_courses_dir()
148
  print(f"[index] building from: {courses_dir}")
149
 
@@ -234,11 +276,13 @@ def retrieve_chunks(question: str, top_k: int = 5) -> str:
234
 
235
  def status_md() -> str:
236
  emb_line = f"- **Embedding**: `{EMBEDDING_PROVIDER}` (免费)" if EMBEDDING_PROVIDER == "huggingface" else f"- **Embedding**: OpenAI (付费)"
 
237
  if INDEX is not None:
238
  return (
239
  "✅ **Index ready**\n\n"
240
  f"- **Dataset**: `{DATASET_ID}`\n"
241
  f"- **Subdir**: `{DATASET_SUBDIR}`\n"
 
242
  f"{emb_line}\n"
243
  f"- **Index dir**: `{str(PERSIST_DIR)}`\n"
244
  )
@@ -246,6 +290,7 @@ def status_md() -> str:
246
  "⚠️ **Index not ready**\n\n"
247
  f"- **Dataset**: `{DATASET_ID}`\n"
248
  f"- **Subdir**: `{DATASET_SUBDIR}`\n"
 
249
  f"{emb_line}\n"
250
  f"- **Index dir**: `{str(PERSIST_DIR)}`\n\n"
251
  f"Error: `{INDEX_ERR or 'unknown'}`"
 
1
  import os
2
  from pathlib import Path
3
+ from typing import Optional, Tuple
4
 
5
  import gradio as gr
6
  from dotenv import load_dotenv
 
21
  DATASET_ID = (os.getenv("GENAI_COURSES_DATASET_ID") or "claudqunwang/genai-courses-data").strip()
22
  DATASET_SUBDIR = (os.getenv("GENAI_COURSES_DATASET_SUBDIR") or "GENAI COURSES").strip()
23
 
24
+ # 方案 A:预构建索引 Dataset。若设置,Space 启动时直接下载加载,无需重新 embedding
25
+ INDEX_DATASET_ID = (os.getenv("INDEX_DATASET_ID") or "claudqunwang/genai-courses-index").strip()
26
+
27
  # Hugging Face Spaces 没有持久化磁盘时,每次重启可能需要重建索引
28
  PERSIST_DIR = Path(os.getenv("GENAI_INDEX_DIR") or "/tmp/genai_courses_index").resolve()
29
 
 
137
  return courses_dir
138
 
139
 
140
+ def _download_and_load_prebuilt_index() -> Tuple[bool, Optional["VectorStoreIndex"]]:
141
+ """方案 A:从 INDEX_DATASET_ID 下载预构建索引并加载到 PERSIST_DIR。成功返回 True。"""
142
+ if not INDEX_DATASET_ID:
143
+ return False, None
144
+ try:
145
+ import shutil
146
+ if PERSIST_DIR.exists():
147
+ shutil.rmtree(PERSIST_DIR)
148
+ PERSIST_DIR.mkdir(parents=True, exist_ok=True)
149
+ from huggingface_hub import snapshot_download
150
+ snapshot_download(
151
+ repo_id=INDEX_DATASET_ID,
152
+ repo_type="dataset",
153
+ local_dir=str(PERSIST_DIR),
154
+ local_dir_use_symlinks=False,
155
+ )
156
+ storage_context = StorageContext.from_defaults(persist_dir=str(PERSIST_DIR))
157
+ idx = load_index_from_storage(storage_context)
158
+ print(f"[index] 已从预构建索引加载: {INDEX_DATASET_ID}")
159
+ return True, idx
160
+ except Exception as e:
161
+ print(f"[index] 预构建索引加载失败: {repr(e)}")
162
+ return False, None
163
+
164
+
165
  def get_index(force_rebuild: bool = False) -> VectorStoreIndex:
166
  _ensure_openai_key()
167
 
 
173
  except Exception as e:
174
  print(f"[index] load failed, rebuilding: {repr(e)}")
175
 
176
+ # 方案 A:优先尝试从预构建索引 Dataset 下载
177
+ if not force_rebuild and INDEX_DATASET_ID:
178
+ ok, idx = _download_and_load_prebuilt_index()
179
+ if ok and idx is not None:
180
+ return idx
181
+ if PERSIST_DIR.exists():
182
+ try:
183
+ import shutil
184
+ shutil.rmtree(PERSIST_DIR)
185
+ except Exception:
186
+ pass
187
+ PERSIST_DIR.mkdir(parents=True, exist_ok=True)
188
+
189
  courses_dir = _get_courses_dir()
190
  print(f"[index] building from: {courses_dir}")
191
 
 
276
 
277
  def status_md() -> str:
278
  emb_line = f"- **Embedding**: `{EMBEDDING_PROVIDER}` (免费)" if EMBEDDING_PROVIDER == "huggingface" else f"- **Embedding**: OpenAI (付费)"
279
+ idx_src = f"- **索引来源**: 预构建 `{INDEX_DATASET_ID}`" if INDEX_DATASET_ID else "- **索引来源**: 运行时构建"
280
  if INDEX is not None:
281
  return (
282
  "✅ **Index ready**\n\n"
283
  f"- **Dataset**: `{DATASET_ID}`\n"
284
  f"- **Subdir**: `{DATASET_SUBDIR}`\n"
285
+ f"{idx_src}\n"
286
  f"{emb_line}\n"
287
  f"- **Index dir**: `{str(PERSIST_DIR)}`\n"
288
  )
 
290
  "⚠️ **Index not ready**\n\n"
291
  f"- **Dataset**: `{DATASET_ID}`\n"
292
  f"- **Subdir**: `{DATASET_SUBDIR}`\n"
293
+ f"{idx_src}\n"
294
  f"{emb_line}\n"
295
  f"- **Index dir**: `{str(PERSIST_DIR)}`\n\n"
296
  f"Error: `{INDEX_ERR or 'unknown'}`"
build_and_upload_index.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 方案 A:本地构建索引并上传到 HF Dataset,Space 启动时直接下载加载,无需每次 embedding。
4
+
5
+ 用法:
6
+ 1. 确保本地有 GENAI COURSES 目录(或设置从 Dataset 下载)
7
+ 2. 安装依赖:pip install -r requirements.txt
8
+ 3. 设置环境变量(与 Space 一致):
9
+ - EMBEDDING_PROVIDER=huggingface # 推荐,不花 OpenAI 钱
10
+ - 或 OPENAI_API_KEY=... # 若用 OpenAI
11
+ 4. 运行:python build_and_upload_index.py
12
+ 5. 首次需创建 Dataset:https://huggingface.co/datasets/new
13
+ - 名称如 genai-courses-index
14
+ - 登录:huggingface-cli login 或 HF_TOKEN 环境变量
15
+
16
+ 上传后,在 Space 的 Variables 设置 INDEX_DATASET_ID=你的用户名/genai-courses-index
17
+ """
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # 项目根目录
23
+ SCRIPT_DIR = Path(__file__).resolve().parent
24
+ PROJECT_ROOT = SCRIPT_DIR.parent.parent
25
+ COURSES_DIR = PROJECT_ROOT / "GENAI COURSES"
26
+
27
+ # 索引输出目录(本地)
28
+ INDEX_OUTPUT = SCRIPT_DIR / "local_index_build"
29
+
30
+ # Dataset 存储预构建索引
31
+ INDEX_DATASET_ID = (os.getenv("INDEX_DATASET_ID") or "claudqunwang/genai-courses-index").strip()
32
+
33
+
34
+ def build_index():
35
+ """与 app.py 相同的索引构建逻辑。"""
36
+ os.chdir(SCRIPT_DIR)
37
+ sys.path.insert(0, str(SCRIPT_DIR))
38
+
39
+ from dotenv import load_dotenv
40
+ load_dotenv(SCRIPT_DIR / ".env")
41
+ load_dotenv(PROJECT_ROOT / ".env")
42
+
43
+ # 必须在 import app 前设置,因为 app 会执行模块级 Settings
44
+ emb = os.getenv("EMBEDDING_PROVIDER", "huggingface").strip().lower()
45
+ if emb == "huggingface":
46
+ os.environ.setdefault("EMBEDDING_PROVIDER", "huggingface")
47
+
48
+ from llama_index.core import (
49
+ Settings,
50
+ SimpleDirectoryReader,
51
+ StorageContext,
52
+ VectorStoreIndex,
53
+ load_index_from_storage,
54
+ )
55
+
56
+ # Embedding 配置(与 app 一致)
57
+ if emb == "huggingface":
58
+ from llama_index.embeddings.huggingface import HuggingFaceEmbedding
59
+ hf_model = os.getenv("HF_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2").strip()
60
+ Settings.embed_model = HuggingFaceEmbedding(model_name=hf_model)
61
+ else:
62
+ from llama_index.embeddings.openai import OpenAIEmbedding
63
+ Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
64
+
65
+ if not COURSES_DIR.exists():
66
+ raise FileNotFoundError(
67
+ f"课程目录不存在:{COURSES_DIR}\n"
68
+ "请将 GENAI COURSES 放在项目根目录,或修改脚本中的 COURSES_DIR。"
69
+ )
70
+
71
+ print(f"📂 读取课程目录: {COURSES_DIR}")
72
+ reader = SimpleDirectoryReader(
73
+ input_dir=str(COURSES_DIR),
74
+ recursive=True,
75
+ required_exts=[".md", ".pdf", ".txt", ".py", ".ipynb", ".docx"],
76
+ )
77
+ documents = reader.load_data()
78
+ print(f"📄 加载 {len(documents)} 个文档块,正在 embedding...")
79
+
80
+ index = VectorStoreIndex.from_documents(documents)
81
+ INDEX_OUTPUT.mkdir(parents=True, exist_ok=True)
82
+ index.storage_context.persist(persist_dir=str(INDEX_OUTPUT))
83
+ print(f"✅ 索引已保存到 {INDEX_OUTPUT}")
84
+ return INDEX_OUTPUT
85
+
86
+
87
+ def upload_index(persist_dir: Path):
88
+ """将索引目录上传到 HF Dataset。"""
89
+ from huggingface_hub import HfApi
90
+
91
+ api = HfApi()
92
+ files = list(persist_dir.rglob("*"))
93
+ files = [f for f in files if f.is_file()]
94
+
95
+ if not files:
96
+ raise RuntimeError(f"索引目录为空: {persist_dir}")
97
+
98
+ print(f"📤 上传 {len(files)} 个文件到 {INDEX_DATASET_ID}...")
99
+ for f in files:
100
+ rel = f.relative_to(persist_dir)
101
+ path_in_repo = str(rel).replace("\\", "/")
102
+ api.upload_file(
103
+ path_or_fileobj=str(f),
104
+ path_in_repo=path_in_repo,
105
+ repo_id=INDEX_DATASET_ID,
106
+ repo_type="dataset",
107
+ )
108
+ print(f" - {path_in_repo}")
109
+ print(f"✅ 上传完成: https://huggingface.co/datasets/{INDEX_DATASET_ID}")
110
+
111
+
112
+ def main():
113
+ print("=" * 50)
114
+ print("GenAICoursesDB 索引构建与上传(方案 A)")
115
+ print("=" * 50)
116
+ persist_dir = build_index()
117
+ upload_index(persist_dir)
118
+ print("\n下一步:在 GenAICoursesDB Space 的 Variables 中添加:")
119
+ print(f" INDEX_DATASET_ID={INDEX_DATASET_ID}")
120
+ print("Space 启动时将从此 Dataset 下载索引,无需重新 embedding。")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()