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 +4 -4
- data/CHANGELOG.md +72 -0
- data/LICENSE-Unicode +39 -0
- data/NOTICE +17 -0
- data/README.md +193 -41
- 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 +250 -90
- data/lib/cjk_index/cli.rb +240 -0
- data/lib/cjk_index/index_file.rb +4 -0
- data/lib/cjk_index/jekyll.rb +23 -7
- data/lib/cjk_index/normalizer.rb +103 -18
- data/lib/cjk_index/postings.rb +218 -0
- data/lib/cjk_index/rake_task.rb +71 -0
- data/lib/cjk_index/runtime.js +243 -29
- data/lib/cjk_index/runtime.rb +1 -0
- data/lib/cjk_index/searcher.rb +102 -38
- data/lib/cjk_index/tokenizer.rb +30 -0
- data/lib/cjk_index/version.rb +1 -1
- data/lib/cjk_index.rb +1 -0
- metadata +16 -4
data/lib/cjk_index/searcher.rb
CHANGED
|
@@ -28,13 +28,25 @@ module CJKIndex
|
|
|
28
28
|
# documents, also after later add, remove and update.
|
|
29
29
|
# prefix: the last Latin word of a query also matches longer words.
|
|
30
30
|
# typos: Latin words of 5+ letters also match words one edit away.
|
|
31
|
+
#
|
|
32
|
+
# path is an index file, or a split index (its directory or its
|
|
33
|
+
# manifest); the shards of a split index are read as queries need them.
|
|
31
34
|
def self.load(path, **options)
|
|
32
|
-
|
|
35
|
+
path = File.join(path, Builder::MANIFEST) if File.directory?(path)
|
|
36
|
+
data = JSON.parse(File.read(path, encoding: "UTF-8"))
|
|
37
|
+
if data.is_a?(Hash) && data["shards"]
|
|
38
|
+
dir = File.dirname(path)
|
|
39
|
+
options = { load_shard: ->(file) { JSON.parse(File.read(File.join(dir, file), encoding: "UTF-8")) } }.merge(options)
|
|
40
|
+
end
|
|
41
|
+
new(data, **options)
|
|
33
42
|
end
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
# load_shard: for the manifest of a split index, a callable that returns
|
|
45
|
+
# a shard (a Hash of token => posting list) given its file name.
|
|
46
|
+
def initialize(index, prefix: true, typos: true, normalizer: nil, load_shard: nil)
|
|
36
47
|
@prefix = prefix
|
|
37
48
|
@typos = typos
|
|
49
|
+
@load_shard = load_shard
|
|
38
50
|
if index.is_a?(Builder)
|
|
39
51
|
@builder = index
|
|
40
52
|
data = index.live_data
|
|
@@ -45,10 +57,11 @@ module CJKIndex
|
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
|
|
48
|
-
@normalizer = normalizer || Normalizer.new(tables: data["variants"])
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
@normalizer = normalizer || @builder&.normalizer || Normalizer.new(tables: data["variants"])
|
|
61
|
+
@normalizer.check!(data)
|
|
62
|
+
|
|
63
|
+
if data["shards"] && !@load_shard
|
|
64
|
+
raise ArgumentError, "cjk_index: a split index needs load_shard (or Searcher.load(directory))"
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
setup(data)
|
|
@@ -149,7 +162,7 @@ module CJKIndex
|
|
|
149
162
|
docs.each do |doc|
|
|
150
163
|
value = filter[:docs][doc]
|
|
151
164
|
if filter[:kind] == "year" then counts[value] += 1 unless value.nil?
|
|
152
|
-
else value.each { |
|
|
165
|
+
else value.each { |id| counts[filter[:keys][id]] += 1 }
|
|
153
166
|
end
|
|
154
167
|
end
|
|
155
168
|
counts.sort_by { |key, count| [-count, filter[:rank][key]] }
|
|
@@ -169,39 +182,54 @@ module CJKIndex
|
|
|
169
182
|
@refs = data["refs"]
|
|
170
183
|
@lengths = data["lengths"]
|
|
171
184
|
@tokens = data["tokens"]
|
|
185
|
+
@encoded = data["format"] == Builder::FORMAT
|
|
186
|
+
@decoded = {}
|
|
187
|
+
if data["shards"]
|
|
188
|
+
@shard_first = data["shards"].map(&:first)
|
|
189
|
+
@shard_file = data["shards"].map(&:last)
|
|
190
|
+
@shards = {}
|
|
191
|
+
end
|
|
172
192
|
@count = data["count"] || @refs.length
|
|
173
193
|
nf = @fields.length
|
|
174
194
|
avg = Array.new(nf, 0.0)
|
|
175
195
|
@lengths.each_with_index { |len, i| avg[i % nf] += len if len }
|
|
176
196
|
@avg_length = avg.map { |s| (v = s / [1, @count].max).zero? ? 1 : v }
|
|
177
|
-
|
|
197
|
+
# A split index lists its Latin tokens in a file, read only for typos.
|
|
198
|
+
@latin_file = data["latin"] unless @tokens
|
|
199
|
+
@latin_tokens = @latin_file ? nil : @tokens.keys.select { |t| latin?(t) }.sort
|
|
200
|
+
@latin_set = @latin_tokens&.to_set
|
|
178
201
|
@slot_of = {}
|
|
179
202
|
@refs.each_with_index { |ref, doc| @slot_of[ref] ||= doc if ref }
|
|
180
203
|
@filters = (data["filters"] || {}).to_h { |name, f| [name, setup_filter(f)] }
|
|
181
204
|
end
|
|
182
205
|
|
|
183
|
-
# Filter values by document:
|
|
206
|
+
# Filter values by document: a year, or the ids of values (value
|
|
207
|
+
# filters), with the folded key of each id, the ids of each key, the
|
|
184
208
|
# form to show for each key and its rank of first appearance.
|
|
185
209
|
def setup_filter(f)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
else
|
|
191
|
-
f["docs"].map { |pairs| pairs&.map(&:first) }
|
|
192
|
-
end
|
|
193
|
-
display = {}
|
|
194
|
-
if f["values"] then f["values"].each { |v| display[Builder.filter_key(v, @normalizer)] ||= v }
|
|
195
|
-
elsif f["kind"] == "value" then f["docs"].each { |pairs| pairs&.each { |key, v| display[key] ||= v } }
|
|
210
|
+
if f["kind"] == "year"
|
|
211
|
+
rank = {}
|
|
212
|
+
f["docs"].each_with_index { |year, doc| rank[year] ||= rank.size if @refs[doc] && !year.nil? }
|
|
213
|
+
return { kind: "year", docs: f["docs"], display: rank.keys.to_h { |year| [year, year] }, rank: rank }
|
|
196
214
|
end
|
|
197
|
-
rank = {}
|
|
198
|
-
docs.each_with_index do |value, doc|
|
|
199
|
-
next unless @refs[doc]
|
|
200
215
|
|
|
201
|
-
|
|
216
|
+
values = f["values"]
|
|
217
|
+
docs = f["docs"]
|
|
218
|
+
unless values
|
|
219
|
+
id_of = {}
|
|
220
|
+
values = []
|
|
221
|
+
docs = docs.map { |pairs| (pairs || []).map { |key, v| id_of[key] ||= (values << v).size - 1 } }
|
|
202
222
|
end
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
keys = values.map { |v| Builder.filter_key(v, @normalizer) }
|
|
224
|
+
ids = Hash.new { |h, key| h[key] = [] }
|
|
225
|
+
keys.each_with_index { |key, id| ids[key] << id }
|
|
226
|
+
display = {}
|
|
227
|
+
values.each_with_index { |v, id| display[keys[id]] ||= v }
|
|
228
|
+
rank = {}
|
|
229
|
+
docs.each_with_index do |list, doc|
|
|
230
|
+
list.each { |id| rank[keys[id]] ||= rank.size } if @refs[doc]
|
|
231
|
+
end
|
|
232
|
+
{ kind: f["kind"], docs: docs, keys: keys, ids: ids, display: display, rank: rank }
|
|
205
233
|
end
|
|
206
234
|
|
|
207
235
|
def refresh
|
|
@@ -222,8 +250,9 @@ module CJKIndex
|
|
|
222
250
|
end
|
|
223
251
|
[f, ->(year) { !year.nil? && range.cover?(year) }]
|
|
224
252
|
else
|
|
225
|
-
|
|
226
|
-
|
|
253
|
+
# Compares value ids, not folded strings.
|
|
254
|
+
ids = Array(condition).flat_map { |v| f[:ids].fetch(Builder.filter_key(v, @normalizer), []) }.to_set
|
|
255
|
+
[f, ->(list) { list.any? { |id| ids.include?(id) } }]
|
|
227
256
|
end
|
|
228
257
|
end
|
|
229
258
|
end
|
|
@@ -237,7 +266,7 @@ module CJKIndex
|
|
|
237
266
|
nf = @fields.length
|
|
238
267
|
map = {}
|
|
239
268
|
tokens.each do |token|
|
|
240
|
-
postings =
|
|
269
|
+
postings = postings(token) or next
|
|
241
270
|
i = 0
|
|
242
271
|
while i < postings.length
|
|
243
272
|
count = postings[i + 2]
|
|
@@ -248,21 +277,56 @@ module CJKIndex
|
|
|
248
277
|
map
|
|
249
278
|
end
|
|
250
279
|
|
|
280
|
+
# Number of the shard that holds token (split index), or -1.
|
|
281
|
+
def shard_index(token) = (@shard_first.bsearch_index { |first| first > token } || @shard_first.length) - 1
|
|
282
|
+
|
|
283
|
+
def shard(i) = @shards[@shard_file[i]] ||= @load_shard.call(@shard_file[i])
|
|
284
|
+
|
|
285
|
+
# The Hash that holds token's posting list if it is in the index, or nil.
|
|
286
|
+
def table(token)
|
|
287
|
+
return @tokens unless @shard_first
|
|
288
|
+
|
|
289
|
+
i = shard_index(token)
|
|
290
|
+
i.negative? ? nil : shard(i)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def token?(token) = !!table(token)&.key?(token)
|
|
294
|
+
|
|
295
|
+
# The posting list of a token as [doc, field, n, pos, ...], or nil.
|
|
296
|
+
def postings(token)
|
|
297
|
+
list = table(token)&.[](token)
|
|
298
|
+
return list unless list && @encoded
|
|
299
|
+
|
|
300
|
+
@decoded[token] ||= Postings.decode(list, @fields.length)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Index words that start with token, token itself left out.
|
|
304
|
+
def with_prefix(token)
|
|
305
|
+
unless @shard_first
|
|
306
|
+
from = @latin_tokens.bsearch_index { |k| k >= token } || @latin_tokens.length
|
|
307
|
+
return @latin_tokens[from..].take_while { |k| k.start_with?(token) }.reject { |k| k == token }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# In token's shard and the next ones that start with it (tokens are sorted).
|
|
311
|
+
first = [shard_index(token), 0].max
|
|
312
|
+
last = first
|
|
313
|
+
last += 1 while last + 1 < @shard_first.length && @shard_first[last + 1].start_with?(token)
|
|
314
|
+
(first..last).flat_map { |i| shard(i).keys.select { |k| k != token && k.start_with?(token) } }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def latin_tokens
|
|
318
|
+
@latin_tokens ||= @load_shard.call(@latin_file).sort
|
|
319
|
+
end
|
|
320
|
+
|
|
251
321
|
# Alternatives for one query token: itself, and for Latin words the index
|
|
252
322
|
# words it is a prefix of (last word only) or one edit away from.
|
|
253
323
|
def alternatives(token, last)
|
|
254
324
|
return [[token], false] unless latin?(token)
|
|
255
325
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
break unless k.start_with?(token)
|
|
261
|
-
|
|
262
|
-
out << k unless k == token
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
out.concat(@latin_tokens.select { |k| one_edit?(token, k) }) if @typos && !@tokens.key?(token) && token.length >= 5
|
|
326
|
+
has = @shard_first ? token?(token) : @latin_set.include?(token)
|
|
327
|
+
out = has ? [token] : []
|
|
328
|
+
out.concat(with_prefix(token)) if @prefix && last
|
|
329
|
+
out.concat(latin_tokens.select { |k| one_edit?(token, k) }) if @typos && !has && token.length >= 5
|
|
266
330
|
[out, !out.empty? && out.first != token]
|
|
267
331
|
end
|
|
268
332
|
|
data/lib/cjk_index/tokenizer.rb
CHANGED
|
@@ -68,6 +68,36 @@ module CJKIndex
|
|
|
68
68
|
phrases(text, unigrams: unigrams, normalizer: normalizer).flatten(1)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# A run of separators, of CJK characters, or of other characters. A
|
|
72
|
+
# separator is never part of a run (・ is in the katakana block).
|
|
73
|
+
CJK_WORD = /[#{CJK.source[1..-2]}&&[^#{SEPARATOR.source[1..-2]}]]/
|
|
74
|
+
RUN = /(#{SEPARATOR.source}+)|(#{CJK_WORD.source}+)|((?:(?!#{SEPARATOR.source})(?!#{CJK.source}).)+)/m
|
|
75
|
+
|
|
76
|
+
# Yields each token of already normalized text with its offset: the
|
|
77
|
+
# tokens of tokens_with_offsets, without building the phrases. Used to
|
|
78
|
+
# build the index, where it is several times faster.
|
|
79
|
+
def each_token(normalized, unigrams: false)
|
|
80
|
+
offset = 0
|
|
81
|
+
normalized.scan(RUN) do |_, cjk, other|
|
|
82
|
+
run = Regexp.last_match(0)
|
|
83
|
+
if cjk
|
|
84
|
+
chars = run.chars
|
|
85
|
+
if chars.length == 1 || unigrams
|
|
86
|
+
chars.each_with_index { |c, i| yield c, offset + i }
|
|
87
|
+
end
|
|
88
|
+
i = 0
|
|
89
|
+
last = chars.length - 1
|
|
90
|
+
while i < last
|
|
91
|
+
yield chars[i] + chars[i + 1], offset + i
|
|
92
|
+
i += 1
|
|
93
|
+
end
|
|
94
|
+
elsif other
|
|
95
|
+
yield run, offset
|
|
96
|
+
end
|
|
97
|
+
offset += run.length
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
71
101
|
# Splits at separators (spaces, punctuation) into phrases, each a list of
|
|
72
102
|
# [token, offset]. A query matches when every phrase occurs somewhere in
|
|
73
103
|
# the document, with the tokens of each phrase at the same relative offsets.
|
data/lib/cjk_index/version.rb
CHANGED
data/lib/cjk_index.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "cjk_index/version"
|
|
4
4
|
require_relative "cjk_index/normalizer"
|
|
5
5
|
require_relative "cjk_index/tokenizer"
|
|
6
|
+
require_relative "cjk_index/postings"
|
|
6
7
|
require_relative "cjk_index/builder"
|
|
7
8
|
require_relative "cjk_index/runtime"
|
|
8
9
|
require_relative "cjk_index/searcher"
|
metadata
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cjk_index
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Satoru Nakamura
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: Builds a bigram search index in Ruby (with kana, old/new kanji and NFKC
|
|
13
13
|
normalization), and queries it in Ruby or with a small browser runtime that returns
|
|
14
|
-
the same results. Includes a
|
|
15
|
-
|
|
14
|
+
the same results. Includes a command-line tool, a Rake task, a Jekyll plugin (with
|
|
15
|
+
a CollectionBuilder preset) and a Bridgetown plugin.
|
|
16
|
+
executables:
|
|
17
|
+
- cjk_index
|
|
16
18
|
extensions: []
|
|
17
19
|
extra_rdoc_files: []
|
|
18
20
|
files:
|
|
19
21
|
- CHANGELOG.md
|
|
20
22
|
- LICENSE
|
|
21
23
|
- LICENSE-OpenCC
|
|
24
|
+
- LICENSE-Unicode
|
|
22
25
|
- NOTICE
|
|
23
26
|
- README.md
|
|
24
27
|
- data/ja-kyujitai.tsv
|
|
28
|
+
- data/ko-hanyang-pua.tsv
|
|
29
|
+
- data/zh-hant-hans-safe.tsv
|
|
25
30
|
- data/zh-hant-hans.tsv
|
|
31
|
+
- exe/cjk_index
|
|
26
32
|
- lib/cjk_index.rb
|
|
33
|
+
- lib/cjk_index/bridgetown.rb
|
|
27
34
|
- lib/cjk_index/builder.rb
|
|
35
|
+
- lib/cjk_index/cli.rb
|
|
28
36
|
- lib/cjk_index/index_file.rb
|
|
29
37
|
- lib/cjk_index/jekyll.rb
|
|
30
38
|
- lib/cjk_index/normalizer.rb
|
|
39
|
+
- lib/cjk_index/postings.rb
|
|
40
|
+
- lib/cjk_index/rake_task.rb
|
|
31
41
|
- lib/cjk_index/runtime.js
|
|
32
42
|
- lib/cjk_index/runtime.rb
|
|
33
43
|
- lib/cjk_index/searcher.rb
|
|
@@ -37,6 +47,8 @@ homepage: https://github.com/nakamura196/cjk_index
|
|
|
37
47
|
licenses:
|
|
38
48
|
- MIT
|
|
39
49
|
- Apache-2.0
|
|
50
|
+
- Unicode-3.0
|
|
51
|
+
- Unlicense
|
|
40
52
|
metadata:
|
|
41
53
|
source_code_uri: https://github.com/nakamura196/cjk_index
|
|
42
54
|
documentation_uri: https://github.com/nakamura196/cjk_index#readme
|