Skip to content

Commit f17599d

Browse files
committed
[fix] bugs in incremental update
1 parent 6f43999 commit f17599d

10 files changed

Lines changed: 318 additions & 102 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ site/
2929

3030
# Legacy
3131
archive/
32+
33+
.codex

config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
QDRANT_PATH: str | None = os.getenv("QDRANT_PATH") # Non-empty: use local embedded mode
4747
QDRANT_COLLECTION_NAME: str = "papers"
4848
QDRANT_PREFER_GRPC: bool = True
49+
QDRANT_TIMEOUT: float = float(os.getenv("QDRANT_TIMEOUT", "300"))
4950

5051
# ── API Server ─────────────────────────────────────────────────────────
5152
API_HOST: str = "0.0.0.0"
@@ -74,3 +75,4 @@
7475
INGEST_BATCH_SIZE: int = 5000
7576
SQLITE_CACHE_SIZE: int = -1024 * 512 # 512 MB (more cache = faster SQLite/FTS5)
7677
SQLITE_MMAP_SIZE: int = 512 * 1024 * 1024 # 512 MB for FTS5 read performance
78+
SQLITE_BUSY_TIMEOUT_MS: int = int(os.getenv("SQLITE_BUSY_TIMEOUT_MS", "300000")) # 5 min

