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.
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CJKIndex
4
+ # The compact encoding of a posting list in format 4, shared by Builder
5
+ # (writing) and Searcher (reading); runtime.js decodes it the same way.
6
+ #
7
+ # A list is a sequence of entries, ordered by document and field. Each
8
+ # entry is written as numbers:
9
+ #
10
+ # key gap, n, first position, gap to the next position, ...
11
+ #
12
+ # where key = document * number of fields + field, and the key gap is the
13
+ # difference to the key of the previous entry minus 1 (the first entry
14
+ # counts from -1). n is the number of positions. Each number is written in
15
+ # printable ASCII: the last digit in base 60 from the first 60 characters
16
+ # of ALPHABET, the digits before it in base 30 from the other 30, most
17
+ # significant first. 0..59 take one character, up to 1,799 two, up to
18
+ # 53,999 three. ALPHABET has neither " nor \, so a list is a JSON string
19
+ # without escapes.
20
+ module Postings
21
+ ALPHABET = ((0x23..0x7d).map(&:chr) - ["\\"]).join.freeze
22
+ LAST = 60
23
+ LEAD = 30
24
+ # The characters that end a number are the bytes below LEAD_BYTE
25
+ # (FINAL_BYTES in the notation of String#count).
26
+ LEAD_BYTE = ALPHABET.getbyte(LAST)
27
+ FINAL_BYTES = "#-[\\]-_"
28
+ # byte => digit; 0...60 ends a number, 60...90 leads.
29
+ DIGIT = Array.new(256, -1).tap { |t| ALPHABET.each_byte.with_index { |b, i| t[b] = i } }.freeze
30
+
31
+ def self.number_slow(value)
32
+ s = ALPHABET[value % LAST]
33
+ value /= LAST
34
+ while value.positive?
35
+ s = ALPHABET[LAST + (value % LEAD)] + s
36
+ value /= LEAD
37
+ end
38
+ s
39
+ end
40
+
41
+ # Every number of up to three characters, written once.
42
+ NUMBERS = Array.new(LAST * LEAD * LEAD) { |v| number_slow(v).freeze }.freeze
43
+
44
+ module_function
45
+
46
+ def number(value) = NUMBERS[value] || number_slow(value)
47
+
48
+ # Appends one entry to out. offsets are ascending positions.
49
+ def append(out, key_gap, offsets)
50
+ out << number(key_gap) << number(offsets.length)
51
+ last = 0
52
+ offsets.each do |pos|
53
+ out << number(pos - last)
54
+ last = pos
55
+ end
56
+ out
57
+ end
58
+
59
+ # All numbers of a list.
60
+ def numbers(str)
61
+ out = []
62
+ acc = 0
63
+ str.each_byte do |b|
64
+ d = DIGIT[b]
65
+ if d >= LAST then acc = (acc * LEAD) + d - LAST
66
+ else
67
+ out << ((acc * LAST) + d)
68
+ acc = 0
69
+ end
70
+ end
71
+ out
72
+ end
73
+
74
+ # Yields key, start, body and stop for each entry: its key, and the byte
75
+ # offsets where the entry starts, where its key gap ends and where it
76
+ # ends. Loops over bytes rather than numbers, so that the changes below
77
+ # can copy the rest of a list as it is.
78
+ def each_entry(str)
79
+ return enum_for(:each_entry, str) unless block_given?
80
+
81
+ key = -1
82
+ i = 0
83
+ len = str.bytesize
84
+ while i < len
85
+ start = i
86
+ acc = 0
87
+ while (d = DIGIT[str.getbyte(i)]) >= LAST
88
+ acc = (acc * LEAD) + d - LAST
89
+ i += 1
90
+ end
91
+ key += (acc * LAST) + d + 1
92
+ i += 1
93
+ body = i
94
+ acc = 0
95
+ while (d = DIGIT[str.getbyte(i)]) >= LAST
96
+ acc = (acc * LEAD) + d - LAST
97
+ i += 1
98
+ end
99
+ n = (acc * LAST) + d
100
+ i += 1
101
+ # Skip n numbers: each ends with a byte below LEAD_BYTE. Many at a
102
+ # time are counted by String#count, which runs in C.
103
+ while n > 8
104
+ t = str.byteslice(i, n).count(FINAL_BYTES)
105
+ i += n
106
+ n -= t
107
+ end
108
+ while n.positive?
109
+ n -= 1 if str.getbyte(i) < LEAD_BYTE
110
+ i += 1
111
+ end
112
+ yield key, start, body, i
113
+ end
114
+ end
115
+
116
+ # Key of the last entry, -1 for an empty list.
117
+ def last_key(str)
118
+ key = -1
119
+ each_entry(str) { |k| key = k }
120
+ key
121
+ end
122
+
123
+ # Each key of a list, in order.
124
+ def each_key(str)
125
+ return enum_for(:each_key, str) unless block_given?
126
+
127
+ each_entry(str) { |key| yield key }
128
+ end
129
+
130
+ # The list without the entries whose keys are in range (consecutive).
131
+ def delete(str, range)
132
+ before = -1
133
+ from = nil
134
+ each_entry(str) do |key, start, body|
135
+ if range.cover?(key)
136
+ from ||= start
137
+ elsif from
138
+ return str.byteslice(0, from) + number(key - before - 1) + str.byteslice(body..)
139
+ elsif key > range.end
140
+ return str
141
+ else
142
+ before = key
143
+ end
144
+ end
145
+ from ? str.byteslice(0, from) : str
146
+ end
147
+
148
+ # The list with an entry for key (not yet in it) and offsets inserted in order.
149
+ def insert(str, key, offsets)
150
+ before = -1
151
+ each_entry(str) do |k, start, body|
152
+ if k > key
153
+ entry = append(+"", key - before - 1, offsets)
154
+ return str.byteslice(0, start) + entry + number(k - key - 1) + str.byteslice(body..)
155
+ end
156
+ before = k
157
+ end
158
+ append(+str, key - before - 1, offsets)
159
+ end
160
+
161
+ # The list with each key's document replaced by numbering[document], which
162
+ # keeps the order (removed documents have no entries).
163
+ def renumber(str, fields, numbering)
164
+ out = +""
165
+ old_before = -1
166
+ new_before = -1
167
+ copied = 0 # str up to here is in out or to be copied as it is
168
+ each_entry(str) do |key, start, body|
169
+ new_key = (numbering[key / fields] * fields) + (key % fields)
170
+ if new_key - new_before != key - old_before
171
+ out << str.byteslice(copied, start - copied) << number(new_key - new_before - 1)
172
+ copied = body
173
+ end
174
+ old_before = key
175
+ new_before = new_key
176
+ end
177
+ copied.zero? ? str : out << str.byteslice(copied..)
178
+ end
179
+
180
+ # The flat list of format 3, [doc, field, n, pos, ..., doc, ...].
181
+ def decode(str, fields)
182
+ nums = numbers(str)
183
+ out = []
184
+ key = -1
185
+ i = 0
186
+ while i < nums.length
187
+ key += nums[i] + 1
188
+ n = nums[i + 1]
189
+ out << (key / fields) << (key % fields) << n
190
+ pos = 0
191
+ j = i + 2
192
+ stop = j + n
193
+ while j < stop
194
+ pos += nums[j]
195
+ out << pos
196
+ j += 1
197
+ end
198
+ i = stop
199
+ end
200
+ out
201
+ end
202
+
203
+ # A flat list of format 2 or 3 in this encoding.
204
+ def encode(flat, fields)
205
+ out = +""
206
+ last = -1
207
+ i = 0
208
+ while i < flat.length
209
+ key = (flat[i] * fields) + flat[i + 1]
210
+ n = flat[i + 2]
211
+ append(out, key - last - 1, flat[i + 3, n])
212
+ last = key
213
+ i += 3 + n
214
+ end
215
+ out
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/tasklib"
5
+ require "fileutils"
6
+ require_relative "cli"
7
+
8
+ module CJKIndex
9
+ # A Rake task that builds an index (and the browser script) from a CSV,
10
+ # JSON or JSON Lines file, usable from any Ruby build (Rails, Sinatra, a
11
+ # plain Rakefile). It does what `cjk_index build` does.
12
+ #
13
+ # require "cjk_index/rake_task"
14
+ #
15
+ # CJKIndex::RakeTask.new(:search_index) do |t|
16
+ # t.source = "data/items.csv"
17
+ # t.output = "public/search/items.json"
18
+ # t.runtime = "public/search/cjk-index.js" # optional
19
+ # t.ref = "id"
20
+ # t.fields = %w[title creator]
21
+ # t.boosts = { "title" => 3 }
22
+ # t.filters = { "type" => "value", "date" => "year" }
23
+ # end
24
+ #
25
+ # rake search_index
26
+ #
27
+ # records can be set instead of source: an Array of Hashes, or an object
28
+ # that responds to call and returns one (e.g. a lambda reading the database).
29
+ class RakeTask < Rake::TaskLib
30
+ attr_accessor :name, :source, :records, :output, :runtime, :ref, :fields, :boosts, :filters, :variants, :format,
31
+ :description
32
+
33
+ def initialize(name = :cjk_index)
34
+ super()
35
+ @name = name
36
+ @boosts = {}
37
+ @filters = {}
38
+ @variants = Normalizer::DEFAULT_TABLES
39
+ @description = "Build the CJK search index"
40
+ yield self if block_given?
41
+ define
42
+ end
43
+
44
+ # Builds the index now; the task calls this.
45
+ def build
46
+ raise CLI::Error, "#{name}: set output" unless output
47
+
48
+ data = records.respond_to?(:call) ? records.call : records
49
+ data ||= source ? CLI.read_records(source.to_s, format || CLI.format_of(source)) : raise(CLI::Error, "#{name}: set source or records")
50
+ builder = CLI.build_index(records: data, ref: ref, fields: fields, boosts: boosts, filters: filters, variants: variants)
51
+ write(output, builder.to_json)
52
+ write(runtime, Runtime.source(normalizer: builder.normalizer)) if runtime
53
+ builder
54
+ end
55
+
56
+ private
57
+
58
+ def define
59
+ desc description
60
+ task(name) do
61
+ builder = build
62
+ puts "#{output}: #{builder.size} records"
63
+ end
64
+ end
65
+
66
+ def write(path, content)
67
+ FileUtils.mkdir_p(File.dirname(path.to_s))
68
+ File.write(path.to_s, content)
69
+ end
70
+ end
71
+ end