wenji 0.4.0__tar.gz

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.
Files changed (93) hide show
  1. wenji-0.4.0/LICENSE +21 -0
  2. wenji-0.4.0/PKG-INFO +521 -0
  3. wenji-0.4.0/README.md +479 -0
  4. wenji-0.4.0/pyproject.toml +92 -0
  5. wenji-0.4.0/setup.cfg +4 -0
  6. wenji-0.4.0/src/wenji/__init__.py +3 -0
  7. wenji-0.4.0/src/wenji/aggregate/__init__.py +462 -0
  8. wenji-0.4.0/src/wenji/aggregate/cache.py +64 -0
  9. wenji-0.4.0/src/wenji/aggregate/llm.py +58 -0
  10. wenji-0.4.0/src/wenji/aggregate/prompts.py +38 -0
  11. wenji-0.4.0/src/wenji/ask/__init__.py +251 -0
  12. wenji-0.4.0/src/wenji/ask/prompts.py +20 -0
  13. wenji-0.4.0/src/wenji/browse/__init__.py +0 -0
  14. wenji-0.4.0/src/wenji/browse/tag.py +121 -0
  15. wenji-0.4.0/src/wenji/classify/__init__.py +246 -0
  16. wenji-0.4.0/src/wenji/classify/axes_loader.py +252 -0
  17. wenji-0.4.0/src/wenji/classify/rules.py +58 -0
  18. wenji-0.4.0/src/wenji/cli/__init__.py +71 -0
  19. wenji-0.4.0/src/wenji/cli/_format.py +91 -0
  20. wenji-0.4.0/src/wenji/cli/aggregate.py +37 -0
  21. wenji-0.4.0/src/wenji/cli/classify.py +47 -0
  22. wenji-0.4.0/src/wenji/cli/corpus.py +148 -0
  23. wenji-0.4.0/src/wenji/cli/doctor.py +51 -0
  24. wenji-0.4.0/src/wenji/cli/download.py +40 -0
  25. wenji-0.4.0/src/wenji/cli/eval.py +371 -0
  26. wenji-0.4.0/src/wenji/cli/ingest.py +72 -0
  27. wenji-0.4.0/src/wenji/cli/inspect.py +55 -0
  28. wenji-0.4.0/src/wenji/cli/rebuild.py +44 -0
  29. wenji-0.4.0/src/wenji/cli/search.py +146 -0
  30. wenji-0.4.0/src/wenji/cli/segment.py +71 -0
  31. wenji-0.4.0/src/wenji/cli/serve.py +112 -0
  32. wenji-0.4.0/src/wenji/cli/set_chunk_strategy.py +89 -0
  33. wenji-0.4.0/src/wenji/cli/stats.py +52 -0
  34. wenji-0.4.0/src/wenji/config/__init__.py +31 -0
  35. wenji-0.4.0/src/wenji/config/defaults.py +31 -0
  36. wenji-0.4.0/src/wenji/config/llm.py +84 -0
  37. wenji-0.4.0/src/wenji/config/loader.py +140 -0
  38. wenji-0.4.0/src/wenji/core/__init__.py +0 -0
  39. wenji-0.4.0/src/wenji/core/chunk.py +165 -0
  40. wenji-0.4.0/src/wenji/core/db.py +84 -0
  41. wenji-0.4.0/src/wenji/core/errors.py +31 -0
  42. wenji-0.4.0/src/wenji/core/hash.py +16 -0
  43. wenji-0.4.0/src/wenji/core/model_download.py +142 -0
  44. wenji-0.4.0/src/wenji/core/normalize.py +33 -0
  45. wenji-0.4.0/src/wenji/core/safety.py +28 -0
  46. wenji-0.4.0/src/wenji/core/schema.sql +133 -0
  47. wenji-0.4.0/src/wenji/eval/__init__.py +132 -0
  48. wenji-0.4.0/src/wenji/eval/jsonl.py +192 -0
  49. wenji-0.4.0/src/wenji/eval/loader_benchmark_v2.py +181 -0
  50. wenji-0.4.0/src/wenji/eval/metrics.py +292 -0
  51. wenji-0.4.0/src/wenji/eval/report.py +216 -0
  52. wenji-0.4.0/src/wenji/eval/sanity_check.py +290 -0
  53. wenji-0.4.0/src/wenji/examples/__init__.py +12 -0
  54. wenji-0.4.0/src/wenji/examples/corpus_christian/__init__.py +8 -0
  55. wenji-0.4.0/src/wenji/examples/corpus_christian/entity_concepts.json +48 -0
  56. wenji-0.4.0/src/wenji/examples/corpus_christian/intent_keywords.json +70 -0
  57. wenji-0.4.0/src/wenji/ingest/__init__.py +454 -0
  58. wenji-0.4.0/src/wenji/ingest/embed.py +140 -0
  59. wenji-0.4.0/src/wenji/ingest/frontmatter.py +85 -0
  60. wenji-0.4.0/src/wenji/ingest/jieba_setup.py +120 -0
  61. wenji-0.4.0/src/wenji/observability/__init__.py +27 -0
  62. wenji-0.4.0/src/wenji/observability/health.py +224 -0
  63. wenji-0.4.0/src/wenji/observability/segment.py +166 -0
  64. wenji-0.4.0/src/wenji/observability/stats.py +106 -0
  65. wenji-0.4.0/src/wenji/search/__init__.py +397 -0
  66. wenji-0.4.0/src/wenji/search/bm25.py +129 -0
  67. wenji-0.4.0/src/wenji/search/entity.py +332 -0
  68. wenji-0.4.0/src/wenji/search/hybrid.py +55 -0
  69. wenji-0.4.0/src/wenji/search/intent.py +145 -0
  70. wenji-0.4.0/src/wenji/search/ranker.py +62 -0
  71. wenji-0.4.0/src/wenji/search/rerank.py +144 -0
  72. wenji-0.4.0/src/wenji/search/rewrite.py +126 -0
  73. wenji-0.4.0/src/wenji/search/rrf.py +151 -0
  74. wenji-0.4.0/src/wenji/search/vector.py +88 -0
  75. wenji-0.4.0/src/wenji/web/__init__.py +0 -0
  76. wenji-0.4.0/src/wenji/web/app.py +1176 -0
  77. wenji-0.4.0/src/wenji/web/branding.py +254 -0
  78. wenji-0.4.0/src/wenji/web/static/aggregate.js +172 -0
  79. wenji-0.4.0/src/wenji/web/static/ask.js +80 -0
  80. wenji-0.4.0/src/wenji/web/static/style.css +653 -0
  81. wenji-0.4.0/src/wenji/web/templates/_bookmark_script.html +102 -0
  82. wenji-0.4.0/src/wenji/web/templates/article.html +142 -0
  83. wenji-0.4.0/src/wenji/web/templates/base.html +124 -0
  84. wenji-0.4.0/src/wenji/web/templates/index.html +139 -0
  85. wenji-0.4.0/src/wenji/web/templates/result.html +49 -0
  86. wenji-0.4.0/src/wenji/web/templates/tag_detail.html +62 -0
  87. wenji-0.4.0/src/wenji/web/templates/tags_index.html +31 -0
  88. wenji-0.4.0/src/wenji.egg-info/PKG-INFO +521 -0
  89. wenji-0.4.0/src/wenji.egg-info/SOURCES.txt +91 -0
  90. wenji-0.4.0/src/wenji.egg-info/dependency_links.txt +1 -0
  91. wenji-0.4.0/src/wenji.egg-info/entry_points.txt +2 -0
  92. wenji-0.4.0/src/wenji.egg-info/requires.txt +20 -0
  93. wenji-0.4.0/src/wenji.egg-info/top_level.txt +1 -0