core/citation/database.py

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,19 @@ def get_connection(readonly: bool = False) -> sqlite3.Connection:
7878
Path(config.PAPERS_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
7979
if readonly:
8080
uri = f"file:{config.PAPERS_DB_PATH}?mode=ro"
81-
conn = sqlite3.connect(uri, uri=True)
81+
conn = sqlite3.connect(
82+
uri,
83+
uri=True,
84+
timeout=config.SQLITE_BUSY_TIMEOUT_MS / 1000,
85+
)
8286
else:
83-
conn = sqlite3.connect(config.PAPERS_DB_PATH)
87+
conn = sqlite3.connect(
88+
config.PAPERS_DB_PATH,
89+
timeout=config.SQLITE_BUSY_TIMEOUT_MS / 1000,
90+
)
91+
conn.execute("PRAGMA journal_mode = WAL")
92+
conn.execute("PRAGMA synchronous = NORMAL")
93+
conn.execute(f"PRAGMA busy_timeout = {config.SQLITE_BUSY_TIMEOUT_MS}")
8494
conn.execute(f"PRAGMA cache_size = {config.SQLITE_CACHE_SIZE}")
8595
return conn
8696

@@ -222,28 +232,59 @@ def delete_by_corpus_ids(conn: sqlite3.Connection, corpus_ids: list[int]) -> Non
222232
"""
223233
if not corpus_ids:
224234
return
225-
placeholders = ",".join("?" for _ in corpus_ids)
226-
paper_ids_subquery = f"(SELECT paper_id FROM corpus_id_mapping WHERE corpus_id IN ({placeholders}))"
227-
conn.execute(
228-
f"DELETE FROM paper_fts_title WHERE paper_id IN {paper_ids_subquery}",
229-
corpus_ids,
230-
)
231-
conn.execute(
232-
f"DELETE FROM paper_fts_combined WHERE paper_id IN {paper_ids_subquery}",
233-
corpus_ids,
234-
)
235-
conn.execute(
236-
f"DELETE FROM arxiv_to_paper WHERE paper_id IN {paper_ids_subquery}",
237-
corpus_ids,
238-
)
239-
conn.execute(
240-
f"DELETE FROM paper_metadata WHERE corpus_id IN ({placeholders})",
241-
corpus_ids,
242-
)
243-
conn.execute(
244-
f"DELETE FROM corpus_id_mapping WHERE corpus_id IN ({placeholders})",
245-
corpus_ids,
246-
)
235+
for i in range(0, len(corpus_ids), config.INGEST_BATCH_SIZE):
236+
batch_ids = corpus_ids[i : i + config.INGEST_BATCH_SIZE]
237+
placeholders = ",".join("?" for _ in batch_ids)
238+
paper_ids_subquery = f"(SELECT paper_id FROM corpus_id_mapping WHERE corpus_id IN ({placeholders}))"
239+
conn.execute(
240+
f"DELETE FROM paper_fts_title WHERE paper_id IN {paper_ids_subquery}",
241+
batch_ids,
242+
)
243+
conn.execute(
244+
f"DELETE FROM paper_fts_combined WHERE paper_id IN {paper_ids_subquery}",
245+
batch_ids,
246+
)
247+
conn.execute(
248+
f"DELETE FROM arxiv_to_paper WHERE paper_id IN {paper_ids_subquery}",
249+
batch_ids,
250+
)
251+
conn.execute(
252+
f"DELETE FROM paper_metadata WHERE corpus_id IN ({placeholders})",
253+
batch_ids,
254+
)
255+
conn.execute(
256+
f"DELETE FROM corpus_id_mapping WHERE corpus_id IN ({placeholders})",
257+
batch_ids,
258+
)
259+
260+
261+
def delete_by_paper_ids(conn: sqlite3.Connection, paper_ids: list[str]) -> None:
262+
"""Delete paper_metadata, corpus_id_mapping, arxiv_to_paper, and FTS tables by paper_id."""
263+
if not paper_ids:
264+
return
265+
for i in range(0, len(paper_ids), config.INGEST_BATCH_SIZE):
266+
batch_ids = paper_ids[i : i + config.INGEST_BATCH_SIZE]
267+
placeholders = ",".join("?" for _ in batch_ids)
268+
conn.execute(
269+
f"DELETE FROM paper_fts_title WHERE paper_id IN ({placeholders})",
270+
batch_ids,
271+
)
272+
conn.execute(
273+
f"DELETE FROM paper_fts_combined WHERE paper_id IN ({placeholders})",
274+
batch_ids,
275+
)
276+
conn.execute(
277+
f"DELETE FROM arxiv_to_paper WHERE paper_id IN ({placeholders})",
278+
batch_ids,
279+
)
280+
conn.execute(
281+
f"DELETE FROM paper_metadata WHERE paper_id IN ({placeholders})",
282+
batch_ids,
283+
)
284+
conn.execute(
285+
f"DELETE FROM corpus_id_mapping WHERE paper_id IN ({placeholders})",
286+
batch_ids,
287+
)
247288

248289

249290
def delete_citations_by_ids(conn: sqlite3.Connection, citation_ids: list[int]) -> None:

core/db_pool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def _create_ro_connection() -> sqlite3.Connection:
2424
conn = sqlite3.connect(uri, uri=True, check_same_thread=False)
2525
conn.execute("PRAGMA journal_mode = WAL")
2626
conn.execute("PRAGMA wal_autocheckpoint = 0")
27-
conn.execute("PRAGMA busy_timeout = 5000")
27+
conn.execute(f"PRAGMA busy_timeout = {config.SQLITE_BUSY_TIMEOUT_MS}")
2828
conn.execute(f"PRAGMA cache_size = {config.SQLITE_CACHE_SIZE}")
2929
conn.execute(f"PRAGMA mmap_size = {config.SQLITE_MMAP_SIZE}")
3030
conn.execute("PRAGMA temp_store = MEMORY")

core/retrieve/dense.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Filter,
1515
FilterSelector,
1616
MatchAny,
17+
PointIdsList,
1718
PointStruct,
1819
VectorParams,
1920
)
@@ -46,6 +47,7 @@ def get_client() -> QdrantClient:
4647
host=config.QDRANT_HOST,
4748
port=config.QDRANT_PORT,
4849
prefer_grpc=config.QDRANT_PREFER_GRPC,
50+
timeout=config.QDRANT_TIMEOUT,
4951
)
5052
print(f"✅ Qdrant connected: {config.QDRANT_HOST}:{config.QDRANT_PORT}")
5153
return _client
@@ -169,16 +171,8 @@ def delete_vectors(paper_ids: list[str]) -> None:
169171
if not paper_ids:
170172
return
171173
client = get_client()
174+
point_ids = [_paper_id_to_point_id(pid) for pid in paper_ids]
172175
client.delete(
173176
collection_name=config.QDRANT_COLLECTION_NAME,
174-
points_selector=FilterSelector(
175-
filter=Filter(
176-
must=[
177-
FieldCondition(
178-
key="paper_id",
179-
match=MatchAny(any=paper_ids),
180-
)
181-
]
182-
)
183-
),
177+
points_selector=PointIdsList(points=point_ids),
184178
)

docs/incremental-update.md

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,42 @@
44

55
---
66

7-
## 1. 一键更新(推荐)
7+
## 1. 执行方式
88

99
```bash
10-
# 更新到最新 S2 release
10+
# 方式 A:一键串联(下载 + 校验 + merge)
1111
bash update.sh
1212

13-
# 更新到指定日期
13+
# 更新到指定目标 release
1414
bash update.sh 2026-03-10
15+
16+
# 方式 B:拆分执行
17+
# 1) 只下载增量 diff
18+
bash update_download.sh 2026-03-10
19+
20+
# 2) 只校验已下载好的增量 diff
21+
bash update_validate.sh 2026-03-10
22+
23+
# 3) 只 merge 已下载好的增量目录
24+
bash update_merge.sh PaperData/incremental/2026-01-27_to_2026-03-10
1525
```
1626

17-
脚本自动完成以下流程:
27+
推荐做法:
28+
29+
1. 网络不稳定、只想先把文件拉下来时,先运行 `update_download.sh`
30+
2. 文件下完后,再运行 `update_validate.sh` 做完整校验与坏文件重下
31+
3. 需要重复调试 merge、断点恢复或只想继续上次合并时,直接运行 `update_merge.sh`
32+
4. `update.sh` 仅作为方便的一键封装,本质上顺序调用三者
33+
34+
`update.sh` 自动完成以下流程:
1835

1936
1.`corpus/current_release.txt` 读取当前版本
2037
2. 查询 S2 API 获取可用 releases,确定目标版本
2138
3. 下载增量 diff 到 `PaperData/incremental/`
22-
4. 合并到 SQLite(paper_metadata、citations、ID 映射)+ FTS5
23-
5. 编码新增/修改论文的 BGE-M3 向量,upsert 到 Qdrant;删除已移除论文的向量
24-
6. 更新 `corpus/current_release.txt` 为最新版本
39+
4. 校验已下载 diff,并对损坏/缺失文件重下
40+
5. 合并到 SQLite(paper_metadata、citations、ID 映射)+ FTS5
41+
6. 编码新增/修改论文的 BGE-M3 向量,upsert 到 Qdrant;删除已移除论文的向量
42+
7. 更新 `corpus/current_release.txt` 为最新版本
2543

2644
### 前置条件
2745

@@ -82,7 +100,8 @@ GET /diffs/{start_release_id}/to/{end_release_id}/{dataset_name}
82100
|-------------|--------------|
83101
| papers | `corpusid` |
84102
| abstracts | `corpusid` |
85-
| paper-ids | `corpusid` |
103+
| paper-ids updates | `corpusid` |
104+
| paper-ids deletes | `sha` |
86105
| authors | `authorid` |
87106
| citations | `citationid` |
88107

@@ -95,13 +114,33 @@ GET /diffs/{start_release_id}/to/{end_release_id}/{dataset_name}
95114
### 4.1 下载增量 diff
96115

97116
```bash
98-
# 自动从 corpus/current_release.txt 读取起始版本,下载到最新
99-
python build_corpus/data/download_incremental_diffs.py
117+
# 推荐:通过 shell 入口下载
118+
bash update_download.sh
119+
120+
# 指定目标 release
121+
bash update_download.sh 2026-03-10
100122

101-
# 指定起止版本
123+
# 单独校验并为损坏文件补下
124+
bash update_validate.sh 2026-03-10
125+
126+
# 或直接调用 Python 脚本
127+
python build_corpus/data/download_incremental_diffs.py
102128
python build_corpus/data/download_incremental_diffs.py --start 2026-01-27 --end 2026-03-10
129+
python build_corpus/data/download_incremental_diffs.py --start 2026-01-27 --end 2026-03-10 --mode validate
103130
```
104131

132+
下载阶段与校验阶段现在分开:
133+
134+
- `update_download.sh`
135+
- 只负责“文件是否存在”
136+
- 若目标文件已存在,直接跳过
137+
- 若上次中断留下 `.tmp`,会自动断点续下
138+
- `update_validate.sh`
139+
- 负责完整校验 `.gz/.jsonl` 内容
140+
- 若文件缺失或损坏,会自动重新下载
141+
- 会在增量目录下维护 `_download_validation_progress.json`
142+
- 已校验通过的文件会被记录,下次校验时直接跳过
143+
105144
### 输出目录结构
106145

107146
```
@@ -130,6 +169,10 @@ PaperData/
130169
### 4.2 合并到数据库
131170

132171
```bash
172+
# 推荐:通过 shell 入口 merge
173+
bash update_merge.sh PaperData/incremental/2026-01-27_to_2026-02-24
174+
175+
# 或直接调用 Python 脚本
133176
python build_corpus/merge_incremental.py PaperData/incremental/2026-01-27_to_2026-02-24
134177
```
135178

@@ -139,6 +182,17 @@ python build_corpus/merge_incremental.py PaperData/incremental/2026-01-27_to_202
139182
- FTS5:刷新 paper_fts_title 和 paper_fts_combined
140183
- Qdrant:编码新增/修改论文的 BGE-M3 向量并 upsert,删除已移除论文的向量
141184

185+
补充说明:
186+
187+
- `merge_incremental.py` 会为每个数据集步骤显示文件级 `tqdm` 进度条
188+
- Qdrant 编码阶段也会显示批量进度
189+
- merge 过程中会在增量目录下写入 `_merge_progress.json`
190+
- `_merge_progress.json` 记录步骤级状态;对 `papers-updates``abstracts-updates``citations-updates``citations-deletes` 这类重步骤,会额外记录已成功 commit 的 chunk offset
191+
- 如果进程中断,重新执行同一 `INCR_DIR` 时会自动跳过已完成步骤并继续
192+
- 若上述重步骤中途失败,会从上一个成功 commit 的 chunk 继续,而不是从步骤开头完全重跑
193+
- 全部完成后 `_merge_progress.json` 会自动删除
194+
- 如果 merge 时发现损坏或不可读的 diff 文件,会直接报错并中止;应先回到下载阶段重新拉取文件
195+
142196
### 4.3 更新版本记录
143197

144198
```bash
@@ -151,9 +205,12 @@ echo "2026-02-24" > corpus/current_release.txt
151205

152206
| 脚本 | 作用 |
153207
|------|------|
154-
| `update.sh` | 一键更新入口(下载 + 合并 + 版本更新) |
208+
| `update.sh` | 一键更新入口(调用下载脚本 + merge 脚本) |
209+
| `update_download.sh` | 只下载增量 diff,不执行完整校验、不执行 merge |
210+
| `update_validate.sh` | 只校验增量 diff,并对缺失/损坏文件重下 |
211+
| `update_merge.sh` | 只对指定增量目录执行 merge,并更新版本记录 |
155212
| `build_corpus/data/download_incremental_diffs.py` | 下载增量 diff 到 PaperData 目录 |
156-
| `build_corpus/merge_incremental.py` | 合并增量到 SQLite + FTS5 + Qdrant |
213+
| `build_corpus/merge_incremental.py` | 合并增量到 SQLite + FTS5 + Qdrant,支持断点续传 |
157214
| `build_corpus/optimize_fts.py` | FTS5 索引优化(建议更新后执行) |
158215

159216
---
@@ -163,5 +220,11 @@ echo "2026-02-24" > corpus/current_release.txt
163220
- 需要配置 `S2_API_KEY``.env` 或环境变量)以访问 Datasets API。
164221
- diff 的 `update_files``delete_files` 为预签名 URL,有时效,建议下载后本地保存。
165222
- 合并时需保证主键一致(如 `corpusid``citationid``authorid`)。
223+
- `paper-ids` 的主键口径需要分开看:`updates``corpusid``deletes``sha`
166224
- 合并完成后增量文件保留在 `PaperData/incremental/` 中,不会自动删除。
167225
- 合并过程中 Qdrant 向量编码需要 GPU,确保有可用的 CUDA 设备。
226+
- 如果只想补文件,不必每次完整校验;先跑 `update_download.sh` 即可。
227+
- 如果只想重复 merge,不要先跑下载/校验;直接执行 `update_merge.sh` 即可。
228+
- `_merge_progress.json` 属于 merge 的运行时状态文件;若一次 merge 已完整成功,它会自动被清理。
229+
- `_download_validation_progress.json` 属于校验阶段的运行时状态文件;会保留在增量目录下,供下次继续校验时复用。
230+
- chunk 或步骤重跑不会在 SQLite 或 Qdrant 中产生“重复数据行”;当前实现依赖 upsert/delete 的幂等性,但会产生额外耗时。

0 commit comments

Comments
 (0)