Skip to main content

uqa_core/
doc_set.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Document-id support sets.
8//!
9//! [`DocSet`] is the carrier for document-level Boolean algebra. Payload-bearing
10//! posting lists project onto this type through [`crate::PostingList::support`];
11//! payload values are deliberately absent, so equality and the Boolean laws
12//! describe exactly the same thing.
13
14use std::ops::{BitAnd, BitOr, Sub};
15
16use crate::DocId;
17
18/// A finite set of document ids stored in ascending order.
19///
20/// The explicit universe passed to [`Self::complement`] determines the finite
21/// Boolean algebra in which a complement is evaluated.
22#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct DocSet {
24    doc_ids: Vec<DocId>,
25}
26
27impl DocSet {
28    /// Construct an empty document set.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Construct a document set from ids in arbitrary order.
34    pub fn from_unsorted(mut doc_ids: Vec<DocId>) -> Self {
35        doc_ids.sort_unstable();
36        doc_ids.dedup();
37        Self { doc_ids }
38    }
39
40    pub(crate) fn from_sorted_unchecked(doc_ids: Vec<DocId>) -> Self {
41        debug_assert!(
42            doc_ids.windows(2).all(|window| window[0] < window[1]),
43            "DocSet::from_sorted_unchecked invariant violated"
44        );
45        Self { doc_ids }
46    }
47
48    /// Set union.
49    pub fn union(&self, other: &Self) -> Self {
50        let mut result = Vec::with_capacity(self.len() + other.len());
51        let (mut left, mut right) = (0, 0);
52
53        while left < self.len() && right < other.len() {
54            match self.doc_ids[left].cmp(&other.doc_ids[right]) {
55                std::cmp::Ordering::Less => {
56                    result.push(self.doc_ids[left]);
57                    left += 1;
58                }
59                std::cmp::Ordering::Equal => {
60                    result.push(self.doc_ids[left]);
61                    left += 1;
62                    right += 1;
63                }
64                std::cmp::Ordering::Greater => {
65                    result.push(other.doc_ids[right]);
66                    right += 1;
67                }
68            }
69        }
70
71        result.extend_from_slice(&self.doc_ids[left..]);
72        result.extend_from_slice(&other.doc_ids[right..]);
73        Self::from_sorted_unchecked(result)
74    }
75
76    /// Set intersection.
77    pub fn intersect(&self, other: &Self) -> Self {
78        let mut result = Vec::with_capacity(self.len().min(other.len()));
79        let (mut left, mut right) = (0, 0);
80
81        while left < self.len() && right < other.len() {
82            match self.doc_ids[left].cmp(&other.doc_ids[right]) {
83                std::cmp::Ordering::Less => left += 1,
84                std::cmp::Ordering::Equal => {
85                    result.push(self.doc_ids[left]);
86                    left += 1;
87                    right += 1;
88                }
89                std::cmp::Ordering::Greater => right += 1,
90            }
91        }
92
93        Self::from_sorted_unchecked(result)
94    }
95
96    /// Set difference, `self \\ other`.
97    pub fn difference(&self, other: &Self) -> Self {
98        let mut result = Vec::with_capacity(self.len());
99        let (mut left, mut right) = (0, 0);
100
101        while left < self.len() && right < other.len() {
102            match self.doc_ids[left].cmp(&other.doc_ids[right]) {
103                std::cmp::Ordering::Less => {
104                    result.push(self.doc_ids[left]);
105                    left += 1;
106                }
107                std::cmp::Ordering::Equal => {
108                    left += 1;
109                    right += 1;
110                }
111                std::cmp::Ordering::Greater => right += 1,
112            }
113        }
114
115        result.extend_from_slice(&self.doc_ids[left..]);
116        Self::from_sorted_unchecked(result)
117    }
118
119    /// Complement relative to an explicit finite universe.
120    pub fn complement(&self, universe: &Self) -> Self {
121        universe.difference(self)
122    }
123
124    /// Return whether `doc_id` belongs to this set.
125    pub fn contains(&self, doc_id: DocId) -> bool {
126        self.doc_ids.binary_search(&doc_id).is_ok()
127    }
128
129    /// Borrow the sorted document-id representation.
130    pub fn as_slice(&self) -> &[DocId] {
131        &self.doc_ids
132    }
133
134    /// Iterate over document ids in ascending order.
135    pub fn iter(&self) -> impl ExactSizeIterator<Item = DocId> + DoubleEndedIterator + '_ {
136        self.doc_ids.iter().copied()
137    }
138
139    /// Number of document ids in the set.
140    pub fn len(&self) -> usize {
141        self.doc_ids.len()
142    }
143
144    /// Whether the set contains no document ids.
145    pub fn is_empty(&self) -> bool {
146        self.doc_ids.is_empty()
147    }
148}
149
150impl FromIterator<DocId> for DocSet {
151    fn from_iter<I: IntoIterator<Item = DocId>>(iter: I) -> Self {
152        Self::from_unsorted(iter.into_iter().collect())
153    }
154}
155
156impl From<Vec<DocId>> for DocSet {
157    fn from(doc_ids: Vec<DocId>) -> Self {
158        Self::from_unsorted(doc_ids)
159    }
160}
161
162impl IntoIterator for DocSet {
163    type Item = DocId;
164    type IntoIter = std::vec::IntoIter<DocId>;
165
166    fn into_iter(self) -> Self::IntoIter {
167        self.doc_ids.into_iter()
168    }
169}
170
171impl<'a> IntoIterator for &'a DocSet {
172    type Item = DocId;
173    type IntoIter = std::iter::Copied<std::slice::Iter<'a, DocId>>;
174
175    fn into_iter(self) -> Self::IntoIter {
176        self.doc_ids.iter().copied()
177    }
178}
179
180impl BitOr for &DocSet {
181    type Output = DocSet;
182
183    fn bitor(self, rhs: Self) -> Self::Output {
184        self.union(rhs)
185    }
186}
187
188impl BitAnd for &DocSet {
189    type Output = DocSet;
190
191    fn bitand(self, rhs: Self) -> Self::Output {
192        self.intersect(rhs)
193    }
194}
195
196impl Sub for &DocSet {
197    type Output = DocSet;
198
199    fn sub(self, rhs: Self) -> Self::Output {
200        self.difference(rhs)
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::DocSet;
207
208    #[test]
209    fn construction_sorts_and_deduplicates() {
210        assert_eq!(
211            DocSet::from_unsorted(vec![3, 1, 3, 2]).as_slice(),
212            &[1, 2, 3]
213        );
214    }
215
216    #[test]
217    fn operations_preserve_set_semantics() {
218        let left = DocSet::from(vec![1, 3, 5]);
219        let right = DocSet::from(vec![2, 3, 4]);
220        let universe = DocSet::from(vec![1, 2, 3, 4, 5]);
221
222        assert_eq!((&left | &right).as_slice(), &[1, 2, 3, 4, 5]);
223        assert_eq!((&left & &right).as_slice(), &[3]);
224        assert_eq!((&left - &right).as_slice(), &[1, 5]);
225        assert_eq!(left.complement(&universe).as_slice(), &[2, 4]);
226    }
227}