Repository navigation
Add bounded nearest-sample seek filter that uses optional indexes for scan optimizations - #136
Merged
Merged
Conversation
Add optional PointID and Bloom sidecar indexes for finalized archives, wire new config settings for index generation and archive write behavior, and introduce nearest-timestamp seek/read support with updated scanner/reader internals and tests.
StephenCWills
requested changes
Oct 7, 2026
Member
|
Also note the build error from Jenkins. It seems |
…estTimestampReader.cs Co-authored-by: Stephen C. Wills <swills@gridprotectionalliance.org>
…edTreeEngineReaderOptions.cs Co-authored-by: Stephen C. Wills <swills@gridprotectionalliance.org>
HistorianServerDatabaseConfig.cs now uses a file-scoped namespace, which the C# 8 setting in the SqlClr project rejected (CS8400). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
This should be fixed now... |
…options wire version SimplifiedArchiveInitializerSettings now always saves version 3 with all index settings instead of choosing between versions 2, 3 and 4; versions 1 and 2 are still readable. Added a comment explaining why SortedTreeEngineReaderOptions only writes version 1 when an index is disabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StephenCWills
approved these changes
Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When using non-synchrophasor data sources, e.g., SCADA-type data, these changes add a way to down-sample openHistorian data whose timestamps do not fall neatly on regular intervals. The existing interval-based seek filter is highly effective for synchrophasor measurements as it can jump to a narrow window around each requested time and skip the data between those windows. With irregular SCADA-like timestamps, however, a sample may fall just outside the target window and be omitted, even though it would be a reasonable representative value for that interval.
This change adds a new
NearestTimestampSeekFilterthat selects the nearest available sample for each requested measurement ID at each target time. For example, a four-hour sampling interval over a year provides roughly 2,190 target times per measurement. The returned records retain their actual timestamps and values; the reader does not interpolate values or move samples onto the target times.A configurable distance limit controls how far the reader may search. By default, this is half the specified sampling interval. For a four-hour interval, the reader searches up to two hours from the target, within the overall query bounds. If no eligible sample exists, that measurement has no result for that target. This keeps long outages visible instead of filling them with increasingly distant samples. Equally distant candidates favor the earlier sample.
There are two search modes:
Nearestconsiders samples before and after the target, with limits (described below), andAtOrAfterconsiders only samples at or after the target, within the configured distance limit. This avoids backward searching, but can select a different sample—or return nothing when the only nearby sample precedes the target (faster reads, forward only, and more memory efficient)Understanding the backward search requires a little context about how the openHistorian archive storage works. Historian records are ordered primarily by timestamp, with measurement IDs ordered within that timeline, the standard
HistorianKey. The tree index lets the reader seek efficiently to a position in time. It does not provide a separate timeline for every measurement ID.Compressed records also introduce a distinction between "finding a location" and "decoding the records there". The compression algorithm used by the openHistorian advances forward, reconstructing records using state established while decoding previous records. It cannot simply start at an arbitrary compressed record and decode its predecessor.
The new reader handles this at the "leaf" level. A leaf is a small block of records near the bottom of the archive's tree structure. To allow for backwards searches, the reader decodes the relevant leaf in a forward direction, as required by the compression algorithm, into a cache of keys and values; this cache can then be processed in reverse order. If needed, the reader will move to the preceding leaf and repeat the process.
This operation provides backward search capability without changing the archive’s compression format or decompressing the entire query range, but at a cost. The buffer is local to the reader and reused one leaf at a time; it is not a persistent reverse index or a cache of the entire archive. Forward-only queries avoid this work. Nearest queries also skip backward searching when exact matches have already resolved the relevant IDs.
The reader stops searching a direction once the remaining timestamps cannot improve the candidates it has found. That preserves the main performance advantage of interval sampling: jumping between target times instead of reading every intervening record. However, it still ends up being a longer scan process, even when just using the forward-direction. For example, if one of the requested measurement IDs has no samples in the search window, the reader cannot stop early: it must scan the rest of that window to confirm the ID is absent, even if it has already found suitable samples for every other requested ID. To help alleviate this issue and improve search performance, you need to know when an ID exists in an archive file, or better, the available time-ranges for an ID.
The code implements two optional companion indexes to address this problem:
.pidx). This lists the point IDs present anywhere in a.d2archive file. It lets a query exclude IDs that cannot be found in that file. This also helps mixed requests: an absent ID no longer forces the reader to continue searching after it has found suitable samples for the IDs that are present..bidx). This optional index divides the timeline into fixed-duration buckets and summarizes which IDs occur in each time-range bucket. It addresses a finer distinction: an ID may exist somewhere in the file but have no data near a particular time-range.I learned about Bloom filters from Josh Patterson, of machine learning fame. A Bloom filter can answer either "definitely absent" or "possibly present". The reader can safely exclude an ID from an archive's search window when every overlapping bucket rules it out. A possible match still executes an ordinary search. False positives therefore cause extra work, not incorrect results.
This initial implementation uses Bloom filters to eliminate IDs from complete search windows. It does not yet jump over individual empty buckets inside a window that might contain a matching sample.
Both indexes are separate external files that live next to a
.d2archive, commonly referred to as a "sidecar". The rollover process collects the needed index information while already streaming records into the new archive, avoiding a second archive scan. Readers validate each sidecar against the archive and captured snapshot before using it. Missing, stale, corrupt, or unsupported indexes fall back to ordinary searching, slow mode. Existing archives remain readable without either index, and no automatic back-fill operation is currently performed, but a process could be added to build or rebuild these indexes. I'll save this for a future PR.The following controls distinguish writing indexes from using them:
LocalOutputAdapter, both GSF and Gemstone flavors, controls index generation. Exact-ID index, i.e., the.pidx, defaults to enabled -- it's a very small file, write impact is low; Bloom index, i.e., the.bidx, is not enabled by default -- the creation cost is higher and the file size is larger, although bucket duration, target false-positive probability, and the per-archive payload budget are configurable -- but for SCADA sources, the Bloom filter can make a huge difference in read speeds, so for data sources with non-aligned timestamps, the filter is recommended..pidxand.bidxare used during read scans. These bypass flags support troubleshooting and performance comparisons without deleting associated index files.