cjk_index 0.1.0 → 1.0.0
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/CHANGELOG.md +125 -0
- data/LICENSE-Unicode +39 -0
- data/NOTICE +17 -0
- data/README.md +257 -27
- data/data/ja-kyujitai.tsv +423 -292
- data/data/ko-hanyang-pua.tsv +5666 -0
- data/data/zh-hant-hans-safe.tsv +2774 -0
- data/exe/cjk_index +6 -0
- data/lib/cjk_index/bridgetown.rb +100 -0
- data/lib/cjk_index/builder.rb +438 -31
- data/lib/cjk_index/cli.rb +240 -0
- data/lib/cjk_index/index_file.rb +203 -0
- data/lib/cjk_index/jekyll.rb +38 -7
- data/lib/cjk_index/normalizer.rb +60 -12
- data/lib/cjk_index/postings.rb +218 -0
- data/lib/cjk_index/rake_task.rb +71 -0
- data/lib/cjk_index/runtime.js +337 -25
- data/lib/cjk_index/runtime.rb +1 -0
- data/lib/cjk_index/searcher.rb +215 -39
- data/lib/cjk_index/tokenizer.rb +30 -0
- data/lib/cjk_index/version.rb +1 -1
- data/lib/cjk_index.rb +2 -0
- metadata +21 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c9ffe634562d740738f6d9b9cf97ade5f5ab6d32a80b6bfd349496435d9e88b
|
|
4
|
+
data.tar.gz: 6cc0aebdedd14b07cdcf9dda3dcc9b849b80d135149444f057daed4cc620c004
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee41c34d99efe6e17f8936f3211b62aaab6fdafa54b714f685458c641a56263a46b506de9f703a4cd1c6f470c32e0d336b1914547655a433641ecbf9cdc5fcc0
|
|
7
|
+
data.tar.gz: 608805cba3a84e8c1bb8425e09c8ff3bdcb2bd4da6443f6ca63eeb6d2e3952e75b0166db871e853383d3531cb40d7f48052cc70540f2ae7fd8bd5ae372424af7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
Indexes built with 0.x should be rebuilt: the `ja` table changed, and the
|
|
6
|
+
index now records a digest of its tables (see below). Formats 2 and 3 are
|
|
7
|
+
still read.
|
|
8
|
+
|
|
9
|
+
Large collections:
|
|
10
|
+
|
|
11
|
+
- Index format `cjk_index/4`: posting lists as compact strings (about a third
|
|
12
|
+
smaller gzipped, three times smaller before compression).
|
|
13
|
+
- Split indexes: `Builder#write_split(dir, shard_size:)` and
|
|
14
|
+
`Builder#each_split_file` write a manifest and shards of about 32 KB; the
|
|
15
|
+
browser fetches shards as queries need them (`index.searchAsync`,
|
|
16
|
+
`index.prepare`). `CJKIndex.load` and `Searcher.load` read a split index;
|
|
17
|
+
Jekyll: `split: true`.
|
|
18
|
+
- `Builder#write(path)` writes one file without building the JSON in memory;
|
|
19
|
+
`Builder.read(path)` reads a file or a split index.
|
|
20
|
+
- Building keeps posting lists encoded and normalizes faster: 3,000 full
|
|
21
|
+
texts take 0.56 GB instead of 2.3 GB at peak, and 37 s instead of 62 s.
|
|
22
|
+
- Updating a loaded index is slower without YJIT (one update about 0.25 s at
|
|
23
|
+
17,000 records); one update and its write in `IndexFile` cost about the
|
|
24
|
+
same as before.
|
|
25
|
+
|
|
26
|
+
Variant tables:
|
|
27
|
+
|
|
28
|
+
- `ja`: 409 pairs (was 291), generated from the Jōyō Kanji Table, the
|
|
29
|
+
Jinmeiyō kanji list and Unihan kZVariant. 籐/藤, 聯/連 and 彎/湾 no longer
|
|
30
|
+
fold.
|
|
31
|
+
- New opt-in tables: `zh-safe` (Traditional/Simplified pairs that are
|
|
32
|
+
one-to-one both ways, so 後/后 and 發/髮 stay apart) and `ko` (old Hangul
|
|
33
|
+
in the Hanyang Private Use Area to standard jamo). `zh` is unchanged.
|
|
34
|
+
- `script/variant_tables.rb` generates the tables from their sources.
|
|
35
|
+
- The index records `variant_digest`, a digest of what its tables fold; Ruby
|
|
36
|
+
and the browser script refuse an index whose digest differs.
|
|
37
|
+
- The browser highlights matches in conjoining jamo, half-width kana with
|
|
38
|
+
sound marks and old Hangul in the Private Use Area.
|
|
39
|
+
|
|
40
|
+
More ways to build an index:
|
|
41
|
+
|
|
42
|
+
- `cjk_index` command: `build` (CSV, JSON or JSON Lines), `search`, `runtime`.
|
|
43
|
+
- `CJKIndex::RakeTask`.
|
|
44
|
+
- Bridgetown 2 plugin (`require "cjk_index/bridgetown"`).
|
|
45
|
+
- Examples for Bridgetown and Rails (`examples/`), notes in
|
|
46
|
+
`docs/notes/adapters.md`.
|
|
47
|
+
- Migration guide from lunr.js, in English and Japanese
|
|
48
|
+
(`docs/migrating-from-lunr.md`).
|
|
49
|
+
|
|
50
|
+
Filters:
|
|
51
|
+
|
|
52
|
+
- A `value` filter compares value ids instead of folded strings, in Ruby
|
|
53
|
+
and in the browser (#14): filtering 17,000 records by one author takes
|
|
54
|
+
1.7 ms instead of 2.7 ms.
|
|
55
|
+
|
|
56
|
+
Fixes:
|
|
57
|
+
|
|
58
|
+
- A `year` filter on a Time or Date (Jekyll gives a post's `date` as a Time)
|
|
59
|
+
gave no year.
|
|
60
|
+
- The Jekyll plugin gave the builder only the index fields of a collection
|
|
61
|
+
document, so a filter on another front-matter field was empty.
|
|
62
|
+
- A query word named like a JavaScript object member ("constructor") scored
|
|
63
|
+
differently in the browser than in Ruby.
|
|
64
|
+
|
|
65
|
+
## 0.2.0 (2026-10-28)
|
|
66
|
+
|
|
67
|
+
- Filters: `Builder.new(filters:)` with `value` and `year` filters;
|
|
68
|
+
`search(query, filter:)` in Ruby and `search(query, { filter })` in the
|
|
69
|
+
browser; a blank query with a filter lists every document that passes;
|
|
70
|
+
`facets(field, results)` counts values. Ruby and the browser give the same
|
|
71
|
+
results and counts.
|
|
72
|
+
- Index updates: `Builder#remove`, `Builder#update`, `Builder.load`. The
|
|
73
|
+
changed index is the same as one rebuilt from scratch.
|
|
74
|
+
- `Searcher.new(builder)` follows later changes to the builder (it used to
|
|
75
|
+
copy the index).
|
|
76
|
+
- `IndexFile`: an index file shared by several processes (e.g. Rails
|
|
77
|
+
workers). Updates take a lock and are written with a rename, so none is
|
|
78
|
+
lost and readers never see a partly written file; readers load the file
|
|
79
|
+
again when it changes. A killed writer leaves the old index intact;
|
|
80
|
+
`lock_timeout:` bounds the wait for another writer.
|
|
81
|
+
- Index format `cjk_index/3` (adds `filters`). The browser script and
|
|
82
|
+
`Searcher` still read `cjk_index/2`.
|
|
83
|
+
- Jekyll: `filters:` for an index; the CollectionBuilder preset filters on
|
|
84
|
+
`metadata-facets-fields` and the year of `date`.
|
|
85
|
+
|
|
86
|
+
## 0.1.1 (2026-10-18)
|
|
87
|
+
|
|
88
|
+
No change to the library. This release fixes the state of the repository
|
|
89
|
+
cited in a paper.
|
|
90
|
+
|
|
91
|
+
- Retrieval quality on two real datasets (69 catalog records from cb-ja-demo,
|
|
92
|
+
16,951 Aozora Bunko works; 15 queries each), against lunr.js with its
|
|
93
|
+
default and Japanese settings: [bench/retrieval/](bench/retrieval/).
|
|
94
|
+
Queries, relevance judgments (committed before the runs), judging rules,
|
|
95
|
+
scripts and raw outputs are included.
|
|
96
|
+
- Load and query times in Chrome and Safari for 1,000, 5,000 and 16,951
|
|
97
|
+
records: [bench/browser/](bench/browser/).
|
|
98
|
+
- README and docs/comparison.md: the recommended size is now the range we
|
|
99
|
+
measured (up to 16,951 records), not "about 20,000".
|
|
100
|
+
- CITATION.cff for citing the software.
|
|
101
|
+
- CODE_OF_CONDUCT.md (Contributor Covenant 2.1), CONTRIBUTING.md, SECURITY.md,
|
|
102
|
+
issue forms and a pull request template.
|
|
103
|
+
|
|
104
|
+
## 0.1.0 (2026-10-02)
|
|
105
|
+
|
|
106
|
+
First release on RubyGems.
|
|
107
|
+
|
|
108
|
+
- Normalizer in pure Ruby: NFKC, lowercase, katakana → hiragana, 291 old-form
|
|
109
|
+
kanji → modern forms, the iteration mark 々. Optional Traditional → Simplified
|
|
110
|
+
Chinese table (3,202 characters, from OpenCC, Apache-2.0). `CJKIndex.normalize`.
|
|
111
|
+
- Tokenizer: positional bigrams for CJK runs, including hangul and old-hangul
|
|
112
|
+
jamo; Latin words as whole tokens.
|
|
113
|
+
- `CJKIndex::Builder`: BM25 index with field boosts, written as JSON.
|
|
114
|
+
- `CJKIndex::Searcher`: exact substring matching, BM25 ranking, highlighting and
|
|
115
|
+
excerpts in pure Ruby; prefix matching and one-typo tolerance for Latin words.
|
|
116
|
+
- Browser runtime with no dependencies (`CJKIndex::Runtime.source`), generated
|
|
117
|
+
from the same tables; the test suite requires identical results in Ruby and
|
|
118
|
+
under Node.
|
|
119
|
+
- Jekyll plugin (`cjk_index/jekyll`) with a CollectionBuilder preset.
|
|
120
|
+
|
|
121
|
+
In the repository (not packaged in the gem):
|
|
122
|
+
|
|
123
|
+
- `examples/search_csv.rb`: search a CSV catalog from the command line, without Jekyll.
|
|
124
|
+
- Benchmarks on Aozora Bunko (up to 16,951 works): [bench/README.md](bench/README.md).
|
|
125
|
+
- Comparison with other search tools: [docs/comparison.md](docs/comparison.md).
|
data/LICENSE-Unicode
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
UNICODE LICENSE V3
|
|
2
|
+
|
|
3
|
+
COPYRIGHT AND PERMISSION NOTICE
|
|
4
|
+
|
|
5
|
+
Copyright © 1991-2026 Unicode, Inc.
|
|
6
|
+
|
|
7
|
+
NOTICE TO USER: Carefully read the following legal agreement. BY
|
|
8
|
+
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
|
9
|
+
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
|
10
|
+
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
|
11
|
+
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
14
|
+
copy of data files and any associated documentation (the "Data Files") or
|
|
15
|
+
software and any associated documentation (the "Software") to deal in the
|
|
16
|
+
Data Files or Software without restriction, including without limitation
|
|
17
|
+
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
|
18
|
+
copies of the Data Files or Software, and to permit persons to whom the
|
|
19
|
+
Data Files or Software are furnished to do so, provided that either (a)
|
|
20
|
+
this copyright and permission notice appear with all copies of the Data
|
|
21
|
+
Files or Software, or (b) this copyright and permission notice appear in
|
|
22
|
+
associated Documentation.
|
|
23
|
+
|
|
24
|
+
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
25
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
26
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
|
27
|
+
THIRD PARTY RIGHTS.
|
|
28
|
+
|
|
29
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
|
30
|
+
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
|
31
|
+
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
32
|
+
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
33
|
+
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
|
34
|
+
FILES OR SOFTWARE.
|
|
35
|
+
|
|
36
|
+
Except as contained in this notice, the name of a copyright holder shall
|
|
37
|
+
not be used in advertising or otherwise to promote the sale, use or other
|
|
38
|
+
dealings in these Data Files or Software without prior written
|
|
39
|
+
authorization of the copyright holder.
|
data/NOTICE
CHANGED
|
@@ -5,3 +5,20 @@ data/zh-hant-hans.tsv is derived from OpenCC (https://github.com/BYVoid/OpenCC),
|
|
|
5
5
|
data/dictionary/TSCharacters.txt, Copyright (c) 2010-2026 Carbo Kuo and contributors,
|
|
6
6
|
licensed under the Apache License, Version 2.0 (see LICENSE-OpenCC).
|
|
7
7
|
Changes are described at the top of that file.
|
|
8
|
+
|
|
9
|
+
data/zh-hant-hans-safe.tsv is derived from OpenCC data/dictionary/TSCharacters.txt and
|
|
10
|
+
STCharacters.txt, under the same license and copyright as above.
|
|
11
|
+
|
|
12
|
+
data/ja-kyujitai.tsv is derived in part from the Unicode Han Database (Unihan), Unicode 18.0.0
|
|
13
|
+
(https://www.unicode.org/Public/18.0.0/ucd/Unihan.zip), Copyright (c) 1991-2026 Unicode, Inc.,
|
|
14
|
+
licensed under the Unicode License v3 (see LICENSE-Unicode). The fields used (kJapaneseOldVariant,
|
|
15
|
+
kJoyoKanji, kJinmeiyoKanji, kZVariant) record the Joyo Kanji Table (Cabinet Notice No. 2 of 2010)
|
|
16
|
+
and the Jinmeiyo Kanji of the Family Register Act Enforcement Regulations, which as official
|
|
17
|
+
notices and ordinances are not subject to copyright in Japan (Copyright Act, art. 13).
|
|
18
|
+
|
|
19
|
+
data/ko-hanyang-pua.tsv is converted from the table in the hypua crate by kiwiyou
|
|
20
|
+
(https://github.com/kiwiyou/hypua, commit 7e47655730d4cfea89564768ebb277b86072eef9), which its
|
|
21
|
+
author released under the Unlicense (a public domain dedication). The mapping is the same as that
|
|
22
|
+
of hypua2jamo by mete0r (LGPL-3.0-or-later code); both are based on the KTUG Hanyang PUA Table
|
|
23
|
+
Project (http://faq.ktug.org/faq/HanyangPuaTableProject), which states no license. See
|
|
24
|
+
docs/notes/variant-tables.md.
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# cjk_index
|
|
2
2
|
|
|
3
|
+
日本語の説明: [docs/ja/README.md](docs/ja/README.md)
|
|
4
|
+
|
|
3
5
|
Dictionary-free full-text search for Chinese, Japanese and Korean text, in
|
|
4
6
|
pure Ruby. Build the index in Ruby, then query it in Ruby (`CJKIndex::Searcher`)
|
|
5
7
|
or in the browser with a small script that has no dependencies. Both return
|
|
@@ -24,7 +26,8 @@ even when it is an exact title. cjk_index instead:
|
|
|
24
26
|
native extension (the same idea as [kensaku](https://github.com/whispcat/kensaku));
|
|
25
27
|
- folds variants on both sides: NFKC, lowercase, katakana → hiragana,
|
|
26
28
|
old-form kanji → modern form (「圖」→「図」), 々 expanded (「佐々木」→「佐佐木」),
|
|
27
|
-
and optionally Traditional ↔ Simplified Chinese (from OpenCC)
|
|
29
|
+
and optionally Traditional ↔ Simplified Chinese (from OpenCC) and old
|
|
30
|
+
Hangul in the Private Use Area;
|
|
28
31
|
- ranks with BM25 per field, with field boosts;
|
|
29
32
|
- marks matches in the original text (`highlight`, `excerpt`), even when the
|
|
30
33
|
query and the text differ only by these variants;
|
|
@@ -32,18 +35,42 @@ even when it is an exact title. cjk_index instead:
|
|
|
32
35
|
one typo;
|
|
33
36
|
- generates the browser script from the same tables as the Ruby code, so
|
|
34
37
|
indexing and querying cannot drift apart (checked by the test suite under
|
|
35
|
-
Node; the script also refuses an index built with other tables)
|
|
38
|
+
Node; the script also refuses an index built with other tables);
|
|
39
|
+
- splits a large index into a small manifest and shards that the browser
|
|
40
|
+
fetches only when a query needs them.
|
|
36
41
|
|
|
37
|
-
The browser script is about
|
|
38
|
-
with the Chinese table as well.
|
|
42
|
+
The browser script is about 9 KB gzipped with the Japanese table, 24 KB
|
|
43
|
+
with the Chinese table as well. Index it from Ruby, Jekyll, Bridgetown, a
|
|
44
|
+
Rake task or the command line.
|
|
39
45
|
|
|
40
|
-
How this compares with Pagefind, kensaku, lunr.js and others:
|
|
46
|
+
How this compares with Pagefind, kensaku, lunr.js, rroonga, tantiny and others:
|
|
41
47
|
[docs/comparison.md](docs/comparison.md).
|
|
42
48
|
Index size and query time on up to 16,951 works of Aozora Bunko:
|
|
43
|
-
[bench/README.md](bench/README.md).
|
|
49
|
+
[bench/README.md](bench/README.md). Retrieval quality against lunr.js:
|
|
50
|
+
[bench/retrieval/](bench/retrieval/). Load and query times in Chrome and
|
|
51
|
+
Safari: [bench/browser/](bench/browser/).
|
|
52
|
+
|
|
53
|
+
When to use it, and when not to:
|
|
54
|
+
|
|
55
|
+
- **Good fit:** catalogs, collection records, blog posts and full texts on
|
|
56
|
+
static sites, command-line tools, scripts and small apps; anywhere you want
|
|
57
|
+
nothing to install outside Ruby, or the same results in Ruby and the
|
|
58
|
+
browser. In one file, the 16,951-record Aozora Bunko catalog is 12.9 MB
|
|
59
|
+
gzipped; as a [split index](#large-collections-split-indexes), the browser
|
|
60
|
+
loads a 72 KB manifest and fetches about 40 KB per query, and 1,000 full
|
|
61
|
+
texts (33 MB gzipped in one file) need about the same.
|
|
62
|
+
- **Not a good fit:** frequent updates to a large index, or search served
|
|
63
|
+
continuously by a server. An update rewrites the whole index, and the index
|
|
64
|
+
is held in memory: building 3,000 full texts (35.6 million characters) takes
|
|
65
|
+
37 s and 0.56 GB. A native engine (rroonga, tantiny) or a database feature
|
|
66
|
+
(pg_bigm) is the better choice there.
|
|
44
67
|
|
|
45
68
|
## Ruby
|
|
46
69
|
|
|
70
|
+
```console
|
|
71
|
+
$ gem install cjk_index
|
|
72
|
+
```
|
|
73
|
+
|
|
47
74
|
```ruby
|
|
48
75
|
require "cjk_index"
|
|
49
76
|
|
|
@@ -71,22 +98,109 @@ Matching, BM25 ranking, highlighting and excerpts follow the browser script
|
|
|
71
98
|
step for step. The test suite runs both on the same index and checks that
|
|
72
99
|
refs, scores, matched fields, highlights and excerpts are identical.
|
|
73
100
|
|
|
74
|
-
###
|
|
101
|
+
### Filters
|
|
75
102
|
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
Fields to filter on are given when the index is built. A `value` filter
|
|
104
|
+
matches whole values, folded like the query (地圖 matches 地図); a `year`
|
|
105
|
+
filter keeps the first four-digit number of a value (`1900-01-01`,
|
|
106
|
+
`1931年`; era dates such as 明治33年 give no year).
|
|
78
107
|
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
...
|
|
108
|
+
```ruby
|
|
109
|
+
builder = CJKIndex::Builder.new(fields: %w[title], filters: {
|
|
110
|
+
"type" => "value", "subject" => { "kind" => "value", "separator" => ";" }, "date" => "year"
|
|
111
|
+
})
|
|
112
|
+
builder.add("item1.html", "title" => "…鳥瞰圖", "type" => "地図", "subject" => "建物;農学部", "date" => "1900")
|
|
113
|
+
|
|
114
|
+
searcher.search("鳥瞰図", filter: { "type" => "地図", "date" => 1890..1910 })
|
|
115
|
+
searcher.search("", filter: { "subject" => %w[建物 植物] }) # no query: every item that passes
|
|
116
|
+
searcher.facets("type", results) # => [{ value: "地図", count: 12 }, ...]
|
|
88
117
|
```
|
|
89
118
|
|
|
119
|
+
Conditions on different fields must all hold; a list matches any of its
|
|
120
|
+
values. A filter only removes results: the scores of the others do not change.
|
|
121
|
+
|
|
122
|
+
### Updating an index
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
builder.update("item1.html", "title" => "…", "type" => "写真") # in place, or added if new
|
|
126
|
+
builder.remove("item2.html")
|
|
127
|
+
|
|
128
|
+
builder = CJKIndex::Builder.load(File.read("search.json")) # change a written index
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
After any changes, `to_h` is the same as for an index built from scratch
|
|
132
|
+
with the remaining documents in the same order (the test suite checks this
|
|
133
|
+
on random sequences of changes). A `Searcher` made from a `Builder` answers
|
|
134
|
+
for its current documents.
|
|
135
|
+
|
|
136
|
+
### Sharing an index file between processes
|
|
137
|
+
|
|
138
|
+
A Rails app usually runs several worker processes, and each holds its own
|
|
139
|
+
copy of the index in memory. `IndexFile` keeps them in step through one file:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# config/initializers/cjk_index.rb
|
|
143
|
+
SEARCH = CJKIndex::IndexFile.new(Rails.root.join("db/search-index.json"))
|
|
144
|
+
|
|
145
|
+
SEARCH.write(builder) # a full build, e.g. in a Rake task
|
|
146
|
+
SEARCH.update { |b| b.update(item.id, item.to_doc) } # after a record changes, in any process
|
|
147
|
+
SEARCH.searcher.search("鳥瞰図") # loads the file again if it has changed
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`update` takes a lock, reads the file, runs the block and writes the result,
|
|
151
|
+
so updates from different processes are applied one after another. The new
|
|
152
|
+
index is written to a temporary file and renamed over the old one: a reader
|
|
153
|
+
gets the old index or the new one, never a partly written file. A reader
|
|
154
|
+
keeps the index it loaded until the file changes; `interval:` limits how
|
|
155
|
+
often it checks. Put several changes in one block to write once.
|
|
156
|
+
|
|
157
|
+
On the Aozora Bunko catalog (16,951 records), a check of an unchanged file
|
|
158
|
+
takes 0.09 ms, and an update about 0.5 s (about 2 s when another process
|
|
159
|
+
wrote last, as the file is read again).
|
|
160
|
+
[bench/index_file.rb](bench/index_file.rb). The lock is `flock`: use a local
|
|
161
|
+
disk, not NFS.
|
|
162
|
+
|
|
163
|
+
The file is never changed in place, so a writer that is killed leaves the
|
|
164
|
+
previous index intact and nothing to repair. A writer waits at most
|
|
165
|
+
`lock_timeout:` seconds (default 60) for another one, then raises
|
|
166
|
+
`CJKIndex::IndexFile::LockTimeout`.
|
|
167
|
+
|
|
168
|
+
An update rewrites the whole file, so its cost grows with the index: 0.08 s
|
|
169
|
+
for 1,000 records, 0.24 s for 5,000, 0.7 s for 16,951. A record edited now
|
|
170
|
+
and then needs nothing special; put a few changes in one block; for an
|
|
171
|
+
import, build the index again and `write` it (11 s for 16,951 records).
|
|
172
|
+
|
|
173
|
+
## Large collections: split indexes
|
|
174
|
+
|
|
175
|
+
Indexes are written in format `cjk_index/4`: each posting list is a compact
|
|
176
|
+
string, about a third smaller gzipped than format 3 and three times smaller
|
|
177
|
+
before compression. Ruby and the browser script still read formats 2 and 3.
|
|
178
|
+
|
|
179
|
+
For full text or tens of thousands of records, write a split index: a small
|
|
180
|
+
manifest (refs, lengths, filters) and shards of about 32 KB that the browser
|
|
181
|
+
fetches only when a query needs them.
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
builder.write_split("assets/search") # index.json and the shards
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const index = await CJKIndex.load("/assets/search/index.json");
|
|
189
|
+
const results = await index.searchAsync("銀河鉄道", { filter: { type: "小説" } });
|
|
190
|
+
// or: await index.prepare(query); index.search(query, options)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`searchAsync` also works on an index in one file, so a page can use it for
|
|
194
|
+
both. A blank query with a filter, `facets`, `highlight` and `excerpt` need
|
|
195
|
+
no shard. On 1,000 Aozora Bunko full texts (33 MB gzipped as one file), the
|
|
196
|
+
manifest is 28 KB and a query fetches 43 KB (median).
|
|
197
|
+
|
|
198
|
+
In Ruby, `CJKIndex::Searcher.load("assets/search")` reads shards as queries
|
|
199
|
+
need them, and `CJKIndex::Builder.read("assets/search")` loads a split index
|
|
200
|
+
to change it. Shard names are digests of their content, so they can be cached
|
|
201
|
+
for long; `write_split` removes the shards the new manifest no longer lists.
|
|
202
|
+
`IndexFile` always writes one file. Measurements: [bench/README.md](bench/README.md).
|
|
203
|
+
|
|
90
204
|
## Browser
|
|
91
205
|
|
|
92
206
|
```html
|
|
@@ -96,7 +210,11 @@ agri_6d032fb5 東京[帝國大學]農學部建物位置圖
|
|
|
96
210
|
index.search("鳥瞰図"); // => [{ ref: "item1.html", score: ..., fields: ["title"] }]
|
|
97
211
|
CJKIndex.highlight("東京帝國大學", "帝国大学"); // => "東京<mark>帝國大學</mark>"
|
|
98
212
|
CJKIndex.excerpt(longText, "鳥瞰図", 80); // highlighted window around the first match
|
|
213
|
+
|
|
214
|
+
var results = index.search("鳥瞰図", { filter: { type: "地図", date: { min: 1890, max: 1910 } } });
|
|
215
|
+
index.facets("type", results); // => [{ value: "地図", count: 12 }, ...]
|
|
99
216
|
});
|
|
217
|
+
// a split index: await index.searchAsync(query, options), see above
|
|
100
218
|
</script>
|
|
101
219
|
```
|
|
102
220
|
|
|
@@ -123,9 +241,18 @@ cjk_index:
|
|
|
123
241
|
data: metadata # rows of _data/metadata.csv
|
|
124
242
|
ref: objectid
|
|
125
243
|
fields: [title, creator, subject]
|
|
244
|
+
filters: # optional
|
|
245
|
+
type: value
|
|
246
|
+
subject: { kind: value, separator: ";" }
|
|
247
|
+
date: year
|
|
248
|
+
- name: texts
|
|
249
|
+
collection: texts
|
|
250
|
+
fields: [title, content]
|
|
251
|
+
split: true # -> assets/cjk-index/texts/index.json and shards
|
|
126
252
|
```
|
|
127
253
|
|
|
128
|
-
The runtime is written to `assets/cjk-index/cjk-index.js`.
|
|
254
|
+
The runtime is written to `assets/cjk-index/cjk-index.js`. Moving a site from
|
|
255
|
+
lunr.js: [docs/migrating-from-lunr.md](docs/migrating-from-lunr.md).
|
|
129
256
|
|
|
130
257
|
### CollectionBuilder
|
|
131
258
|
|
|
@@ -139,25 +266,125 @@ cjk_index:
|
|
|
139
266
|
The preset follows CollectionBuilder's own search settings: fields with
|
|
140
267
|
`index: true` in `_data/config-search.csv`, and child objects only when
|
|
141
268
|
`search-child-objects` is on. Result refs are the same `id` values as in
|
|
142
|
-
`assets/js/lunr-store.js`.
|
|
269
|
+
`assets/js/lunr-store.js`. Filters default to the theme's
|
|
270
|
+
`metadata-facets-fields` (values separated by `;`) and a year from the `date`
|
|
271
|
+
column; set `filters:` to change them. A working example:
|
|
143
272
|
https://github.com/nakamura196/cb-ja-demo
|
|
144
273
|
|
|
274
|
+
## Bridgetown
|
|
275
|
+
|
|
276
|
+
Tested with Bridgetown 2.2.2. Node is not needed.
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# config/initializers.rb
|
|
280
|
+
require "cjk_index/bridgetown"
|
|
281
|
+
|
|
282
|
+
Bridgetown.configure do |config|
|
|
283
|
+
init :cjk_index, indexes: [
|
|
284
|
+
{ name: "posts", collection: "posts", fields: %w[title content], boosts: { title: 3 },
|
|
285
|
+
filters: { category: "value", date: "year" } }
|
|
286
|
+
]
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
This writes `output/cjk-index/posts.json` and `output/cjk-index/cjk-index.js`.
|
|
291
|
+
Use `data:` and `ref:` instead of `collection:` for rows of
|
|
292
|
+
`src/_data/*.csv`. See [examples/bridgetown](examples/bridgetown).
|
|
293
|
+
|
|
294
|
+
## Command line and Rake
|
|
295
|
+
|
|
296
|
+
```console
|
|
297
|
+
$ cjk_index build items.csv --ref id --fields title,creator --boost title=3 --filter type --filter date=year -o index.json --runtime cjk-index.js
|
|
298
|
+
$ cjk_index search index.json 鳥瞰図 --filter type=地図 --filter date=1890..1910
|
|
299
|
+
$ cjk_index runtime --variants ja,zh-safe -o cjk-index.js
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`build` reads CSV, a JSON array or JSON Lines, from a file or from standard
|
|
303
|
+
input (`-`). `--filter subject=value:";"` splits values on `;`. Add `--json`
|
|
304
|
+
to `search` for machine-readable results. CSV needs the `csv` gem, which is
|
|
305
|
+
no longer a default gem from Ruby 3.4.
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# Rakefile
|
|
309
|
+
require "cjk_index/rake_task"
|
|
310
|
+
|
|
311
|
+
CJKIndex::RakeTask.new(:search_index) do |t|
|
|
312
|
+
t.source = "data/items.csv" # or t.records = -> { Item.all.map(&:to_search) }
|
|
313
|
+
t.output = "public/search/items.json"
|
|
314
|
+
t.runtime = "public/search/cjk-index.js"
|
|
315
|
+
t.fields = %w[title creator]
|
|
316
|
+
t.boosts = { "title" => 3 }
|
|
317
|
+
t.filters = { "type" => "value", "date" => "year" }
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### A script with no dependencies
|
|
322
|
+
|
|
323
|
+
[examples/search_csv.rb](examples/search_csv.rb) searches a CSV catalog from
|
|
324
|
+
the command line, in about 40 lines of plain Ruby:
|
|
325
|
+
|
|
326
|
+
```console
|
|
327
|
+
$ ruby examples/search_csv.rb items.csv 实录 --ref objectid --variants ja,zh
|
|
328
|
+
12 results for 实录
|
|
329
|
+
jitsu_51ed985b 宣祖[実録] 第1帙 第1冊 巻2戊辰元年(1568)
|
|
330
|
+
...
|
|
331
|
+
$ ruby examples/search_csv.rb items.csv 帝国大学 --ref objectid
|
|
332
|
+
3 results for 帝国大学
|
|
333
|
+
agri_6d032fb5 東京[帝國大學]農學部建物位置圖
|
|
334
|
+
...
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Rails
|
|
338
|
+
|
|
339
|
+
[examples/rails](examples/rails) (Rails 8.1.4, tested with Puma and four
|
|
340
|
+
workers) keeps one index for all worker processes with `IndexFile`: an
|
|
341
|
+
initializer, a model concern that updates the index after each commit, a
|
|
342
|
+
search action with filters, and a Rake task that rebuilds the index.
|
|
343
|
+
Notes on all the adapters: [docs/notes/adapters.md](docs/notes/adapters.md).
|
|
344
|
+
|
|
145
345
|
## Variant tables
|
|
146
346
|
|
|
147
347
|
| name | folds | source |
|
|
148
348
|
|---|---|---|
|
|
149
|
-
| `ja` (default) |
|
|
150
|
-
| `zh` |
|
|
349
|
+
| `ja` (default) | 409 Japanese old forms and glyph variants → one form (圖→図, 證→証, 禱/祷, 剝/剥) | Jōyō Kanji Table (2010), Jinmeiyō kanji list and kZVariant, via Unihan 18.0 (Unicode License v3) |
|
|
350
|
+
| `zh-safe` | 2,767 Traditional → Simplified pairs that are one-to-one both ways | OpenCC (Apache-2.0) |
|
|
351
|
+
| `zh` | 3,202 Traditional → Simplified (as in 0.1) | OpenCC `TSCharacters.txt` (Apache-2.0); ambiguous entries dropped |
|
|
352
|
+
| `ko` | 5,660 old Hangul characters in the Hanyang Private Use Area → standard jamo | hypua table (Unlicense), from the KTUG Hanyang PUA Table Project |
|
|
151
353
|
|
|
152
354
|
With several tables, characters linked by any of them fold to one form,
|
|
153
|
-
preferring the table listed first (`[ja, zh]`: 圖, 図, 图 → 図).
|
|
154
|
-
|
|
155
|
-
|
|
355
|
+
preferring the table listed first (`[ja, zh-safe]`: 圖, 図, 图 → 図).
|
|
356
|
+
|
|
357
|
+
`zh` also merges characters that are distinct in Japanese and in
|
|
358
|
+
Traditional Chinese: 後 and 后, 發 and 髮 (both 发), 機 and 机. Combined with
|
|
359
|
+
`ja`, it merges 45 such groups of Jōyō and Jinmeiyō kanji. `zh-safe` leaves
|
|
360
|
+
those pairs out, so a Simplified query 发展 does not find 發展. Use `zh-safe`
|
|
361
|
+
for Japanese sites that should also match Simplified queries, and `zh` for
|
|
362
|
+
Chinese sites.
|
|
363
|
+
|
|
364
|
+
Hangul needs no table: precomposed syllables and conjoining jamo are folded
|
|
365
|
+
by Unicode normalization. `ko` is for old Hangul stored in the Private Use
|
|
366
|
+
Area by HWP and the Hanyang fonts; a query typed in standard jamo then finds
|
|
367
|
+
and highlights it.
|
|
368
|
+
|
|
369
|
+
Browser script size, gzipped: no table 6.3 KB, `ja` 9.0 KB, `ja, zh-safe`
|
|
370
|
+
24.1 KB, `ja, zh` 25.9 KB, `ja, ko` 36.1 KB.
|
|
371
|
+
|
|
372
|
+
The tables are UTF-8 TSV files in `data/` ("variant<TAB>folded", an optional
|
|
373
|
+
comment column, `#` comments); `script/variant_tables.rb` generates them from
|
|
374
|
+
their sources. Sources, rules and counts:
|
|
375
|
+
[docs/notes/variant-tables.md](docs/notes/variant-tables.md). An index
|
|
376
|
+
records a digest of what its tables fold, and Ruby and the browser script
|
|
377
|
+
refuse an index built with other table contents: rebuild the index after
|
|
378
|
+
changing tables or upgrading from 0.x.
|
|
156
379
|
|
|
157
380
|
## Status
|
|
158
381
|
|
|
159
|
-
0.
|
|
160
|
-
|
|
382
|
+
1.0. On RubyGems: `gem install cjk_index`
|
|
383
|
+
(https://rubygems.org/gems/cjk_index). Changes: [CHANGELOG.md](CHANGELOG.md).
|
|
384
|
+
Development is supported by the Ruby Association Grant 2026.
|
|
385
|
+
|
|
386
|
+
Not yet checked by a native reader: the `ko` table and the Korean examples
|
|
387
|
+
([docs/review/](docs/review/)). Reports are welcome.
|
|
161
388
|
|
|
162
389
|
## Development
|
|
163
390
|
|
|
@@ -166,6 +393,9 @@ bundle install
|
|
|
166
393
|
bundle exec rake test # the runtime tests need node
|
|
167
394
|
```
|
|
168
395
|
|
|
396
|
+
Bug reports and pull requests: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
397
|
+
Security problems: [SECURITY.md](SECURITY.md).
|
|
398
|
+
|
|
169
399
|
## License
|
|
170
400
|
|
|
171
401
|
MIT
|