use std::collections::{HashMap, HashSet};
use super::segment_scope::Scope;
use super::{CorpusStats, QueryOpts, TextMatch, TextSegment};
use crate::positions::{Positions, walk};
use crate::token::{tokenize, tokenize_spans};
impl TextSegment {
pub fn phrase_matches(
&self,
phrase: &[u8],
limit: usize,
stats: Option<&CorpusStats>,
) -> Vec<TextMatch> {
if limit == 0 {
return Vec::new();
}
let toks = tokenize(phrase);
match toks.len() {
0 => return Vec::new(),
1 => return self.matches_scored(phrase, limit, stats),
_ => {}
}
if self.positions.is_none() {
return Vec::new();
}
let (n_docs, avgdl) = self.corpus_stats(stats);
let sc = Scope { stats, n_docs, avgdl, want: &[] };
let mut scores: HashMap<u32, f64> = HashMap::new();
self.add_phrase(&toks, &mut scores, &sc);
self.select_top(&scores, limit, &[], None, None)
}
pub fn matches_query(
&self,
text: &[u8],
limit: usize,
stats: Option<&CorpusStats>,
) -> Vec<TextMatch> {
self.matches_query_typo(text, limit, stats, 0)
}
pub fn matches_query_typo(
&self,
text: &[u8],
limit: usize,
stats: Option<&CorpusStats>,
typo: u32,
) -> Vec<TextMatch> {
self.matches_query_with(text, limit, QueryOpts { stats, typo, ..QueryOpts::default() })
}
pub fn matches_query_with(
&self,
text: &[u8],
limit: usize,
opts: QueryOpts,
) -> Vec<TextMatch> {
self.matches_query_faceted(text, limit, opts, &[]).hits
}
pub fn matches_query_faceted(
&self,
text: &[u8],
limit: usize,
opts: QueryOpts,
facets: &[crate::Facet],
) -> crate::FacetedMatches {
let empty = || crate::FacetedMatches { hits: Vec::new(), facets: vec![Vec::new(); facets.len()] };
if limit == 0 {
return empty();
}
let Some(want) = self.normalize_scope(opts.fields) else {
return empty();
};
let (bare, phrases, prefixes) = parse_clauses(text);
if phrases.is_empty()
&& prefixes.is_empty()
&& opts.typo == 0
&& want.is_empty()
&& opts.filter.is_empty()
&& opts.sort.is_none()
&& opts.distinct.is_none()
&& facets.is_empty()
{
return crate::FacetedMatches {
hits: self.matches_scored(text, limit, opts.stats),
facets: Vec::new(),
};
}
if self.docs.is_empty() {
return empty();
}
let scores = self.accumulate_clauses(bare, &phrases, &prefixes, &want, &opts);
crate::FacetedMatches {
facets: facets.iter().map(|f| self.count_facet(&scores, opts.filter, *f)).collect(),
hits: self.select_top(&scores, limit, opts.filter, opts.sort, opts.distinct),
}
}
fn accumulate_clauses(
&self,
bare: Vec<Vec<u8>>,
phrases: &[Vec<Vec<u8>>],
prefixes: &[Vec<u8>],
want: &[usize],
opts: &QueryOpts,
) -> HashMap<u32, f64> {
let (n_docs, avgdl) = self.scope_stats(opts.stats, want);
let sc = Scope { stats: opts.stats, n_docs, avgdl, want };
let mut terms = bare;
terms.sort();
terms.dedup();
let mut scores: HashMap<u32, f64> = HashMap::new();
for t in &terms {
self.add_typo(t, opts.typo, &mut scores, &sc);
}
for phrase in phrases {
self.add_phrase(phrase, &mut scores, &sc);
}
for pfx in prefixes {
self.add_prefix(pfx, &mut scores, &sc);
}
scores
}
pub fn matches_prefix(
&self,
prefix: &[u8],
limit: usize,
stats: Option<&CorpusStats>,
) -> Vec<TextMatch> {
if limit == 0 || prefix.is_empty() || self.docs.is_empty() {
return Vec::new();
}
let pfx: Vec<u8> = prefix.iter().map(u8::to_ascii_lowercase).collect();
let (n_docs, avgdl) = self.corpus_stats(stats);
let sc = Scope { stats, n_docs, avgdl, want: &[] };
let mut scores: HashMap<u32, f64> = HashMap::new();
self.add_prefix(&pfx, &mut scores, &sc);
self.select_top(&scores, limit, &[], None, None)
}
pub fn query_df_terms(&self, text: &[u8]) -> Vec<Vec<u8>> {
self.query_df_terms_typo(text, 0)
}
pub fn query_df_terms_typo(&self, text: &[u8], typo: u32) -> Vec<Vec<u8>> {
let (bare, phrases, prefixes) = parse_clauses(text);
let mut terms: Vec<Vec<u8>> = Vec::new();
for t in &bare {
if typo == 0 {
terms.push(t.clone());
} else {
terms.extend(self.expand_typo(t, typo).into_iter().map(<[u8]>::to_vec));
}
}
for phrase in &phrases {
terms.extend(phrase.iter().cloned());
}
for pfx in &prefixes {
terms.extend(self.expand_prefix(pfx).into_iter().map(<[u8]>::to_vec));
}
terms.sort();
terms.dedup();
terms
}
pub fn query_df_in(&self, text: &[u8], opts: QueryOpts) -> Vec<(Vec<u8>, u32)> {
let want = self.normalize_scope(opts.fields).unwrap_or_default();
self.query_df_terms_typo(text, opts.typo)
.into_iter()
.map(|t| {
let df = match self.fields.as_ref() {
Some(fs) if !want.is_empty() => fs.docs_in(&t, &want).len(),
_ => self.postings.get(&t).map_or(0, super::Buckets::len),
};
(t, df as u32)
})
.collect()
}
fn normalize_scope(&self, want: &[usize]) -> Option<Vec<usize>> {
let mut w = want.to_vec();
w.sort_unstable();
w.dedup();
if w.is_empty() {
return Some(Vec::new());
}
if self.fields.is_none() {
return (w == [0]).then(Vec::new);
}
Some(w)
}
fn add_phrase(&self, toks: &[Vec<u8>], scores: &mut HashMap<u32, f64>, sc: &Scope) {
let Some(pos) = self.positions.as_ref() else { return };
let Some(anchor) = self.rarest_anchor(toks) else { return };
let distinct = distinct_tokens(toks);
for id in pos.ids(anchor) {
if self.phrase_hit(pos, toks, id, sc) {
*scores.entry(id).or_insert(0.0) += self.clause_score(&distinct, id, sc);
}
}
}
fn phrase_hit(&self, pos: &Positions, toks: &[Vec<u8>], id: u32, sc: &Scope) -> bool {
if !sc.scoped() {
return phrase_occurs(pos, toks, id);
}
let starts = phrase_starts(pos, toks, id);
let len = toks.len() as u32;
starts.iter().any(|&s| self.phrase_in_scope(id, s, len, sc.want))
}
}
impl TextSegment {
pub fn highlight_spans(&self, key: &[u8], query: &[u8]) -> Vec<(usize, Vec<(usize, usize)>)> {
let Some((_, _, fields)) = self.docs.get(key) else {
return Vec::new();
};
let (bare, phrases, prefixes) = parse_clauses(query);
let terms: HashSet<&[u8]> = bare.iter().map(Vec::as_slice).collect();
let mut out = Vec::new();
for (fi, (text, _weight)) in fields.iter().enumerate() {
let mut spans = field_spans(&tokenize_spans(text), &terms, &phrases, &prefixes);
if !spans.is_empty() {
spans.sort_unstable();
spans.dedup();
out.push((fi, spans));
}
}
out
}
}
fn field_spans(
toks: &[(Vec<u8>, usize, usize)],
terms: &HashSet<&[u8]>,
phrases: &[Vec<Vec<u8>>],
prefixes: &[Vec<u8>],
) -> Vec<(usize, usize)> {
let mut spans = Vec::new();
for (t, s, e) in toks {
if terms.contains(t.as_slice()) || prefixes.iter().any(|p| t.starts_with(p.as_slice())) {
spans.push((*s, *e));
}
}
for phrase in phrases {
let last = toks.len().saturating_sub(phrase.len() - 1);
for start in 0..last {
if (0..phrase.len()).all(|k| toks[start + k].0 == phrase[k]) {
for (_, s, e) in &toks[start..start + phrase.len()] {
spans.push((*s, *e));
}
}
}
}
spans
}
fn distinct_tokens(toks: &[Vec<u8>]) -> Vec<Vec<u8>> {
let mut d = toks.to_vec();
d.sort();
d.dedup();
d
}
fn phrase_occurs(pos: &Positions, toks: &[Vec<u8>], id: u32) -> bool {
let Some(first) = pos.blob(&toks[0], id) else {
return false;
};
walk(first).any(|start| {
toks.iter().enumerate().skip(1).all(|(i, t)| {
pos.blob(t, id).is_some_and(|b| walk(b).any(|p| p == start + i as u32))
})
})
}
fn phrase_starts(pos: &Positions, toks: &[Vec<u8>], id: u32) -> HashSet<u32> {
let Some(first) = pos.get(&toks[0], id) else {
return HashSet::new();
};
let mut starts: HashSet<u32> = first.into_iter().collect();
for (i, t) in toks.iter().enumerate().skip(1) {
let Some(offs) = pos.get(t, id) else {
return HashSet::new();
};
let shifted: HashSet<u32> =
offs.iter().filter_map(|&p| p.checked_sub(i as u32)).collect();
starts.retain(|s| shifted.contains(s));
if starts.is_empty() {
return starts;
}
}
starts
}
type Clauses = (Vec<Vec<u8>>, Vec<Vec<Vec<u8>>>, Vec<Vec<u8>>);
fn parse_clauses(text: &[u8]) -> Clauses {
let mut bare: Vec<Vec<u8>> = Vec::new();
let mut phrases: Vec<Vec<Vec<u8>>> = Vec::new();
let mut prefixes: Vec<Vec<u8>> = Vec::new();
let mut plain: Vec<u8> = Vec::new();
let mut i = 0;
while i < text.len() {
if text[i] != b'"' {
plain.push(text[i]);
i += 1;
continue;
}
extend_plain(&plain, &mut bare, &mut prefixes);
plain.clear();
let start = i + 1;
match text[start..].iter().position(|&b| b == b'"') {
Some(off) => {
let toks = tokenize(&text[start..start + off]);
if toks.len() >= 2 {
phrases.push(toks);
} else {
bare.extend(toks);
}
i = start + off + 1;
}
None => {
extend_plain(&text[start..], &mut bare, &mut prefixes);
i = text.len();
}
}
}
extend_plain(&plain, &mut bare, &mut prefixes);
(bare, phrases, prefixes)
}
fn extend_plain(plain: &[u8], bare: &mut Vec<Vec<u8>>, prefixes: &mut Vec<Vec<u8>>) {
for word in plain.split(u8::is_ascii_whitespace) {
match word.strip_suffix(b"*") {
Some(stem) if !stem.is_empty() => {
prefixes.push(stem.iter().map(u8::to_ascii_lowercase).collect());
}
_ => bare.extend(tokenize(word)),
}
}
}