<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: amru195704</title>
    <description>The latest articles on DEV Community by amru195704 (@_28151d01bd865fb3d4d2).</description>
    <link>https://dev.to/_28151d01bd865fb3d4d2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121642%2F208cccc2-e028-4277-8840-242cc229d15f.png</url>
      <title>DEV Community: amru195704</title>
      <link>https://dev.to/_28151d01bd865fb3d4d2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_28151d01bd865fb3d4d2"/>
    <language>en</language>
    <item>
      <title>Indexing Where the Answer Cannot Be: Skipping Blocks on an Inverted Regex, 3.10 s to 0.11 s</title>
      <dc:creator>amru195704</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:47:03 +0000</pubDate>
      <link>https://dev.to/_28151d01bd865fb3d4d2/indexing-where-the-answer-cannot-be-skipping-blocks-on-an-inverted-regex-310-s-to-011-s-5blj</link>
      <guid>https://dev.to/_28151d01bd865fb3d4d2/indexing-where-the-answer-cannot-be-skipping-blocks-on-an-inverted-regex-310-s-to-011-s-5blj</guid>
      <description>&lt;p&gt;Out of 92,239,125 lines, exactly one was a hit.&lt;/p&gt;

&lt;p&gt;I build a search tool for very large text files, and one of its two commands keeps a persistent index. Asking it for the lines of a 3 GB OpenStreetMap XML file that do &lt;em&gt;not&lt;/em&gt; match &lt;code&gt;^ +&amp;lt;&lt;/code&gt; returns a single line — and the build before this one still ran the regular expression over all 92 million of them to find it.&lt;/p&gt;

&lt;p&gt;This post is about the index we added to avoid that, what it bought, and the fairly large class of queries it does nothing for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a normal index cannot help here
&lt;/h2&gt;

&lt;p&gt;An inverted index — the ordinary "which chunks contain this token" kind — answers one question: &lt;em&gt;where is it?&lt;/em&gt; Search for a literal and every chunk without it can stay closed.&lt;/p&gt;

&lt;p&gt;That does not invert. The answer to &lt;code&gt;-v '^ +&amp;lt;'&lt;/code&gt; is not "chunks without &lt;code&gt;^ +&amp;lt;&lt;/code&gt;"; it is "lines that do not match it", and those lines sit inside the very chunks that &lt;em&gt;do&lt;/em&gt; contain matches. Skip a chunk because the pattern occurs in it and you silently drop the non-matching lines living there.&lt;/p&gt;

&lt;p&gt;The same hole opens for any regular expression with no mandatory literal to prefilter on — &lt;code&gt;[0-9]{3}-[0-9]{4}"&lt;/code&gt; has nothing you can look up. Both cases degrade to scanning everything: 1.5 s for the inverted regex and 4.7 s for the plain one over 3 GB, even with the file already decompressed and indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: index the &lt;em&gt;shape&lt;/em&gt; of each line
&lt;/h2&gt;

