1use std::collections::{BTreeMap, BTreeSet, HashMap};
15use std::io::BufWriter;
16
17use memmap2::Mmap;
18use roaring::RoaringBitmap;
19use serde::{Deserialize, Serialize};
20
21use crate::error::{Error, Result};
22use crate::fsutil::{write_atomic, AtomicFile};
23use crate::paths::Paths;
24use crate::trigram::{Trigram, TrigramQuery};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct DocMeta {
29 pub path: String,
31 pub lang: String,
33 pub size: u64,
34 pub hash: u64,
36 pub lines: u32,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct SymbolEntry {
42 pub doc_id: u32,
43 pub name: String,
44 pub kind: String,
46 pub line_start: u32,
47 pub line_end: u32,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub container: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub signature: Option<String>,
54}
55
56#[derive(Debug, Clone)]
58pub struct RawSymbol {
59 pub name: String,
60 pub kind: String,
61 pub line_start: u32,
62 pub line_end: u32,
63 pub container: Option<String>,
64 pub signature: Option<String>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum RefKind {
73 Call,
74 Import,
75}
76
77impl RefKind {
78 pub fn as_str(self) -> &'static str {
79 match self {
80 RefKind::Call => "call",
81 RefKind::Import => "import",
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct RefEntry {
89 pub doc_id: u32,
90 pub name: String,
91 pub kind: RefKind,
92 pub line: u32,
93 pub column: u32,
94}
95
96#[derive(Debug, Clone)]
98pub struct RawRef {
99 pub name: String,
100 pub kind: RefKind,
101 pub line: u32,
102 pub column: u32,
103}
104
105#[derive(Default)]
107pub struct SegmentWriter {
108 docs: Vec<DocMeta>,
109 syms: Vec<SymbolEntry>,
110 refs: Vec<RefEntry>,
111 postings: BTreeMap<Trigram, RoaringBitmap>,
112}
113
114impl SegmentWriter {
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn is_empty(&self) -> bool {
120 self.docs.is_empty()
121 }
122
123 pub fn doc_count(&self) -> usize {
124 self.docs.len()
125 }
126
127 pub fn symbol_count(&self) -> usize {
128 self.syms.len()
129 }
130
131 pub fn add_doc(
133 &mut self,
134 meta: DocMeta,
135 trigrams: &BTreeSet<Trigram>,
136 symbols: Vec<RawSymbol>,
137 refs: Vec<RawRef>,
138 ) -> u32 {
139 let doc_id = self.docs.len() as u32;
140 self.docs.push(meta);
141 for t in trigrams {
142 self.postings.entry(*t).or_default().insert(doc_id);
143 }
144 for s in symbols {
145 self.syms.push(SymbolEntry {
146 doc_id,
147 name: s.name,
148 kind: s.kind,
149 line_start: s.line_start,
150 line_end: s.line_end,
151 container: s.container,
152 signature: s.signature,
153 });
154 }
155 for r in refs {
156 self.refs.push(RefEntry {
157 doc_id,
158 name: r.name,
159 kind: r.kind,
160 line: r.line,
161 column: r.column,
162 });
163 }
164 doc_id
165 }
166
167 pub fn write(self, paths: &Paths, seg_id: u64) -> Result<()> {
169 std::fs::create_dir_all(paths.segments_dir())
170 .map_err(|e| Error::io(paths.segments_dir(), e))?;
171 let (post_blob, fst_entries) = build_postings_blob(self.postings)?;
172 write_segment_files(
173 paths,
174 seg_id,
175 &self.docs,
176 &self.syms,
177 &self.refs,
178 &fst_entries,
179 &post_blob,
180 )
181 }
182}
183
184fn write_segment_files(
187 paths: &Paths,
188 seg_id: u64,
189 docs: &[DocMeta],
190 syms: &[SymbolEntry],
191 refs: &[RefEntry],
192 fst_entries: &[(Trigram, u64)],
193 post_blob: &[u8],
194) -> Result<()> {
195 let fst_path = paths.fst_file(seg_id);
198 let mut fst_out = AtomicFile::create(&fst_path)?;
199 let mut builder = fst::MapBuilder::new(BufWriter::new(fst_out.file()))?;
200 for (tri, offset) in fst_entries {
201 builder.insert(tri, *offset)?;
202 }
203 builder.finish()?;
204 fst_out.commit()?;
205
206 write_atomic(&paths.post_file(seg_id), post_blob)?;
207 write_atomic(&paths.docs_file(seg_id), &serde_json::to_vec(docs)?)?;
208 write_atomic(&paths.syms_file(seg_id), &serde_json::to_vec(syms)?)?;
209 write_atomic(&paths.refs_file(seg_id), &serde_json::to_vec(refs)?)?;
210
211 let mut live = RoaringBitmap::new();
213 live.insert_range(0..docs.len() as u32);
214 write_bitmap(&paths.live_file(seg_id), &live)?;
215
216 Ok(())
217}
218
219type PostingsBlob = (Vec<u8>, Vec<(Trigram, u64)>);
222
223fn build_postings_blob(postings: BTreeMap<Trigram, RoaringBitmap>) -> Result<PostingsBlob> {
226 let mut post_blob: Vec<u8> = Vec::new();
227 let mut fst_entries: Vec<(Trigram, u64)> = Vec::with_capacity(postings.len());
228 for (tri, mut bm) in postings.into_iter() {
229 bm.optimize();
230 let offset = post_blob.len() as u64;
231 bm.serialize_into(&mut post_blob)
232 .map_err(|e| Error::other(format!("roaring serialize: {e}")))?;
233 fst_entries.push((tri, offset));
234 }
235 Ok((post_blob, fst_entries))
236}
237
238pub(crate) fn write_segment_from_parts(
240 paths: &Paths,
241 seg_id: u64,
242 docs: &[DocMeta],
243 syms: &[SymbolEntry],
244 refs: &[RefEntry],
245 postings: BTreeMap<Trigram, RoaringBitmap>,
246) -> Result<()> {
247 std::fs::create_dir_all(paths.segments_dir())
248 .map_err(|e| Error::io(paths.segments_dir(), e))?;
249 let (post_blob, fst_entries) = build_postings_blob(postings)?;
250 write_segment_files(paths, seg_id, docs, syms, refs, &fst_entries, &post_blob)
251}
252
253pub struct Segment {
255 pub id: u64,
256 fst: fst::Map<Mmap>,
257 post: Mmap,
258 pub docs: Vec<DocMeta>,
259 pub syms: Vec<SymbolEntry>,
260 pub refs: Vec<RefEntry>,
261 live: RoaringBitmap,
262 sym_order: Vec<u32>,
266 sym_start: Vec<u32>,
269 ref_order: Vec<u32>,
271 ref_start: Vec<u32>,
273 sym_name_lower: Vec<String>,
275 call_by_name: HashMap<Box<str>, Vec<u32>>,
279}
280
281impl Segment {
282 pub fn open(paths: &Paths, seg_id: u64) -> Result<Segment> {
283 let fst_path = paths.fst_file(seg_id);
284 let fst_file = std::fs::File::open(&fst_path).map_err(|e| Error::io(&fst_path, e))?;
285 let fst_mmap = unsafe { Mmap::map(&fst_file).map_err(|e| Error::io(&fst_path, e))? };
286 let fst = fst::Map::new(fst_mmap)?;
287
288 let post_path = paths.post_file(seg_id);
289 let post_file = std::fs::File::open(&post_path).map_err(|e| Error::io(&post_path, e))?;
290 let post = unsafe { Mmap::map(&post_file).map_err(|e| Error::io(&post_path, e))? };
291
292 let docs_path = paths.docs_file(seg_id);
293 let docs: Vec<DocMeta> = serde_json::from_slice(
294 &std::fs::read(&docs_path).map_err(|e| Error::io(&docs_path, e))?,
295 )?;
296
297 let syms_path = paths.syms_file(seg_id);
298 let syms: Vec<SymbolEntry> = serde_json::from_slice(
299 &std::fs::read(&syms_path).map_err(|e| Error::io(&syms_path, e))?,
300 )?;
301
302 let refs_path = paths.refs_file(seg_id);
305 let refs: Vec<RefEntry> = match std::fs::read(&refs_path) {
306 Ok(bytes) => serde_json::from_slice(&bytes)?,
307 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
308 Err(e) => return Err(Error::io(&refs_path, e)),
309 };
310
311 let live = read_bitmap(&paths.live_file(seg_id))?;
312
313 let (sym_order, sym_start) = build_doc_index(docs.len(), syms.iter().map(|s| s.doc_id));
314 let (ref_order, ref_start) = build_doc_index(docs.len(), refs.iter().map(|r| r.doc_id));
315 let sym_name_lower = syms.iter().map(|s| s.name.to_ascii_lowercase()).collect();
316
317 let mut call_by_name: HashMap<Box<str>, Vec<u32>> = HashMap::new();
318 for (i, r) in refs.iter().enumerate() {
319 if r.kind == RefKind::Call {
320 call_by_name
321 .entry(r.name.as_str().into())
322 .or_default()
323 .push(i as u32);
324 }
325 }
326
327 Ok(Segment {
328 id: seg_id,
329 fst,
330 post,
331 docs,
332 syms,
333 refs,
334 live,
335 sym_order,
336 sym_start,
337 ref_order,
338 ref_start,
339 sym_name_lower,
340 call_by_name,
341 })
342 }
343
344 pub fn doc(&self, doc_id: u32) -> Option<&DocMeta> {
345 self.docs.get(doc_id as usize)
346 }
347
348 pub fn doc_syms(&self, doc_id: u32) -> impl Iterator<Item = &SymbolEntry> {
350 let d = doc_id as usize;
351 let (lo, hi) = if d + 1 < self.sym_start.len() {
352 (self.sym_start[d] as usize, self.sym_start[d + 1] as usize)
353 } else {
354 (0, 0)
355 };
356 self.sym_order[lo..hi]
357 .iter()
358 .map(move |&i| &self.syms[i as usize])
359 }
360
361 pub fn doc_refs(&self, doc_id: u32) -> impl Iterator<Item = &RefEntry> {
363 let d = doc_id as usize;
364 let (lo, hi) = if d + 1 < self.ref_start.len() {
365 (self.ref_start[d] as usize, self.ref_start[d + 1] as usize)
366 } else {
367 (0, 0)
368 };
369 self.ref_order[lo..hi]
370 .iter()
371 .map(move |&i| &self.refs[i as usize])
372 }
373
374 pub fn calls_to(&self, name: &str) -> impl Iterator<Item = &RefEntry> {
378 self.call_by_name
379 .get(name)
380 .into_iter()
381 .flatten()
382 .map(move |&i| &self.refs[i as usize])
383 }
384
385 pub fn sym_name_lower(&self, i: usize) -> &str {
387 &self.sym_name_lower[i]
388 }
389
390 pub fn is_live(&self, doc_id: u32) -> bool {
391 self.live.contains(doc_id)
392 }
393
394 pub fn live_count(&self) -> u64 {
395 self.live.len()
396 }
397
398 fn posting(&self, tri: Trigram) -> Result<Option<RoaringBitmap>> {
400 match self.fst.get(tri) {
401 Some(offset) => Ok(Some(self.posting_at(offset)?)),
402 None => Ok(None),
403 }
404 }
405
406 fn posting_at(&self, offset: u64) -> Result<RoaringBitmap> {
408 let slice = &self.post[offset as usize..];
409 RoaringBitmap::deserialize_from(slice)
410 .map_err(|e| Error::Corrupt(format!("posting list: {e}")))
411 }
412
413 pub(crate) fn remap_postings(
416 &self,
417 remap: &std::collections::HashMap<u32, u32>,
418 out: &mut BTreeMap<Trigram, RoaringBitmap>,
419 ) -> Result<()> {
420 use fst::Streamer;
421 let mut stream = self.fst.stream();
422 while let Some((key, offset)) = stream.next() {
423 if key.len() != 3 {
424 continue;
425 }
426 let tri: Trigram = [key[0], key[1], key[2]];
427 let bm = self.posting_at(offset)?;
428 let dest = out.entry(tri).or_default();
429 for old in bm.iter() {
430 if let Some(&new_id) = remap.get(&old) {
431 dest.insert(new_id);
432 }
433 }
434 }
435 Ok(())
436 }
437
438 pub fn all_live(&self) -> RoaringBitmap {
440 self.live.clone()
441 }
442
443 fn intersect_group(&self, group: &[Trigram]) -> Result<RoaringBitmap> {
446 let mut lists: Vec<RoaringBitmap> = Vec::with_capacity(group.len());
447 for tri in group {
448 lists.push(self.posting(*tri)?.unwrap_or_default());
449 }
450 lists.sort_by_key(|b| b.len());
451 let mut acc: Option<RoaringBitmap> = None;
452 for p in lists {
453 acc = Some(match acc.take() {
454 None => p,
455 Some(a) => a & p,
456 });
457 if acc.as_ref().map(|b| b.is_empty()).unwrap_or(false) {
458 break;
459 }
460 }
461 Ok(acc.unwrap_or_default())
462 }
463
464 fn union_clause(&self, clause: &[Trigram]) -> Result<RoaringBitmap> {
466 let mut acc = RoaringBitmap::new();
467 for tri in clause {
468 if let Some(bm) = self.posting(*tri)? {
469 acc |= bm;
470 }
471 }
472 Ok(acc)
473 }
474
475 pub fn candidates(&self, query: &TrigramQuery) -> Result<RoaringBitmap> {
478 if query.is_unconstrained() {
479 return Ok(self.all_live());
480 }
481
482 let dnf_active =
485 !query.or_groups.is_empty() && query.or_groups.iter().all(|g| !g.is_empty());
486 let mut result = if dnf_active {
487 let mut acc: Option<RoaringBitmap> = None;
488 for group in &query.or_groups {
489 let g = self.intersect_group(group)?;
490 acc = Some(match acc.take() {
491 None => g,
492 Some(a) => a | g,
493 });
494 }
495 acc.unwrap_or_default()
496 } else {
497 self.all_live()
498 };
499
500 let cnf_active =
502 !query.and_clauses.is_empty() && query.and_clauses.iter().all(|c| !c.is_empty());
503 if cnf_active {
504 for clause in &query.and_clauses {
505 result &= self.union_clause(clause)?;
506 if result.is_empty() {
507 break;
508 }
509 }
510 }
511
512 result &= &self.live;
513 Ok(result)
514 }
515}
516
517fn build_doc_index(n: usize, doc_ids: impl Iterator<Item = u32> + Clone) -> (Vec<u32>, Vec<u32>) {
520 let mut counts = vec![0u32; n + 1];
521 let mut total = 0usize;
522 for d in doc_ids.clone() {
523 let d = d as usize;
524 if d < n {
525 counts[d] += 1;
526 total += 1;
527 }
528 }
529 let mut start = vec![0u32; n + 1];
531 let mut acc = 0u32;
532 for d in 0..n {
533 start[d] = acc;
534 acc += counts[d];
535 }
536 start[n] = acc;
537 let mut order = vec![0u32; total];
539 let mut cursor: Vec<u32> = start[..n].to_vec();
540 for (i, d) in doc_ids.enumerate() {
541 let d = d as usize;
542 if d < n {
543 order[cursor[d] as usize] = i as u32;
544 cursor[d] += 1;
545 }
546 }
547 (order, start)
548}
549
550pub(crate) fn write_bitmap(path: &std::path::Path, bm: &RoaringBitmap) -> Result<()> {
551 let mut buf = Vec::with_capacity(bm.serialized_size());
552 bm.serialize_into(&mut buf)
553 .map_err(|e| Error::other(format!("roaring serialize: {e}")))?;
554 write_atomic(path, &buf)
555}
556
557pub(crate) fn read_bitmap(path: &std::path::Path) -> Result<RoaringBitmap> {
558 let bytes = std::fs::read(path).map_err(|e| Error::io(path, e))?;
559 RoaringBitmap::deserialize_from(&bytes[..])
560 .map_err(|e| Error::Corrupt(format!("live bitmap: {e}")))
561}