wenji-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 notoriouslab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
wenji-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,521 @@
1
+ Metadata-Version: 2.4
2
+ Name: wenji
3
+ Version: 0.4.0
4
+ Summary: Generic Chinese markdown RAG framework: hybrid BM25 + vector search, axis-aware classification, jitter-aware eval.
5
+ Author: notoriouslab
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/notoriouslab/wenji
8
+ Project-URL: Repository, https://github.com/notoriouslab/wenji
9
+ Keywords: rag,chinese,search,markdown,bm25,vector,embedding
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Text Processing :: Indexing
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: <3.13,>=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: jieba>=0.42
23
+ Requires-Dist: numpy>=1.24
24
+ Requires-Dist: onnxruntime>=1.17
25
+ Requires-Dist: tokenizers>=0.19
26
+ Requires-Dist: huggingface-hub>=0.20
27
+ Requires-Dist: typer>=0.12
28
+ Requires-Dist: fastapi>=0.110
29
+ Requires-Dist: jinja2>=3.1
30
+ Requires-Dist: uvicorn>=0.29
31
+ Requires-Dist: pyyaml>=6.0
32
+ Requires-Dist: httpx>=0.27
33
+ Requires-Dist: python-frontmatter>=1.1
34
+ Requires-Dist: markdown-it-py[linkify]>=3.0
35
+ Requires-Dist: tqdm>=4.66
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=8.0; extra == "dev"
38
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
39
+ Requires-Dist: ruff>=0.15; extra == "dev"
40
+ Requires-Dist: build>=1.2; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ <div align="center">
44
+
45
+ # wenji 文集
46
+
47
+ **丟一個 markdown 資料夾,得到混合搜尋 + Web UI。零 LLM 建索引,語料越大越穩。**
48
+
49
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)](https://www.python.org/)
50
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
51
+ [![CI](https://github.com/notoriouslab/wenji/actions/workflows/ci.yml/badge.svg)](https://github.com/notoriouslab/wenji/actions/workflows/ci.yml)
52
+
53
+ 🇹🇼 [繁體中文](#繁體中文) · 🇬🇧 [English](#english)
54
+
55
+ </div>
56
+
57
+ > ⚠️ **Pre-1.0 — API 可能在 minor 版本間變更。**
58
+
59
+ ---
60
+
61
+ ## 繁體中文
62
+
63
+ ### 為什麼選 wenji?
64
+
65
+ 大多數 RAG 框架預設「LLM-default」:建索引用 LLM 抽 entity、LLM 做 community summary、LLM 重排序。成本隨語料線性成長,LLM 不可用時整個系統停擺。
66
+
67
+ wenji 走相反路線 — **LLM-essential, not LLM-default**:建索引零 LLM 呼叫;LLM 只允許在查詢時用,必須 cache,必須有確定性 fallback。結果:LLM 成本 = `unique queries × cache miss rate`,與語料大小無關。
68
+
69
+ **典型使用場景**
70
+
71
+ - 📚 **中文知識語料搜尋** — 講道、課堂筆記、法律條文、古典詩詞、技術文章,中英混合支援
72
+ - 🖥️ **本地部署,零外部服務** — 一個 Python 行程 + 一個 SQLite 檔,不租 vector DB
73
+ - 🏷️ **Tag + 分類軸瀏覽** — Web UI sidebar 篩選,`/tags` tag 索引頁,`?tag=X` 精確過濾
74
+ - 🤖 **選配 LLM query rewrite** — 短查詢擴詞,任何 OpenAI-compatible endpoint,結果 SQLite cache
75
+ - 📊 **Eval 對齊** — JSONL eval runner,80q 基準,pass@3 partial+ 77.5%(v0.3.6 rewrite-off)
76
+
77
+ **3 大特色**
78
+
79
+ | 特色 | 實現方式 |
80
+ |------|---------|
81
+ | 零 LLM 建索引 | SQLite FTS5 (BM25) + ONNX BGE-M3 INT8,byte-identical 重建 |
82
+ | 完全本地 | 無 vector DB,無外部 API,一個 SQLite 檔 |
83
+ | 可量測 | JSONL eval runner,80q 基準,pass@3 partial+ 77.5%(v0.3.6 rewrite-off) |
84
+
85
+ ---
86
+
87
+ ### 三行快速上手
88
+
89
+ > 示範語料 `examples/articles/` 只隨 source checkout 提供;有自己的 markdown 目錄可直接 ingest。
90
+
91
+ ```bash
92
+ pip install wenji
93
+ git clone https://github.com/notoriouslab/wenji # 只為了拿示範語料
94
+ wenji ingest dir wenji/examples/articles/ --db wenji.db
95
+ wenji serve --db wenji.db --port 8000
96
+ ```
97
+
98
+ 打開瀏覽器 `http://127.0.0.1:8000`,得到完整 Web UI(搜尋 + tag 瀏覽 + 文章閱讀器)。
99
+
100
+ 支援 Python 3.10–3.12(3.13 尚未支援)。第一次跑 `wenji ingest` 或 `wenji search` 會自動下載 ONNX BGE-M3 INT8 embed model(~600 MB)至 user cache。macOS arm64 / Linux x86_64 內建 `libsimple` binary;其他平台用 `wenji download-model` 手動抓。
101
+
102
+ ---
103
+
104
+ ### 常見使用路徑
105
+
106
+ #### 場景 1:CLI 快速搜尋
107
+
108
+ ```bash
109
+ wenji search "勞動契約" --db wenji.db --top-k 5
110
+ ```
111
+
112
+ 輸出前 5 筆,含 BM25 / 向量分數、chunk-level 摘要、chunk 索引(可 deep link 至 `/article/<id>#c<n>`)。
113
+
114
+ #### 場景 2:Web UI + tag 瀏覽
115
+
116
+ ```bash
117
+ wenji serve --db wenji.db --port 8000
118
+ ```
119
+
120
+ | 路由 | 功能 |
121
+ |------|------|
122
+ | `/` | 搜尋頁,含分軸 sidebar filter |
123
+ | `/tags` | tag 索引頁(tag → 文章數) |
124
+ | `/tag/<name>` | 單一 tag 的文章列表 |
125
+ | `/article/<id>` | 文章閱讀器(sticky TOC、scroll-spy、query-aware 自動捲動) |
126
+
127
+ > ⚠️ **Production 部署 checklist**(`wenji serve` 預設沒有認證、沒有速率限制):
128
+ > - 設 `WENJI_API_KEY=<random-32-bytes>` 開啟 API key auth;同時關掉 `/docs` `/openapi.json` 自動文件(沒設 API key 時這兩個端點公開)
129
+ > - 設 `WENJI_CORS_ORIGINS=https://your-frontend.example.com`(預設 empty 拒所有 cross-origin)
130
+ > - 綁 `127.0.0.1` 走反代(nginx / Caddy)+ 反代層做 rate limit(`/api/ask` 一次呼叫等於一次 LLM 計費)
131
+ > - Docker / systemd:用 `EnvironmentFile=/etc/wenji.env` 載入 `WENJI_*`(不要 inline `Environment=` 或 docker `-e`,會被 `systemctl show` / `ps` 看到)
132
+ > - `axes.yaml` 為選配;缺檔不影響 ingest/search,只是 sidebar 不會有分軸
133
+
134
+ #### 場景 3:選配 LLM query rewrite
135
+
136
+ ```bash
137
+ # 複製 .env.example 為 .env 後填入下列值(請勿 commit;.gitignore 已內建):
138
+ # WENJI_LLM_BASE_URL=https://api.groq.com/openai/v1
139
+ # WENJI_LLM_API_KEY=<your-key>
140
+ # WENJI_LLM_MODEL=llama-3.3-70b-versatile
141
+
142
+ direnv allow . # 或 `source .env`,二選一載入
143
+ wenji serve --db wenji.db
144
+ ```
145
+
146
+ 詳細的 `.env` 安全建議(不要 `export` 進 shell rc、不要 `-e` 給 docker)見「進階設定 → LLM Query Rewrite」。
147
+
148
+ rewrite 格式:1-3 組關鍵詞以 `|` 分隔(BM25-friendly);結果 SQLite cache(預設 30 天 TTL)。
149
+
150
+ > **注意**:v0.3.6 rewrite-on 比 rewrite-off 低 3.7pp(73.8% vs 77.5%),wenji 預設 rewrite-off。建議先跑 rewrite-off 基準再決定是否開啟。
151
+
152
+ #### 場景 4:Domain corpus(corpus-christian 範例)
153
+
154
+ ```bash
155
+ wenji serve --db wenji.db \
156
+ --entity-source example:corpus-christian \
157
+ --intent-source example:corpus-christian
158
+ ```
159
+
160
+ 啟動 entity scoring + intent classification 層,Searcher pipeline 升級為:RRF merge with intent boost → entity scoring/filter。省略 flags 時退化為純 RRF + chunk signals(仍優於 v0.3.5 線性 hybrid)。
161
+
162
+ #### 場景 5:Eval A/B 基準測試
163
+
164
+ > 前置:先在另一 terminal 跑 `wenji serve --db wenji.db`(eval runner 透過 `/api/search` 打 80q 基準);snapshot `tests/benchmark_80_v2_snapshot.json` 已內建 repo 內。
165
+ >
166
+ > Smoke 建議:改動 retrieval pipeline 後,先用 snapshot 前 10 題跑 mini-baseline(手動 `jq '.categories[].questions |= .[:3]' snapshot.json > smoke.json` 之類)確認沒大幅退步,再跑全 80q。
167
+
168
+ ```bash
169
+ wenji eval run-benchmark --no-rewrite --db wenji.db --out r0_off.json
170
+ wenji eval run-benchmark --enable-rewrite --db wenji.db --out r0_on.json
171
+ ```
172
+
173
+ 輸出標準 JSON 格式(per-question `gold_path_match`、`pass@3`、`MRR@5`)。`run_id` 自動補 `_rewrite_on` / `_rewrite_off` 後綴。用 `wenji eval sanity-eyeball --baseline-output <path>` 做人工雙閘門驗收。
174
+
175
+ > 數值有 LLM 抖動:rewrite-on 數值 ±1.5pp 視為 jitter 範圍內(同一基準重跑可能差 ±1.5pp);超過 1.5pp 才算 retrieval regression。
176
+
177
+ ---
178
+
179
+ ### 核心概念
180
+
181
+ #### 搜尋架構
182
+
183
+ `Searcher.search()` v0.3.6 執行 11 步 pipeline:
184
+
185
+ ```
186
+ query rewrite → entity detect → intent detect → alias expand
187
+ → BM25 + vector → chunk BM25 → RRF merge (intent boost)
188
+ → entity scoring + filter → ranker hooks → cross-encoder rerank → snippet hydration
189
+ ```
190
+
191
+ 省略 `--entity-source` / `--intent-source` 時,entity/intent 步驟 skip,降級為 RRF + chunk signals。
192
+
193
+ #### 核心模組
194
+
195
+ | 模組 | 作用 |
196
+ |------|------|
197
+ | `wenji.ingest` | Disk-as-SSOT 切入:frontmatter、NFKC 正規化、deterministic ID、content hash、4 種切塊策略 |
198
+ | `wenji.search` | 混合檢索 11 步 pipeline(BM25 + vector + RRF + entity/intent + rerank) |
199
+ | `wenji.classify` | 跟語料無關的多軸 rule engine,user-supplied `axes.yaml` |
200
+ | `wenji.eval` | JSONL eval runner,multi-path gold set,jitter-aware gate |
201
+ | `wenji.ask` | RAG 問答(`POST /api/ask`),chunk-level citation,30 天 LLM cache |
202
+ | `wenji.observability` | corpus 快照 + query pipeline trace |
203
+
204
+ #### 分類引擎
205
+
206
+ ```yaml
207
+ # axes.yaml — 摘錄自 examples/axes.yaml
208
+ axes:
209
+ - id: sermon
210
+ name: 講道
211
+ short: 講道
212
+ order: 1
213
+ description: 講道 / 信仰主題長文
214
+ rules:
215
+ - source_type: sermon
216
+ primary: true
217
+ ```
218
+
219
+ 每條 rule 支援 `source_type` / `tag` / `title_regex`(regex search)/ `subtype` 多欄位 AND 組合,及 hierarchical `parent: <id>`。Axes 是 derived data,隨時 `wenji classify` / `wenji rebuild` 重建,不動原始 markdown。
220
+
221
+ #### Observability
222
+
223
+ ```bash
224
+ wenji stats --db wenji.db # articles / chunks / indices 快照
225
+ wenji segment "因信稱義" --db wenji.db # query pipeline trace(tokens、fts_form、rewrite)
226
+ ```
227
+
228
+ 等效 HTTP 端點:`GET /api/stats`、`GET /api/segment?q=`。
229
+
230
+ ---
231
+
232
+ ### 進階設定
233
+
234
+ #### LLM Query Rewrite
235
+
236
+ 任何 OpenAI-compatible endpoint(Groq、OpenRouter、Together、Gemini、vLLM、llama.cpp …)。**強烈建議**用 `.env` + `direnv` 載入,避免 API key 寫進 shell rc 或被 process listing 看到:
237
+
238
+ ```bash
239
+ # .env (請複製 .env.example 為 .env 後填入;不要 commit)
240
+ WENJI_LLM_BASE_URL=https://api.groq.com/openai/v1
241
+ WENJI_LLM_API_KEY=<your-key>
242
+ WENJI_LLM_MODEL=llama-3.3-70b-versatile
243
+ WENJI_LLM_TIMEOUT=10.0 # 選配,預設 10s
244
+ WENJI_LLM_REWRITE_CACHE_TTL_DAYS=30 # 選配,預設 30 天
245
+ ```
246
+
247
+ > ⚠️ 確認 `.gitignore` 含 `.env` 與 `.env.*`(已內建)。**不要** `export WENJI_LLM_API_KEY=...` 寫進 `~/.zshrc`/`~/.bashrc`,也不要傳 `-e WENJI_LLM_API_KEY=...` 給 docker(會被 `ps` 看到)。
248
+
249
+ 單次覆蓋:`wenji serve --enable-rewrite` / `--no-rewrite`(兩者互斥)。
250
+
251
+ **LLM 失敗 fallback**:rewrite endpoint 超時 / 5xx → rewriter skip,retrieval pipeline 不受影響(仍走原 query);`/api/ask` 在 LLM 失敗時 `answer=null` 但 `citations` 仍正常填值。
252
+
253
+ #### Entity / Intent Sources
254
+
255
+ 多來源,last-write-wins on key collision:
256
+
257
+ ```bash
258
+ wenji serve --db wenji.db \
259
+ --entity-source example:corpus-christian \
260
+ --entity-source /path/to/my_entities.json \
261
+ --intent-source example:corpus-christian
262
+ ```
263
+
264
+ 或 env var(comma-separated):
265
+
266
+ ```bash
267
+ export WENJI_ENTITY_SOURCES=example:corpus-christian,/path/to/my_entities.json
268
+ export WENJI_INTENT_SOURCES=example:corpus-christian
269
+ ```
270
+
271
+ Network URLs(`http://`、`https://`)被 source loader 拒絕;只接受 `example:<name>` 和本機路徑。**注意**:本機路徑目前沒有沙箱(Path traversal 防護待 v0.4),請只指向你信任的目錄。多來源 last-write-wins:右邊覆蓋左邊(`--entity-source A --entity-source B` → B 的 keys 優先)。
272
+
273
+ #### Web 部署:站點 URL / SEO / CORS(v0.3.7+)
274
+
275
+ 對外發行時,由 env vars 控制 SEO meta 與 CORS(**全部 unset 時預設不暴露任何品牌 / 不允許任何 cross-origin**,最安全 zero-config):
276
+
277
+ ```bash
278
+ # .env
279
+ WENJI_SITE_URL=https://wenji.example.com # 啟用 canonical / og:* / JSON-LD
280
+ WENJI_SITE_NAME=My Wenji # 可選,最長 256 字
281
+ WENJI_OG_IMAGE_URL=https://wenji.example.com/og.png # 可選;⚠️ 此 host 會收到所有訪客 IP / UA
282
+ WENJI_CORS_ORIGINS=https://my-frontend.example.com,https://api.example.com
283
+ ```
284
+
285
+ URL host 啟動時做白名單驗證:拒 userinfo(`https://a@b.com`)、私網 IP、IDN homograph、控制字元、非預設 port、percent-encoded host —— fail-fast 啟動失敗。CORS 拒 `*` / `null` / wildcard subdomain / 非 https。
286
+
287
+ > Local dev SPA:跑 `localhost:5173` 連 `/api/*` 會被預設 CORS 擋下;開發時設 `WENJI_CORS_ORIGINS=http://localhost:5173 WENJI_ALLOW_HTTP_CORS=1`。
288
+
289
+ 部署前用 `wenji doctor --db wenji.db` 驗 db 一致性(cross-table sanity + sample FTS MATCH);exit 1 代表 db 不一致、`wenji serve` 啟動會拒絕 bind port。非中文 corpus 加 `--sample-keywords k1,k2,k3` override。
290
+
291
+ #### 升級指南
292
+
293
+ wenji 用 markdown 為 SSOT,舊 db 升級沒有 migration script —— 直接重建:
294
+
295
+ ```bash
296
+ rm wenji.db && wenji ingest dir <markdown-dir> --db wenji.db
297
+ ```
298
+
299
+ 支援平台:
300
+
301
+ | 平台 | 狀態 | 備註 |
302
+ |------|------|------|
303
+ | macOS arm64(M1+) | ✅ supported | 內建 libsimple binary |
304
+ | Linux x86_64 | ✅ supported | 內建 libsimple binary |
305
+ | macOS x86_64(Intel) | ⚠️ experimental | 需自行編譯 libsimple |
306
+ | Linux ARM | ⚠️ experimental | 需自行編譯 libsimple |
307
+ | Windows | ❌ unsupported | libsimple 無 .dll |
308
+
309
+ 中國大陸 / 受限網路:Hugging Face 模型下載可設 `HF_ENDPOINT=https://hf-mirror.com`。
310
+
311
+ ---
312
+
313
+ ### 進階參考
314
+
315
+ #### CLI 子命令
316
+
317
+ | 命令 | 用途 |
318
+ |------|------|
319
+ | `wenji ingest dir <path>` | 從 markdown 目錄建索引 |
320
+ | `wenji search <query>` | CLI 搜尋 |
321
+ | `wenji serve` | 啟動 FastAPI + Web UI |
322
+ | `wenji classify` | 套用 axes.yaml |
323
+ | `wenji rebuild` | 重建 derived tables(byte-identical) |
324
+ | `wenji stats` | corpus 快照 |
325
+ | `wenji segment <query>` | query pipeline trace |
326
+ | `wenji eval run-benchmark` | 跑 80q 基準 |
327
+ | `wenji eval sanity-eyeball` | 人工雙閘門驗收 |
328
+ | `wenji eval migrate-jsonl` | 舊版 eval JSONL 轉換 |
329
+ | `wenji doctor` | db consistency 健康檢查(部署前驗 db) |
330
+ | `wenji inspect-chunks <file>` | 預覽單檔切塊結果 |
331
+ | `wenji set-chunk-strategy` | 寫 frontmatter `chunk_strategy` |
332
+ | `wenji corpus trim` | 按 article_id / content_hash 刪除 |
333
+ | `wenji download-model` | 手動下載 ONNX model + libsimple |
334
+ | `wenji aggregate clear-cache` | 清除 LLM cache |
335
+
336
+ #### 選型建議
337
+
338
+ **何時開 rewrite?**
339
+
340
+ | 情境 | 建議 |
341
+ |------|------|
342
+ | 短查詢(1-2 字)召回率差 | 嘗試 rewrite-on,先跑 A/B 確認效果 |
343
+ | 向量召回已夠強 | 預設 off(rewrite 可能注入噪音) |
344
+ | Baseline 重現 | `--no-rewrite` 鎖定 |
345
+
346
+ **何時用 entity/intent?**
347
+
348
+ | 情境 | 建議 |
349
+ |------|------|
350
+ | 純 RRF 效果已夠 | 省略 `--entity-source`,不需準備詞典 |
351
+ | 專業語料(神學、法律、醫學) | `--entity-source` + `--intent-source` 提升精確度 |
352
+ | 自訂 domain | Python API: `EntityScorer.from_sources()` / `IntentClassifier.from_sources()` |
353
+
354
+ ---
355
+
356
+ ### 整合與生態
357
+
358
+ **notoriouslab 相關專案**
359
+
360
+ | 專案 | 說明 |
361
+ |------|------|
362
+ | [trad-zh-search](https://github.com/notoriouslab/trad-zh-search) | 繁體中文文本預處理工具 — CKIP 分詞 + bigram 索引生成,附可選擇的領域字典系統;可單獨搭配主流搜尋引擎使用 |
363
+ | [vault-search](https://github.com/notoriouslab/vault-search) | Obsidian 本地語意搜尋與發掘 — 中文友善,無雲端、無 API Key、無訂閱費 |
364
+
365
+ **擴展點**:`RankerHook` Protocol — `boost(article, query, context) -> float`,duck typing 滿足即可。詳見 [docs/extending.md](docs/extending.md)。
366
+
367
+ ---
368
+
369
+ ### 貢獻
370
+
371
+ ```bash
372
+ pip install -e ".[dev]"
373
+ ruff check src/wenji tests/wenji # linter
374
+ pytest # unit(634 tests)
375
+ pytest -m integration # 真實 ONNX(需下載 ~600 MB)
376
+ ```
377
+
378
+ 詳見 [CONTRIBUTING.md](CONTRIBUTING.md)。
379
+
380
+ ### 授權
381
+
382
+ [MIT](LICENSE) © 2026 notoriouslab
383
+
384
+ ---
385
+
386
+ ## English
387
+
388
+ ### At a glance
389
+
390
+ | | |
391
+ |---|---|
392
+ | **What** | Drop a folder of markdown files in, get hybrid search + a web UI out. |
393
+ | **Who for** | Anyone with a Chinese (or mixed-language) markdown corpus — sermons, lecture notes, legal text, classical poetry, blog posts — who wants real search without renting a vector DB. |
394
+ | **Stack** | SQLite FTS5 (BM25) + ONNX BGE-M3 (vector) + libsimple (CJK tokenizer) + FastAPI + Jinja2 |
395
+ | **Indexing cost** | **Zero LLM calls.** Deterministic, byte-identical rebuild from disk. |
396
+ | **LLM use** | Optional, query-time only, cached, with a structured fallback that works without any LLM. |
397
+ | **Deploy size** | One Python process, one SQLite file. No external services. |
398
+ | **Tested on** | Python 3.10 / 3.11 / 3.12 — 641 tests (634 unit + 7 integration). 3.13 not yet supported (`pyproject.toml` pins `requires-python = ">=3.10,<3.13"`). |
399
+
400
+ ### Why wenji?
401
+
402
+ Most RAG frameworks are built around an "LLM-default" assumption: extract entities with an LLM during ingest, build community summaries with an LLM, re-rank with an LLM. The cost grows with the corpus, and the system stops working when the LLM is unavailable.
403
+
404
+ wenji is built on the opposite premise — **LLM-essential, not LLM-default**:
405
+
406
+ 1. The indexing pipeline performs **zero** LLM calls.
407
+ 2. LLM use is restricted to query time, must be cached, and must have a deterministic structured fallback.
408
+ 3. Entity dictionaries, classification axes, and chunking strategies are user-supplied, not LLM-derived.
409
+
410
+ The result: LLM cost scales with `unique queries × cache miss rate`, not with corpus size.
411
+
412
+ ### Quickstart (4 commands)
413
+
414
+ > The demo corpus in `examples/articles/` ships only with the source checkout; point `ingest` at your own markdown directory otherwise.
415
+
416
+ ```bash
417
+ pip install wenji
418
+ git clone https://github.com/notoriouslab/wenji # demo corpus only
419
+ wenji ingest dir wenji/examples/articles/ --db wenji.db
420
+ wenji serve --db wenji.db --port 8000
421
+ ```
422
+
423
+ Then open `http://127.0.0.1:8000` — full Web UI with search, tag browsing, and article viewer.
424
+
425
+ To search from the command line:
426
+
427
+ ```bash
428
+ wenji search "勞動契約" --db wenji.db --top-k 5
429
+ ```
430
+
431
+ ### Common paths
432
+
433
+ | Scenario | Command |
434
+ |----------|---------|
435
+ | CLI search | `wenji search "<query>" --db wenji.db` |
436
+ | Web UI + tag browsing | `wenji serve --db wenji.db` → `/tags`, `/tag/<name>` |
437
+ | LLM query rewrite | Set `WENJI_LLM_*` env vars (see below) |
438
+ | Domain corpus (entity/intent) | `wenji serve --entity-source example:corpus-christian --intent-source example:corpus-christian` |
439
+ | Eval A/B | `wenji eval run-benchmark --no-rewrite` vs `--enable-rewrite` |
440
+
441
+ ### Search pipeline (v0.3.6)
442
+
443
+ `Searcher.search()` runs an 11-step pipeline:
444
+
445
+ ```
446
+ query rewrite → entity detect → intent detect → alias expand
447
+ → BM25 + vector → chunk BM25 → RRF merge (intent boost)
448
+ → entity scoring + filter → ranker hooks → cross-encoder rerank → snippet hydration
449
+ ```
450
+
451
+ Without `--entity-source` / `--intent-source`, the entity/intent steps are skipped and the pipeline degrades to RRF + chunk signals (still an improvement over v0.3.5 linear hybrid).
452
+
453
+ ### Core modules
454
+
455
+ | Module | Purpose |
456
+ |--------|---------|
457
+ | `wenji.ingest` | Disk-as-SSOT markdown ingest: frontmatter, NFKC normalization, deterministic IDs, content hashing, 4 chunking strategies. |
458
+ | `wenji.search` | Hybrid retrieval: 11-step pipeline (BM25 + vector + RRF + entity/intent + rerank). |
459
+ | `wenji.classify` | Corpus-agnostic multi-axis rule engine. Drop your `axes.yaml`, get tagged articles. |
460
+ | `wenji.eval` | JSONL-driven eval runner with jitter-aware gates. |
461
+ | `wenji.ask` | RAG question answering (`POST /api/ask`), chunk-level citations, 30-day LLM cache. |
462
+ | `wenji.observability` | Corpus snapshot + query pipeline trace (`/api/stats`, `/api/segment`). |
463
+
464
+ ### LLM query rewrite (optional, v0.3.2+)
465
+
466
+ Any OpenAI-compatible endpoint (Groq, OpenRouter, Together, Gemini, vLLM, llama.cpp…):
467
+
468
+ ```bash
469
+ export WENJI_LLM_BASE_URL=https://api.groq.com/openai/v1
470
+ export WENJI_LLM_API_KEY=<your-key>
471
+ export WENJI_LLM_MODEL=llama-3.3-70b-versatile
472
+ ```
473
+
474
+ Per-invocation override: `wenji serve --enable-rewrite` / `--no-rewrite`. The `/api/search` response includes a `rewritten_query` field (`null` when unchanged).
475
+
476
+ > **Note**: v0.3.6 rewrite-on (73.8%) is 3.7pp below rewrite-off (77.5%) because wenji's vector recall is already strong. Default is rewrite-off. Run A/B with `wenji eval run-benchmark` before enabling in production.
477
+
478
+ ### Configuration
479
+
480
+ ```yaml
481
+ # axes.yaml — excerpt from examples/axes.yaml
482
+ axes:
483
+ - id: sermon
484
+ name: 講道
485
+ short: 講道
486
+ order: 1
487
+ description: 講道 / 信仰主題長文
488
+ rules:
489
+ - source_type: sermon
490
+ primary: true
491
+ ```
492
+
493
+ Each rule supports `source_type` / `tag` / `title_regex` (regex search) / `subtype` fields combined with AND, plus hierarchical `parent: <id>`. Axes are derived data — `wenji rebuild` always regenerates them deterministically.
494
+
495
+ ### Ecosystem
496
+
497
+ | Project | Description |
498
+ |---------|-------------|
499
+ | [trad-zh-search](https://github.com/notoriouslab/trad-zh-search) | Traditional Chinese text preprocessing: CKIP segmentation + bigram index generation, with optional domain dictionaries. Works standalone with any major search engine. |
500
+ | [vault-search](https://github.com/notoriouslab/vault-search) | Obsidian local semantic search and discovery — Chinese-friendly, no cloud, no API key, no subscription. Your notes never leave your machine. |
501
+
502
+ Custom ranking: implement the `RankerHook` Protocol (`boost(article, query, context) -> float`) and pass it to `Searcher`. Duck typing — no import required. See [docs/extending.md](docs/extending.md).
503
+
504
+ ### Star History
505
+
506
+ [![Star History Chart](https://api.star-history.com/svg?repos=notoriouslab/wenji&type=Date)](https://star-history.com/#notoriouslab/wenji&Date)
507
+
508
+ ### Development
509
+
510
+ ```bash
511
+ pip install -e ".[dev]"
512
+ ruff check src/wenji tests/wenji
513
+ pytest # unit
514
+ pytest -m integration # ~600 MB model download, real ONNX
515
+ ```
516
+
517
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full PR flow.
518
+
519
+ ### License
520
+
521
+ [MIT](LICENSE) © 2026 notoriouslab