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.
- wenji-0.4.0/LICENSE +21 -0
- wenji-0.4.0/PKG-INFO +521 -0
- wenji-0.4.0/README.md +479 -0
- wenji-0.4.0/pyproject.toml +92 -0
- wenji-0.4.0/setup.cfg +4 -0
- wenji-0.4.0/src/wenji/__init__.py +3 -0
- wenji-0.4.0/src/wenji/aggregate/__init__.py +462 -0
- wenji-0.4.0/src/wenji/aggregate/cache.py +64 -0
- wenji-0.4.0/src/wenji/aggregate/llm.py +58 -0
- wenji-0.4.0/src/wenji/aggregate/prompts.py +38 -0
- wenji-0.4.0/src/wenji/ask/__init__.py +251 -0
- wenji-0.4.0/src/wenji/ask/prompts.py +20 -0
- wenji-0.4.0/src/wenji/browse/__init__.py +0 -0
- wenji-0.4.0/src/wenji/browse/tag.py +121 -0
- wenji-0.4.0/src/wenji/classify/__init__.py +246 -0
- wenji-0.4.0/src/wenji/classify/axes_loader.py +252 -0
- wenji-0.4.0/src/wenji/classify/rules.py +58 -0
- wenji-0.4.0/src/wenji/cli/__init__.py +71 -0
- wenji-0.4.0/src/wenji/cli/_format.py +91 -0
- wenji-0.4.0/src/wenji/cli/aggregate.py +37 -0
- wenji-0.4.0/src/wenji/cli/classify.py +47 -0
- wenji-0.4.0/src/wenji/cli/corpus.py +148 -0
- wenji-0.4.0/src/wenji/cli/doctor.py +51 -0
- wenji-0.4.0/src/wenji/cli/download.py +40 -0
- wenji-0.4.0/src/wenji/cli/eval.py +371 -0
- wenji-0.4.0/src/wenji/cli/ingest.py +72 -0
- wenji-0.4.0/src/wenji/cli/inspect.py +55 -0
- wenji-0.4.0/src/wenji/cli/rebuild.py +44 -0
- wenji-0.4.0/src/wenji/cli/search.py +146 -0
- wenji-0.4.0/src/wenji/cli/segment.py +71 -0
- wenji-0.4.0/src/wenji/cli/serve.py +112 -0
- wenji-0.4.0/src/wenji/cli/set_chunk_strategy.py +89 -0
- wenji-0.4.0/src/wenji/cli/stats.py +52 -0
- wenji-0.4.0/src/wenji/config/__init__.py +31 -0
- wenji-0.4.0/src/wenji/config/defaults.py +31 -0
- wenji-0.4.0/src/wenji/config/llm.py +84 -0
- wenji-0.4.0/src/wenji/config/loader.py +140 -0
- wenji-0.4.0/src/wenji/core/__init__.py +0 -0
- wenji-0.4.0/src/wenji/core/chunk.py +165 -0
- wenji-0.4.0/src/wenji/core/db.py +84 -0
- wenji-0.4.0/src/wenji/core/errors.py +31 -0
- wenji-0.4.0/src/wenji/core/hash.py +16 -0
- wenji-0.4.0/src/wenji/core/model_download.py +142 -0
- wenji-0.4.0/src/wenji/core/normalize.py +33 -0
- wenji-0.4.0/src/wenji/core/safety.py +28 -0
- wenji-0.4.0/src/wenji/core/schema.sql +133 -0
- wenji-0.4.0/src/wenji/eval/__init__.py +132 -0
- wenji-0.4.0/src/wenji/eval/jsonl.py +192 -0
- wenji-0.4.0/src/wenji/eval/loader_benchmark_v2.py +181 -0
- wenji-0.4.0/src/wenji/eval/metrics.py +292 -0
- wenji-0.4.0/src/wenji/eval/report.py +216 -0
- wenji-0.4.0/src/wenji/eval/sanity_check.py +290 -0
- wenji-0.4.0/src/wenji/examples/__init__.py +12 -0
- wenji-0.4.0/src/wenji/examples/corpus_christian/__init__.py +8 -0
- wenji-0.4.0/src/wenji/examples/corpus_christian/entity_concepts.json +48 -0
- wenji-0.4.0/src/wenji/examples/corpus_christian/intent_keywords.json +70 -0
- wenji-0.4.0/src/wenji/ingest/__init__.py +454 -0
- wenji-0.4.0/src/wenji/ingest/embed.py +140 -0
- wenji-0.4.0/src/wenji/ingest/frontmatter.py +85 -0
- wenji-0.4.0/src/wenji/ingest/jieba_setup.py +120 -0
- wenji-0.4.0/src/wenji/observability/__init__.py +27 -0
- wenji-0.4.0/src/wenji/observability/health.py +224 -0
- wenji-0.4.0/src/wenji/observability/segment.py +166 -0
- wenji-0.4.0/src/wenji/observability/stats.py +106 -0
- wenji-0.4.0/src/wenji/search/__init__.py +397 -0
- wenji-0.4.0/src/wenji/search/bm25.py +129 -0
- wenji-0.4.0/src/wenji/search/entity.py +332 -0
- wenji-0.4.0/src/wenji/search/hybrid.py +55 -0
- wenji-0.4.0/src/wenji/search/intent.py +145 -0
- wenji-0.4.0/src/wenji/search/ranker.py +62 -0
- wenji-0.4.0/src/wenji/search/rerank.py +144 -0
- wenji-0.4.0/src/wenji/search/rewrite.py +126 -0
- wenji-0.4.0/src/wenji/search/rrf.py +151 -0
- wenji-0.4.0/src/wenji/search/vector.py +88 -0
- wenji-0.4.0/src/wenji/web/__init__.py +0 -0
- wenji-0.4.0/src/wenji/web/app.py +1176 -0
- wenji-0.4.0/src/wenji/web/branding.py +254 -0
- wenji-0.4.0/src/wenji/web/static/aggregate.js +172 -0
- wenji-0.4.0/src/wenji/web/static/ask.js +80 -0
- wenji-0.4.0/src/wenji/web/static/style.css +653 -0
- wenji-0.4.0/src/wenji/web/templates/_bookmark_script.html +102 -0
- wenji-0.4.0/src/wenji/web/templates/article.html +142 -0
- wenji-0.4.0/src/wenji/web/templates/base.html +124 -0
- wenji-0.4.0/src/wenji/web/templates/index.html +139 -0
- wenji-0.4.0/src/wenji/web/templates/result.html +49 -0
- wenji-0.4.0/src/wenji/web/templates/tag_detail.html +62 -0
- wenji-0.4.0/src/wenji/web/templates/tags_index.html +31 -0
- wenji-0.4.0/src/wenji.egg-info/PKG-INFO +521 -0
- wenji-0.4.0/src/wenji.egg-info/SOURCES.txt +91 -0
- wenji-0.4.0/src/wenji.egg-info/dependency_links.txt +1 -0
- wenji-0.4.0/src/wenji.egg-info/entry_points.txt +2 -0
- wenji-0.4.0/src/wenji.egg-info/requires.txt +20 -0
- 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
|
+
[](https://www.python.org/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
[](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
|
+
[](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
|