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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d4df8b57af0d9d2e2755bf87cc9fdb60313d87f68ab17160f5ff4f9f28460d9
4
- data.tar.gz: 3bc33035945c48d226c328c58eab9c012f7a93874edde8a3adc2c5a92d9972f3
3
+ metadata.gz: 9c9ffe634562d740738f6d9b9cf97ade5f5ab6d32a80b6bfd349496435d9e88b
4
+ data.tar.gz: 6cc0aebdedd14b07cdcf9dda3dcc9b849b80d135149444f057daed4cc620c004
5
5
  SHA512:
6
- metadata.gz: 3016fee0464ea949e62360c4d4bfbaf3a4f1a7271c711efe1096b4958ce754b5bf4059566395eb11980fc92a8c55d953989e6562b0fa0429e1e25036a7d277e7
7
- data.tar.gz: f7dade68d837d456467e6d1c8fe6e8fbba3335432c7735414df161627942137d626a7a8f270423085957b162e0fa75c3aa709dd7279635e1c4a6c0bb47a9ff2e
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 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
- 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
- ### Without Jekyll
101
+ ### Filters
75
102
 
76
- [examples/search_csv.rb](examples/search_csv.rb) searches a CSV catalogue from
77
- the command line, in about 40 lines of plain Ruby:
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
- ```console
80
- $ ruby examples/search_csv.rb items.csv 实录 --ref objectid --variants ja,zh
81
- 12 results for 实录
82
- jitsu_51ed985b 宣祖[実録] 第1帙 第1冊 巻2戊辰元年(1568)
83
- ...
84
- $ ruby examples/search_csv.rb items.csv 帝国大学 --ref objectid
85
- 3 results for 帝国大学
86
- agri_6d032fb5 東京[帝國大學]農學部建物位置圖
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`. A working example:
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) | 291 Japanese old forms → modern forms | written for cjk_index |
150
- | `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 |
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]`: 圖, 図, 图 → 図). The `zh`
154
- table also merges characters that are distinct in Japanese (後 and 后), so
155
- 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
+ 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.1, early. Not yet on RubyGems. Planned: index sharding for large sites, a
160
- Bridgetown adapter, a CLI, and size and speed benchmarks.
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