gigatoken 0.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 +7 -0
- data/Cargo.lock +3016 -0
- data/Cargo.toml +135 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/exe/gigatoken +9 -0
- data/ext/gigatoken/Cargo.toml +24 -0
- data/ext/gigatoken/extconf.rb +19 -0
- data/ext/gigatoken/src/error.rs +20 -0
- data/ext/gigatoken/src/gvl.rs +122 -0
- data/ext/gigatoken/src/lib.rs +50 -0
- data/ext/gigatoken/src/sentencepiece.rs +205 -0
- data/ext/gigatoken/src/sources.rs +207 -0
- data/ext/gigatoken/src/tokenizer.rs +571 -0
- data/lib/gigatoken/cli/bench.rb +64 -0
- data/lib/gigatoken/cli/support.rb +132 -0
- data/lib/gigatoken/cli/validate.rb +55 -0
- data/lib/gigatoken/cli.rb +18 -0
- data/lib/gigatoken/hub.rb +242 -0
- data/lib/gigatoken/packed_result.rb +48 -0
- data/lib/gigatoken/tokenizer.rb +117 -0
- data/lib/gigatoken/version.rb +5 -0
- data/lib/gigatoken.rb +29 -0
- data/rust-toolchain.toml +8 -0
- data/src/batch.rs +1808 -0
- data/src/bindings/bridge.rs +396 -0
- data/src/bindings/hub.rs +42 -0
- data/src/bindings/matcher.rs +114 -0
- data/src/bindings/mod.rs +14 -0
- data/src/bindings/padding.rs +177 -0
- data/src/bindings/pretokenize.rs +53 -0
- data/src/bindings/sources.rs +273 -0
- data/src/bindings/train.rs +125 -0
- data/src/bpe/mod.rs +1217 -0
- data/src/bpe/pretoken_cache.rs +495 -0
- data/src/bpe/sentencepiece.rs +1485 -0
- data/src/bpe/tiktoken.rs +2555 -0
- data/src/bpe_train.rs +351 -0
- data/src/input/decompress.rs +11 -0
- data/src/input/file_source.rs +514 -0
- data/src/input/jsonl.rs +94 -0
- data/src/input/mod.rs +333 -0
- data/src/input/parquet.rs +303 -0
- data/src/lib.rs +578 -0
- data/src/load_tokenizer/hf.rs +1036 -0
- data/src/load_tokenizer/hub.rs +344 -0
- data/src/load_tokenizer/mod.rs +3 -0
- data/src/load_tokenizer/tiktoken.rs +87 -0
- data/src/main.rs +95 -0
- data/src/pretokenize/fast/cl100k.rs +426 -0
- data/src/pretokenize/fast/cl100k_family.rs +891 -0
- data/src/pretokenize/fast/deepseek_v3.rs +605 -0
- data/src/pretokenize/fast/kimi.rs +281 -0
- data/src/pretokenize/fast/mask.rs +1486 -0
- data/src/pretokenize/fast/mod.rs +446 -0
- data/src/pretokenize/fast/nemotron.rs +138 -0
- data/src/pretokenize/fast/o200k.rs +347 -0
- data/src/pretokenize/fast/o200k_family.rs +1734 -0
- data/src/pretokenize/fast/olmo3.rs +505 -0
- data/src/pretokenize/fast/qwen2.rs +429 -0
- data/src/pretokenize/fast/qwen3_5.rs +541 -0
- data/src/pretokenize/fast/r50k.rs +1250 -0
- data/src/pretokenize/mod.rs +1079 -0
- data/src/pretokenize/options.rs +188 -0
- data/src/pretokenize/pretoken.rs +20 -0
- data/src/pretokenize/pretokenize_traits.rs +49 -0
- data/src/pretokenize/reference/avx512.rs +522 -0
- data/src/pretokenize/reference/combinator.rs +572 -0
- data/src/pretokenize/reference/mod.rs +28 -0
- data/src/pretokenize/reference/simd.rs +852 -0
- data/src/pretokenize/reference/state_machine.rs +365 -0
- data/src/pretokenize/unicode.rs +546 -0
- data/src/test_hub.rs +28 -0
- data/src/token.rs +42 -0
- metadata +161 -0
data/src/input/mod.rs
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! Layered input abstraction: Resource → Document → (Pretoken via pretokenize module).
|
|
2
|
+
//!
|
|
3
|
+
//! - **Resource**: a handle to a contiguous byte buffer (file, bytes, string).
|
|
4
|
+
//! - **DocumentIter**: splits a byte buffer on a configurable separator, yielding documents.
|
|
5
|
+
//! - Parallel chunking: `Resource::par_document_chunks` returns N document iterators
|
|
6
|
+
//! with chunk boundaries aligned to separator positions.
|
|
7
|
+
|
|
8
|
+
use memchr::memmem;
|
|
9
|
+
use memmap2::Mmap;
|
|
10
|
+
use std::path::Path;
|
|
11
|
+
|
|
12
|
+
pub(crate) mod decompress;
|
|
13
|
+
pub mod file_source;
|
|
14
|
+
pub mod jsonl;
|
|
15
|
+
pub mod parquet;
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Document — owned/borrowed byte buffer used by jsonl iterator
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Clone)]
|
|
22
|
+
pub struct Document<'a>(std::borrow::Cow<'a, [u8]>);
|
|
23
|
+
|
|
24
|
+
impl<'a> From<&'a [u8]> for Document<'a> {
|
|
25
|
+
fn from(value: &'a [u8]) -> Self {
|
|
26
|
+
Document(std::borrow::Cow::Borrowed(value))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl From<Vec<u8>> for Document<'_> {
|
|
31
|
+
fn from(value: Vec<u8>) -> Self {
|
|
32
|
+
Document(value.into())
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl AsRef<[u8]> for Document<'_> {
|
|
37
|
+
fn as_ref(&self) -> &[u8] {
|
|
38
|
+
&self.0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// DocRef — lightweight reference used internally by the pretokenizer
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
#[derive(Debug, Clone, Copy)]
|
|
47
|
+
pub(crate) struct DocRef<'a>(pub &'a [u8]);
|
|
48
|
+
|
|
49
|
+
impl<'a> From<&'a [u8]> for DocRef<'a> {
|
|
50
|
+
fn from(value: &'a [u8]) -> Self {
|
|
51
|
+
DocRef(value)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl<'a> std::ops::Deref for DocRef<'a> {
|
|
56
|
+
type Target = &'a [u8];
|
|
57
|
+
fn deref(&self) -> &Self::Target {
|
|
58
|
+
&self.0
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Resource trait
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/// A contiguous byte buffer that can be split into documents and parallel chunks.
|
|
67
|
+
pub trait Resource: Sync {
|
|
68
|
+
fn as_bytes(&self) -> &[u8];
|
|
69
|
+
|
|
70
|
+
/// Iterate documents by splitting on `separator`.
|
|
71
|
+
/// If separator is empty, the entire buffer is one document.
|
|
72
|
+
fn documents<'a>(&'a self, separator: &'a [u8]) -> DocumentIter<'a> {
|
|
73
|
+
DocumentIter::new(self.as_bytes(), separator)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Split into `n` chunk iterators, each yielding documents.
|
|
77
|
+
/// Chunk boundaries are aligned to separator positions so no document
|
|
78
|
+
/// is split across chunks.
|
|
79
|
+
fn par_document_chunks<'a>(
|
|
80
|
+
&'a self,
|
|
81
|
+
separator: &'a [u8],
|
|
82
|
+
n: usize,
|
|
83
|
+
) -> Vec<DocumentIter<'a>> {
|
|
84
|
+
par_document_chunks(self.as_bytes(), separator, n)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Blanket implementations
|
|
89
|
+
|
|
90
|
+
impl Resource for [u8] {
|
|
91
|
+
fn as_bytes(&self) -> &[u8] {
|
|
92
|
+
self
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl Resource for Vec<u8> {
|
|
97
|
+
fn as_bytes(&self) -> &[u8] {
|
|
98
|
+
self
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl Resource for str {
|
|
103
|
+
fn as_bytes(&self) -> &[u8] {
|
|
104
|
+
str::as_bytes(self)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
impl Resource for String {
|
|
109
|
+
fn as_bytes(&self) -> &[u8] {
|
|
110
|
+
str::as_bytes(self)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl Resource for Mmap {
|
|
115
|
+
fn as_bytes(&self) -> &[u8] {
|
|
116
|
+
self
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// MmappedFile
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/// Owns a memory-mapped file and implements `Resource`.
|
|
125
|
+
pub struct MmappedFile {
|
|
126
|
+
mmap: Mmap,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl MmappedFile {
|
|
130
|
+
pub fn open(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
|
|
131
|
+
let file = std::fs::File::open(path)?;
|
|
132
|
+
let mmap = unsafe { Mmap::map(&file)? };
|
|
133
|
+
Ok(Self { mmap })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
impl Resource for MmappedFile {
|
|
138
|
+
fn as_bytes(&self) -> &[u8] {
|
|
139
|
+
&self.mmap
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// DocumentIter
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/// Zero-copy iterator that splits a byte slice on a separator, yielding documents.
|
|
148
|
+
/// Empty documents (consecutive separators) are skipped.
|
|
149
|
+
pub struct DocumentIter<'a> {
|
|
150
|
+
bytes: &'a [u8],
|
|
151
|
+
separator: &'a [u8],
|
|
152
|
+
/// Prebuilt searcher for `separator`, constructed once instead of per
|
|
153
|
+
/// yielded document.
|
|
154
|
+
finder: memmem::Finder<'a>,
|
|
155
|
+
position: usize,
|
|
156
|
+
end: usize,
|
|
157
|
+
finished: bool,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
impl<'a> DocumentIter<'a> {
|
|
161
|
+
pub fn new(bytes: &'a [u8], separator: &'a [u8]) -> Self {
|
|
162
|
+
Self::new_range(bytes, separator, 0, bytes.len())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn new_range(bytes: &'a [u8], separator: &'a [u8], start: usize, end: usize) -> Self {
|
|
166
|
+
Self {
|
|
167
|
+
bytes,
|
|
168
|
+
separator,
|
|
169
|
+
finder: memmem::Finder::new(separator),
|
|
170
|
+
position: start,
|
|
171
|
+
end,
|
|
172
|
+
finished: false,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
impl<'a> Iterator for DocumentIter<'a> {
|
|
178
|
+
type Item = &'a [u8];
|
|
179
|
+
|
|
180
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
181
|
+
loop {
|
|
182
|
+
if self.finished || self.position >= self.end {
|
|
183
|
+
return None;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let search_range = &self.bytes[self.position..self.end];
|
|
187
|
+
|
|
188
|
+
// Empty separator means "no splitting" — yield entire remainder as one document
|
|
189
|
+
if self.separator.is_empty() {
|
|
190
|
+
self.finished = true;
|
|
191
|
+
return if search_range.is_empty() {
|
|
192
|
+
None
|
|
193
|
+
} else {
|
|
194
|
+
Some(search_range)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
match self.finder.find(search_range) {
|
|
199
|
+
Some(offset) => {
|
|
200
|
+
let doc = &self.bytes[self.position..self.position + offset];
|
|
201
|
+
self.position += offset + self.separator.len();
|
|
202
|
+
// Skip empty documents
|
|
203
|
+
if !doc.is_empty() {
|
|
204
|
+
return Some(doc);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
None => {
|
|
208
|
+
// No more separators; yield the remainder
|
|
209
|
+
self.finished = true;
|
|
210
|
+
return if search_range.is_empty() {
|
|
211
|
+
None
|
|
212
|
+
} else {
|
|
213
|
+
Some(search_range)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Parallel document chunking
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/// Split `bytes` into `n` document iterators with chunk boundaries aligned
|
|
226
|
+
/// to separator positions. Each iterator covers a disjoint range.
|
|
227
|
+
fn par_document_chunks<'a>(
|
|
228
|
+
bytes: &'a [u8],
|
|
229
|
+
separator: &'a [u8],
|
|
230
|
+
n: usize,
|
|
231
|
+
) -> Vec<DocumentIter<'a>> {
|
|
232
|
+
if n <= 1 || bytes.is_empty() || separator.is_empty() {
|
|
233
|
+
return vec![DocumentIter::new(bytes, separator)];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let chunk_size = bytes.len() / n;
|
|
237
|
+
let finder = memmem::Finder::new(separator);
|
|
238
|
+
|
|
239
|
+
let mut boundaries = Vec::with_capacity(n + 1);
|
|
240
|
+
boundaries.push(0usize);
|
|
241
|
+
|
|
242
|
+
for i in 1..n {
|
|
243
|
+
let target = i * chunk_size;
|
|
244
|
+
// Scan forward from target to find the next separator
|
|
245
|
+
match finder.find(&bytes[target..]) {
|
|
246
|
+
Some(offset) => {
|
|
247
|
+
let boundary = target + offset + separator.len();
|
|
248
|
+
boundaries.push(boundary);
|
|
249
|
+
}
|
|
250
|
+
None => {
|
|
251
|
+
// No separator found; remaining data goes in the last chunk
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
boundaries.push(bytes.len());
|
|
257
|
+
boundaries.dedup();
|
|
258
|
+
|
|
259
|
+
boundaries
|
|
260
|
+
.windows(2)
|
|
261
|
+
.map(|w| DocumentIter::new_range(bytes, separator, w[0], w[1]))
|
|
262
|
+
.collect()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Tests
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
#[cfg(test)]
|
|
270
|
+
mod tests {
|
|
271
|
+
use super::*;
|
|
272
|
+
|
|
273
|
+
#[test]
|
|
274
|
+
fn test_document_iter_basic() {
|
|
275
|
+
let data = b"hello<|endoftext|>world<|endoftext|>foo";
|
|
276
|
+
let sep = b"<|endoftext|>";
|
|
277
|
+
let docs: Vec<&[u8]> = data.as_slice().documents(sep).collect();
|
|
278
|
+
assert_eq!(docs, vec![b"hello".as_slice(), b"world", b"foo"]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[test]
|
|
282
|
+
fn test_document_iter_no_separator() {
|
|
283
|
+
let data = b"hello world";
|
|
284
|
+
let docs: Vec<&[u8]> = data.as_slice().documents(b"<|endoftext|>").collect();
|
|
285
|
+
assert_eq!(docs, vec![b"hello world".as_slice()]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn test_document_iter_empty_separator() {
|
|
290
|
+
let data = b"hello world";
|
|
291
|
+
let docs: Vec<&[u8]> = data.as_slice().documents(b"").collect();
|
|
292
|
+
assert_eq!(docs, vec![b"hello world".as_slice()]);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn test_document_iter_consecutive_separators() {
|
|
297
|
+
let data = b"a<SEP><SEP>b";
|
|
298
|
+
let docs: Vec<&[u8]> = data.as_slice().documents(b"<SEP>").collect();
|
|
299
|
+
assert_eq!(docs, vec![b"a".as_slice(), b"b"]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn test_document_iter_separator_at_edges() {
|
|
304
|
+
let data = b"<SEP>hello<SEP>";
|
|
305
|
+
let docs: Vec<&[u8]> = data.as_slice().documents(b"<SEP>").collect();
|
|
306
|
+
assert_eq!(docs, vec![b"hello".as_slice()]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
fn test_par_document_chunks_basic() {
|
|
311
|
+
let sep = b"<|endoftext|>";
|
|
312
|
+
let parts: Vec<&str> = (0..100)
|
|
313
|
+
.map(|i| if i % 10 == 9 { "doc" } else { "word " })
|
|
314
|
+
.collect();
|
|
315
|
+
let data = parts.join(std::str::from_utf8(sep).unwrap());
|
|
316
|
+
let bytes = data.as_bytes();
|
|
317
|
+
|
|
318
|
+
let chunks = bytes.par_document_chunks(sep, 4);
|
|
319
|
+
// All documents should be found across all chunks
|
|
320
|
+
let all_docs: Vec<&[u8]> = chunks.into_iter().flatten().collect();
|
|
321
|
+
let single_docs: Vec<&[u8]> = bytes.documents(sep).collect();
|
|
322
|
+
assert_eq!(all_docs, single_docs);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn test_par_chunks_single_thread() {
|
|
327
|
+
let data = b"a<SEP>b<SEP>c";
|
|
328
|
+
let chunks = data.as_slice().par_document_chunks(b"<SEP>", 1);
|
|
329
|
+
assert_eq!(chunks.len(), 1);
|
|
330
|
+
let docs: Vec<&[u8]> = chunks.into_iter().flatten().collect();
|
|
331
|
+
assert_eq!(docs, vec![b"a".as_slice(), b"b", b"c"]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
//! Parquet input: one document per row, text taken from a string or binary
|
|
2
|
+
//! column. Parquet pages are decoded through the arrow reader, so documents
|
|
3
|
+
//! are materialized as owned buffers rather than borrowed from an mmap like
|
|
4
|
+
//! the byte-stream formats. Null rows become empty documents so results
|
|
5
|
+
//! stay row-aligned with the source table. Row groups are the parallel work
|
|
6
|
+
//! units; documents always come back in row order.
|
|
7
|
+
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
use std::fs::File;
|
|
10
|
+
use std::io;
|
|
11
|
+
use std::path::Path;
|
|
12
|
+
|
|
13
|
+
use arrow_array::cast::AsArray;
|
|
14
|
+
use arrow_array::types::{ByteArrayType, ByteViewType};
|
|
15
|
+
use arrow_array::{Array, GenericByteArray, GenericByteViewArray, RecordBatch};
|
|
16
|
+
use arrow_schema::DataType;
|
|
17
|
+
use parquet::arrow::ProjectionMask;
|
|
18
|
+
use parquet::arrow::arrow_reader::{ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
|
|
19
|
+
use rayon::prelude::*;
|
|
20
|
+
use rustc_hash::FxBuildHasher;
|
|
21
|
+
|
|
22
|
+
use crate::pretokenize::pretokenize_as_iter;
|
|
23
|
+
|
|
24
|
+
fn parquet_err(path: &Path, err: impl std::fmt::Display) -> io::Error {
|
|
25
|
+
io::Error::new(
|
|
26
|
+
io::ErrorKind::InvalidData,
|
|
27
|
+
format!("{}: {err}", path.display()),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn open_builder(path: &Path) -> io::Result<ParquetRecordBatchReaderBuilder<File>> {
|
|
32
|
+
let file = File::open(path)
|
|
33
|
+
.map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", path.display())))?;
|
|
34
|
+
ParquetRecordBatchReaderBuilder::try_new(file).map_err(|e| parquet_err(path, e))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn n_row_groups(path: &Path) -> io::Result<usize> {
|
|
38
|
+
Ok(open_builder(path)?.metadata().num_row_groups())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Reader over `row_groups` (all when None) that decodes only `column`.
|
|
42
|
+
/// `column` must be a top-level field of the file's schema.
|
|
43
|
+
fn open_reader(
|
|
44
|
+
path: &Path,
|
|
45
|
+
column: &str,
|
|
46
|
+
row_groups: Option<Vec<usize>>,
|
|
47
|
+
) -> io::Result<ParquetRecordBatchReader> {
|
|
48
|
+
let builder = open_builder(path)?;
|
|
49
|
+
if builder.schema().index_of(column).is_err() {
|
|
50
|
+
let available: Vec<&str> = builder
|
|
51
|
+
.schema()
|
|
52
|
+
.fields()
|
|
53
|
+
.iter()
|
|
54
|
+
.map(|f| f.name().as_str())
|
|
55
|
+
.collect();
|
|
56
|
+
return Err(parquet_err(
|
|
57
|
+
path,
|
|
58
|
+
format!(
|
|
59
|
+
"no column {column:?} (available: {})",
|
|
60
|
+
available.join(", ")
|
|
61
|
+
),
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
let mask = ProjectionMask::columns(builder.parquet_schema(), [column]);
|
|
65
|
+
let builder = builder.with_projection(mask);
|
|
66
|
+
let builder = match row_groups {
|
|
67
|
+
Some(rgs) => builder.with_row_groups(rgs),
|
|
68
|
+
None => builder,
|
|
69
|
+
};
|
|
70
|
+
builder.build().map_err(|e| parquet_err(path, e))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Visit each row of the batch's single projected column as document bytes.
|
|
74
|
+
/// Null rows visit as empty slices, keeping documents row-aligned.
|
|
75
|
+
fn for_each_row(
|
|
76
|
+
batch: &RecordBatch,
|
|
77
|
+
path: &Path,
|
|
78
|
+
column: &str,
|
|
79
|
+
f: &mut impl FnMut(&[u8]),
|
|
80
|
+
) -> io::Result<()> {
|
|
81
|
+
fn visit_bytes<T: ByteArrayType>(arr: &GenericByteArray<T>, f: &mut impl FnMut(&[u8]))
|
|
82
|
+
where
|
|
83
|
+
T::Native: AsRef<[u8]>,
|
|
84
|
+
{
|
|
85
|
+
for i in 0..arr.len() {
|
|
86
|
+
if arr.is_null(i) {
|
|
87
|
+
f(b"");
|
|
88
|
+
} else {
|
|
89
|
+
f(arr.value(i).as_ref());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
fn visit_views<T: ByteViewType>(arr: &GenericByteViewArray<T>, f: &mut impl FnMut(&[u8]))
|
|
94
|
+
where
|
|
95
|
+
T::Native: AsRef<[u8]>,
|
|
96
|
+
{
|
|
97
|
+
for i in 0..arr.len() {
|
|
98
|
+
if arr.is_null(i) {
|
|
99
|
+
f(b"");
|
|
100
|
+
} else {
|
|
101
|
+
f(arr.value(i).as_ref());
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let col = batch.column(0);
|
|
107
|
+
match col.data_type() {
|
|
108
|
+
DataType::Utf8 => visit_bytes(col.as_string::<i32>(), f),
|
|
109
|
+
DataType::LargeUtf8 => visit_bytes(col.as_string::<i64>(), f),
|
|
110
|
+
DataType::Utf8View => visit_views(col.as_string_view(), f),
|
|
111
|
+
DataType::Binary => visit_bytes(col.as_binary::<i32>(), f),
|
|
112
|
+
DataType::LargeBinary => visit_bytes(col.as_binary::<i64>(), f),
|
|
113
|
+
DataType::BinaryView => visit_views(col.as_binary_view(), f),
|
|
114
|
+
other => {
|
|
115
|
+
return Err(parquet_err(
|
|
116
|
+
path,
|
|
117
|
+
format!(
|
|
118
|
+
"column {column:?} has unsupported type {other} \
|
|
119
|
+
(expected a string or binary column)"
|
|
120
|
+
),
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
Ok(())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Visit every document (row) of `column` in the given row groups (all when
|
|
128
|
+
/// None), in row order, on the calling thread.
|
|
129
|
+
pub fn for_each_doc(
|
|
130
|
+
path: &Path,
|
|
131
|
+
column: &str,
|
|
132
|
+
row_groups: Option<Vec<usize>>,
|
|
133
|
+
mut f: impl FnMut(&[u8]),
|
|
134
|
+
) -> io::Result<()> {
|
|
135
|
+
for batch in open_reader(path, column, row_groups)? {
|
|
136
|
+
let batch = batch.map_err(|e| parquet_err(path, e))?;
|
|
137
|
+
for_each_row(&batch, path, column, &mut f)?;
|
|
138
|
+
}
|
|
139
|
+
Ok(())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// All documents of `column` in `path`, one owned buffer per row, in row
|
|
143
|
+
/// order. Parallel over row groups when `parallel`; strictly on the calling
|
|
144
|
+
/// thread otherwise (the serial encode paths must never touch rayon).
|
|
145
|
+
pub fn read_docs(path: &Path, column: &str, parallel: bool) -> io::Result<Vec<Vec<u8>>> {
|
|
146
|
+
if !parallel {
|
|
147
|
+
let mut docs = Vec::new();
|
|
148
|
+
for_each_doc(path, column, None, |d| docs.push(d.to_vec()))?;
|
|
149
|
+
return Ok(docs);
|
|
150
|
+
}
|
|
151
|
+
let per_group: Vec<Vec<Vec<u8>>> = (0..n_row_groups(path)?)
|
|
152
|
+
.into_par_iter()
|
|
153
|
+
.map(|rg| {
|
|
154
|
+
let mut docs = Vec::new();
|
|
155
|
+
for_each_doc(path, column, Some(vec![rg]), |d| docs.push(d.to_vec()))?;
|
|
156
|
+
Ok(docs)
|
|
157
|
+
})
|
|
158
|
+
.collect::<io::Result<_>>()?;
|
|
159
|
+
Ok(per_group.into_iter().flatten().collect())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Parallel pretokenization of `column`, one rayon task per row group.
|
|
163
|
+
/// Documents are visited batch-by-batch without materializing the file.
|
|
164
|
+
pub fn pretokenize_par(
|
|
165
|
+
path: &Path,
|
|
166
|
+
column: &str,
|
|
167
|
+
) -> io::Result<HashMap<Vec<u8>, usize, FxBuildHasher>> {
|
|
168
|
+
(0..n_row_groups(path)?)
|
|
169
|
+
.into_par_iter()
|
|
170
|
+
.map(|rg| {
|
|
171
|
+
let mut counts: HashMap<Vec<u8>, usize, FxBuildHasher> = HashMap::default();
|
|
172
|
+
for_each_doc(path, column, Some(vec![rg]), |doc| {
|
|
173
|
+
for pretoken in pretokenize_as_iter(doc) {
|
|
174
|
+
*counts.entry(pretoken.as_ref().to_vec()).or_default() += 1;
|
|
175
|
+
}
|
|
176
|
+
})?;
|
|
177
|
+
Ok(counts)
|
|
178
|
+
})
|
|
179
|
+
.try_reduce(HashMap::default, |mut acc, counts| {
|
|
180
|
+
if acc.is_empty() {
|
|
181
|
+
return Ok(counts);
|
|
182
|
+
}
|
|
183
|
+
for (k, v) in counts {
|
|
184
|
+
*acc.entry(k).or_default() += v;
|
|
185
|
+
}
|
|
186
|
+
Ok(acc)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[cfg(test)]
|
|
191
|
+
mod tests {
|
|
192
|
+
use super::*;
|
|
193
|
+
use arrow_array::{BinaryArray, Int64Array, StringArray};
|
|
194
|
+
use parquet::arrow::ArrowWriter;
|
|
195
|
+
use parquet::file::properties::WriterProperties;
|
|
196
|
+
use std::sync::Arc;
|
|
197
|
+
|
|
198
|
+
/// Write a single-row-group-capped parquet file with a nullable string
|
|
199
|
+
/// "text" column, a binary "raw" column, and an int "id" column.
|
|
200
|
+
fn write_fixture(path: &Path, texts: &[Option<&str>], max_row_group_size: usize) {
|
|
201
|
+
let schema = Arc::new(arrow_schema::Schema::new(vec![
|
|
202
|
+
arrow_schema::Field::new("id", DataType::Int64, false),
|
|
203
|
+
arrow_schema::Field::new("text", DataType::Utf8, true),
|
|
204
|
+
arrow_schema::Field::new("raw", DataType::Binary, true),
|
|
205
|
+
]));
|
|
206
|
+
let ids = Int64Array::from((0..texts.len() as i64).collect::<Vec<_>>());
|
|
207
|
+
let text_col = StringArray::from(texts.to_vec());
|
|
208
|
+
let raw_col = BinaryArray::from(
|
|
209
|
+
texts
|
|
210
|
+
.iter()
|
|
211
|
+
.map(|t| t.map(|s| s.as_bytes()))
|
|
212
|
+
.collect::<Vec<_>>(),
|
|
213
|
+
);
|
|
214
|
+
let batch = RecordBatch::try_new(
|
|
215
|
+
schema.clone(),
|
|
216
|
+
vec![Arc::new(ids), Arc::new(text_col), Arc::new(raw_col)],
|
|
217
|
+
)
|
|
218
|
+
.unwrap();
|
|
219
|
+
let props = WriterProperties::builder()
|
|
220
|
+
.set_max_row_group_row_count(Some(max_row_group_size))
|
|
221
|
+
.build();
|
|
222
|
+
let file = File::create(path).unwrap();
|
|
223
|
+
let mut writer = ArrowWriter::try_new(file, schema, Some(props)).unwrap();
|
|
224
|
+
writer.write(&batch).unwrap();
|
|
225
|
+
writer.close().unwrap();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const TEXTS: [Option<&str>; 7] = [
|
|
229
|
+
Some("The quick brown fox"),
|
|
230
|
+
Some("jumps over the lazy dog."),
|
|
231
|
+
None,
|
|
232
|
+
Some("She sells seashells"),
|
|
233
|
+
Some(""),
|
|
234
|
+
Some("by the seashore."),
|
|
235
|
+
Some("Peter Piper picked a peck"),
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
fn expected_docs() -> Vec<Vec<u8>> {
|
|
239
|
+
TEXTS
|
|
240
|
+
.iter()
|
|
241
|
+
.map(|t| t.unwrap_or("").as_bytes().to_vec())
|
|
242
|
+
.collect()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#[test]
|
|
246
|
+
fn test_read_docs_row_order_and_nulls() {
|
|
247
|
+
let dir = tempfile::tempdir().unwrap();
|
|
248
|
+
let path = dir.path().join("docs.parquet");
|
|
249
|
+
write_fixture(&path, &TEXTS, 1024);
|
|
250
|
+
assert_eq!(read_docs(&path, "text", false).unwrap(), expected_docs());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_read_docs_parallel_multiple_row_groups() {
|
|
255
|
+
let dir = tempfile::tempdir().unwrap();
|
|
256
|
+
let path = dir.path().join("docs.parquet");
|
|
257
|
+
write_fixture(&path, &TEXTS, 2); // 4 row groups for 7 rows
|
|
258
|
+
assert!(n_row_groups(&path).unwrap() > 1);
|
|
259
|
+
assert_eq!(read_docs(&path, "text", true).unwrap(), expected_docs());
|
|
260
|
+
assert_eq!(read_docs(&path, "text", false).unwrap(), expected_docs());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[test]
|
|
264
|
+
fn test_read_binary_column() {
|
|
265
|
+
let dir = tempfile::tempdir().unwrap();
|
|
266
|
+
let path = dir.path().join("docs.parquet");
|
|
267
|
+
write_fixture(&path, &TEXTS, 3);
|
|
268
|
+
assert_eq!(read_docs(&path, "raw", true).unwrap(), expected_docs());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn test_missing_column_lists_available() {
|
|
273
|
+
let dir = tempfile::tempdir().unwrap();
|
|
274
|
+
let path = dir.path().join("docs.parquet");
|
|
275
|
+
write_fixture(&path, &TEXTS, 1024);
|
|
276
|
+
let err = read_docs(&path, "content", false).unwrap_err().to_string();
|
|
277
|
+
assert!(err.contains("no column \"content\""), "{err}");
|
|
278
|
+
assert!(err.contains("text"), "{err}");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[test]
|
|
282
|
+
fn test_non_string_column_rejected() {
|
|
283
|
+
let dir = tempfile::tempdir().unwrap();
|
|
284
|
+
let path = dir.path().join("docs.parquet");
|
|
285
|
+
write_fixture(&path, &TEXTS, 1024);
|
|
286
|
+
let err = read_docs(&path, "id", false).unwrap_err().to_string();
|
|
287
|
+
assert!(err.contains("unsupported type"), "{err}");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#[test]
|
|
291
|
+
fn test_pretokenize_matches_per_doc_counts() {
|
|
292
|
+
let dir = tempfile::tempdir().unwrap();
|
|
293
|
+
let path = dir.path().join("docs.parquet");
|
|
294
|
+
write_fixture(&path, &TEXTS, 2);
|
|
295
|
+
let mut expected: HashMap<Vec<u8>, usize, FxBuildHasher> = HashMap::default();
|
|
296
|
+
for doc in expected_docs() {
|
|
297
|
+
for pretoken in pretokenize_as_iter(&doc) {
|
|
298
|
+
*expected.entry(pretoken.as_ref().to_vec()).or_default() += 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
assert_eq!(pretokenize_par(&path, "text").unwrap(), expected);
|
|
302
|
+
}
|
|
303
|
+
}
|