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 pub container: Option<String>,
54 pub signature: Option<String>,
56}
57
58#[derive(Debug, Clone)]
60pub struct RawSymbol {
61 pub name: String,
62 pub kind: String,
63 pub line_start: u32,
64 pub line_end: u32,
65 pub container: Option<String>,
66 pub signature: Option<String>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "lowercase")]
74pub enum RefKind {
75 Call,
76 Import,
77}
78
79impl RefKind {
80 pub fn as_str(self) -> &'static str {
81 match self {
82 RefKind::Call => "call",
83 RefKind::Import => "import",
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct RefEntry {
91 pub doc_id: u32,
92 pub name: String,
93 pub kind: RefKind,
94 pub line: u32,
95 pub column: u32,
96}
97
98#[derive(Debug, Clone)]
100pub struct RawRef {
101 pub name: String,
102 pub kind: RefKind,
103 pub line: u32,
104 pub column: u32,
105}
106
107#[derive(Default)]
109pub struct SegmentWriter {
110 docs: Vec<DocMeta>,
111 syms: Vec<SymbolEntry>,
112 refs: Vec<RefEntry>,
113 postings: BTreeMap<Trigram, RoaringBitmap>,
114}
115
116impl SegmentWriter {
117 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn is_empty(&self) -> bool {
122 self.docs.is_empty()
123 }
124
125 pub fn doc_count(&self) -> usize {
126 self.docs.len()
127 }
128
129 pub fn symbol_count(&self) -> usize {
130 self.syms.len()
131 }
132
133 pub fn add_doc(
135 &mut self,
136 meta: DocMeta,
137 trigrams: &BTreeSet<Trigram>,
138 symbols: Vec<RawSymbol>,
139 refs: Vec<RawRef>,
140 ) -> u32 {
141 let doc_id = self.docs.len() as u32;
142 self.docs.push(meta);
143 for t in trigrams {
144 self.postings.entry(*t).or_default().insert(doc_id);
145 }
146 for s in symbols {
147 self.syms.push(SymbolEntry {
148 doc_id,
149 name: s.name,
150 kind: s.kind,
151 line_start: s.line_start,
152 line_end: s.line_end,
153 container: s.container,
154 signature: s.signature,
155 });
156 }
157 for r in refs {
158 self.refs.push(RefEntry {
159 doc_id,
160 name: r.name,
161 kind: r.kind,
162 line: r.line,
163 column: r.column,
164 });
165 }
166 doc_id
167 }
168
169 pub fn write(self, paths: &Paths, seg_id: u64) -> Result<()> {
171 std::fs::create_dir_all(paths.segments_dir())
172 .map_err(|e| Error::io(paths.segments_dir(), e))?;
173 let (post_blob, fst_entries) = build_postings_blob(self.postings)?;
174 write_segment_files(
175 paths,
176 seg_id,
177 &self.docs,
178 &self.syms,
179 &self.refs,
180 &fst_entries,
181 &post_blob,
182 )
183 }
184}
185
186fn write_segment_files(
189 paths: &Paths,
190 seg_id: u64,
191 docs: &[DocMeta],
192 syms: &[SymbolEntry],
193 refs: &[RefEntry],
194 fst_entries: &[(Trigram, u64)],
195 post_blob: &[u8],
196) -> Result<()> {
197 let fst_path = paths.fst_file(seg_id);
200 let mut fst_out = AtomicFile::create(&fst_path)?;
201 let mut builder = fst::MapBuilder::new(BufWriter::new(fst_out.file()))?;
202 for (tri, offset) in fst_entries {
203 builder.insert(tri, *offset)?;
204 }
205 builder.finish()?;
206 fst_out.commit()?;
207
208 write_atomic(&paths.post_file(seg_id), post_blob)?;
209 write_atomic(&paths.docs_file(seg_id), &postcard::to_allocvec(docs)?)?;
213 write_atomic(&paths.syms_file(seg_id), &postcard::to_allocvec(syms)?)?;
214 write_atomic(&paths.refs_file(seg_id), &postcard::to_allocvec(refs)?)?;
215
216 let mut live = RoaringBitmap::new();
218 live.insert_range(0..docs.len() as u32);
219 write_bitmap(&paths.live_file(seg_id), &live)?;
220
221 Ok(())
222}
223
224type PostingsBlob = (Vec<u8>, Vec<(Trigram, u64)>);
227
228fn build_postings_blob(postings: BTreeMap<Trigram, RoaringBitmap>) -> Result<PostingsBlob> {
231 let mut post_blob: Vec<u8> = Vec::new();
232 let mut fst_entries: Vec<(Trigram, u64)> = Vec::with_capacity(postings.len());
233 for (tri, mut bm) in postings.into_iter() {
234 bm.optimize();
235 let offset = post_blob.len() as u64;
236 bm.serialize_into(&mut post_blob)
237 .map_err(|e| Error::other(format!("roaring serialize: {e}")))?;
238 fst_entries.push((tri, offset));
239 }
240 Ok((post_blob, fst_entries))
241}
242
243pub(crate) fn write_segment_from_parts(
245 paths: &Paths,
246 seg_id: u64,
247 docs: &[DocMeta],
248 syms: &[SymbolEntry],
249 refs: &[RefEntry],
250 postings: BTreeMap<Trigram, RoaringBitmap>,
251) -> Result<()> {
252 std::fs::create_dir_all(paths.segments_dir())
253 .map_err(|e| Error::io(paths.segments_dir(), e))?;
254 let (post_blob, fst_entries) = build_postings_blob(postings)?;
255 write_segment_files(paths, seg_id, docs, syms, refs, &fst_entries, &post_blob)
256}
257
258pub struct Segment {
260 pub id: u64,
261 fst: fst::Map<Mmap>,
262 post: Mmap,
263 pub docs: Vec<DocMeta>,
264 pub syms: Vec<SymbolEntry>,
265 pub refs: Vec<RefEntry>,
266 live: RoaringBitmap,
267 sym_order: Vec<u32>,
271 sym_start: Vec<u32>,
274 ref_order: Vec<u32>,
276 ref_start: Vec<u32>,
278 sym_name_lower: Vec<String>,
280 call_by_name: HashMap<Box<str>, Vec<u32>>,
284}
285
286impl Segment {
287 pub fn open(paths: &Paths, seg_id: u64) -> Result<Segment> {
288 let fst_path = paths.fst_file(seg_id);
289 let fst_file = std::fs::File::open(&fst_path).map_err(|e| Error::io(&fst_path, e))?;
290 let fst_mmap = unsafe { Mmap::map(&fst_file).map_err(|e| Error::io(&fst_path, e))? };
291 let fst = fst::Map::new(fst_mmap)?;
292 fst.as_fst()
299 .verify()
300 .map_err(|e| Error::Corrupt(format!("fst checksum: {e}")))?;
301
302 let post_path = paths.post_file(seg_id);
303 let post_file = std::fs::File::open(&post_path).map_err(|e| Error::io(&post_path, e))?;
304 let post = unsafe { Mmap::map(&post_file).map_err(|e| Error::io(&post_path, e))? };
305
306 let docs_path = paths.docs_file(seg_id);
307 let docs: Vec<DocMeta> = postcard::from_bytes(
308 &std::fs::read(&docs_path).map_err(|e| Error::io(&docs_path, e))?,
309 )?;
310
311 let syms_path = paths.syms_file(seg_id);
312 let syms: Vec<SymbolEntry> = postcard::from_bytes(
313 &std::fs::read(&syms_path).map_err(|e| Error::io(&syms_path, e))?,
314 )?;
315
316 let refs_path = paths.refs_file(seg_id);
319 let refs: Vec<RefEntry> = match std::fs::read(&refs_path) {
320 Ok(bytes) => postcard::from_bytes(&bytes)?,
321 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
322 Err(e) => return Err(Error::io(&refs_path, e)),
323 };
324
325 let live = read_bitmap(&paths.live_file(seg_id))?;
326
327 let (sym_order, sym_start) = build_doc_index(docs.len(), syms.iter().map(|s| s.doc_id));
328 let (ref_order, ref_start) = build_doc_index(docs.len(), refs.iter().map(|r| r.doc_id));
329 let sym_name_lower = syms.iter().map(|s| s.name.to_ascii_lowercase()).collect();
330
331 let mut call_by_name: HashMap<Box<str>, Vec<u32>> = HashMap::new();
332 for (i, r) in refs.iter().enumerate() {
333 if r.kind == RefKind::Call {
334 call_by_name
335 .entry(r.name.as_str().into())
336 .or_default()
337 .push(i as u32);
338 }
339 }
340
341 Ok(Segment {
342 id: seg_id,
343 fst,
344 post,
345 docs,
346 syms,
347 refs,
348 live,
349 sym_order,
350 sym_start,
351 ref_order,
352 ref_start,
353 sym_name_lower,
354 call_by_name,
355 })
356 }
357
358 pub fn doc(&self, doc_id: u32) -> Option<&DocMeta> {
359 self.docs.get(doc_id as usize)
360 }
361
362 pub fn doc_syms(&self, doc_id: u32) -> impl Iterator<Item = &SymbolEntry> {
364 let d = doc_id as usize;
365 let (lo, hi) = if d + 1 < self.sym_start.len() {
366 (self.sym_start[d] as usize, self.sym_start[d + 1] as usize)
367 } else {
368 (0, 0)
369 };
370 self.sym_order[lo..hi]
371 .iter()
372 .map(move |&i| &self.syms[i as usize])
373 }
374
375 pub fn doc_refs(&self, doc_id: u32) -> impl Iterator<Item = &RefEntry> {
377 let d = doc_id as usize;
378 let (lo, hi) = if d + 1 < self.ref_start.len() {
379 (self.ref_start[d] as usize, self.ref_start[d + 1] as usize)
380 } else {
381 (0, 0)
382 };
383 self.ref_order[lo..hi]
384 .iter()
385 .map(move |&i| &self.refs[i as usize])
386 }
387
388 pub fn calls_to(&self, name: &str) -> impl Iterator<Item = &RefEntry> {
392 self.call_by_name
393 .get(name)
394 .into_iter()
395 .flatten()
396 .map(move |&i| &self.refs[i as usize])
397 }
398
399 pub fn sym_name_lower(&self, i: usize) -> &str {
401 &self.sym_name_lower[i]
402 }
403
404 pub fn is_live(&self, doc_id: u32) -> bool {
405 self.live.contains(doc_id)
406 }
407
408 pub fn live_count(&self) -> u64 {
409 self.live.len()
410 }
411
412 fn posting(&self, tri: Trigram) -> Result<Option<RoaringBitmap>> {
414 match self.fst.get(tri) {
415 Some(offset) => Ok(Some(self.posting_at(offset)?)),
416 None => Ok(None),
417 }
418 }
419
420 fn posting_at(&self, offset: u64) -> Result<RoaringBitmap> {
427 let start = offset as usize;
428 let slice = self.post.get(start..).ok_or_else(|| {
429 Error::Corrupt(format!(
430 "posting offset {start} out of range for postings blob of length {}",
431 self.post.len()
432 ))
433 })?;
434 RoaringBitmap::deserialize_from(slice)
435 .map_err(|e| Error::Corrupt(format!("posting list: {e}")))
436 }
437
438 pub(crate) fn remap_postings(
441 &self,
442 remap: &std::collections::HashMap<u32, u32>,
443 out: &mut BTreeMap<Trigram, RoaringBitmap>,
444 ) -> Result<()> {
445 use fst::Streamer;
446 let mut stream = self.fst.stream();
447 while let Some((key, offset)) = stream.next() {
448 if key.len() != 3 {
449 continue;
450 }
451 let tri: Trigram = [key[0], key[1], key[2]];
452 let bm = self.posting_at(offset)?;
453 let dest = out.entry(tri).or_default();
454 for old in bm.iter() {
455 if let Some(&new_id) = remap.get(&old) {
456 dest.insert(new_id);
457 }
458 }
459 }
460 Ok(())
461 }
462
463 pub fn all_live(&self) -> RoaringBitmap {
465 self.live.clone()
466 }
467
468 fn intersect_group(&self, group: &[Trigram]) -> Result<RoaringBitmap> {
471 let mut lists: Vec<RoaringBitmap> = Vec::with_capacity(group.len());
472 for tri in group {
473 lists.push(self.posting(*tri)?.unwrap_or_default());
474 }
475 lists.sort_by_key(|b| b.len());
476 let mut acc: Option<RoaringBitmap> = None;
477 for p in lists {
478 acc = Some(match acc.take() {
479 None => p,
480 Some(a) => a & p,
481 });
482 if acc.as_ref().map(|b| b.is_empty()).unwrap_or(false) {
483 break;
484 }
485 }
486 Ok(acc.unwrap_or_default())
487 }
488
489 fn union_clause(&self, clause: &[Trigram]) -> Result<RoaringBitmap> {
491 let mut acc = RoaringBitmap::new();
492 for tri in clause {
493 if let Some(bm) = self.posting(*tri)? {
494 acc |= bm;
495 }
496 }
497 Ok(acc)
498 }
499
500 pub fn candidates(&self, query: &TrigramQuery) -> Result<RoaringBitmap> {
503 if query.is_unconstrained() {
504 return Ok(self.all_live());
505 }
506
507 let dnf_active =
510 !query.or_groups.is_empty() && query.or_groups.iter().all(|g| !g.is_empty());
511 let mut result = if dnf_active {
512 let mut acc: Option<RoaringBitmap> = None;
513 for group in &query.or_groups {
514 let g = self.intersect_group(group)?;
515 acc = Some(match acc.take() {
516 None => g,
517 Some(a) => a | g,
518 });
519 }
520 acc.unwrap_or_default()
521 } else {
522 self.all_live()
523 };
524
525 let cnf_active =
527 !query.and_clauses.is_empty() && query.and_clauses.iter().all(|c| !c.is_empty());
528 if cnf_active {
529 for clause in &query.and_clauses {
530 result &= self.union_clause(clause)?;
531 if result.is_empty() {
532 break;
533 }
534 }
535 }
536
537 result &= &self.live;
538 Ok(result)
539 }
540}
541
542fn build_doc_index(n: usize, doc_ids: impl Iterator<Item = u32> + Clone) -> (Vec<u32>, Vec<u32>) {
545 let mut counts = vec![0u32; n + 1];
546 let mut total = 0usize;
547 for d in doc_ids.clone() {
548 let d = d as usize;
549 if d < n {
550 counts[d] += 1;
551 total += 1;
552 }
553 }
554 let mut start = vec![0u32; n + 1];
556 let mut acc = 0u32;
557 for d in 0..n {
558 start[d] = acc;
559 acc += counts[d];
560 }
561 start[n] = acc;
562 let mut order = vec![0u32; total];
564 let mut cursor: Vec<u32> = start[..n].to_vec();
565 for (i, d) in doc_ids.enumerate() {
566 let d = d as usize;
567 if d < n {
568 order[cursor[d] as usize] = i as u32;
569 cursor[d] += 1;
570 }
571 }
572 (order, start)
573}
574
575pub(crate) fn write_bitmap(path: &std::path::Path, bm: &RoaringBitmap) -> Result<()> {
576 let mut buf = Vec::with_capacity(bm.serialized_size());
577 bm.serialize_into(&mut buf)
578 .map_err(|e| Error::other(format!("roaring serialize: {e}")))?;
579 write_atomic(path, &buf)
580}
581
582pub(crate) fn read_bitmap(path: &std::path::Path) -> Result<RoaringBitmap> {
583 let bytes = std::fs::read(path).map_err(|e| Error::io(path, e))?;
584 RoaringBitmap::deserialize_from(&bytes[..])
585 .map_err(|e| Error::Corrupt(format!("live bitmap: {e}")))
586}