Skip to content

Commit 0289ecb

Browse files
committed
Generate meaningful segment ids using UUIDv7
Closes #971. Segment ids were generated as random UUIDv4. Those 16 bytes carried no information: they were not sortable and told you nothing about when a segment was created. This switches non-test segment id generation to UUIDv7 (RFC 9562), which embeds a 48-bit millisecond creation timestamp in the most significant bits followed by random bits. The result is still universally unique and still lock-free to generate from any indexing thread (the properties the original UUIDv4 was chosen for), while additionally being: - chronologically sortable: `SegmentId`'s `Ord` is a byte comparison, so ids now sort by creation time. - self-describing: the new `SegmentId::creation_time()` recovers the embedded timestamp for debugging. This is fully backward compatible. The on-disk format is unchanged (a 32-char hex uuid). Ids from older indices are UUIDv4 and keep working; `creation_time()` returns `None` for them (and for the autoincrement ids used in tests). Note: this deliberately does not encode a hostname/label or merge origin into the id. As discussed in #971, that information is a better fit for segment attributes on `SegmentMeta` (#999) than for the id itself, and avoids the unresolved question of how labels should be combined on merge.
1 parent 31ca1a8 commit 0289ecb

3 files changed

Lines changed: 64 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Tantivy 0.26 (Unreleased)
3838
- Make `Language` hashable [#2763](https://github.com/quickwit-oss/tantivy/pull/2763)(@philippemnoel)
3939
- Improve `space_usage` reporting for JSON fields and columnar data [#2761](https://github.com/quickwit-oss/tantivy/pull/2761)(@PSeitz-dd)
4040
- Split `Term` into `Term` and `IndexingTerm` [#2744](https://github.com/quickwit-oss/tantivy/pull/2744) [#2750](https://github.com/quickwit-oss/tantivy/pull/2750)(@PSeitz-dd @PSeitz)
41+
- Generate segment ids as time-ordered UUIDv7, making them chronologically sortable and exposing `SegmentId::creation_time` [#971](https://github.com/quickwit-oss/tantivy/issues/971)(@Divyesh-k)
4142

4243
## Performance
4344
- **Aggregation**

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ serde = { version = "1.0.219", features = ["derive"] }
3535
serde_json = "1.0.140"
3636
fs4 = { version = "0.13.1", optional = true }
3737
levenshtein_automata = "0.2.1"
38-
uuid = { version = "1.0.0", features = ["v4", "serde"] }
38+
uuid = { version = "1.0.0", features = ["v4", "v7", "serde"] }
3939
crossbeam-channel = "0.5.4"
4040
rust-stemmers = { version = "1.2.0", optional = true }
4141
downcast-rs = "2.0.1"

src/index/segment_id.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ use uuid::Uuid;
1616
/// by a UUID which is used to prefix the filenames
1717
/// of all of the file associated with the segment.
1818
///
19+
/// Segments created by tantivy use a UUIDv7, which embeds the segment's
20+
/// creation time in its most significant bits. As a result segment ids sort
21+
/// chronologically and the creation time can be recovered through
22+
/// [`SegmentId::creation_time`]. Ids read from older indices (created before
23+
/// this change) are UUIDv4 and remain fully supported; for those
24+
/// [`SegmentId::creation_time`] returns `None`.
25+
///
1926
/// In unit test, for reproducibility, the `SegmentId` are
2027
/// simply generated in an autoincrement fashion.
2128
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -40,7 +47,14 @@ fn create_uuid() -> Uuid {
4047

4148
#[cfg(not(test))]
4249
fn create_uuid() -> Uuid {
43-
Uuid::new_v4()
50+
// UUIDv7 embeds a 48-bit millisecond creation timestamp in its most significant
51+
// bits, followed by random bits. This keeps segment ids universally unique and
52+
// lock-free to generate from any indexing thread (like the previous v4), while
53+
// additionally making them:
54+
// - chronologically sortable (ids sort by creation time), and
55+
// - self-describing (the creation time can be recovered, see
56+
// `SegmentId::creation_time`).
57+
Uuid::now_v7()
4458
}
4559

4660
impl SegmentId {
@@ -74,6 +88,24 @@ impl SegmentId {
7488
pub fn from_uuid_string(uuid_string: &str) -> Result<SegmentId, SegmentIdParseError> {
7589
FromStr::from_str(uuid_string)
7690
}
91+
92+
/// Returns the creation time embedded in the segment id, if available.
93+
///
94+
/// Segments created by recent versions of tantivy use a UUIDv7, whose most
95+
/// significant bits encode the millisecond timestamp at which the id (and
96+
/// hence the segment) was created. This can be handy when investigating an
97+
/// index: it tells you when each segment was produced without relying on
98+
/// filesystem timestamps.
99+
///
100+
/// Returns `None` for segment ids that do not carry a timestamp, i.e. ids
101+
/// from older indices (UUIDv4) and the autoincrement ids used in tests.
102+
pub fn creation_time(&self) -> Option<std::time::SystemTime> {
103+
// `get_timestamp` returns `Some` only for UUID versions that carry a
104+
// timestamp (v7 here); it is `None` for v4.
105+
let timestamp = self.0.get_timestamp()?;
106+
let (secs, nanos) = timestamp.to_unix();
107+
Some(std::time::UNIX_EPOCH + std::time::Duration::new(secs, u32::from(nanos)))
108+
}
77109
}
78110

79111
/// Error type used when parsing a `SegmentId` from a string fails.
@@ -139,4 +171,33 @@ mod tests {
139171
// one extra char
140172
assert!(SegmentId::from_uuid_string("a5c4dfcbdfe645089129e308e26d5523b").is_err());
141173
}
174+
175+
#[test]
176+
fn test_creation_time_none_for_v4() {
177+
// A legacy UUIDv4 id (version nibble `4`) carries no timestamp.
178+
let v4 = SegmentId::from_uuid_string("a5c4dfcbdfe645089129e308e26d5523").unwrap();
179+
assert!(v4.creation_time().is_none());
180+
}
181+
182+
#[test]
183+
fn test_creation_time_some_for_v7() {
184+
use std::time::{Duration, SystemTime};
185+
186+
use uuid::Uuid;
187+
188+
// Build a UUIDv7 directly so this test does not depend on the (test-only)
189+
// autoincrement id generation used elsewhere.
190+
let before = SystemTime::now();
191+
let seg = SegmentId(Uuid::now_v7());
192+
let after = SystemTime::now();
193+
194+
let created = seg
195+
.creation_time()
196+
.expect("a v7 segment id must expose a creation time");
197+
198+
// v7 has millisecond precision, so allow a small slack around the window.
199+
let slack = Duration::from_millis(1);
200+
assert!(created >= before - slack);
201+
assert!(created <= after + slack);
202+
}
142203
}

0 commit comments

Comments
 (0)