wp2txt 2.2.0 → 2.3.1
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.
- checksums.yaml +4 -4
- data/.dockerignore +2 -1
- data/.gitignore +3 -0
- data/CHANGELOG.md +19 -0
- data/DEVELOPMENT.md +2 -1
- data/DEVELOPMENT_ja.md +2 -1
- data/Dockerfile +10 -1
- data/README.md +36 -56
- data/README_ja.md +33 -1
- data/Rakefile +31 -5
- data/bin/wp2txt +1 -0
- data/bin/wp2txt-mcp +19 -20
- data/docs/RESEARCH.md +207 -0
- data/lib/wp2txt/cli.rb +43 -0
- data/lib/wp2txt/corpus.rb +385 -23
- data/lib/wp2txt/index_commands.rb +83 -0
- data/lib/wp2txt/langlinks_importer.rb +273 -0
- data/lib/wp2txt/metadata_index.rb +68 -2
- data/lib/wp2txt/multistream.rb +29 -0
- data/lib/wp2txt/output_path.rb +27 -0
- data/lib/wp2txt/regex.rb +7 -0
- data/lib/wp2txt/text_processing.rb +8 -0
- data/lib/wp2txt/utils.rb +17 -1
- data/lib/wp2txt/version.rb +1 -1
- data/spec/auto_download_spec.rb +77 -0
- data/spec/corpus_spec.rb +25 -0
- data/spec/docs_sync_spec.rb +39 -0
- data/spec/langlinks_importer_spec.rb +308 -0
- data/spec/multi_dump_attach_spec.rb +174 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/meta_db_fixture.rb +53 -0
- data/spec/titles_output_path_spec.rb +338 -0
- data/spec/utils_spec.rb +129 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4062c47480d0801e7a07d8e3bec7c65be8efeeda2eae30d398f9872810d321cd
|
|
4
|
+
data.tar.gz: 25bb3aa8a4d5baefc4616a8a4b754ec63e92d3e0b86b22ccc0ead2929a39739e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff61afbb989613e286af8bea784e4494c8f71b0dd52d24d3d038e72c5d77f9196d899039d9ff8c5e4b16461c044670d8cca6bc3feda55aa8cdbc31cbc30e012b
|
|
7
|
+
data.tar.gz: ddffdfb2452a3710d527bc7deeae52818cb9c2b4dedd433b2c7ecb06db74e6e389edf127976567a3269583531acf0aabc2ea9058d7422b5b675de9bae2e227fa
|
data/.dockerignore
CHANGED
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.1] - 2026-08-12
|
|
9
|
+
|
|
10
|
+
- **Container images no longer carry private files**: the image is built from the working tree with `git init && git add -A`, but the build context excluded `.gitignore`, so files ignored locally were tracked inside the container and shipped in the image — 2.3.0's images contained the maintainer's private research notes under `/wp2txt/research-notes`. The context now keeps `.gitignore` (so the container's file list matches a local `gem build`), explicitly excludes `research-notes` and `.bundle`, and the throwaway `.git` (a blob copy of every added file) is deleted after the build. `rake check_image` builds and inspects an image for private paths, and `rake push` refuses to publish without that check. The published gems were never affected; 2.3.0 images have been withdrawn
|
|
11
|
+
- **Docker Hub retired**: images are published to GitHub Container Registry only (`ghcr.io/yohasebe/wp2txt`). The Docker Hub repository is no longer updated
|
|
12
|
+
- **Fix: multi-line `<ref>` references broken by element splitting — also fixes `--extract-citations` on multi-line cite templates**: `Article#parse` splits elements at newlines, so any reference written across lines (very common for multi-line `{{cite …}}` templates) had its `[ref]` and `[/ref]` markers land in separate elements, invisible to `remove_ref`. The markers and raw reference markup leaked into the output, and — more importantly — `--extract-citations` silently never fired for multi-line cite templates, producing inconsistent output versus their single-line equivalents. `make_reference` now drops empty references outright and flattens the rest onto a single line before element splitting, so multi-line references behave exactly like single-line ones. Ordinary paragraph breaks outside references are unaffected
|
|
13
|
+
- **Fix: `[ref]` markers destroyed by external-link processing**: `process_external_links` stripped the brackets of the `[ref]`/`[/ref]` markers produced by `make_reference` (their contents took the single-word branch), so `remove_ref` could no longer find them and tag names plus reference bodies leaked into extracted text — even with `--ref`, the kept markers came out broken. The markers are now hidden behind placeholders for the duration of the bracket scan and restored afterwards. Downstream corpora no longer contain `ref…/ref` residue, which broke tokenization/sentence splitting and mixed bibliographic text into body prose
|
|
14
|
+
- **Runaway-query hardening**: a `query_sql` child process now sets its own kernel-enforced CPU limit (`RLIMIT_CPU`, the query timeout plus a small grace) in addition to the parent's wall-clock kill. Previously a child orphaned by the parent's death — an interrupted test run, a closed terminal, a crashed server — kept executing forever: sqlite3 holds the GVL inside `sqlite3_step`, so Ruby never reaches a signal-safe point and even SIGTERM is ignored. Observed in the wild as two processes spinning at 99% CPU for over five days. The CPU limit is strictly more permissive than the existing wall-clock deadline, so no query that would otherwise succeed is affected
|
|
15
|
+
- **Test-suite hang guard**: each example now runs under a wall-clock timeout (120s default; `WP2TXT_SPEC_TIMEOUT=0` disables it, the `:no_timeout` tag exempts an example). The suite deliberately exercises runaway queries, so a wedged example must fail rather than spin
|
|
16
|
+
|
|
17
|
+
## [2.3.0] - 2026-07-24
|
|
18
|
+
|
|
19
|
+
- **Documentation split**: README now focuses on text extraction; the research layer (indexes, exhaustive queries, full-text search, langlinks, cross-language SQL, MCP server) is documented in the new [Research Infrastructure Guide](docs/RESEARCH.md), including the complete MCP tool table
|
|
20
|
+
- **Container images on GHCR**: images are now published to `ghcr.io/yohasebe/wp2txt` (Docker Hub `yohasebe/wp2txt` is maintained as a mirror)
|
|
21
|
+
|
|
22
|
+
- **`extract_corpus` `titles:` argument**: Extract an explicit set of article titles (e.g. a set determined via `query_sql`) — titles are normalized (MediaWiki rules), deduplicated preserving input order, and one redirect hop is resolved; missing titles (including redirects to nowhere) are skipped and reported as `not_found` (count + 20-title sample, also in the `.meta.json` sidecar). Mutually exclusive with the filter arguments (set operations belong in SQL); capped at 10,000 titles. The sidecar records `titles_count` + `titles_sha256` (order-independent) for reproducibility, enumerating the full list when ≤100 titles. Also available via `start_extract_job`
|
|
23
|
+
- **`query_sql` `output_path:` argument**: Write ALL rows of a large result to a JSONL file (with a `.meta.json` sidecar recording the SQL, dump version, attach configuration, row count, and tool version) and return only a summary + 3-row sample — the extract_corpus D4 pattern generalized to SQL. Writes happen in the forked child (so the 30s SIGKILL deadline covers them) to a `.partial` file that the parent atomically renames on success and removes on every failure path (child crash, timeout kill, query error). Hard cap `SQL_FILE_ROW_LIMIT` = 5M rows (`truncated` flag), cells clipped at 64KB (`cells_clipped` count), duplicate column names are made unique (`_2` suffix), `limit` is ignored in this mode, and existing files require `overwrite: true`. The MCP layer's output-dir confinement is now a shared helper (`Wp2txt::OutputPath.confine`) used by extract_corpus, start_extract_job, and query_sql alike
|
|
24
|
+
- **Interlanguage links (`--import-langlinks`)**: Import the official `{lang}wiki-{date}-langlinks.sql.gz` dump into the Tier 1 metadata index as a `langlinks` table (`ll_from` = source page_id, `ll_lang` = target language, `ll_title` = normalized target title). Version pinning is enforced: the langlinks file's dump name must equal the built index's dump version — a mismatch is rejected with no override. Streams the MySQL dump without loading it whole (escape-safe tuple parser, 10k-row transaction batches, indexes created after the load). `--langlinks-langs` restricts imported target languages (e.g. `en,de,fr,zh,ko`); re-import requires `-U` (otherwise a no-op reporting the previous import time). Provenance (source file, size, import time, wp2txt version, language filter, row count) is stamped into the index and reported by `dump_info`; a post-import sanity check samples titles per language and warns when the join rate against a locally installed target edition falls below 90%
|
|
25
|
+
- **Multi-dump ATTACH in `query_sql`**: The MCP `query_sql` tool gains an optional `attach` argument (language codes only, never paths): `attach: ["en"]` read-only ATTACHes that language's locally installed metadata DB as `en_meta` and its FTS DB (when built) as `en_fts`, both sharing the main DB's schema. Codes are validated, only installed indexes are accepted, path resolution is server-side (same-date dump preferred; otherwise the latest build with a `dump_mismatch` note in the response's `attached` metadata). User SQL still cannot contain ATTACH/DETACH — attachments are issued by server code only, via `mode=ro` URIs on a read-only connection. Combined with the langlinks table, this enables single-query cross-language comparisons (e.g. section-structure diffs of article pairs)
|
|
26
|
+
|
|
8
27
|
## [2.2.0] - 2026-07-22
|
|
9
28
|
|
|
10
29
|
- **Index hardening (design review follow-up)**: `ord` now has identical semantics in `page_sections` and `fts_map` (lead = 0, first heading = 1; schema v2 — rebuild indexes with `--build-index -U`); section headings are normalized identically in both indexes (decorated headings like `== '''X''' ==` now match section filters); indexes record the wp2txt version (and the FTS index a rendering-config digest) so `dump_info` can flag indexes built by code whose text cleaning differs; rebuilds are atomic (built alongside, renamed on completion — a failed rebuild no longer destroys the working index)
|
data/DEVELOPMENT.md
CHANGED
|
@@ -395,7 +395,8 @@ info = Wp2txt::Bz2Validator.file_info("/path/to/file.bz2")
|
|
|
395
395
|
Build and push Docker images:
|
|
396
396
|
|
|
397
397
|
```bash
|
|
398
|
-
rake
|
|
398
|
+
rake check_image # Builds the image locally and verifies it carries no private files
|
|
399
|
+
rake push # Verifies, then builds multi-arch and pushes to GHCR
|
|
399
400
|
```
|
|
400
401
|
|
|
401
402
|
## Release Process
|
data/DEVELOPMENT_ja.md
CHANGED
|
@@ -395,7 +395,8 @@ info = Wp2txt::Bz2Validator.file_info("/path/to/file.bz2")
|
|
|
395
395
|
Dockerイメージのビルドとプッシュ:
|
|
396
396
|
|
|
397
397
|
```bash
|
|
398
|
-
rake
|
|
398
|
+
rake check_image # ローカルでイメージをビルドし、私的ファイルの混入がないか検証
|
|
399
|
+
rake push # 検証したうえでマルチアーキテクチャでビルドしGHCRにプッシュ
|
|
399
400
|
```
|
|
400
401
|
|
|
401
402
|
## リリースプロセス
|
data/Dockerfile
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
FROM ruby:4.0.1-alpine3.23
|
|
2
2
|
|
|
3
|
+
# Link the GHCR package to the source repository
|
|
4
|
+
LABEL org.opencontainers.image.source="https://github.com/yohasebe/wp2txt"
|
|
5
|
+
|
|
3
6
|
WORKDIR /wp2txt
|
|
4
7
|
COPY . ./
|
|
5
8
|
RUN rm -f Gemfile.lock
|
|
6
9
|
|
|
7
|
-
# Install dependencies (git is required by gemspec's `git ls-files`)
|
|
10
|
+
# Install dependencies (git is required by gemspec's `git ls-files`).
|
|
11
|
+
# The repository's .gitignore is copied in deliberately: `git add -A` must
|
|
12
|
+
# honour it so the file list here matches a local `gem build` — without it,
|
|
13
|
+
# ignored material (private notes, scratch files) would land in the image.
|
|
14
|
+
# The throwaway .git is removed afterwards: it holds a blob copy of every
|
|
15
|
+
# added file and is dead weight in the published image.
|
|
8
16
|
RUN apk update && \
|
|
9
17
|
apk upgrade && \
|
|
10
18
|
apk add --no-cache \
|
|
@@ -14,6 +22,7 @@ RUN apk update && \
|
|
|
14
22
|
build-base curl-dev wget && \
|
|
15
23
|
git init && git add -A && \
|
|
16
24
|
bundle install -j4 && \
|
|
25
|
+
rm -rf /wp2txt/.git && \
|
|
17
26
|
apk del .build-packages
|
|
18
27
|
|
|
19
28
|
# lbzip2 is not available as an Alpine package; build from source
|
data/README.md
CHANGED
|
@@ -34,7 +34,7 @@ WP2TXT extracts plain text and category information from Wikipedia dump files. I
|
|
|
34
34
|
- **Multilingual support** - Category and redirect detection for 350+ Wikipedia languages
|
|
35
35
|
- **Streaming processing** - Process large dumps without intermediate files
|
|
36
36
|
- **JSON output** - Machine-readable JSONL format for data pipelines
|
|
37
|
-
- **Offline
|
|
37
|
+
- **Offline research indexes** - Exhaustive, version-pinned queries over categories, section headings, and full text; interlanguage links for cross-edition SQL
|
|
38
38
|
- **MCP server** - Expose a local dump to LLM agents (Claude, ChatGPT, Gemini, local models) for grounded, reproducible corpus work
|
|
39
39
|
|
|
40
40
|
## Use Cases
|
|
@@ -79,19 +79,21 @@ The Docker image bundles everything — Ruby, decompression tools, and the MCP s
|
|
|
79
79
|
**Interactive / batch CLI:**
|
|
80
80
|
|
|
81
81
|
```shell
|
|
82
|
-
docker run -it -v /path/to/localdata:/data yohasebe/wp2txt
|
|
82
|
+
docker run -it -v /path/to/localdata:/data ghcr.io/yohasebe/wp2txt
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
The `wp2txt` command is available inside the container. Use `/data` for input/output files.
|
|
86
86
|
|
|
87
|
+
Images are published to GitHub Container Registry (`ghcr.io/yohasebe/wp2txt`). The former Docker Hub repository (`yohasebe/wp2txt`) is no longer updated as of 2.3.1.
|
|
88
|
+
|
|
87
89
|
**MCP server (no Ruby required on the host):**
|
|
88
90
|
|
|
89
91
|
```shell
|
|
90
92
|
# Build the indexes once (cached in a named volume so they persist)
|
|
91
|
-
docker run -it -v wp2txt:/root/.wp2txt yohasebe/wp2txt wp2txt --build-index --fulltext -L ja
|
|
93
|
+
docker run -it -v wp2txt:/root/.wp2txt ghcr.io/yohasebe/wp2txt wp2txt --build-index --fulltext -L ja
|
|
92
94
|
|
|
93
95
|
# Register with an MCP client, e.g. Claude Code
|
|
94
|
-
claude mcp add wp2txt -- docker run -i --rm -v wp2txt:/root/.wp2txt yohasebe/wp2txt wp2txt-mcp -L ja
|
|
96
|
+
claude mcp add wp2txt -- docker run -i --rm -v wp2txt:/root/.wp2txt ghcr.io/yohasebe/wp2txt wp2txt-mcp -L ja
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
Note: use `-i` (not `-it`) when running the MCP server — a TTY would corrupt the JSON-RPC stream. The named volume (`wp2txt:`) holds downloaded dumps and indexes; without it they are lost when the container exits.
|
|
@@ -253,63 +255,28 @@ By default, citation templates are removed. Use `--extract-citations` to extract
|
|
|
253
255
|
|
|
254
256
|
Supported: `{{cite book}}`, `{{cite web}}`, `{{cite news}}`, `{{cite journal}}`, `{{Citation}}`, etc.
|
|
255
257
|
|
|
256
|
-
##
|
|
257
|
-
|
|
258
|
-
Build a local SQLite index of a dump — per-article categories, section headings, redirects, and the category hierarchy — extracted from the dump itself (no API access, no rate limits, version-pinned for reproducibility):
|
|
259
|
-
|
|
260
|
-
$ wp2txt --build-index --lang=ja # downloads dump if needed; ja: ~15 min, ~1.7 GB index
|
|
261
|
-
|
|
262
|
-
Then run exhaustive queries entirely offline:
|
|
263
|
-
|
|
264
|
-
# All film articles (recursing 3 levels of subcategories) that have a plot section
|
|
265
|
-
$ wp2txt --find-articles --in-category "映画作品" -D 3 --has-section "あらすじ" --lang=ja
|
|
266
|
-
|
|
267
|
-
# Machine-readable output with total count
|
|
268
|
-
$ wp2txt --find-articles --in-category "Films" -D 2 -j json --limit 100 --lang=en
|
|
269
|
-
|
|
270
|
-
Unlike web/API access, these queries are *exhaustive* (they scan every article, not search-ranked results) and *reproducible* (pinned to a specific dump version). Answering "which of the 1.5M articles have X" takes milliseconds once the index is built.
|
|
271
|
-
|
|
272
|
-
### Full-Text Search
|
|
273
|
-
|
|
274
|
-
Add `--fulltext` to also build an FTS5 index over the article text itself (tokenizer auto-selected: character-trigram for Japanese/Chinese/Korean, word-based for space-delimited languages):
|
|
275
|
-
|
|
276
|
-
$ wp2txt --build-index --fulltext --lang=ja
|
|
277
|
-
$ wp2txt --search "タイムループ" --in-category "映画作品" -D 3 --lang=ja
|
|
278
|
-
|
|
279
|
-
Search hits report the article, section, and a snippet; totals are exhaustive counts, so `0 matches` is a verifiable absence claim for that dump version. The index is contentless (stores only the inverted index; snippets are re-rendered from the dump), keeping disk overhead moderate.
|
|
258
|
+
## Research Infrastructure (Indexes, Exhaustive Queries, MCP)
|
|
280
259
|
|
|
281
|
-
|
|
260
|
+
Beyond text extraction, wp2txt can turn a dump into a **local, version-pinned research
|
|
261
|
+
database**: SQLite indexes over categories, section headings, redirects, and (optionally)
|
|
262
|
+
the full article text, plus interlanguage links for cross-edition comparison — all
|
|
263
|
+
queryable offline, exhaustively, and exposed to LLM agents via an MCP server.
|
|
282
264
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
Example client configuration (e.g., Claude Desktop / Claude Code):
|
|
290
|
-
|
|
291
|
-
```json
|
|
292
|
-
{
|
|
293
|
-
"mcpServers": {
|
|
294
|
-
"wp2txt": { "command": "wp2txt-mcp", "args": ["--lang", "ja"] }
|
|
295
|
-
}
|
|
296
|
-
}
|
|
265
|
+
```console
|
|
266
|
+
$ wp2txt --build-index --fulltext --lang=ja # build the indexes
|
|
267
|
+
$ wp2txt --find-articles --in-category "映画作品" -D 3 --has-section "あらすじ" --lang=ja
|
|
268
|
+
$ wp2txt --search "タイムループ" --in-category "映画作品" -D 3 --lang=ja
|
|
269
|
+
$ wp2txt --import-langlinks -L ja --langlinks-langs en,de,fr,zh,ko
|
|
270
|
+
$ wp2txt-mcp --lang=ja # stdio MCP server for LLM agents
|
|
297
271
|
```
|
|
298
272
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|------|---------|
|
|
303
|
-
| `dump_info` | Dump version, index tiers, corpus statistics |
|
|
304
|
-
| `get_article` / `get_sections` / `list_headings` | Single-article access (redirect-aware) |
|
|
305
|
-
| `find_articles` | Exhaustive filtered listing (category recursion, section headings, title match) |
|
|
306
|
-
| `category_tree` / `section_stats` | Scope exploration and heading frequency discovery |
|
|
307
|
-
| `section_cooccurrence` | Verify section-alias hypotheses (synonyms rarely co-occur in one article) |
|
|
308
|
-
| `save_alias_set` etc. | Persist verified per-dump alias groups (server-side co-occurrence guardrail) |
|
|
309
|
-
| `extract_corpus` | Filtered extraction to JSONL + reproducibility sidecar; optional RAG chunking |
|
|
310
|
-
| `start_extract_job` / `job_status` / `cancel_job` | Background jobs for large extractions |
|
|
273
|
+
Unlike web/API access, these queries scan every article (a `0 matches` result is a
|
|
274
|
+
verifiable absence claim for that dump version) and are reproducible: extractions record
|
|
275
|
+
the dump version and query in a `.meta.json` sidecar.
|
|
311
276
|
|
|
312
|
-
|
|
277
|
+
**→ See the [Research Infrastructure Guide](docs/RESEARCH.md)** for index building,
|
|
278
|
+
exhaustive queries, full-text search, interlanguage links, cross-language SQL, the full
|
|
279
|
+
MCP tool list, and design principles.
|
|
313
280
|
|
|
314
281
|
## Command Line Options
|
|
315
282
|
|
|
@@ -384,6 +351,15 @@ Design principles: large results go to disk (the model receives a summary plus a
|
|
|
384
351
|
-v, --version Print version
|
|
385
352
|
-h, --help Show help
|
|
386
353
|
|
|
354
|
+
### Research infrastructure options
|
|
355
|
+
|
|
356
|
+
--build-index Build the metadata index (add --fulltext for FTS)
|
|
357
|
+
--find-articles / --search Exhaustive offline queries (see docs/RESEARCH.md)
|
|
358
|
+
--import-langlinks Import interlanguage links (version-matched)
|
|
359
|
+
--fts-optimize Optimize an existing full-text index
|
|
360
|
+
|
|
361
|
+
See the [Research Infrastructure Guide](docs/RESEARCH.md) for details.
|
|
362
|
+
|
|
387
363
|
## Configuration File
|
|
388
364
|
|
|
389
365
|
Create persistent settings with:
|
|
@@ -425,6 +401,10 @@ Turbo mode (default) splits bz2 into XML chunks first, then processes in paralle
|
|
|
425
401
|
|
|
426
402
|
See [CHANGELOG.md](CHANGELOG.md) for detailed release notes.
|
|
427
403
|
|
|
404
|
+
**v2.3.0 (July 2026)**: Interlanguage links import, cross-language SQL (multi-dump ATTACH), explicit-title extraction, SQL results to file with reproducibility sidecars, GHCR image publishing.
|
|
405
|
+
|
|
406
|
+
**v2.2.0 (July 2026)**: Offline metadata index, FTS5 full-text search, MCP server, query_sql escape hatch, extraction jobs.
|
|
407
|
+
|
|
428
408
|
**v2.1.0 (February 2026)**: SQLite caching, Ractor parallelism (Ruby 4.0+), template expansion, content markers, Docker image update.
|
|
429
409
|
|
|
430
410
|
**v2.0.0 (January 2026)**: Auto-download mode, category-based extraction, article extraction by title, JSON output, streaming processing, Ruby 4.0 support.
|
data/README_ja.md
CHANGED
|
@@ -34,6 +34,8 @@ WP2TXTはWikipediaダンプファイルからプレーンテキストとカテ
|
|
|
34
34
|
- **多言語対応** - 350以上のWikipedia言語でカテゴリ・リダイレクトを検出
|
|
35
35
|
- **ストリーミング処理** - 中間ファイルなしで大規模ダンプを処理
|
|
36
36
|
- **JSON出力** - データパイプライン向けの機械可読JSONL形式
|
|
37
|
+
- **オフライン研究索引** - カテゴリ・節見出し・全文に対する悉皆的・版固定クエリ、言語間リンクによる言語版横断SQL
|
|
38
|
+
- **MCPサーバー** - ローカルダンプをLLMエージェント(Claude、ChatGPT、Gemini、ローカルモデル)に公開
|
|
37
39
|
|
|
38
40
|
## ユースケース
|
|
39
41
|
|
|
@@ -43,6 +45,8 @@ wp2txtは以下の用途に適しています:
|
|
|
43
45
|
- トピック領域を横断した比較言語研究
|
|
44
46
|
- NLPタスク向けのメタデータ付きWikipediaテキスト抽出
|
|
45
47
|
- 並行カテゴリ構造を利用した対照言語研究
|
|
48
|
+
- 版固定のRAG知識ベース・LLM評価データセット
|
|
49
|
+
- web検索では原理的に不可能な悉皆的言明(「このカテゴリのどの記事にもXへの言及がない」)
|
|
46
50
|
|
|
47
51
|
## データアクセス
|
|
48
52
|
|
|
@@ -71,11 +75,13 @@ Windows:[Bzip2 for Windows](http://gnuwin32.sourceforge.net/packages/bzip2.htm
|
|
|
71
75
|
### Docker(代替方法)
|
|
72
76
|
|
|
73
77
|
```shell
|
|
74
|
-
docker run -it -v /path/to/localdata:/data yohasebe/wp2txt
|
|
78
|
+
docker run -it -v /path/to/localdata:/data ghcr.io/yohasebe/wp2txt
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
`wp2txt`コマンドはコンテナ内で使用可能です。入出力には`/data`ディレクトリを使用してください。
|
|
78
82
|
|
|
83
|
+
イメージはGitHub Container Registry(`ghcr.io/yohasebe/wp2txt`)で公開されています。旧 Docker Hub リポジトリ(`yohasebe/wp2txt`)は 2.3.1 以降更新されません。
|
|
84
|
+
|
|
79
85
|
## 基本的な使い方
|
|
80
86
|
|
|
81
87
|
### 自動ダウンロードと処理(推奨)
|
|
@@ -327,6 +333,28 @@ defaults:
|
|
|
327
333
|
|
|
328
334
|
コマンドラインオプションは設定ファイルの設定を上書きします。
|
|
329
335
|
|
|
336
|
+
## 研究基盤(索引・悉皆クエリ・MCP)
|
|
337
|
+
|
|
338
|
+
テキスト抽出に加えて、wp2txtはダンプを**ローカルな版固定研究データベース**に変換できます。
|
|
339
|
+
カテゴリ・節見出し・リダイレクト・(オプションで)記事全文のSQLite索引、言語版横断比較のための
|
|
340
|
+
言語間リンク(langlinks)— すべてオフラインで悉皆的にクエリでき、MCPサーバー経由で
|
|
341
|
+
LLMエージェントにも公開できます。
|
|
342
|
+
|
|
343
|
+
```console
|
|
344
|
+
$ wp2txt --build-index --fulltext --lang=ja # 索引の構築
|
|
345
|
+
$ wp2txt --find-articles --in-category "映画作品" -D 3 --has-section "あらすじ" --lang=ja
|
|
346
|
+
$ wp2txt --search "タイムループ" --in-category "映画作品" -D 3 --lang=ja
|
|
347
|
+
$ wp2txt --import-langlinks -L ja --langlinks-langs en,de,fr,zh,ko
|
|
348
|
+
$ wp2txt-mcp --lang=ja # LLMエージェント向けMCPサーバー
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
web/API アクセスと異なり、これらのクエリは全記事を走査します(`0件` は当該ダンプ版に対する
|
|
352
|
+
検証可能な不在の言明になります)。抽出結果には dump 版とクエリを記録した `.meta.json`
|
|
353
|
+
サイドカーが付き、再現可能です。
|
|
354
|
+
|
|
355
|
+
**→ 詳細は [Research Infrastructure Guide](docs/RESEARCH.md)(英語)を参照**:
|
|
356
|
+
索引構築、悉皆クエリ、全文検索、言語間リンク、言語版横断SQL、MCPツール一覧、設計原則。
|
|
357
|
+
|
|
330
358
|
## パフォーマンス
|
|
331
359
|
|
|
332
360
|
MacBook Air M4でのベンチマーク結果(7並列プロセス、ターボモード、ダウンロード時間除く):
|
|
@@ -347,6 +375,10 @@ MacBook Air M4でのベンチマーク結果(7並列プロセス、ターボ
|
|
|
347
375
|
|
|
348
376
|
詳細なリリースノートは[CHANGELOG.md](CHANGELOG.md)を参照してください。
|
|
349
377
|
|
|
378
|
+
**v2.3.0(2026年7月)**: 言語間リンク取り込み、言語版横断SQL(multi-dump ATTACH)、明示タイトル集合の抽出、SQL結果のファイル出力と再現性サイドカー、GHCRイメージ公開。
|
|
379
|
+
|
|
380
|
+
**v2.2.0(2026年7月)**: オフラインメタデータ索引、FTS5全文検索、MCPサーバー、query_sql、抽出ジョブ。
|
|
381
|
+
|
|
350
382
|
**v2.1.0(2026年2月)**: SQLiteキャッシュ、Ractor並列処理(Ruby 4.0+)、テンプレート展開、コンテンツマーカー、Dockerイメージ更新。
|
|
351
383
|
|
|
352
384
|
**v2.0.0(2026年1月)**: 自動ダウンロードモード、カテゴリベース抽出、タイトル指定抽出、JSON出力、ストリーミング処理、Ruby 4.0サポート。
|
data/Rakefile
CHANGED
|
@@ -32,14 +32,40 @@ Rake::Task["build"].enhance([:normalize_permissions])
|
|
|
32
32
|
# Docker
|
|
33
33
|
# =============================================================================
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# Paths that must never reach a published image. The image is built from the
|
|
36
|
+
# working tree, so anything ignored locally (private notes, scratch files)
|
|
37
|
+
# would otherwise ride along; 2.3.0's images shipped research-notes/ this way.
|
|
38
|
+
IMAGE_FORBIDDEN_PATHS = %w[/wp2txt/research-notes /wp2txt/tmp /wp2txt/.git /wp2txt/CLAUDE.md /wp2txt/.claude].freeze
|
|
39
|
+
|
|
40
|
+
desc "Verify a built image contains no private material (run before pushing)"
|
|
41
|
+
task :verify_image, [:tag] do |_t, args|
|
|
42
|
+
tag = args[:tag] || "wp2txt-verify:local"
|
|
43
|
+
checks = IMAGE_FORBIDDEN_PATHS.map { |p| "test -e #{p} && echo LEAK:#{p}" }.join("; ")
|
|
44
|
+
out = `docker run --rm #{tag} sh -c '#{checks}; true' 2>&1`
|
|
45
|
+
leaks = out.lines.grep(/^LEAK:/).map(&:strip)
|
|
46
|
+
abort "Image #{tag} contains private paths:\n #{leaks.join("\n ")}" unless leaks.empty?
|
|
47
|
+
|
|
48
|
+
puts "OK: #{tag} contains none of #{IMAGE_FORBIDDEN_PATHS.join(', ')}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "Build the image locally and verify it, without pushing"
|
|
52
|
+
task :check_image do
|
|
53
|
+
sh "docker build -t wp2txt-verify:local ."
|
|
54
|
+
Rake::Task[:verify_image].invoke
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "Build and push Docker images to GHCR (verifies a local build first)"
|
|
58
|
+
task push: :check_image do
|
|
59
|
+
# Docker Hub was retired after 2.3.0; GHCR (ghcr.io/yohasebe/wp2txt) is the
|
|
60
|
+
# only registry. Requires `docker buildx use multiarch` and a ghcr.io login.
|
|
37
61
|
sh <<-SCRIPT.strip_heredoc, { verbose: false }
|
|
38
62
|
/bin/bash -xeu <<'BASH'
|
|
39
|
-
# docker buildx create --name
|
|
40
|
-
# docker buildx use
|
|
63
|
+
# docker buildx create --name multiarch
|
|
64
|
+
# docker buildx use multiarch
|
|
41
65
|
# docker buildx inspect --bootstrap
|
|
42
|
-
docker buildx build --platform linux/amd64,linux/arm64
|
|
66
|
+
docker buildx build --platform linux/amd64,linux/arm64 \
|
|
67
|
+
-t ghcr.io/yohasebe/wp2txt:#{Wp2txt::VERSION} -t ghcr.io/yohasebe/wp2txt:latest \
|
|
68
|
+
. --push
|
|
43
69
|
BASH
|
|
44
70
|
SCRIPT
|
|
45
71
|
end
|
data/bin/wp2txt
CHANGED
|
@@ -711,6 +711,7 @@ class WpApp
|
|
|
711
711
|
return run_find_articles(opts) if opts[:find_articles]
|
|
712
712
|
return run_search(opts) if opts[:search]
|
|
713
713
|
return run_fts_optimize(opts) if opts[:fts_optimize]
|
|
714
|
+
return run_import_langlinks(opts) if opts[:import_langlinks]
|
|
714
715
|
|
|
715
716
|
# Determine input source
|
|
716
717
|
if opts[:from_category] && opts[:lang]
|
data/bin/wp2txt-mcp
CHANGED
|
@@ -24,6 +24,7 @@ require "optimist"
|
|
|
24
24
|
require "json"
|
|
25
25
|
require_relative "../lib/wp2txt/corpus"
|
|
26
26
|
require_relative "../lib/wp2txt/corpus_jobs"
|
|
27
|
+
require_relative "../lib/wp2txt/output_path"
|
|
27
28
|
require_relative "../lib/wp2txt/version"
|
|
28
29
|
|
|
29
30
|
opts = Optimist.options do
|
|
@@ -159,16 +160,23 @@ end
|
|
|
159
160
|
|
|
160
161
|
server.define_tool(
|
|
161
162
|
name: "query_sql",
|
|
162
|
-
description: "Read-only SQL (SELECT/WITH only) against the index databases — the escape hatch for questions the fixed tools cannot express (multi-way intersections, aggregations, joins). Call describe_schema first to see the tables. Main DB: pages(page_id, title, namespace, redirect_to, text_length), page_categories(page_id, category), page_sections(page_id, heading, level, ord), category_hierarchy(child, parent), alias_sets. When the fulltext tier exists, the FTS DB is attached as `fts` (fts.fts_sections MATCH ..., fts.fts_map). Filter articles with namespace = 0 AND redirect_to IS NULL. Rows are capped by `limit` (default 200, max 1000) with truncated flag; long cells are clipped. Queries are killed after 30 seconds (the error explains the likely cause); the tables are large (millions of rows), so constrain joins with indexed keys (page_id) and add WHERE filters.",
|
|
163
|
+
description: "Read-only SQL (SELECT/WITH only) against the index databases — the escape hatch for questions the fixed tools cannot express (multi-way intersections, aggregations, joins). Call describe_schema first to see the tables. Main DB: pages(page_id, title, namespace, redirect_to, text_length), page_categories(page_id, category), page_sections(page_id, heading, level, ord), category_hierarchy(child, parent), alias_sets; a langlinks table (ll_from = source page_id, ll_lang, ll_title) is also present when interlanguage links were imported via wp2txt --import-langlinks. When the fulltext tier exists, the FTS DB is attached as `fts` (fts.fts_sections MATCH ..., fts.fts_map). Cross-language: pass attach: [\"en\",\"de\"] to read-only ATTACH other locally installed languages' indexes, referenced as {lang}_meta.pages / {lang}_meta.page_sections / ... and {lang}_fts when that language has a fulltext tier — attached DBs share the main DB's schema. Hyphenated language codes map to underscored aliases (e.g. zh-yue → zh_yue_meta). Only installed languages are accepted (the main language itself cannot be attached); the response's `attached` field lists what was attached (dump_name, fts, dump_mismatch when the dates differ). Join langlinks (ll_from = page_id) to map articles across languages. Filter articles with namespace = 0 AND redirect_to IS NULL. Rows are capped by `limit` (default 200, max 1000) with truncated flag; long cells are clipped. output_path: write ALL rows to a JSONL file (with a .meta.json recording the SQL, dump versions, and attach configuration) and return only a summary + 3-row sample — use this whenever the result is large; `limit` is ignored in this mode. Queries are killed after 30 seconds (the error explains the likely cause); the tables are large (millions of rows), so constrain joins with indexed keys (page_id) and add WHERE filters.",
|
|
163
164
|
input_schema: {
|
|
164
165
|
properties: {
|
|
165
166
|
sql: { type: "string" },
|
|
166
|
-
limit: { type: "integer", minimum: 1, maximum: 1000 }
|
|
167
|
+
limit: { type: "integer", minimum: 1, maximum: 1000 },
|
|
168
|
+
attach: { type: "array", items: { type: "string" },
|
|
169
|
+
description: "Language codes of other locally installed dumps to ATTACH read-only as {lang}_meta / {lang}_fts (default: none)" },
|
|
170
|
+
output_path: { type: "string", description: "JSONL output path for ALL rows; relative paths resolve under the server's output dir" },
|
|
171
|
+
overwrite: { type: "boolean", description: "Replace an existing output file (default: refuse)" }
|
|
167
172
|
},
|
|
168
173
|
required: ["sql"]
|
|
169
174
|
}
|
|
170
|
-
) do |sql:, limit: 200, server_context:|
|
|
171
|
-
respond
|
|
175
|
+
) do |sql:, limit: 200, attach: [], output_path: nil, overwrite: false, server_context:|
|
|
176
|
+
respond do
|
|
177
|
+
path = output_path && Wp2txt::OutputPath.confine(output_path, OUTPUT_DIR, overwrite: overwrite)
|
|
178
|
+
corpus.query_sql(sql, limit: limit, attach: attach, output_path: path, overwrite: overwrite)
|
|
179
|
+
end
|
|
172
180
|
end
|
|
173
181
|
|
|
174
182
|
server.define_tool(
|
|
@@ -246,7 +254,7 @@ end
|
|
|
246
254
|
|
|
247
255
|
server.define_tool(
|
|
248
256
|
name: "save_alias_set",
|
|
249
|
-
description: "Persist a named set of section-heading alias groups for this dump (e.g. [[\"あらすじ\",\"ストーリー\",\"物語\"]]). A server-side guardrail re-checks co-occurrence: a pair that appears in the same article more than 20% of the time (when both headings occur in at least 100 articles) is treated as distinct sections, not synonyms, and blocks the save with saved:false and the offending pairs listed — fix the groups, or pass force:true only if you verified them another way (e.g., by reading section contents with get_sections). The response echoes the saved groups and the exact criteria used. Saved sets are referenced via alias_set in find_articles / get_sections / extract_corpus and recorded in extraction metadata for reproducibility.",
|
|
257
|
+
description: "Persist a named set of section-heading alias groups for this dump (e.g. [[\"あらすじ\",\"ストーリー\",\"物語\"]]). A server-side guardrail re-checks co-occurrence: a pair that appears in the same article more than 20% of the time (when both headings occur in at least 100 articles) is treated as distinct sections, not synonyms, and blocks the save with saved:false and the offending pairs listed — fix the groups, or pass force:true only if you verified them another way (e.g., by reading section contents with get_sections). The response echoes the saved groups and the exact criteria used. Saved sets are referenced via alias_set in find_articles / get_sections / extract_corpus and recorded in extraction metadata for reproducibility. A refused save is signaled by saved:false in the RESULT (not an exception); verify persistence with list_alias_sets.",
|
|
250
258
|
input_schema: {
|
|
251
259
|
properties: {
|
|
252
260
|
name: { type: "string" },
|
|
@@ -279,6 +287,8 @@ EXTRACT_SCHEMA = {
|
|
|
279
287
|
properties: {
|
|
280
288
|
output_path: { type: "string", description: "JSONL output path; relative paths resolve under the server's output dir" },
|
|
281
289
|
content: { type: "string", enum: ["sections", "summary", "full", "wikitext"] },
|
|
290
|
+
titles: { type: "array", items: { type: "string" }, maxItems: 10_000,
|
|
291
|
+
description: "Explicit article titles to extract (mutually exclusive with the filter arguments)" },
|
|
282
292
|
sections: { type: "array", items: { type: "string" } },
|
|
283
293
|
alias_set: { type: "string" },
|
|
284
294
|
category: { type: "string" },
|
|
@@ -294,23 +304,12 @@ EXTRACT_SCHEMA = {
|
|
|
294
304
|
required: ["output_path", "content"]
|
|
295
305
|
}.freeze
|
|
296
306
|
|
|
297
|
-
def extract_params(output_path:, content:, sections: nil, alias_set: nil, category: nil,
|
|
307
|
+
def extract_params(output_path:, content:, titles: nil, sections: nil, alias_set: nil, category: nil,
|
|
298
308
|
depth: 0, categories: nil, category_match: nil,
|
|
299
309
|
title_match: nil, limit: 0, chunk_size: nil, chunk_overlap: 0,
|
|
300
310
|
overwrite: false)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
# replace existing outputs
|
|
304
|
-
path = File.expand_path(output_path, OUTPUT_DIR)
|
|
305
|
-
base = File.expand_path(OUTPUT_DIR)
|
|
306
|
-
unless path == base || path.start_with?(base + File::SEPARATOR)
|
|
307
|
-
raise ArgumentError, "output_path must stay within the server output directory (#{base})"
|
|
308
|
-
end
|
|
309
|
-
if File.exist?(path) && !overwrite
|
|
310
|
-
raise ArgumentError, "output file already exists: #{path} (pass overwrite: true to replace it)"
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
{ output_path: path, content: content,
|
|
311
|
+
{ output_path: Wp2txt::OutputPath.confine(output_path, OUTPUT_DIR, overwrite: overwrite),
|
|
312
|
+
content: content, titles: titles,
|
|
314
313
|
sections: sections, alias_set: alias_set, category: category, depth: depth,
|
|
315
314
|
categories: categories, category_match: category_match,
|
|
316
315
|
title_match: title_match, limit: limit, chunk_size: chunk_size, chunk_overlap: chunk_overlap }
|
|
@@ -318,7 +317,7 @@ end
|
|
|
318
317
|
|
|
319
318
|
server.define_tool(
|
|
320
319
|
name: "extract_corpus",
|
|
321
|
-
description: "Extract matching articles to a JSONL file on disk (with a .meta.json sidecar recording dump version and query for reproducibility). Returns a summary and the first 3 records only — the full corpus stays on disk; process it with your own tools. content: 'sections' (named sections only), 'summary' (lead text), 'full' (whole cleaned article), or 'wikitext' (raw markup, for mining infoboxes/templates that cleaning replaces with markers). Filters are the same as find_articles (category/depth, categories, category_match, title_match). Optional chunk_size/chunk_overlap produce one RAG-ready record per chunk with section_path and chunk_index (not with wikitext). Synchronous, capped at 5000 articles (result flags truncation) — use start_extract_job for larger extractions. Dry-run the filters with find_articles first.",
|
|
320
|
+
description: "Extract matching articles to a JSONL file on disk (with a .meta.json sidecar recording dump version and query for reproducibility). Returns a summary and the first 3 records only — the full corpus stays on disk; process it with your own tools. content: 'sections' (named sections only), 'summary' (lead text), 'full' (whole cleaned article), or 'wikitext' (raw markup, for mining infoboxes/templates that cleaning replaces with markers). Filters are the same as find_articles (category/depth, categories, category_match, title_match). titles: extract an explicit list of articles (e.g. a set determined via query_sql); mutually exclusive with the filter arguments; redirects are resolved; missing titles are reported in not_found. Optional chunk_size/chunk_overlap produce one RAG-ready record per chunk with section_path and chunk_index (not with wikitext). Synchronous, capped at 5000 articles (result flags truncation) — use start_extract_job for larger extractions. Dry-run the filters with find_articles first.",
|
|
322
321
|
input_schema: EXTRACT_SCHEMA
|
|
323
322
|
) do |server_context:, **args|
|
|
324
323
|
respond { corpus.extract_corpus(**extract_params(**args)) }
|