uqa_storage/key_value/
inverted_index.rs1use super::codec::{
10 blob_to_positions, decode_u64_value, doc_length_key, key_with_tag, other_error,
11 posting_cluster_positions_key, posting_cluster_score_key, posting_document_key, read_str,
12 read_u64, reverse_posting_key, single_str_key, string_value,
13};
14use super::occurrence_keys as keys;
15use super::{
16 Analyzer, AnalyzerPhase, Arc, BTreeMap, BTreeSet, DocId, FieldName, IndexStats, InvertedIndex,
17 KeyValueBatch, KeyValueStore, Payload, PostingEntry, PostingList, StorageBackendResult,
18 TAG_METADATA, TAG_POSTING, TAG_REVERSE_POSTING,
19};
20use crate::clustered_postings::{
21 cluster_id, decode_all_scores, decode_occurrence_cluster, decode_term_keys, encode_cluster,
22 encode_occurrence_cluster, encode_term_keys, encode_terms, score_count, ClusterPosting,
23 ClusteredPostingCursor, EncodedScoreCluster, OccurrencePosting,
24};
25use crate::inverted_index::{
26 analyze_index_field, AnalyzerBindings, IndexedFieldMetadata, IndexedFieldRevision,
27};
28use crate::{PostingCursor, TokenTermKey};
29
30mod controlled;
31mod data;
32mod format;
33mod migration;
34mod mutation;
35mod queries;
36mod rebuild;
37mod trait_impl;
38
39use migration::{migrate_legacy_forward_postings, migrate_legacy_reverse_postings};
40
41const FORMAT_METADATA_KEY: &str = "inverted_index_format";
42const CLUSTERED_FORMAT_NAME: &str = "clustered-v1";
43const MIGRATION_PAGE_SIZE: usize = 1_024;
44
45type ClusterKey = (FieldName, TokenTermKey, u64);
46type DocumentFields = BTreeMap<FieldName, FieldSnapshot>;
47type StagedDocuments = BTreeMap<DocId, DocumentFields>;
48type ClusterChanges = BTreeMap<ClusterKey, BTreeMap<DocId, Option<OccurrencePosting>>>;
49
50#[derive(Clone)]
51struct FieldSnapshot {
52 metadata: IndexedFieldMetadata,
53 terms: BTreeMap<TokenTermKey, Vec<uqa_core::TokenOccurrence>>,
54}
55
56#[derive(Debug, Clone, Copy)]
57struct FieldStats {
58 revision: IndexedFieldRevision,
59 doc_count: u64,
60 total_length: u64,
61}
62
63#[derive(Clone)]
65pub struct KeyValueInvertedIndex {
66 store: Arc<dyn KeyValueStore>,
67 table: String,
68 bindings: AnalyzerBindings,
69}
70
71impl KeyValueInvertedIndex {
72 pub fn new(
73 store: Arc<dyn KeyValueStore>,
74 table: impl Into<String>,
75 analyzer: Analyzer,
76 ) -> Self {
77 Self {
78 store,
79 table: table.into(),
80 bindings: AnalyzerBindings::new(analyzer),
81 }
82 }
83
84 pub(crate) fn migrate_legacy_storage(store: &dyn KeyValueStore) -> StorageBackendResult<()> {
85 let marker = single_str_key(TAG_METADATA, FORMAT_METADATA_KEY)?;
86 if let Some(format) = store.get(&marker)? {
87 if format == CLUSTERED_FORMAT_NAME.as_bytes() {
88 return Ok(());
89 }
90 return Err(other_error(format!(
91 "unsupported KeyValue inverted-index format `{}`",
92 String::from_utf8_lossy(&format)
93 )));
94 }
95 if store.in_transaction() {
96 return Self::migrate_legacy_storage_in_transaction(store, &marker);
97 }
98
99 store.begin_transaction()?;
100 let migration = Self::migrate_legacy_storage_in_transaction(store, &marker);
101 match migration {
102 Ok(()) => store.commit_transaction(),
103 Err(error) => match store.rollback_transaction() {
104 Ok(()) => Err(error),
105 Err(rollback) => Err(other_error(format!(
106 "{error}; KeyValue posting migration rollback also failed: {rollback}"
107 ))),
108 },
109 }
110 }
111
112 fn migrate_legacy_storage_in_transaction(
113 store: &dyn KeyValueStore,
114 marker: &[u8],
115 ) -> StorageBackendResult<()> {
116 let posting_count = migrate_legacy_forward_postings(store)?;
117 let reverse_count = migrate_legacy_reverse_postings(store)?;
118 if posting_count != reverse_count {
119 return Err(other_error(format!(
120 "cannot migrate inconsistent KeyValue postings: {posting_count} forward rows and {reverse_count} reverse rows"
121 )));
122 }
123 store.put(marker, &string_value(CLUSTERED_FORMAT_NAME))
124 }
125}