1use super::{PublishedSnapshot, QueryCancellation, QueryError, SnapshotLease, Store, StoreError};
4use crate::ingest::{
5 ActiveSegment, DocId, DocumentVersion, GlobalRowId, RowSource, SearchCandidate,
6};
7use std::cell::Cell;
8use std::num::NonZeroUsize;
9use std::sync::Arc;
10
11pub type HybridSnippets =
14 Result<Vec<Option<crate::fts::query::OwnedLexicalSnippet>>, MaterializationError>;
15
16#[derive(Debug)]
18pub enum HybridPreparationError<E> {
19 Preparation(E),
21 Search(crate::fusion::FusionError),
23}
24
25impl<E> From<crate::fusion::FusionError> for HybridPreparationError<E> {
26 fn from(error: crate::fusion::FusionError) -> Self {
27 Self::Search(error)
28 }
29}
30
31impl<E> From<QueryError> for HybridPreparationError<E> {
32 fn from(error: QueryError) -> Self {
33 Self::Search(error.into())
34 }
35}
36
37impl<E: std::fmt::Display> std::fmt::Display for HybridPreparationError<E> {
38 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 Self::Preparation(error) => error.fmt(formatter),
41 Self::Search(error) => error.fmt(formatter),
42 }
43 }
44}
45
46impl<E: std::error::Error + 'static> std::error::Error for HybridPreparationError<E> {
47 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48 match self {
49 Self::Preparation(error) => Some(error),
50 Self::Search(error) => Some(error),
51 }
52 }
53}
54
55#[cfg(any(test, feature = "test-seams"))]
56#[derive(Default)]
57pub(super) struct TestMaterializationWork {
58 pub(super) admissions: std::sync::atomic::AtomicU64,
59 pub(super) reverse_version_lookups: std::sync::atomic::AtomicU64,
60 pub(super) direct_row_lookups: std::sync::atomic::AtomicU64,
61 #[cfg(feature = "query-timing")]
62 pub(super) lexical_admission_locks: std::sync::atomic::AtomicU64,
63 #[cfg(feature = "query-timing")]
64 pub(super) lexical_admission_lock_nanos: std::sync::atomic::AtomicU64,
65}
66
67#[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
68impl TestMaterializationWork {
69 pub(super) fn observe_lexical_lock(&self, started: std::time::Instant) {
70 use std::sync::atomic::Ordering::Relaxed;
71 self.lexical_admission_locks.fetch_add(1, Relaxed);
72 self.lexical_admission_lock_nanos.fetch_add(
73 started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
74 Relaxed,
75 );
76 }
77}
78
79#[cfg(any(test, feature = "test-seams"))]
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub struct MaterializationTestCounters {
83 pub admissions: u64,
85 pub reverse_version_lookups: u64,
87 pub direct_row_lookups: u64,
89 #[cfg(feature = "query-timing")]
91 pub lexical_admission_locks: u64,
92 #[cfg(feature = "query-timing")]
94 pub lexical_admission_lock_nanos: u64,
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct MaterializedRow {
100 pub document: DocumentVersion,
102 pub row_id: GlobalRowId,
104 pub text: String,
106}
107
108#[derive(Debug)]
110pub enum MaterializationError {
111 RankOutOfRange {
113 rank: usize,
115 returned: usize,
117 },
118 MissingIdentity {
120 row_id: GlobalRowId,
122 },
123 MissingText {
125 row_id: GlobalRowId,
127 document: DocumentVersion,
129 },
130 MissingSource {
132 row_id: GlobalRowId,
134 },
135 InvalidLexicalSource {
137 source: u32,
139 },
140 MissingFusedIdentity {
142 document: DocId,
144 },
145 IdentityMismatch {
147 row_id: GlobalRowId,
149 expected: DocumentVersion,
151 actual: Option<DocumentVersion>,
153 },
154 AllocationFailed {
156 bytes: usize,
158 },
159 ArithmeticOverflow,
161 Query(QueryError),
163 Storage(StoreError),
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub(crate) struct VersionMismatch {
169 pub(crate) expected: DocumentVersion,
170 pub(crate) actual: Option<DocumentVersion>,
171}
172
173pub(crate) fn require_document_version(
174 expected: DocumentVersion,
175 actual: Option<DocumentVersion>,
176) -> Result<(), VersionMismatch> {
177 if actual == Some(expected) {
178 Ok(())
179 } else {
180 Err(VersionMismatch { expected, actual })
181 }
182}
183
184impl std::fmt::Display for MaterializationError {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Self::RankOutOfRange { rank, returned } => {
188 write!(f, "result rank {rank} is outside {returned} returned rows")
189 }
190 Self::MissingIdentity { row_id } => {
191 write!(f, "result row {row_id:?} has no document identity")
192 }
193 Self::MissingText { row_id, document } => write!(
194 f,
195 "result row {row_id:?} for {document:?} has no stored text"
196 ),
197 Self::MissingSource { row_id } => {
198 write!(f, "result row {row_id:?} is outside the admitted snapshot")
199 }
200 Self::InvalidLexicalSource { source } => write!(
201 f,
202 "lexical result source {source} is outside the admitted assembly"
203 ),
204 Self::MissingFusedIdentity { document } => write!(
205 f,
206 "fused document {document:?} has no ranked source identity"
207 ),
208 Self::IdentityMismatch {
209 row_id,
210 expected,
211 actual,
212 } => write!(
213 f,
214 "result row {row_id:?} expected {expected:?}, found {actual:?}"
215 ),
216 Self::AllocationFailed { bytes } => {
217 write!(f, "result text allocation of {bytes} bytes failed")
218 }
219 Self::ArithmeticOverflow => f.write_str("result materialization counter overflow"),
220 Self::Query(error) => error.fmt(f),
221 Self::Storage(error) => error.fmt(f),
222 }
223 }
224}
225impl std::error::Error for MaterializationError {
226 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
227 match self {
228 Self::Storage(error) => Some(error),
229 Self::Query(error) => Some(error),
230 _ => None,
231 }
232 }
233}
234
235pub struct QueryMaterializer<'a> {
237 #[cfg(any(test, feature = "test-seams"))]
238 pub(super) store: &'a Store,
239 pub(super) snapshot: &'a PublishedSnapshot,
240 pub(super) active: &'a ActiveSegment,
241 pub(super) cancellation: &'a QueryCancellation<'a>,
242 pub(super) address: &'a dyn Fn(usize) -> Result<ResultAddress, MaterializationError>,
243 pub(super) counts: Cell<crate::diag::MaterializationCounters>,
244}
245
246#[derive(Clone, Copy, Eq, PartialEq)]
247pub(super) struct ResultAddress {
248 pub(super) row_id: GlobalRowId,
249 pub(super) document: Option<DocumentVersion>,
250}
251
252pub(super) type HybridAddresses = super::stats::Accounted<Vec<(DocId, ResultAddress)>>;
253
254impl From<&SearchCandidate> for ResultAddress {
255 fn from(candidate: &SearchCandidate) -> Self {
256 Self {
257 row_id: candidate.row_id(),
258 document: candidate.document(),
259 }
260 }
261}
262
263impl QueryMaterializer<'_> {
264 pub fn text(&self, rank: usize) -> Result<MaterializedRow, MaterializationError> {
266 self.check()?;
267 let address = (self.address)(rank)?;
268 self.materialize_address(address)
269 }
270
271 #[cfg(any(test, feature = "test-seams"))]
273 pub fn text_with_test_document(
274 &self,
275 rank: usize,
276 document: Option<DocumentVersion>,
277 ) -> Result<MaterializedRow, MaterializationError> {
278 self.check()?;
279 let mut address = (self.address)(rank)?;
280 address.document = document;
281 self.materialize_address(address)
282 }
283
284 fn materialize_address(
285 &self,
286 address: ResultAddress,
287 ) -> Result<MaterializedRow, MaterializationError> {
288 let row_id = address.row_id;
289 let document = address
290 .document
291 .ok_or(MaterializationError::MissingIdentity { row_id })?;
292 #[cfg(any(test, feature = "test-seams"))]
293 self.store
294 .text_materialization_work
295 .direct_row_lookups
296 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
297 let mut counts = self.counts.get();
298 counts.row_lookups = counts
299 .row_lookups
300 .checked_add(1)
301 .ok_or(MaterializationError::ArithmeticOverflow)?;
302 self.counts.set(counts);
303 let row = row_id.local_row() as usize;
304 let (actual, source) = match row_id.source() {
305 RowSource::Active => (
306 self.active.document(row),
307 self.active
308 .text(row)
309 .map_err(MaterializationError::Storage)?,
310 ),
311 RowSource::Sealed(id) => {
312 let segment = self
313 .snapshot
314 .segments()
315 .iter()
316 .find(|segment| segment.meta().id == id)
317 .ok_or(MaterializationError::MissingSource { row_id })?;
318 let actual = segment
319 .document_version(row)
320 .map_err(StoreError::Segment)
321 .map_err(MaterializationError::Storage)?;
322 let source = segment
323 .query_stored_text()
324 .map_err(StoreError::Segment)
325 .map_err(MaterializationError::Storage)?
326 .and_then(|text| text.row(row).flatten());
327 (actual, source)
328 }
329 };
330 require_document_version(document, actual).map_err(|mismatch| {
331 MaterializationError::IdentityMismatch {
332 row_id,
333 expected: mismatch.expected,
334 actual: mismatch.actual,
335 }
336 })?;
337 let source = source.ok_or(MaterializationError::MissingText { row_id, document })?;
338 let mut text = String::new();
339 text.try_reserve_exact(source.len()).map_err(|_| {
340 MaterializationError::AllocationFailed {
341 bytes: source.len(),
342 }
343 })?;
344 text.push_str(source);
345 let mut counts = self.counts.get();
346 counts.text_copies = counts
347 .text_copies
348 .checked_add(1)
349 .ok_or(MaterializationError::ArithmeticOverflow)?;
350 counts.text_bytes = counts
351 .text_bytes
352 .checked_add(source.len() as u64)
353 .ok_or(MaterializationError::ArithmeticOverflow)?;
354 self.counts.set(counts);
355 self.check()?;
356 Ok(MaterializedRow {
357 document,
358 row_id,
359 text,
360 })
361 }
362
363 fn check(&self) -> Result<(), MaterializationError> {
364 self.cancellation
365 .check_graph()
366 .map_err(super::map_scan_error)
367 .map_err(MaterializationError::Query)
368 }
369}
370
371impl Store {
372 pub fn search_hybrid_with_text_deferred<'vector, E, R>(
382 &self,
383 prepare_vector: impl FnOnce() -> Result<crate::ingest::SearchRequest<'vector>, E>,
384 lexical: &crate::fts::search::TermQuery,
385 query: &crate::fusion::HybridQuery,
386 options: impl Into<super::SearchOptions>,
387 control: super::QueryControl,
388 finish: impl FnOnce(&crate::ingest::StoreHybridSearchOutcome, &QueryMaterializer<'_>) -> R,
389 ) -> Result<(crate::ingest::StoreHybridSearchOutcome, R), HybridPreparationError<E>> {
390 self.search_hybrid_pinned_with_text(
391 prepare_vector,
392 None,
393 None,
394 super::PinnedLexicalQuery::Term(lexical),
395 query,
396 options.into(),
397 control,
398 finish,
399 )
400 }
401
402 #[allow(clippy::too_many_arguments)] fn search_hybrid_pinned_with_text<'vector, E, R>(
404 &self,
405 prepare_vector: impl FnOnce() -> Result<crate::ingest::SearchRequest<'vector>, E>,
406 filter: Option<&super::QueryFilter>,
407 eligible: Option<&[crate::ingest::DocId]>,
408 lexical: super::PinnedLexicalQuery<'_>,
409 query: &crate::fusion::HybridQuery,
410 options: super::SearchOptions,
411 control: super::QueryControl,
412 finish: impl FnOnce(&crate::ingest::StoreHybridSearchOutcome, &QueryMaterializer<'_>) -> R,
413 ) -> Result<(crate::ingest::StoreHybridSearchOutcome, R), HybridPreparationError<E>> {
414 self.search_hybrid_prepared_then(
415 prepare_vector,
416 filter,
417 eligible,
418 lexical,
419 query,
420 options,
421 control,
422 true,
423 None,
424 |mut outcome, snapshot, active, cancellation, addresses| {
425 cancellation.check_graph().map_err(super::map_scan_error)?;
426 let address = |rank| {
427 let hit =
428 outcome
429 .hits
430 .get(rank)
431 .ok_or(MaterializationError::RankOutOfRange {
432 rank,
433 returned: outcome.hits.len(),
434 })?;
435 let addresses = addresses
436 .ok_or(MaterializationError::MissingFusedIdentity { document: hit.key })?;
437 let index = addresses
438 .binary_search_by_key(&hit.key, |(key, _)| *key)
439 .map_err(|_| MaterializationError::MissingFusedIdentity {
440 document: hit.key,
441 })?;
442 addresses
443 .get(index)
444 .map(|(_, address)| *address)
445 .ok_or(MaterializationError::MissingFusedIdentity { document: hit.key })
446 };
447 let materializer = QueryMaterializer {
448 #[cfg(any(test, feature = "test-seams"))]
449 store: self,
450 snapshot,
451 active,
452 cancellation,
453 address: &address,
454 counts: Cell::new(Default::default()),
455 };
456 let value = finish(&outcome, &materializer);
457 let counts = materializer.counts.get();
458 outcome.diagnostics.materialization = Some(counts);
459 cancellation.check_graph().map_err(super::map_scan_error)?;
460 Ok((outcome, value))
461 },
462 )
463 }
464
465 #[cfg(any(test, feature = "test-seams"))]
467 #[must_use]
468 pub fn query_materialization_test_counters(&self) -> MaterializationTestCounters {
469 use std::sync::atomic::Ordering::Relaxed;
470 MaterializationTestCounters {
471 admissions: self.text_materialization_work.admissions.load(Relaxed),
472 reverse_version_lookups: self
473 .text_materialization_work
474 .reverse_version_lookups
475 .load(Relaxed),
476 direct_row_lookups: self
477 .text_materialization_work
478 .direct_row_lookups
479 .load(Relaxed),
480 #[cfg(feature = "query-timing")]
481 lexical_admission_locks: self
482 .text_materialization_work
483 .lexical_admission_locks
484 .load(Relaxed),
485 #[cfg(feature = "query-timing")]
486 lexical_admission_lock_nanos: self
487 .text_materialization_work
488 .lexical_admission_lock_nanos
489 .load(Relaxed),
490 }
491 }
492 pub fn search_hybrid_with_text<R>(
494 &self,
495 vector: crate::ingest::SearchRequest<'_>,
496 lexical: &crate::fts::search::TermQuery,
497 query: &crate::fusion::HybridQuery,
498 options: impl Into<super::SearchOptions>,
499 control: super::QueryControl,
500 finish: impl FnOnce(&crate::ingest::StoreHybridSearchOutcome, &QueryMaterializer<'_>) -> R,
501 ) -> Result<(crate::ingest::StoreHybridSearchOutcome, R), crate::fusion::FusionError> {
502 self.search_hybrid_pinned_with_text(
503 || Ok::<_, std::convert::Infallible>(vector),
504 vector.filter,
505 vector.eligible,
506 super::PinnedLexicalQuery::Term(lexical),
507 query,
508 options.into(),
509 control,
510 finish,
511 )
512 .map_err(|error| match error {
513 HybridPreparationError::Preparation(never) => match never {},
514 HybridPreparationError::Search(error) => error,
515 })
516 }
517
518 pub fn search_lexical_with_snippets(
529 &self,
530 query: &crate::fts::search::TermQuery,
531 k: usize,
532 window: NonZeroUsize,
533 control: super::QueryControl,
534 ) -> Result<
535 (
536 crate::ingest::StoreLexicalSearchOutcome,
537 Vec<crate::fts::query::OwnedLexicalSnippet>,
538 ),
539 crate::ingest::StoreLexicalError,
540 > {
541 self.search_lexical_filtered_with_snippets(query, k, window, control, None)
542 }
543
544 pub fn search_lexical_filtered_with_snippets(
546 &self,
547 query: &crate::fts::search::TermQuery,
548 k: usize,
549 window: NonZeroUsize,
550 control: super::QueryControl,
551 filter: Option<&super::QueryFilter>,
552 ) -> Result<
553 (
554 crate::ingest::StoreLexicalSearchOutcome,
555 Vec<crate::fts::query::OwnedLexicalSnippet>,
556 ),
557 crate::ingest::StoreLexicalError,
558 > {
559 self.search_lexical_then(
560 query,
561 k,
562 control,
563 filter,
564 |outcome, snapshot, active, assembly, hits, cancellation| {
565 let mut snippets = Vec::with_capacity(hits.len());
566 for hit in hits {
567 cancellation.check_graph().map_err(super::map_scan_error)?;
568 let (text, _) = super::structured_lexical_row(
569 snapshot,
570 active,
571 &assembly.sources,
572 hit.doc,
573 )?;
574 snippets.push(
575 crate::fts::snippet::owned_window(
576 &self.tokenizer,
577 text,
578 &query.terms,
579 window,
580 )
581 .ok_or(
582 crate::ingest::StoreLexicalError::MissingSnippetMatch {
583 segment: hit.doc.segment,
584 row: hit.doc.row,
585 },
586 )?,
587 );
588 }
589 Ok((outcome, snippets))
590 },
591 )
592 }
593
594 pub fn search_hybrid_with_snippets(
603 &self,
604 vector: crate::ingest::SearchRequest<'_>,
605 lexical: &crate::fts::search::TermQuery,
606 query: &crate::fusion::HybridQuery,
607 options: impl Into<super::SearchOptions>,
608 control: super::QueryControl,
609 window: NonZeroUsize,
610 ) -> Result<(crate::ingest::StoreHybridSearchOutcome, HybridSnippets), crate::fusion::FusionError>
611 {
612 self.hybrid_snippets(
613 vector,
614 super::PinnedLexicalQuery::Term(lexical),
615 query,
616 options.into(),
617 control,
618 window,
619 )
620 }
621
622 pub fn search_hybrid_structured_with_snippets(
629 &self,
630 vector: crate::ingest::SearchRequest<'_>,
631 lexical: &crate::fts::query::LexicalQuery,
632 query: &crate::fusion::HybridQuery,
633 options: impl Into<super::SearchOptions>,
634 control: super::QueryControl,
635 window: NonZeroUsize,
636 ) -> Result<(crate::ingest::StoreHybridSearchOutcome, HybridSnippets), crate::fusion::FusionError>
637 {
638 self.hybrid_snippets(
639 vector,
640 super::PinnedLexicalQuery::Structured(lexical),
641 query,
642 options.into(),
643 control,
644 window,
645 )
646 }
647
648 fn hybrid_snippets(
649 &self,
650 vector: crate::ingest::SearchRequest<'_>,
651 lexical: super::PinnedLexicalQuery<'_>,
652 query: &crate::fusion::HybridQuery,
653 options: super::SearchOptions,
654 control: super::QueryControl,
655 window: NonZeroUsize,
656 ) -> Result<(crate::ingest::StoreHybridSearchOutcome, HybridSnippets), crate::fusion::FusionError>
657 {
658 let flat_terms = match lexical {
659 super::PinnedLexicalQuery::Term(query) => Some(&query.terms),
660 super::PinnedLexicalQuery::Structured(_) => None,
661 };
662 self.search_hybrid_pinned_with_text(
663 || Ok::<_, std::convert::Infallible>(vector),
664 vector.filter,
665 vector.eligible,
666 lexical,
667 query,
668 options,
669 control,
670 |outcome, materializer| {
671 let expanded;
672 let terms: &[Vec<u8>] = match flat_terms {
673 Some(terms) => terms,
674 None => {
675 expanded = outcome
676 .lexical_expansions
677 .iter()
678 .map(|expansion| expansion.term.clone())
679 .collect::<Vec<_>>();
680 &expanded
681 }
682 };
683 let mut snippets = Vec::with_capacity(outcome.hits.len());
684 for rank in 0..outcome.hits.len() {
685 let row = match materializer.text(rank) {
686 Ok(row) => row,
687 Err(MaterializationError::MissingText { .. }) => {
688 snippets.push(None);
689 continue;
690 }
691 Err(error) => return Err(error),
692 };
693 snippets.push(crate::fts::snippet::owned_window(
694 &self.tokenizer,
695 &row.text,
696 terms,
697 window,
698 ));
699 }
700 Ok(snippets)
701 },
702 )
703 .map_err(|error| match error {
704 HybridPreparationError::Preparation(never) => match never {},
705 HybridPreparationError::Search(error) => error,
706 })
707 }
708
709 pub fn search_lexical_with_text<R>(
711 &self,
712 query: &crate::fts::search::TermQuery,
713 k: usize,
714 control: super::QueryControl,
715 finish: impl FnOnce(&crate::ingest::StoreLexicalSearchOutcome, &QueryMaterializer<'_>) -> R,
716 ) -> Result<(crate::ingest::StoreLexicalSearchOutcome, R), crate::ingest::StoreLexicalError>
717 {
718 self.search_lexical_then(
719 query,
720 k,
721 control,
722 None,
723 |mut outcome, snapshot, active, assembly, hits, cancellation| {
724 cancellation.check_graph().map_err(super::map_scan_error)?;
725 let address = |rank| {
726 let candidate = outcome.candidates.get(rank).ok_or(
727 MaterializationError::RankOutOfRange {
728 rank,
729 returned: outcome.candidates.len(),
730 },
731 )?;
732 let hit = hits.get(rank).ok_or(MaterializationError::RankOutOfRange {
733 rank,
734 returned: hits.len(),
735 })?;
736 Ok(ResultAddress {
737 row_id: lexical_row_id(snapshot, &assembly.sources, hit.doc)?,
738 document: Some(candidate.document),
739 })
740 };
741 let materializer = QueryMaterializer {
742 #[cfg(any(test, feature = "test-seams"))]
743 store: self,
744 snapshot,
745 active,
746 cancellation,
747 address: &address,
748 counts: Cell::new(Default::default()),
749 };
750 let value = finish(&outcome, &materializer);
751 let counts = materializer.counts.get();
752 outcome.diagnostics.materialization = Some(counts);
753 cancellation.check_graph().map_err(super::map_scan_error)?;
754 Ok((outcome, value))
755 },
756 )
757 }
758
759 pub fn search_with_text<R>(
761 &self,
762 request: crate::ingest::SearchRequest<'_>,
763 k: usize,
764 options: impl Into<super::SearchOptions>,
765 control: super::QueryControl,
766 finish: impl FnOnce(&crate::ingest::SearchOutcome, &QueryMaterializer<'_>) -> R,
767 ) -> Result<(crate::ingest::SearchOutcome, R), super::QueryError> {
768 self.search_with_graph_bound_mode_then(
769 request,
770 k,
771 options.into(),
772 control,
773 super::GraphBoundMode::Shared,
774 |mut outcome, admitted, control| {
775 let lease =
776 SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
777 let cancellation = QueryCancellation::new(control, &lease);
778 cancellation.check_graph().map_err(super::map_scan_error)?;
779 let address = |rank| {
780 outcome.candidates.get(rank).map(ResultAddress::from).ok_or(
781 MaterializationError::RankOutOfRange {
782 rank,
783 returned: outcome.candidates.len(),
784 },
785 )
786 };
787 let materializer = QueryMaterializer {
788 #[cfg(any(test, feature = "test-seams"))]
789 store: self,
790 counts: Cell::new(Default::default()),
791 snapshot: &admitted.snapshot,
792 active: &admitted.active_segment,
793 cancellation: &cancellation,
794 address: &address,
795 };
796 let value = finish(&outcome, &materializer);
797 let counts = materializer.counts.get();
798 outcome.diagnostics.materialization = Some(counts);
799 cancellation.check_graph().map_err(super::map_scan_error)?;
800 Ok((outcome, value))
801 },
802 )
803 }
804}
805
806fn lexical_row_id(
807 snapshot: &PublishedSnapshot,
808 sources: &[super::StructuredLexicalSource],
809 doc: crate::fts::search::GlobalDocId,
810) -> Result<GlobalRowId, MaterializationError> {
811 let source =
812 sources
813 .get(doc.segment as usize)
814 .ok_or(MaterializationError::InvalidLexicalSource {
815 source: doc.segment,
816 })?;
817 let source = match source {
818 super::StructuredLexicalSource::Active => RowSource::Active,
819 super::StructuredLexicalSource::Sealed(ordinal) => RowSource::Sealed(
820 snapshot
821 .segments()
822 .get(*ordinal)
823 .ok_or(MaterializationError::InvalidLexicalSource {
824 source: doc.segment,
825 })?
826 .meta()
827 .id,
828 ),
829 };
830 Ok(GlobalRowId::new(source, doc.row))
831}