cjk_index 0.2.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 843127d7f858d567b1e4eb0d37975111612feab1bdf63fe9d8bc6e4dbaf0e0b1
4
- data.tar.gz: 78c8ab89bbaf4477aa0e8b51f72de64503bb71e2401ed13c013eeed40bc3f41c
3
+ metadata.gz: b8780b3265cb3e02580edd66360721d6a9ea245c3b8465c49e499e0b1bb6d420
4
+ data.tar.gz: a9c1233d9e57d170ba90c25e7e0ecf9af6b10e89640584855dbe6fae5b358418
5
5
  SHA512:
6
- metadata.gz: f8aef83a37b2b0644cab2d22a0bea337a467ea9c89a142c4a4fe2c702e49cfb4afcf1a9ef4146aa9a656b54ec95ebb9055d53a0ec3439808ea970533ba1588c9
7
- data.tar.gz: d88440121eb7f3ebc1239ae28d12b80b7ac706a70065b66c160b0ceaf33f6e434d093bb53e8a1aca6e8f26cbed1ce79556e48eba8837132c05c04c42352e51fa
6
+ metadata.gz: 205fff662f662ae191a3bb437351531473bcbb8943758d13b7b6593aeabb425814fb8a4cc779f5a028b3d0ff2c6c895ab90efb49ed6d9a6423d31f63f62bb8ed
7
+ data.tar.gz: 4a82cf20daf35f34bb5596645e373e4c1902722f5fd91c6541a77c8039f6d97a97d8421c68d20761c29914140d4e8201aa3d5409b883458ff2417deb633dde6b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 (2026-10-28)
4
+
5
+ - `variants` also takes table files of your own: one group per line,
6
+ tab-separated, the form to fold to first (the format of the itaiji lists
7
+ that collection databases keep). Paths are relative to the site's folder
8
+ (Jekyll, Bridgetown) or the current folder; the index records the path as
9
+ written and a digest of the contents. `Builder.load` and `Builder.read`
10
+ take `normalizer:` to read such an index from another folder.
11
+ - `Searcher.new(builder)` folds with the builder's normalizer.
12
+
13
+ ## 1.0.0 (2026-10-28)
14
+
15
+ Indexes built with 0.x should be rebuilt: the `ja` table changed, and the
16
+ index now records a digest of its tables (see below). Formats 2 and 3 are
17
+ still read.
18
+
19
+ Large collections:
20
+
21
+ - Index format `cjk_index/4`: posting lists as compact strings (about a third
22
+ smaller gzipped, three times smaller before compression).
23
+ - Split indexes: `Builder#write_split(dir, shard_size:)` and
24
+ `Builder#each_split_file` write a manifest and shards of about 32 KB; the
25
+ browser fetches shards as queries need them (`index.searchAsync`,
26
+ `index.prepare`). `CJKIndex.load` and `Searcher.load` read a split index;
27
+ Jekyll: `split: true`.
28
+ - `Builder#write(path)` writes one file without building the JSON in memory;
29
+ `Builder.read(path)` reads a file or a split index.
30
+ - Building keeps posting lists encoded and normalizes faster: 3,000 full
31
+ texts take 0.56 GB instead of 2.3 GB at peak, and 37 s instead of 62 s.
32
+ - Updating a loaded index is slower without YJIT (one update about 0.25 s at
33
+ 17,000 records); one update and its write in `IndexFile` cost about the
34
+ same as before.
35
+
36
+ Variant tables:
37
+
38
+ - `ja`: 409 pairs (was 291), generated from the Jōyō Kanji Table, the
39
+ Jinmeiyō kanji list and Unihan kZVariant. 籐/藤, 聯/連 and 彎/湾 no longer
40
+ fold.
41
+ - New opt-in tables: `zh-safe` (Traditional/Simplified pairs that are
42
+ one-to-one both ways, so 後/后 and 發/髮 stay apart) and `ko` (old Hangul
43
+ in the Hanyang Private Use Area to standard jamo). `zh` is unchanged.
44
+ - `script/variant_tables.rb` generates the tables from their sources.
45
+ - The index records `variant_digest`, a digest of what its tables fold; Ruby
46
+ and the browser script refuse an index whose digest differs.
47
+ - The browser highlights matches in conjoining jamo, half-width kana with
48
+ sound marks and old Hangul in the Private Use Area.
49
+
50
+ More ways to build an index:
51
+
52
+ - `cjk_index` command: `build` (CSV, JSON or JSON Lines), `search`, `runtime`.
53
+ - `CJKIndex::RakeTask`.
54
+ - Bridgetown 2 plugin (`require "cjk_index/bridgetown"`).
55
+ - Examples for Bridgetown and Rails (`examples/`), notes in
56
+ `docs/notes/adapters.md`.
57
+ - Migration guide from lunr.js, in English and Japanese
58
+ (`docs/migrating-from-lunr.md`).
59
+
60
+ Filters:
61
+
62
+ - A `value` filter compares value ids instead of folded strings, in Ruby
63
+ and in the browser (#14): filtering 17,000 records by one author takes
64
+ 1.7 ms instead of 2.7 ms.
65
+
66
+ Fixes:
67
+
68
+ - A `year` filter on a Time or Date (Jekyll gives a post's `date` as a Time)
69
+ gave no year.
70
+ - The Jekyll plugin gave the builder only the index fields of a collection
71
+ document, so a filter on another front-matter field was empty.
72
+ - A query word named like a JavaScript object member ("constructor") scored
73
+ differently in the browser than in Ruby.
74
+
3
75
  ## 0.2.0 (2026-10-28)
4
76
 
5
77
  - Filters: `Builder.new(filters:)` with `value` and `year` filters;
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,10 +35,13 @@ 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 6.5 KB gzipped with the Japanese table, 24 KB
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
46
  How this compares with Pagefind, kensaku, lunr.js, rroonga, tantiny and others:
41
47
  [docs/comparison.md](docs/comparison.md).
@@ -46,17 +52,18 @@ Safari: [bench/browser/](bench/browser/).
46
52
 
47
53
  When to use it, and when not to:
48
54
 
49
- - **Good fit:** catalogs, collection records or blog posts, from a few
50
- thousand records up to the 16,951 we measured (19.3 MB gzipped at that size,
51
- a heavy download on slow connections; above about 10,000 records, consider
52
- splitting the index); static sites, command-line tools, scripts and small apps; anywhere
53
- you want nothing to install outside Ruby, or the same results in Ruby and
54
- the browser.
55
- - **Not a good fit (yet):** large amounts of full text, frequent updates, or
56
- search served continuously by a server. The whole index is held in memory
57
- and shipped as one file: about 5 bytes per character gzipped, and 3 GB of
58
- memory to build 35.6 million characters. A native engine (rroonga, tantiny)
59
- or a database feature (pg_bigm) is the better choice there.
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.
60
67
 
61
68
  ## Ruby
62
69
 
@@ -147,9 +154,9 @@ gets the old index or the new one, never a partly written file. A reader
147
154
  keeps the index it loaded until the file changes; `interval:` limits how
148
155
  often it checks. Put several changes in one block to write once.
149
156
 
150
- On the Aozora Bunko catalog (16,951 records, 61 MB file), a check of an
151
- unchanged file takes 0.09 ms, loading a changed file 0.6–0.7 s, and an update
152
- 0.6 s (2.1 s when another process wrote last, as the file is read again).
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).
153
160
  [bench/index_file.rb](bench/index_file.rb). The lock is `flock`: use a local
154
161
  disk, not NFS.
155
162
 
@@ -163,22 +170,37 @@ for 1,000 records, 0.24 s for 5,000, 0.7 s for 16,951. A record edited now
163
170
  and then needs nothing special; put a few changes in one block; for an
164
171
  import, build the index again and `write` it (11 s for 16,951 records).
165
172
 
166
- ### Without Jekyll
173
+ ## Large collections: split indexes
167
174
 
168
- [examples/search_csv.rb](examples/search_csv.rb) searches a CSV catalog from
169
- the command line, in about 40 lines of plain Ruby:
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.
170
178
 
171
- ```console
172
- $ ruby examples/search_csv.rb items.csv 实录 --ref objectid --variants ja,zh
173
- 12 results for 实录
174
- jitsu_51ed985b 宣祖[実録] 第1帙 第1冊 巻2戊辰元年(1568)
175
- ...
176
- $ ruby examples/search_csv.rb items.csv 帝国大学 --ref objectid
177
- 3 results for 帝国大学
178
- agri_6d032fb5 東京[帝國大學]農學部建物位置圖
179
- ...
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
180
185
  ```
181
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
+
182
204
  ## Browser
183
205
 
184
206
  ```html
@@ -192,6 +214,7 @@ agri_6d032fb5 東京[帝國大學]農學部建物位置圖
192
214
  var results = index.search("鳥瞰図", { filter: { type: "地図", date: { min: 1890, max: 1910 } } });
193
215
  index.facets("type", results); // => [{ value: "地図", count: 12 }, ...]
194
216
  });
217
+ // a split index: await index.searchAsync(query, options), see above
195
218
  </script>
196
219
  ```
197
220
 
@@ -222,9 +245,14 @@ cjk_index:
222
245
  type: value
223
246
  subject: { kind: value, separator: ";" }
224
247
  date: year
248
+ - name: texts
249
+ collection: texts
250
+ fields: [title, content]
251
+ split: true # -> assets/cjk-index/texts/index.json and shards
225
252
  ```
226
253
 
227
- 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).
228
256
 
229
257
  ### CollectionBuilder
230
258
 
@@ -243,25 +271,149 @@ The preset follows CollectionBuilder's own search settings: fields with
243
271
  column; set `filters:` to change them. A working example:
244
272
  https://github.com/nakamura196/cb-ja-demo
245
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
+
246
345
  ## Variant tables
247
346
 
248
347
  | name | folds | source |
249
348
  |---|---|---|
250
- | `ja` (default) | 291 Japanese old forms → modern forms | written for cjk_index |
251
- | `zh` | 3,202 Traditional → Simplified Chinese | OpenCC `TSCharacters.txt` (Apache-2.0); ambiguous entries dropped |
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 |
252
353
 
253
354
  With several tables, characters linked by any of them fold to one form,
254
- preferring the table listed first (`[ja, zh]`: 圖, 図, 图 → 図). The `zh`
255
- table also merges characters that are distinct in Japanese (後 and 后), so
256
- it is opt-in.
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
+ ### A table of your own
370
+
371
+ Any entry of `variants` that is not one of the names above is a table file
372
+ of your own, relative to the site's folder (Jekyll, Bridgetown) or the
373
+ current folder (CLI, Rake, Ruby). One group per line, tab-separated: the
374
+ form to fold to, then its variants, one character each. This is the format
375
+ of the itaiji lists that collection databases often keep already.
376
+
377
+ ```text
378
+ # itaiji.tsv
379
+ 与 與
380
+ 宴 醼 讌
381
+ 嶋 嶌
382
+ ```
383
+
384
+ ```yaml
385
+ cjk_index:
386
+ variants: [ja, _data/itaiji.tsv]
387
+ ```
388
+
389
+ Each cell is folded first (NFKC, lowercase, katakana → hiragana), and the
390
+ table listed first still picks the form a group folds to. The index records
391
+ the entry as written (`_data/itaiji.tsv`), not the full path, and its digest
392
+ covers the file's contents, so editing the file means rebuilding the index.
393
+ To read such an index from another folder in Ruby, pass the normalizer:
394
+ `Builder.load(json, normalizer: CJKIndex::Normalizer.new(tables: [...], base: dir))`.
395
+ Wide lists merge more than spelling: a list that puts 燕 with 宴 makes
396
+ a search for 燕 (swallow) find 宴 (banquet). Check a list against your own material first.
397
+
398
+ Browser script size, gzipped: no table 6.3 KB, `ja` 9.0 KB, `ja, zh-safe`
399
+ 24.1 KB, `ja, zh` 25.9 KB, `ja, ko` 36.1 KB.
400
+
401
+ The tables are UTF-8 TSV files in `data/` ("variant<TAB>folded", an optional
402
+ comment column, `#` comments); `script/variant_tables.rb` generates them from
403
+ their sources. Sources, rules and counts:
404
+ [docs/notes/variant-tables.md](docs/notes/variant-tables.md). An index
405
+ records a digest of what its tables fold, and Ruby and the browser script
406
+ refuse an index built with other table contents: rebuild the index after
407
+ changing tables or upgrading from 0.x.
257
408
 
258
409
  ## Status
259
410
 
260
- 0.2. On RubyGems: `gem install cjk_index`
261
- (https://rubygems.org/gems/cjk_index). 0.2 adds filters, index updates and
262
- an index file shared by several processes. Planned for 1.0: index sharding
263
- for large sites (see the [benchmarks](bench/README.md)), a Bridgetown
264
- adapter, a CLI, and variant tables from official sources.
411
+ 1.0. On RubyGems: `gem install cjk_index`
412
+ (https://rubygems.org/gems/cjk_index). Changes: [CHANGELOG.md](CHANGELOG.md).
413
+ Development is supported by the Ruby Association Grant 2026.
414
+
415
+ Not yet checked by a native reader: the `ko` table and the Korean examples
416
+ ([docs/review/](docs/review/)). Reports are welcome.
265
417
 
266
418
  ## Development
267
419