&lt;p&gt;Huge machine-generated files have far fewer kinds of line than lines. The same row repeats for millions of lines with different numbers and different words in it. So throw away what differs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs of digits become &lt;code&gt;9&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;runs of letters become &lt;code&gt;a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;runs of bytes above &lt;code&gt;0x80&lt;/code&gt; become &lt;code&gt;u&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;punctuation and whitespace are kept as-is&lt;/li&gt;
&lt;li&gt;truncate at 160 bytes and flag the skeleton as partial&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;tag k="highway" v="residential"/&amp;gt;&lt;/code&gt; becomes &lt;code&gt;&amp;lt;a a="a" a="a"/&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Across 92.24 million lines there are 1.395 million distinct skeletons, and &lt;strong&gt;the most common 65,534 cover 98.56% of all lines&lt;/strong&gt;. Number those, store the per-block arrays of line numbers (&lt;code&gt;u16&lt;/code&gt;, zstd-compressed) in the index, and the query becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the regular expression against the &lt;strong&gt;dictionary&lt;/strong&gt; — tens of thousands of entries, about 5 ms.&lt;/li&gt;
&lt;li&gt;Label every shape &lt;em&gt;always matches&lt;/em&gt;, &lt;em&gt;never matches&lt;/em&gt;, or &lt;em&gt;undecidable&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Open only the blocks that hold lines to emit or lines marked undecidable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inverting is now free: you read the same table backwards. The 1 MB blocks that cannot possibly contain an answer are never decompressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Mac (Apple M4, 10 cores, 32 GB, external USB SSD), 3 GB OpenStreetMap XML, hot cache, against &lt;code&gt;ripgrep 15.2.0&lt;/code&gt; with hit counts verified equal.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Hits&lt;/th&gt;
&lt;th&gt;ripgrep (s)&lt;/th&gt;
&lt;th&gt;index-based (s)&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;^ +&amp;lt;&lt;/code&gt; inverted regex&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3.10&lt;/td&gt;
&lt;td&gt;0.11&lt;/td&gt;
&lt;td&gt;28.18x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;="&lt;/code&gt; inverted literal&lt;/td&gt;
&lt;td&gt;7,698,790&lt;/td&gt;
&lt;td&gt;2.08&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;3.85x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[0-9]{3}-[0-9]{4}"&lt;/code&gt; regex&lt;/td&gt;
&lt;td&gt;14,951&lt;/td&gt;
&lt;td&gt;2.35&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;5.60x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the first row is the skeleton working. The other two come from the index itself and are unchanged by it — which the isolated measurement makes obvious. Toggling the mechanism off on the same file (development build, median of three, output to &lt;code&gt;/dev/null&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;off (s)&lt;/th&gt;
&lt;th&gt;on (s)&lt;/th&gt;
&lt;th&gt;direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;^ +&amp;lt;&lt;/code&gt; inverted regex&lt;/td&gt;
&lt;td&gt;1.510&lt;/td&gt;
&lt;td&gt;0.062&lt;/td&gt;
&lt;td&gt;24.4x faster with it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;^ +&amp;lt;&lt;/code&gt; regex&lt;/td&gt;
&lt;td&gt;4.721&lt;/td&gt;
&lt;td&gt;2.378&lt;/td&gt;
&lt;td&gt;1.98x faster with it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;="&lt;/code&gt; inverted literal&lt;/td&gt;
&lt;td&gt;0.518&lt;/td&gt;
&lt;td&gt;0.522&lt;/td&gt;
&lt;td&gt;level, marginally slower with it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[0-9]{3}-[0-9]{4}"&lt;/code&gt; regex&lt;/td&gt;
&lt;td&gt;0.405&lt;/td&gt;
&lt;td&gt;0.420&lt;/td&gt;
&lt;td&gt;level, marginally slower with it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;\d+\.\d{6,}&lt;/code&gt; regex&lt;/td&gt;
&lt;td&gt;0.393&lt;/td&gt;
&lt;td&gt;0.413&lt;/td&gt;
&lt;td&gt;level, marginally slower with it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;="&lt;/code&gt; plain string&lt;/td&gt;
&lt;td&gt;2.815&lt;/td&gt;
&lt;td&gt;2.896&lt;/td&gt;
&lt;td&gt;level, marginally slower with it on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three regimes. Decidable by shape with few hits: 24x. Decidable with many hits: 1.98x, because writing 92 million lines becomes the ceiling. Not decidable: 0.92x to 0.99x — &lt;strong&gt;slightly slower&lt;/strong&gt;, since the per-block arrays get read for nothing. &lt;code&gt;[0-9]{3}-[0-9]{4}"&lt;/code&gt; has hits in 703 of 724 blocks; there is nothing to skip.&lt;/p&gt;

&lt;p&gt;One caveat on the first table: the harness flagged ripgrep's hot run on the inverted regex as not having warmed properly (cold 3.57 s, hot 3.10 s, nearly identical). Cold against cold is 3.57 s to 0.18 s, 19.8x. The order of magnitude survives either reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is useful without my tool: three things that decide whether a summary index pays off
&lt;/h2&gt;

&lt;p&gt;This generalises past my file format, and the failures were more instructive than the wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is the summary set small enough to evaluate the whole query against?&lt;/strong&gt; The entire trick is that you can take a pattern written for the real data and apply it, unchanged, to the summary — but only if the summary is small enough to scan exhaustively per query. 65,534 entries at ~5 ms, fine. We also tried this on a bundle of 60,000 Linux kernel source files: the shapes scattered into 4.085 million distinct values, the top 65,534 covered only 70.65% of lines, and we now refuse to build the index below a 90% coverage threshold. Source code does not repeat its line shapes. Logs, XML and CSV do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How small is the undecidable fraction, and where does it live?&lt;/strong&gt; This nearly killed the whole thing. Lines that missed the dictionary — just 1.4% of them — turned out to be spread across &lt;em&gt;every&lt;/em&gt; block. Undecidable means "go read the original", so a single stray line per block keeps that block open. The inverted regex with its one hit still opened &lt;strong&gt;672 of 724 blocks&lt;/strong&gt;: less than a tenth skipped, for a 6.3% larger index. 1.4% sounds negligible; distribution is what matters, not the percentage.&lt;/p&gt;

&lt;p&gt;The fix was to spend part of the budget on precision instead of coverage: when the shape count overflows, reserve 8,192 of the slots for &lt;em&gt;16-character prefixes&lt;/em&gt; of a skeleton, and give every dictionary-miss one of those. Coverage of the numbering went to 100.00%, blocks opened went 672 → 1, and that is where the 24.4x came from. Note what that means in general: &lt;strong&gt;an exact index of 98.6% of your data can be worth less than an approximate index of 100% of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Does it fail safe, and have you proved that?&lt;/strong&gt; A summary that wrongly says "cannot match" does not produce an error — it produces a &lt;em&gt;quietly incomplete answer&lt;/em&gt;, which is the worst failure mode a search tool has. Everything ambiguous has to fall to "undecidable", which is always safe because it just costs a read. Concretely, that meant: &lt;code&gt;\d&lt;/code&gt; matches full-width digits in .NET, so a shape containing &lt;code&gt;u&lt;/code&gt; that doesn't match goes to undecidable rather than never-matches; lines over 64 KB become "other" because the matcher only looks at the first 64 KB; and a small purpose-built parser decides whether a pattern is even eligible, rejecting anything it isn't sure about.&lt;/p&gt;

&lt;p&gt;Proving it took a randomised cross-check: 1,500 lines × 800 patterns = &lt;strong&gt;1.2 million pairs&lt;/strong&gt;, comparing the shape verdict against running the regex on the actual text, with invalid UTF-8, control characters and full-width digits in the corpus. Zero mismatches. Plus byte-for-byte output comparison with the mechanism on and off across 18 patterns, CRLF and BOM variants, bundled archives, and lines spanning block boundaries. If you build something that decides not to read data, budget more time for this than for the optimisation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The index grows 6.3% (251.8 MB → 267.8 MB on the 3 GB file), and 3.4 s to build the section.&lt;/li&gt;
&lt;li&gt;Queries that cannot skip get 1% to 8% slower. For plain strings there is now a cut-off — if more than half the blocks would be opened, don't bother — but the equivalent on the regex path isn't implemented yet.&lt;/li&gt;
&lt;li&gt;Everything above is one machine. The Windows and Linux builds pass the same correctness tests; I don't compare seconds across operating systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I make this tool — &lt;code&gt;uvf&lt;/code&gt; is the free build, &lt;code&gt;uvp&lt;/code&gt; is the paid one — so treat the ratios as the author's own measurements and the method as the part worth checking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full write-up with every pattern and timing: &lt;a href="https://uvp.y42u.net/en/blog/uwview-fs02-line-skeleton-en/" rel="noopener noreferrer"&gt;https://uvp.y42u.net/en/blog/uwview-fs02-line-skeleton-en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/amru195704/UwView" rel="noopener noreferrer"&gt;https://github.com/amru195704/UwView&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cli</category>
      <category>performance</category>
      <category>regex</category>
      <category>logging</category>
    </item>
    <item>
      <title>Storing Legacy Encodings as UTF-8 Inside the Index: 5.4x to 38.8x Over ripgrep -E, and the Line-Number Bug That Nearly Stopped It</title>
      <dc:creator>amru195704</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:57:37 +0000</pubDate>
      <link>https://dev.to/_28151d01bd865fb3d4d2/storing-legacy-encodings-as-utf-8-inside-the-index-54x-to-388x-over-ripgrep-e-and-the-2iln</link>
      <guid>https://dev.to/_28151d01bd865fb3d4d2/storing-legacy-encodings-as-utf-8-inside-the-index-54x-to-388x-over-ripgrep-e-and-the-2iln</guid>
      <description>&lt;p&gt;The decode ran on every query.&lt;/p&gt;

&lt;p&gt;I build a search tool for very large text files, and one of its two commands keeps a persistent index. On a 3 GB Shift-JIS file, that index removed the second read of the file but not the second &lt;em&gt;decode&lt;/em&gt; — every query still turned 3 GB of CP932 bytes into characters before it could decide anything.&lt;/p&gt;

&lt;p&gt;This post is about moving the index to UTF-8: what it bought, what it cost, and the one decoding detail that quietly corrupted line numbers until we caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up front: the numbers
&lt;/h2&gt;

&lt;p&gt;Measured on a Mac (Apple M4, 10 cores, 32 GB, external USB SSD), on OpenStreetMap XML (2,892 MB) converted with &lt;code&gt;iconv&lt;/code&gt; to each encoding, against &lt;code&gt;ripgrep 15.2.0&lt;/code&gt; with &lt;code&gt;-E &amp;lt;encoding&amp;gt;&lt;/code&gt; so it decodes the same text. Cold means the page cache was purged and ten seconds elapsed; hot is the run straight after. Two runs combined:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;ripgrep -E (s)&lt;/th&gt;
&lt;th&gt;index-based search (s)&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shift-JIS 2,887 MB, plain string&lt;/td&gt;
&lt;td&gt;4.44&lt;/td&gt;
&lt;td&gt;0.48&lt;/td&gt;
&lt;td&gt;9.25x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shift-JIS, &lt;code&gt;-i&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4.79&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;6.06x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shift-JIS, regex&lt;/td&gt;
&lt;td&gt;4.69&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;5.79x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shift-JIS, inverted match&lt;/td&gt;
&lt;td&gt;6.53&lt;/td&gt;
&lt;td&gt;1.20&lt;/td&gt;
&lt;td&gt;5.44x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EUC-JP 2,887 MB (range over 7 patterns)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;5.38x to 23.26x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF-16LE 5,765 MB (range over 7 patterns)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;8.06x to 38.79x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the loss, which matters more than the wins: &lt;strong&gt;the pass that builds the index is slower than ripgrep&lt;/strong&gt; — 4.93 s vs 3.35 s on Shift-JIS, 5.01 s vs 3.32 s on EUC-JP, 7.76 s vs 6.53 s on UTF-16LE. For a file you search once, &lt;code&gt;rg -E&lt;/code&gt; is the right tool and nothing here changes that.&lt;/p&gt;

&lt;p&gt;Output was compared against ripgrep on 7 patterns across 3 encodings — 21 queries, byte-identical, same exit codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the index kept the source encoding in the first place
&lt;/h2&gt;

&lt;p&gt;Not an oversight. The index records the &lt;strong&gt;byte offset of every line in the source file&lt;/strong&gt;, and several features depend on those offsets lining up: opening the original file in the viewer and injecting an index a CLI run produced, and restoring the original file from the index.&lt;/p&gt;

&lt;p&gt;Convert the text and every offset moves. A two-byte Shift-JIS character is three bytes in UTF-8, so line 400,000 no longer begins at the same byte. An index with shifted offsets injected into a view of the source file shows the wrong lines.&lt;/p&gt;

&lt;p&gt;So the change was not "call a converter". It was: convert, and then make sure a converted index can never reach a code path that expects source offsets. In practice that meant giving the converted format its own version number, refusing to read older ones, and making the verification step (length plus timestamp, with the header timestamp written as 0) fail by construction for every injection path. The converted index is always opened standalone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is useful without my tool: three things that bite when you move text to UTF-8
&lt;/h2&gt;

&lt;p&gt;This is the section I would have wanted to read before starting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A naive decoder can eat your newlines.&lt;/strong&gt; The default CP932 decoder in .NET treats &lt;code&gt;0x82 0x0A&lt;/code&gt; — the lead byte of a two-byte character followed by a line feed — as &lt;em&gt;one&lt;/em&gt; character, and the newline disappears. Decode a damaged or truncated Shift-JIS file as a single stream and you silently end up with fewer lines than the file has, with every line number after that point wrong. Our fix: when the newline count of a decoded region does not match the source, decode that region line by line. If you are writing anything that reports line numbers, test with a file whose last character is cut in half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Round-trips cleanly" is not one property, it is two.&lt;/strong&gt; Character-identical and byte-identical are different promises. Characters with two code points — the NEC-selected IBM extensions in CP932, where the same glyph exists at &lt;code&gt;ED40&lt;/code&gt; and &lt;code&gt;FA5C&lt;/code&gt; — come back with whichever one your decoder normalises to. We ended up promising character-identical only, and saying so in the help text. Dropping the byte-comparison pass on the way in also took index construction from 9.67 s to 5.33 s, so the weaker promise paid for itself. If someone downstream verifies archives by hash, they need to know which promise they have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Replacement characters are a UI decision, not a correctness one.&lt;/strong&gt; The spec said use U+FFFD for undecodable bytes. But the viewer already displayed "・" for them, because that is what the platform decoder does. Storing U+FFFD in the index would have changed search output for existing users. We stored "・" instead, and the output stayed byte-identical to the previous release. Pick whichever you like, but pick it once, and make the decode path and the display path agree.&lt;/p&gt;

&lt;p&gt;A fourth, smaller one: UTF-16 at scale found a latent bug. Searching a 6 GB UTF-16 file in its original encoding failed with &lt;code&gt;Specified argument was out of the range&lt;/code&gt;, because the carry-over bytes of a character split across a read boundary could exceed the read window. Files up to about 1,000 MB were fine, which is exactly why it survived so long. If you buffer-and-decode, write a test at a size larger than your buffer arithmetic assumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the conversion actually bought
&lt;/h2&gt;

&lt;p&gt;On a hot cache, the per-query numbers for the Shift-JIS file land on top of the numbers for the same content stored as UTF-8: 0.15 s vs 0.15 s for a plain string, 0.33 s vs 0.34 s case-insensitive, 0.30 s vs 0.32 s for a regex, 0.53 s vs 0.54 s for an inverted match.&lt;/p&gt;

&lt;p&gt;That is the whole point, stated as a general rule: &lt;strong&gt;an index can absorb the per-query transformation, not just the per-query read.&lt;/strong&gt; Decoding, newline counting, normalisation, case folding — these are not reads, they are "turning bytes into characters", and most of them can be done once at build time and stored.&lt;/p&gt;

&lt;p&gt;The price is that your offsets stop matching the source. Which is a design problem, not a performance one: either you collapse to a single path, or you support both and test the product of every combination. I wrote the option to keep the source encoding, then deleted it, for exactly that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still does not work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EUC-JP supplementary kanji (JIS X 0212) are not decoded; they come back as "・".&lt;/li&gt;
&lt;li&gt;Restoring to the original encoding got &lt;em&gt;slower&lt;/em&gt; first: 7.88 s against 3.44 s for the previous build, which never converted at all — the old build 2.29x faster. Rebuilt as 8 MB chunks decompressed and converted in parallel and written in order, with chunk boundaries aligned to UTF-8 character boundaries (each chunk reads three extra bytes ahead; continuation bytes at the head belong to the previous chunk). That brought it to 1.03 s hot; in the release measurement it was 3.52 s cold, which includes writing 3 GB to disk.&lt;/li&gt;
&lt;li&gt;Everything above is measured on one machine. The Windows and Linux builds pass the same correctness tests, but I do not compare seconds across operating systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I make this tool — &lt;code&gt;uvf&lt;/code&gt; is the free build, &lt;code&gt;uvp&lt;/code&gt; is the paid one — so treat the ratios as the author's own measurements and the method as the part worth checking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full write-up with every pattern and timing: &lt;a href="https://uvp.y42u.net/en/blog/uwview-fs01-sjis-utf8-index-en/" rel="noopener noreferrer"&gt;https://uvp.y42u.net/en/blog/uwview-fs01-sjis-utf8-index-en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/amru195704/UwView" rel="noopener noreferrer"&gt;https://github.com/amru195704/UwView&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cli</category>
      <category>performance</category>
      <category>encoding</category>
      <category>logging</category>
    </item>
  </channel>
</rss>
