Corpus, Separators, and Baseline
For documents $D_0,\ldots,D_{k-1}$, index $T=D_0\#_0D_1\#_1\cdots D_{k-1}\#_{k-1}\$$. Every separator is unique and outside the query alphabet; the sentinel occurs once and is globally smallest. Maintain an origin array mapping content positions to (document_id, offset) and structural positions to no document.
The baseline searches each document directly or binary-searches an uncompressed suffix array. It provides an independent oracle for FM-index counts and locations. For banana, bandana, and ananas, the indexed sequence contains 19 content symbols, three separators, and one sentinel.
| Statistic or structure | Sample value | Calculation | Logical payload | Purpose |
|---|---|---|---|---|
| Documents | 3 | banana, bandana, ananas | Application metadata | Identity and display |
| Document characters | 19 | $6+7+6$ | 19 symbols | Searchable content |
| Separators | 3 | One per document | 3 symbols | Prevent cross-document matches |
| Sentinel | 1 | One global minimum | 1 symbol | Terminate suffix order |
| Indexed length | 23 | $19+3+1$ | 23 symbols | BWT and suffix count |
| Full suffix array | 23 positions | One position per suffix | 92 bytes at 32 bits | Baseline order and location |
| Inverse suffix array | 23 ranks | One rank per position | 92 bytes at 32 bits | Position-to-row mapping |
| BWT | 23 symbols | One preceding symbol per row | 23 bytes with byte alphabet | Compressed-index foundation |
| Naive suffix materialization | 276 symbol slots | $23+22+\cdots+1$ | At least 276 units | Educational only |
BWT and Rank Data Structure
Given suffix array SA, BWT row $r$ stores the symbol preceding SA[r]. Let $C[c]$ count indexed symbols smaller than $c$ and $\operatorname{rank}_c(i)$ count $c$ in BWT[0:i]. Backward search updates $[l,r)$ by $l'=C[c]+\operatorname{rank}_c(l)$ and $r'=C[c]+\operatorname{rank}_c(r)$.
The implementation uses rank checkpoints every $q$ positions and suffix-array samples at text positions divisible by $s$. Rank scans at most $q-1$ BWT symbols; locating applies LF until it reaches a sample, requiring at most $s-1$ steps under text-position sampling.
Counting and Locating Matches
Backward search yields one suffix-array interval. Counting returns its width. Locating enumerates its rows, follows LF to sampled suffix positions, reconstructs global positions, and maps them to (document, offset). Count-only indexes need no SA samples; locate and display features add samples or access to original text. Results may remain in suffix order or be sorted by document and offset.
flowchart TD
A["Pattern"] --> B["Backward search with C and rank"]
B --> C["Suffix-array interval [l, r)"]
C --> D["Count = r − l"]
C --> E["Enumerate rows"]
E --> F{"SA value sampled?"}
F -- "No" --> G["Apply LF; increment steps"]
G --> F
F -- "Yes" --> H["Recover global position modulo N"]
H --> I["Origin map"]
I --> J{"Content position?"}
J -- "No" --> K["Discard structural position"]
J -- "Yes" --> L["Emit document and offset"]
Compression and Parameter Study
Checkpoint rate $q$ trades rank space for at most $q-1$ scanned symbols. Sampling rate $s$ trades SA-sample space for at most $s-1$ LF steps per occurrence. Count latency is independent of $s$, while locate-heavy workloads may be dominated by LF walks and output size. Compare serialized bytes, not host-object overhead, and report median plus tail latency.
Tune the Index
Build every pair $q\in\{16,32,64,128,256\}$ and $s\in\{4,8,16,32,64\}$. Run absent, count-heavy, and locate-heavy query sets. Record BWT, rank, and sample bytes; median and p99 count/locate latency; LF steps per occurrence; and output count.
Given a 64 MiB limit, p99 count below 500 microseconds, and p99 locate below 5 milliseconds, choose the smallest qualifying index. Verify that changing $s$ leaves counts unchanged, maximum LF steps do not exceed $s-1$, increasing $q$ reduces checkpoint bytes while increasing scan work, and all configurations reproduce baseline locations.
Correctness and Robustness
Compare every query with direct per-document search. Include every substring of small documents, absent symbols, repeated patterns, matches at boundaries, empty documents, and strings that would match only across a separator. Assert count == len(locate) and that every located slice equals the query.
Reject missing or duplicate sentinels, reused separators, invalid samples, malformed checkpoints, non-monotone $C$ tables, and incompatible versions. Persist encoding and normalization policies with the index.
The sentinel must occur exactly once and compare smaller than every other symbol; every document separator must lie outside the query alphabet and differ from the sentinel. These invariants make suffix order unambiguous and prevent ordinary-symbol patterns from crossing document boundaries.