Skip to content

Commit 2196ee3

Browse files
committed
intersect multiple search queries
1 parent cca2880 commit 2196ee3

2 files changed

Lines changed: 124 additions & 3 deletions

File tree

src/am.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,6 +1756,65 @@ mod tests {
17561756
Ok(())
17571757
}
17581758

1759+
#[pg_test]
1760+
pub fn test_multiple_ilike_scan_keys_are_intersected() -> spi::Result<()> {
1761+
Spi::connect_mut(|client| -> spi::Result<()> {
1762+
client.update(
1763+
"CREATE TABLE and_key_docs (id SERIAL PRIMARY KEY, text TEXT NOT NULL)",
1764+
None,
1765+
&[],
1766+
)?;
1767+
client.update(
1768+
"INSERT INTO and_key_docs (text) VALUES
1769+
('Must call util.foo from here'),
1770+
('MUST route through UTIL.bar'),
1771+
('Must call helper.foo instead'),
1772+
('see util.foo only'),
1773+
('irrelevant row')",
1774+
None,
1775+
&[],
1776+
)?;
1777+
client.update(
1778+
"CREATE INDEX idx_and_key_docs_text_zoekt ON and_key_docs USING pg_zoekt (text)",
1779+
None,
1780+
&[],
1781+
)?;
1782+
Ok(())
1783+
})?;
1784+
1785+
Spi::run("SELECT pg_zoekt_seal('idx_and_key_docs_text_zoekt'::regclass)")?;
1786+
1787+
let seq_count: i64 = Spi::get_one(
1788+
"SET enable_seqscan = on; \
1789+
SET enable_indexscan = off; \
1790+
SET enable_bitmapscan = off; \
1791+
SELECT count(*) FROM and_key_docs
1792+
WHERE text ILIKE '%Must%' AND text ILIKE '%util.%';",
1793+
)?
1794+
.unwrap_or(0);
1795+
1796+
let idx_count: i64 = Spi::get_one(
1797+
"SET enable_seqscan = off; \
1798+
SET enable_indexscan = on; \
1799+
SET enable_bitmapscan = on; \
1800+
SELECT count(*) FROM and_key_docs
1801+
WHERE text ILIKE '%Must%' AND text ILIKE '%util.%';",
1802+
)?
1803+
.unwrap_or(0);
1804+
1805+
assert_eq!(
1806+
seq_count, 2,
1807+
"test fixture should produce two exact matches"
1808+
);
1809+
assert_eq!(
1810+
idx_count, seq_count,
1811+
"index scan should intersect multiple scan keys"
1812+
);
1813+
1814+
Spi::run("DROP TABLE IF EXISTS and_key_docs")?;
1815+
Ok(())
1816+
}
1817+
17591818
#[pg_test]
17601819
pub fn test_wildcard_segments_respect_multiple_starts() -> spi::Result<()> {
17611820
Spi::connect_mut(|client| -> spi::Result<()> {

src/query.rs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,41 @@ impl ScanState {
8181
}
8282
}
8383

84+
fn tid_cmp(a: &pg_sys::ItemPointerData, b: &pg_sys::ItemPointerData) -> Ordering {
85+
let a_blk = (a.ip_blkid.bi_hi as u32) << 16 | a.ip_blkid.bi_lo as u32;
86+
let b_blk = (b.ip_blkid.bi_hi as u32) << 16 | b.ip_blkid.bi_lo as u32;
87+
match a_blk.cmp(&b_blk) {
88+
Ordering::Equal => a.ip_posid.cmp(&b.ip_posid),
89+
other => other,
90+
}
91+
}
92+
93+
fn intersect_scan_states(mut left: ScanState, right: ScanState) -> ScanState {
94+
let mut matches = Vec::with_capacity(left.matches.len().min(right.matches.len()));
95+
let mut lidx = 0usize;
96+
let mut ridx = 0usize;
97+
98+
while lidx < left.matches.len() && ridx < right.matches.len() {
99+
match tid_cmp(&left.matches[lidx].tid, &right.matches[ridx].tid) {
100+
Ordering::Less => lidx += 1,
101+
Ordering::Greater => ridx += 1,
102+
Ordering::Equal => {
103+
matches.push(MatchEntry {
104+
tid: left.matches[lidx].tid,
105+
// Every clause must hold, so any lossy clause forces a heap recheck.
106+
recheck: left.matches[lidx].recheck || right.matches[ridx].recheck,
107+
});
108+
lidx += 1;
109+
ridx += 1;
110+
}
111+
}
112+
}
113+
114+
left.matches = matches;
115+
left.cursor = 0;
116+
left
117+
}
118+
84119
fn scan_keys_to_pattern(keys: pg_sys::ScanKey, nkeys: i32) -> Option<String> {
85120
if keys.is_null() || nkeys <= 0 {
86121
return None;
@@ -1527,12 +1562,11 @@ fn add_matches_from_regex_branches(
15271562
Ok(())
15281563
}
15291564

1530-
unsafe fn build_scan_state(
1565+
unsafe fn build_scan_state_for_key(
15311566
index_relation: pg_sys::Relation,
15321567
keys: pg_sys::ScanKey,
1533-
nkeys: std::os::raw::c_int,
15341568
) -> ScanState {
1535-
let pattern = scan_keys_to_pattern(keys, nkeys);
1569+
let pattern = scan_keys_to_pattern(keys, 1);
15361570
let pattern_str = if let Some(p) = pattern {
15371571
p
15381572
} else {
@@ -1688,6 +1722,34 @@ unsafe fn build_scan_state(
16881722
}
16891723
}
16901724

1725+
unsafe fn build_scan_state(
1726+
index_relation: pg_sys::Relation,
1727+
keys: pg_sys::ScanKey,
1728+
nkeys: std::os::raw::c_int,
1729+
) -> ScanState {
1730+
if keys.is_null() || nkeys <= 0 {
1731+
return ScanState::default();
1732+
}
1733+
1734+
let mut combined: Option<ScanState> = None;
1735+
for idx in 0..(nkeys as usize) {
1736+
let key = unsafe { keys.add(idx) };
1737+
let state = unsafe { build_scan_state_for_key(index_relation, key) };
1738+
combined = Some(match combined.take() {
1739+
Some(existing) => intersect_scan_states(existing, state),
1740+
None => state,
1741+
});
1742+
if combined
1743+
.as_ref()
1744+
.is_some_and(|state| state.matches.is_empty())
1745+
{
1746+
break;
1747+
}
1748+
}
1749+
1750+
combined.unwrap_or_default()
1751+
}
1752+
16911753
fn build_full_regex_scan_state(index_relation: pg_sys::Relation) -> ScanState {
16921754
let mut state = ScanState::default();
16931755
state.tombstones =

0 commit comments

Comments
 (0)