use std::borrow::Cow;
use crate::Position;
use crate::query::{Query, QueryItem};
use super::SegmentIndex;
pub fn search<'a, I: SegmentIndex>(
index: &'a I,
query: &Query,
after: Position,
) -> impl Iterator<Item = Position> + 'a {
let locals = match query {
Query::All => (0..index.len()).collect(),
Query::Items(items) => {
let mut locals = Vec::new();
for item in items {
item_locals(index, item, &mut locals);
}
locals.sort_unstable();
locals.dedup();
locals
}
};
let base = index.base().get();
locals
.into_iter()
.map(move |local| Position::new(base + local as u64))
.filter(move |global| *global > after)
}
fn item_locals<I: SegmentIndex>(index: &I, item: &QueryItem, out: &mut Vec<u32>) {
let type_ids: Option<Vec<u16>> = if item.types.is_empty() {
None
} else {
let ids: Vec<u16> = item
.types
.iter()
.filter_map(|t| index.type_id(t.as_str()))
.collect();
if ids.is_empty() {
return;
}
Some(ids)
};
let keep = |local: u32| match &type_ids {
None => true,
Some(ids) => ids.contains(&index.type_at(local)),
};
if item.tags.is_empty() {
out.extend((0..index.len()).filter(|&local| keep(local)));
} else {
let mut lists: Vec<Cow<'_, [u32]>> = Vec::with_capacity(item.tags.len());
for tag in item.tags.iter() {
match index.term_postings(tag.as_str()) {
Some(list) => lists.push(list),
None => return,
}
}
out.extend(intersect(lists).into_iter().filter(|&local| keep(local)));
}
}
fn intersect(mut lists: Vec<Cow<'_, [u32]>>) -> Vec<u32> {
lists.sort_by_key(|list| list.len());
let (shortest, rest) = lists.split_first().expect("intersect called with no lists");
shortest
.iter()
.copied()
.filter(|x| rest.iter().all(|list| list.binary_search(x).is_ok()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Event, EventType, Tag, Tags};
use crate::index::ActiveTail;
use crate::query::QueryItem;
use smallvec::SmallVec;
fn ty(s: &str) -> EventType {
EventType::new(s).unwrap()
}
fn tags(items: &[&str]) -> Tags {
Tags::new(
items
.iter()
.map(|s| Tag::new(*s).unwrap())
.collect::<SmallVec<[Tag; 4]>>(),
)
.unwrap()
}
fn event(type_str: &str, tag_strs: &[&str]) -> Event {
Event::new(&ty(type_str), &tags(tag_strs), b"").unwrap()
}
fn fixture() -> ActiveTail {
let events = [
event("Registered", &[]),
event("Enrolled", &["course:c1"]),
event("Enrolled", &["course:c1", "student:s1"]),
event("Renamed", &["student:s1"]),
event("Registered", &["course:c1"]),
];
let index = ActiveTail::new(Position::new(1));
for (i, ev) in events.iter().enumerate() {
index
.push(Position::new(1 + i as u64), ev.as_ref())
.unwrap();
}
index
}
fn run(index: &ActiveTail, query: &Query, after: u64) -> Vec<u64> {
search(&index.view_full(), query, Position::new(after))
.map(|p| p.get())
.collect()
}
#[test]
fn spec_empty_types_matches_any_type() {
let index = fixture();
let q = Query::item(QueryItem::with_tags(tags(&["course:c1"])));
assert_eq!(run(&index, &q, 0), vec![2, 3, 5]);
}
#[test]
fn spec_empty_tags_constrains_only_on_type() {
let index = fixture();
let q = Query::item(QueryItem::of_types(vec![ty("Registered")]));
assert_eq!(run(&index, &q, 0), vec![1, 5]);
}
#[test]
fn spec_empty_item_matches_everything() {
let index = fixture();
let q = Query::item(QueryItem::default());
assert_eq!(run(&index, &q, 0), vec![1, 2, 3, 4, 5]);
}
#[test]
fn spec_empty_items_matches_nothing() {
let index = fixture();
let q = Query::items(Vec::new());
assert_eq!(run(&index, &q, 0), Vec::<u64>::new());
}
#[test]
fn spec_all_matches_everything_and_after_is_whole_log() {
let index = fixture();
assert_eq!(run(&index, &Query::all(), 0), vec![1, 2, 3, 4, 5]);
}
#[test]
fn and_within_item_tags() {
let index = fixture();
let q = Query::item(QueryItem::with_tags(tags(&["course:c1", "student:s1"])));
assert_eq!(run(&index, &q, 0), vec![3]);
}
#[test]
fn or_across_items() {
let index = fixture();
let q = Query::items(vec![
QueryItem::of_types(vec![ty("Renamed")]),
QueryItem::with_tags(tags(&["course:c1"])),
]);
assert_eq!(run(&index, &q, 0), vec![2, 3, 4, 5]);
}
#[test]
fn type_and_tag_together() {
let index = fixture();
let q = Query::item(QueryItem::new(vec![ty("Enrolled")], tags(&["course:c1"])));
assert_eq!(run(&index, &q, 0), vec![2, 3]);
}
#[test]
fn absent_tag_matches_nothing() {
let index = fixture();
let q = Query::item(QueryItem::with_tags(tags(&["ghost:x"])));
assert_eq!(run(&index, &q, 0), Vec::<u64>::new());
}
#[test]
fn absent_type_matches_nothing() {
let index = fixture();
let q = Query::item(QueryItem::of_types(vec![ty("Ghost")]));
assert_eq!(run(&index, &q, 0), Vec::<u64>::new());
}
#[test]
fn partly_absent_types_keep_the_known_ones() {
let index = fixture();
let q = Query::item(QueryItem::of_types(vec![ty("Registered"), ty("Ghost")]));
assert_eq!(run(&index, &q, 0), vec![1, 5]);
}
#[test]
fn after_is_exclusive() {
let index = fixture();
let q = Query::item(QueryItem::with_tags(tags(&["course:c1"])));
assert_eq!(run(&index, &q, 2), vec![3, 5]);
assert_eq!(run(&index, &q, 3), vec![5]);
assert_eq!(run(&index, &q, 5), Vec::<u64>::new());
assert_eq!(run(&index, &Query::all(), 2), vec![3, 4, 5]);
}
#[test]
fn output_is_ascending_and_deduped_across_overlapping_items() {
let index = fixture();
let q = Query::items(vec![
QueryItem::with_tags(tags(&["course:c1"])), QueryItem::with_tags(tags(&["student:s1"])), ]);
assert_eq!(run(&index, &q, 0), vec![2, 3, 4, 5]);
}
}