uqa_storage/inverted_index/
analysis.rs1use std::collections::BTreeMap;
10
11use uqa_analysis::{
12 AnalysisError, AnalyzerFingerprint, CompiledAnalyzer, SourceOffsets, TokenLengthPolicy,
13};
14use uqa_core::{TokenOccurrence, TokenOffsets};
15
16use crate::{StorageBackendError, StorageBackendResult, TokenTermKey};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct IndexedFieldMetadata {
21 pub analyzer_fingerprint: AnalyzerFingerprint,
22 pub occurrence_format_version: u8,
23 pub length_policy: TokenLengthPolicy,
24 pub length: u64,
25 pub final_offsets: TokenOffsets,
26 pub final_position_increment: u32,
27}
28
29impl IndexedFieldMetadata {
30 pub fn new(analyzer: &CompiledAnalyzer, field: &AnalyzedField) -> Self {
31 Self {
32 analyzer_fingerprint: analyzer.descriptor().fingerprint(),
33 occurrence_format_version: crate::clustered_postings::OCCURRENCE_FORMAT_VERSION,
34 length_policy: analyzer.descriptor().length_policy(),
35 length: field.length,
36 final_offsets: field.final_offsets,
37 final_position_increment: field.final_position_increment,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AnalyzedField {
44 pub length: u64,
45 pub terms: BTreeMap<TokenTermKey, Vec<TokenOccurrence>>,
46 pub final_offsets: TokenOffsets,
47 pub final_position_increment: u32,
48}
49
50pub fn analyze_index_field(
64 analyzer: &CompiledAnalyzer,
65 text: &str,
66) -> StorageBackendResult<AnalyzedField> {
67 analyze_index_field_with_poll(analyzer, text, || Ok(()))
68}
69
70pub fn analyze_index_field_cancellable(
72 analyzer: &CompiledAnalyzer,
73 text: &str,
74 cancellation: &uqa_core::CancellationToken,
75) -> StorageBackendResult<AnalyzedField> {
76 analyze_index_field_with_poll(analyzer, text, || {
77 cancellation.check().map_err(|_| AnalysisError::Cancelled)
78 })
79 .map_err(|error| match error {
80 StorageBackendError::Analysis(AnalysisError::Cancelled) => {
81 StorageBackendError::Cancelled(uqa_core::QueryCancelled)
82 }
83 other => other,
84 })
85}
86
87fn analyze_index_field_with_poll(
88 analyzer: &CompiledAnalyzer,
89 text: &str,
90 mut poll: impl FnMut() -> Result<(), AnalysisError>,
91) -> StorageBackendResult<AnalyzedField> {
92 let output = analyzer
93 .analyze_tokens_budgeted(
94 text,
95 &uqa_core::memory::MemoryBudget::new(usize::MAX),
96 &mut poll,
97 )?
98 .into_parts()
99 .0;
100 let mut staged = AnalyzedField {
101 length: 0,
102 terms: BTreeMap::new(),
103 final_offsets: source_offsets(output.final_offsets())?,
104 final_position_increment: output.final_position_increment(),
105 };
106 let mut position = -1_i64;
107 for token in output.tokens() {
108 poll()?;
109 position = position
110 .checked_add(i64::from(token.position_increment()))
111 .ok_or(AnalysisError::TokenPositionOverflow)?;
112 let position = u32::try_from(position).map_err(|_| AnalysisError::TokenPositionOverflow)?;
113 let offsets = token.offsets().map(source_offsets).transpose()?;
114 if offsets.is_some_and(|offsets| {
115 offsets.end_utf8 > staged.final_offsets.end_utf8
116 || offsets.end_utf16 > staged.final_offsets.end_utf16
117 }) {
118 return Err(StorageBackendError::Other(
119 "token occurrence exceeds original source".into(),
120 ));
121 }
122 let occurrence = TokenOccurrence {
123 position,
124 position_length: token.position_length(),
125 offsets,
126 };
127 occurrence
128 .validate()
129 .map_err(|error| StorageBackendError::Other(error.to_string()))?;
130 staged
131 .terms
132 .entry(TokenTermKey::from_term(token.term()))
133 .or_default()
134 .push(occurrence);
135 if analyzer.descriptor().length_policy() == TokenLengthPolicy::EmittedTokens
136 || token.position_increment() > 0
137 {
138 staged.length = staged
139 .length
140 .checked_add(1)
141 .ok_or_else(|| super::counter_error("document token count"))?;
142 }
143 }
144 poll()?;
145 Ok(staged)
146}
147
148mod query;
149pub use query::{
150 analyze_query_graph, analyze_query_graph_budgeted, analyze_query_terms,
151 analyze_query_terms_budgeted,
152};
153
154fn source_offsets(offsets: &SourceOffsets) -> StorageBackendResult<TokenOffsets> {
155 fn offset(value: usize) -> StorageBackendResult<u64> {
156 u64::try_from(value).map_err(|_| super::counter_error("source offset"))
157 }
158 Ok(TokenOffsets {
159 start_utf8: offset(offsets.utf8.start)?,
160 end_utf8: offset(offsets.utf8.end)?,
161 start_utf16: offset(offsets.utf16.start)?,
162 end_utf16: offset(offsets.utf16.end)?,
163 })
164}
165
166#[cfg(test)]
167#[path = "analysis/index_tests.rs"]
168mod index_tests;