Skip to content

Commit 2908c46

Browse files
robert3005claude
andcommitted
Implement LIKE in vortex instead of falling back to arrow
Replace the arrow-string backed LIKE executor with a native implementation over canonical VarBinView arrays, dropping the arrow-string dependency from vortex-array. Patterns compile once into a LikePattern: wildcard-free patterns and prefix/suffix/substring shapes use direct byte comparisons (memchr's SIMD substring search for contains, ASCII case-insensitive variants for ILIKE over ASCII data), everything else translates the SQL pattern to an anchored regex over the value bytes, using the same translation as arrow-string so semantics are unchanged. Non-constant pattern children reuse the previous row's compiled pattern while the pattern bytes repeat. Evaluation exploits the view layout: equality is a single masked 16-byte compare for needles that fit inline, the prefix path rejects lanes with a branch-free masked compare of the view's inline 4-byte prefix without touching the data buffers, the suffix path slices exactly the trailing needle bytes out of the view or its buffer, and contains/equality reject on the view length before dereferencing. Benchmarked against the previous arrow implementation (benches/like.rs, 64Ki strings, median): exact 100us -> 18us, prefix 144us -> 48us, suffix 171us -> 105us, contains 876us -> 732us, regex 924us -> 744us, ilike contains 2.52ms -> 2.03ms, per-row patterns 824us -> 565us. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <github@robertk.io>
1 parent a649d75 commit 2908c46

6 files changed

Lines changed: 868 additions & 20 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ rand_distr = "0.6"
225225
ratatui = { version = "0.30", default-features = false }
226226
regex = "1.11.0"
227227
regex-automata = "0.4"
228+
regex-syntax = "0.8"
228229
reqwest = { version = "0.13.0", features = [
229230
"blocking",
230231
"charset",

vortex-array/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ arrow-data = { workspace = true }
3131
arrow-ord = { workspace = true }
3232
arrow-schema = { workspace = true, features = ["canonical_extension_types"] }
3333
arrow-select = { workspace = true }
34-
arrow-string = { workspace = true }
3534
async-lock = { workspace = true }
3635
bytes = { workspace = true }
3736
cfg-if = { workspace = true }
@@ -45,6 +44,7 @@ humansize = { workspace = true }
4544
inventory = { workspace = true }
4645
itertools = { workspace = true }
4746
jiff = { workspace = true }
47+
memchr = { workspace = true }
4848
num-traits = { workspace = true }
4949
num_enum = { workspace = true }
5050
parking_lot = { workspace = true }
@@ -55,6 +55,8 @@ primitive-types = { workspace = true, optional = true, features = [
5555
] }
5656
prost = { workspace = true }
5757
rand = { workspace = true }
58+
regex = { workspace = true }
59+
regex-syntax = { workspace = true }
5860
rstest = { workspace = true, optional = true }
5961
rstest_reuse = { workspace = true, optional = true }
6062
rustc-hash = { workspace = true }
@@ -138,6 +140,10 @@ harness = false
138140
name = "kleene_bool"
139141
harness = false
140142

143+
[[bench]]
144+
name = "like"
145+
harness = false
146+
141147
[[bench]]
142148
name = "interleave"
143149
harness = false

vortex-array/benches/like.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![expect(clippy::unwrap_used)]
5+
6+
use divan::Bencher;
7+
use rand::RngExt;
8+
use rand::SeedableRng;
9+
use rand::distr::Uniform;
10+
use rand::prelude::StdRng;
11+
use vortex_array::ArrayRef;
12+
use vortex_array::IntoArray;
13+
use vortex_array::VortexSessionExecute;
14+
use vortex_array::arrays::BoolArray;
15+
use vortex_array::arrays::ConstantArray;
16+
use vortex_array::arrays::VarBinViewArray;
17+
use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
18+
use vortex_array::scalar_fn::fns::like::Like;
19+
use vortex_array::scalar_fn::fns::like::LikeOptions;
20+
21+
fn main() {
22+
divan::main();
23+
}
24+
25+
const ARRAY_SIZE: usize = 65_536;
26+
27+
/// Random lowercase strings of 4..=24 bytes, some with a `hello` infix.
28+
fn strings() -> ArrayRef {
29+
let mut rng = StdRng::seed_from_u64(0);
30+
let len_dist = Uniform::new_inclusive(4usize, 24).unwrap();
31+
VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|i| {
32+
let len = rng.sample(len_dist);
33+
let mut s: String = (0..len)
34+
.map(|_| char::from(rng.random_range(b'a'..=b'z')))
35+
.collect();
36+
if i % 7 == 0 {
37+
s.insert_str(len / 2, "hello");
38+
}
39+
s
40+
}))
41+
.into_array()
42+
}
43+
44+
fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) {
45+
let session = vortex_array::array_session();
46+
let array = strings();
47+
bencher
48+
.with_inputs(|| {
49+
(
50+
Like.try_new_array(
51+
ARRAY_SIZE,
52+
options,
53+
[
54+
array.clone(),
55+
ConstantArray::new(pattern, ARRAY_SIZE).into_array(),
56+
],
57+
)
58+
.unwrap(),
59+
session.create_execution_ctx(),
60+
)
61+
})
62+
.bench_values(|(array, mut ctx)| array.execute::<BoolArray>(&mut ctx).unwrap());
63+
}
64+
65+
#[divan::bench]
66+
fn like_exact(bencher: Bencher) {
67+
bench_like(bencher, "hello", LikeOptions::default());
68+
}
69+
70+
#[divan::bench]
71+
fn like_prefix(bencher: Bencher) {
72+
bench_like(bencher, "hello%", LikeOptions::default());
73+
}
74+
75+
#[divan::bench]
76+
fn like_suffix(bencher: Bencher) {
77+
bench_like(bencher, "%hello", LikeOptions::default());
78+
}
79+
80+
#[divan::bench]
81+
fn like_contains(bencher: Bencher) {
82+
bench_like(bencher, "%hello%", LikeOptions::default());
83+
}
84+
85+
#[divan::bench]
86+
fn like_regex(bencher: Bencher) {
87+
bench_like(bencher, "h_llo%w%d", LikeOptions::default());
88+
}
89+
90+
#[divan::bench]
91+
fn like_per_row_patterns(bencher: Bencher) {
92+
let session = vortex_array::array_session();
93+
let array = strings();
94+
// A non-constant pattern child takes the per-row path; repeated patterns hit the
95+
// compile cache.
96+
let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array();
97+
bencher
98+
.with_inputs(|| {
99+
(
100+
Like.try_new_array(
101+
ARRAY_SIZE,
102+
LikeOptions::default(),
103+
[array.clone(), patterns.clone()],
104+
)
105+
.unwrap(),
106+
session.create_execution_ctx(),
107+
)
108+
})
109+
.bench_values(|(array, mut ctx)| array.execute::<BoolArray>(&mut ctx).unwrap());
110+
}
111+
112+
#[divan::bench]
113+
fn ilike_contains(bencher: Bencher) {
114+
bench_like(
115+
bencher,
116+
"%HELLO%",
117+
LikeOptions {
118+
negated: false,
119+
case_insensitive: true,
120+
},
121+
);
122+
}

0 commit comments

Comments
 (0)