1use crate::index_store::MmapIndexReader;
18use crate::resolve::{
19 entry_has_tags, entry_is_retired, match_entry_with_fields, rank_and_build, tokenize,
20 CorpusStats, SearchResult,
21};
22
23pub fn store_search(
25 reader: &MmapIndexReader,
26 query: &str,
27 artifact_type: Option<&str>,
28 tags: &[String],
29 live_only: bool,
30) -> SearchResult {
31 let timing = crate::timing::enabled();
32 let tokenize_started = timing.then(std::time::Instant::now);
33 let terms = tokenize(query);
34 if let Some(started) = tokenize_started {
35 crate::timing::emit(
36 "search.query_tokenize",
37 started.elapsed(),
38 &[("terms", terms.len() as u64)],
39 );
40 }
41 let empty = || SearchResult {
42 query: query.to_string(),
43 artifact_type: artifact_type.map(str::to_string),
44 matches: Vec::new(),
45 };
46 if terms.is_empty() {
47 return empty();
48 }
49 let tag_filter: Vec<String> = tags.iter().map(|t| crate::pycompat::py_casefold(t)).collect();
50
51 let mut postings = Vec::new();
55 let mut distinct = std::collections::HashSet::new();
56 let mut postings_duration = std::time::Duration::ZERO;
57 let mut merge_duration = std::time::Duration::ZERO;
58 for term in &terms {
59 if !distinct.insert(term.as_str()) {
60 continue;
61 }
62 let started = timing.then(std::time::Instant::now);
63 let decoded = reader.prefix_docids(term);
64 if let Some(started) = started {
65 postings_duration += started.elapsed();
66 }
67 match decoded {
68 Ok(docids) => postings.push(docids),
69 Err(_) => return empty(), }
71 }
72 postings.sort_by_key(std::collections::BTreeSet::len);
73 let started = timing.then(std::time::Instant::now);
74 let mut postings = postings.into_iter();
75 let mut candidates = postings.next().unwrap_or_default();
76 for docids in postings {
77 candidates.retain(|docid| docids.contains(docid));
78 if candidates.is_empty() {
79 break;
80 }
81 }
82 if let Some(started) = started {
83 merge_duration += started.elapsed();
84 }
85 crate::timing::emit(
86 "search.postings_decode",
87 postings_duration,
88 &[("terms", terms.len() as u64)],
89 );
90 crate::timing::emit(
91 "search.candidate_merge",
92 merge_duration,
93 &[("candidates", candidates.len() as u64)],
94 );
95
96 let mut matched = Vec::new();
97 let candidate_count = candidates.len() as u64;
98 let mut row_decode_duration = std::time::Duration::ZERO;
99 let mut row_tokenize_duration = std::time::Duration::ZERO;
100 let mut matching_duration = std::time::Duration::ZERO;
101 for docid in candidates {
102 let started = timing.then(std::time::Instant::now);
103 let decoded = reader.full_entry(docid);
104 if let Some(started) = started {
105 row_decode_duration += started.elapsed();
106 }
107 let Ok(entry) = decoded else {
108 continue;
109 };
110 if let Some(t) = artifact_type {
111 if entry.artifact_type != t {
112 continue;
113 }
114 }
115 if !tag_filter.is_empty() && !entry_has_tags(&entry, &tag_filter) {
116 continue;
117 }
118 let started = timing.then(std::time::Instant::now);
119 let decoded_fields = reader.field_tokens(docid);
120 if let Some(started) = started {
121 row_tokenize_duration += started.elapsed();
122 }
123 let Ok(fields) = decoded_fields else {
124 continue;
125 };
126 let started = timing.then(std::time::Instant::now);
127 let matched_entry = match_entry_with_fields(&entry, &fields, &terms);
128 if let Some(started) = started {
129 matching_duration += started.elapsed();
130 }
131 if let Some(m) = matched_entry {
132 matched.push((entry, fields, m));
133 }
134 }
135 crate::timing::emit(
136 "search.row_decode",
137 row_decode_duration,
138 &[("candidates", candidate_count)],
139 );
140 crate::timing::emit(
141 "search.row_tokenize",
142 row_tokenize_duration,
143 &[("candidates", candidate_count)],
144 );
145 crate::timing::emit(
146 "search.matching",
147 matching_duration,
148 &[("matched", matched.len() as u64)],
149 );
150 if live_only && !matched.is_empty() {
151 matched.retain(|(entry, _, _)| !entry_is_retired(entry));
152 }
153 if matched.is_empty() {
154 return empty();
155 }
156
157 let n = i64::from(reader.doc_count);
160 let mut avglen = [0.0f64; 6];
161 for (i, sum) in reader.field_length_sums.iter().enumerate() {
162 avglen[i] = if n != 0 { *sum as f64 / n as f64 } else { 0.0 };
163 }
164 let mut df: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
165 let mut occurrences: std::collections::HashMap<&str, i64> = std::collections::HashMap::new();
166 for term in &terms {
167 *occurrences.entry(term.as_str()).or_insert(0) += 1;
168 }
169 for (term, occ) in occurrences {
170 let doc_count = reader
171 .prefix_docids(term)
172 .map(|d| d.len() as i64)
173 .unwrap_or(0);
174 df.insert(term.to_string(), occ * doc_count);
176 }
177 let stats = CorpusStats { n, df, avglen };
178
179 let scored: Vec<_> = matched
180 .iter()
181 .map(|(entry, fields, m)| (entry, fields, m.clone()))
182 .collect();
183 rank_and_build(query, artifact_type, scored, &terms, &stats)
184}
185
186pub fn store_find_decisions(reader: &MmapIndexReader, topic: &str) -> SearchResult {
190 let mut result = store_search(reader, topic, Some("decision"), &[], false);
191 let live: std::collections::HashSet<String> =
192 reader.live_decision_paths().unwrap_or_default().into_iter().collect();
193 result.matches.retain(|m| live.contains(&m.path));
194 result
195}
196
197pub fn store_resolve(
200 reader: &MmapIndexReader,
201 artifact_id: &str,
202) -> crate::resolve::ResolutionResult {
203 use crate::resolve::{OUTCOME_DUPLICATE, OUTCOME_NOT_FOUND, OUTCOME_RESOLVED};
204 let wanted =
205 crate::pycompat::py_casefold(crate::pycompat::py_strip(artifact_id));
206 let docids = reader.alias_docids(&wanted).unwrap_or_default();
207 if docids.is_empty() {
208 return crate::resolve::ResolutionResult {
209 artifact_id: artifact_id.to_string(),
210 outcome: OUTCOME_NOT_FOUND,
211 artifact: None,
212 duplicate_paths: Vec::new(),
213 };
214 }
215 if docids.len() > 1 {
216 let mut paths: Vec<String> = docids
217 .iter()
218 .filter_map(|&docid| reader.entry_path(docid).ok())
219 .collect();
220 paths.sort();
221 return crate::resolve::ResolutionResult {
222 artifact_id: artifact_id.to_string(),
223 outcome: OUTCOME_DUPLICATE,
224 artifact: None,
225 duplicate_paths: paths,
226 };
227 }
228 match reader.identity_entry(docids[0]) {
229 Ok(entry) => crate::resolve::ResolutionResult {
235 artifact_id: artifact_id.to_string(),
236 outcome: OUTCOME_RESOLVED,
237 artifact: Some(crate::resolve::resolved_from_entry(&entry)),
238 duplicate_paths: Vec::new(),
239 },
240 Err(_) => crate::resolve::ResolutionResult {
241 artifact_id: artifact_id.to_string(),
242 outcome: OUTCOME_NOT_FOUND,
243 artifact: None,
244 duplicate_paths: Vec::new(),
245 },
246 }
247}
248
249pub fn store_identity_entries(
252 reader: &MmapIndexReader,
253) -> Vec<crate::resolve::IndexEntry> {
254 (0..reader.doc_count)
255 .filter_map(|docid| reader.identity_entry(docid).ok())
256 .collect()
257}
258
259pub fn find_decisions_in(
262 entries: &[crate::resolve::IndexEntry],
263 live_paths: &[String],
264 topic: &str,
265) -> SearchResult {
266 let mut result = crate::resolve::search_index(entries, topic, Some("decision"), &[]);
267 let live: std::collections::HashSet<&str> = live_paths.iter().map(String::as_str).collect();
268 result.matches.retain(|m| live.contains(m.path.as_str()));
269 result
270}