Skip to content

[improvement](be) Move jieba into the Doris tree and cut chinese text over a flat dictionary - #70266

Draft
airborne12 wants to merge 10 commits into
apache:masterfrom
airborne12:snii-analysis-t7-chinese
Draft

airborne12 wants to merge 10 commits into
apache:masterfrom
airborne12:snii-analysis-t7-chinese

Conversation

@airborne12

@airborne12 airborne12 commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Related PR: #70265 (depends on)

Problem Summary: Move jieba into the Doris tree for the chinese parser and replace its trie of hash maps with flat arrays: 3.2x (coarse) and 2.3x (fine) as fast, less memory, the same tokens.

Release note

The chinese parser tokenizes about 3.2 times as fast in the coarse mode and 2.3 times as fast in the fine mode, and its dictionaries take less memory.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

🤖 Generated with Claude Code

airborne12 and others added 10 commits November 3, 2026 12:27
…kens

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The SNII text analysis is moving off CLucene one analyzer at a
time, and every step has to show two things against the code before it: that it
is faster, and that every row still gives the same tokens. There was no tool for
either.

benchmark_analyzer.hpp measures every builtin parser and a few custom chains on
the path the SNII writer takes for a row (a new stream per row) and with one
stream across rows, over corpora given as `.rows` files. With
ANALYZER_BENCH_DIGEST set it registers only the benchmark "digest", which writes
one line per analyzer, corpus and row: the number of terms and a digest of their
bytes and positions. Diffing the files of two binaries names every row whose
tokens changed, on real text and with the real kuromoji dictionary, which the
unit tests cannot hold.

analyzer_ab.py runs two binaries in alternating rounds and reports the median of
the per-round throughput ratios, because the host load moves a single
measurement by 10 to 20 percent. analyzer_corpus.py builds the corpora; the
Chinese and Japanese ones are cut from the Wikipedia CirrusSearch dumps of
2025-12-29, so the same date gives the same corpus. analyzer_benchmark.md lists
the steps.

The analysis rewrite is measured against this commit, branch
snii-analysis-bench-baseline.

### Release note

None

### Check List (For Author)

- Test: Manual test
    - BUILD_TYPE=RELEASE DORIS_BENCHMARK_HEADER=benchmark_analyzer.hpp
      ./build.sh --benchmark builds benchmark_test.
    - The digest mode over six corpora of 8 MB (four mixed ones, zh_wiki and
      ja_wiki) writes 5,240,060 lines in 67 s. A build with the separate
      dynamic_cast fix of the tokenizer base class, which keeps every token,
      gives a byte-identical file.
    - analyzer_ab.py, five rounds: that fix measures 1.84 on basic and 1.64 on
      custom standard + lowercase. The A/A run stayed within 0.94 to 1.08 per
      analyzer on a host at load 140 to 280, more than the 5% the method
      allows, so finer comparisons need a quieter host.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…being frozen

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: Freezing the output of every text analyzer, in the next commit,
ran into three bugs that no frozen digest can hold.

1. The word_delimiter filter threw std::out_of_range for a token holding a NUL
   byte when catenate_words, catenate_numbers or catenate_all was set.
   WordDelimiterConcatenation::append called std::string::append(const char*,
   pos, n), which resolves to the string_view overload and measures the text up
   to its first NUL, so for "foo\0bar" the second part started past the end.
   The SNII and V1/V2 writers catch only CLuceneError and doris::Exception, so
   the error left the writer. The part is now appended from its offset.
2. IKAnalyzer::tokenStream created its tokenizer with new and then called
   reset(), which throws when a value starts with a character cut short, such as
   "\xE4\xB8". The tokenizer and its buffers, about 100 KB, leaked once per such
   value, on the query path and on the SNII write path, which build a stream per
   value. A unique_ptr now owns it until reset() returns.
3. The IK dictionary was loaded under std::call_once, which is used up even when
   loading fails. After one failure, for example a dictionary path that is fixed
   later, every IK analyzer kept failing until the BE restarted, and in the unit
   tests the result of the IK tests depended on whether a test with a bad path
   ran first. Dictionary::initial now publishes the dictionary only after it has
   loaded, and the next call tries again. reload() and getSingleton(cfg), which
   only existed to recover from that state, are removed.

### Release note

Fix the word_delimiter filter failing on values with a NUL byte when a catenate
option is set, and a memory leak of the IK analyzer on values it cannot decode.

### Check List (For Author)

- Test: Unit Test
    - New: WordDelimiterFilterTest.CatenatePartsAcrossNul, and
      IKTokenizerTest.TestUndecodableValueThrowsWithoutLeakingTheStream, whose
      leak LeakSanitizer reports when the binary exits. Changed:
      IKTokenizerTest.TestDictionaryExceptionHandling checks that a failed load
      is retried and that the first loaded configuration stays.
    - ./run-be-ut.sh --run --filter='WordDelimiterFilterTest*:IKTokenizerTest*'
      passes on ASAN, also three times with GTEST_SHUFFLE=1 together with the
      golden tests of the next commit.
- Behavior changed: Yes. A value with a NUL byte under word_delimiter with a
  catenate option is indexed instead of failing; a failed IK dictionary load is
  retried by the next IK analyzer instead of failing until a restart.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The SNII text analysis is about to be rewritten without
CLucene. A change in what an analyzer emits raises no error anywhere: queries
just stop finding the rows indexed with the old tokens. So the output is frozen
before any implementation changes, and every later step has to keep it.

AnalysisGolden runs 155 analyzer configurations over 13 corpus families. The
configurations are written the way users write them, as index properties and
analyzer, normalizer, tokenizer and filter policies, so the matrix does not move
when the implementation behind it does: every builtin parser with its lower_case
and stopwords values, every custom tokenizer, token filter and char filter with
its settings (pairwise covering arrays for the 16 pinyin and 9 word_delimiter
switches), the index's own char filter, normalizers, and the normalize() of
prefix and wildcard queries. The families aim at known risks: token windows at
255 bytes and at 255 UTF-16 code units, batch sizes of 255 to 513 tokens,
characters across 4096-byte chunks, invalid UTF-8, sampled jieba and IK
dictionary words, unknown Han strings, every pair of word break classes, sampled
BMP code points, NUL bytes and long lines. Each configuration runs with a new
stream per line, as queries and the SNII writer read values, and with one stream
across lines, as the V1/V2 writers do. Both must give the same XXH3 digest, the
one frozen in analysis_golden.txt; on the current code no configuration
disagrees. ANALYSIS_GOLDEN_FULL=1 scans every code point against
analysis_golden_full.txt. A test checks that every component AnalysisFactoryMgr
registers is in some configuration, and that the ICU analyzers give the same
output with Turkish as the default locale. The kuromoji configurations use a
small dictionary the test builds, because the real one is not in the
repository. The 4030 jobs run on up to 16 threads.

AnalysisDifferential compares the production english, standard, unicode,
custom standard and chinese analyzers with their old implementations, which stay
linked into the unit tests, on the same families and on one second of random
input, and shows that an injected difference is caught.

AnalysisWriteGolden freezes what the writers make of the tokens: the bytes of
the SNII file, and, because V1/V2 files carry timestamps, the terms with their
documents and positions read back, for chinese, ik, icu and a custom chain.
AnnWriteGolden freezes every sub-file of HNSW, IVF and IVF_ON_DISK indexes in V2
and in SNII, built on one OpenMP thread from vectors whose distances are exact
in float, and written twice to show the bytes repeat.

Lines of the digest files are only added: ANALYSIS_GOLDEN_REGEN=<commit> appends
the missing ones and never rewrites one; ANALYSIS_GOLDEN_DUMP writes the token
streams to diff two binaries.

check-header-deps.py gets two rules for the code moving off CLucene (snii/,
analysis/, query/, common/ and ann/ under storage/index): no file there, sources
included, and no project header it reaches may include a CLucene header, and
the code there may not name a CLucene symbol once comments and strings are
stripped, because the PCH makes CLucene visible without an include. What still
breaks the rules is listed by the file that breaks it, 31 and 51 entries; a
missing or stale entry fails the gate, so the lists only shrink.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh --run --filter='*AnalysisGolden*:*AnalysisDifferential*:*AnalysisWriteGolden*:*AnnWriteGolden*:SniiWriterGoldenBytes*:SniiGoldenCorpus*'
      passes on ASAN. The default set takes 33 s of wall time on 16 threads
      for 515 s of work; ANALYSIS_GOLDEN_FULL=1 takes 920 s.
    - GTEST_SHUFFLE=1 with '*AnalysisGolden*:*IK*' passes three times, seeds
      56420, 13481 and 56363.
    - The same tests pass on a RELEASE build, and both builds write
      byte-identical digests (ANALYSIS_GOLDEN_WRITE, 2065 lines). The ANN
      vectors have small integer coordinates for that: with fractional ones
      the two builds wrote different HNSW graphs, because -O0 and -O3 round
      faiss's distance sums differently.
    - build-support/run-clang-tidy.sh --base origin/master reports no warning
      on the changed lines.
    - Mutations, each built, run and reverted: SimpleTokenizer<char> cutting
      english tokens at 254 bytes instead of 255 fails the 4 english
      configurations; basic splitting tokens over 255 bytes instead of
      truncating them fails the 6 basic ones; IK lexemes ending one character
      later fail the 10 IK ones; jieba without its lowercasing fails the 2
      chinese ones with lower_case=true. Removing only the first-byte condition
      of that lowercasing changes no digest: no token of the shipped
      dictionaries starts with a non-ASCII byte and holds an ASCII letter.
    - build-support/tests/test-build-hygiene-header-deps.sh passes, with new
      cases for an angle include two hops from a root, a root source file,
      stale entries in both tables, and CLucene words in comments and strings.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wo CLucene adapters

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The SNII text analysis is moving off CLucene. Today every
consumer (the SNII writer, the query side, the V1/V2 writers) reaches an
analyzer through lucene::analysis::Analyzer, TokenStream and Token, one virtual
call per token, so no analyzer can leave CLucene before every consumer does,
and the other way round. This adds the interface both sides move to, with no
production caller yet, and the two adapters that let consumers and analyzers
move one at a time.

The interface is in storage/index/inverted/analysis/ and includes no CLucene
header; the gate adds no exception for it.
- TokenBatch: the tokens of one next_batch() call, each a term view and a
  position increment. A term points into the value, into memory the stream
  keeps, or into the batch's arena, which never moves a term it handed out.
  clear() keeps the token array and the arena's largest block;
  release_oversized() frees everything once the batch keeps more than 64 KiB,
  so one huge value does not pin its memory until the segment ends.
- TextTokenStream, with TextTokenizer and TextTokenFilter: reset(text) binds a
  value, next_batch() hands out up to 256 tokens and returns false, with an
  empty batch, only at the end, and reserved_bytes() reports what survives a
  value.
- PerTokenTokenizer and PerTokenFilter: bases for components that make one
  token at a time; the base copies each term into the batch. A filter reads its
  input through a private batch, so it may hold an input token across batches,
  as word_delimiter holds a token while it hands out its parts.
- TextAnalyzer: immutable after it is built, shared by threads, makes streams.
The 64 KiB high water mark and release_oversized_scratch() move from
token_stream.h to a new scratch.h, so the batch uses them without CLucene.

The adapters are in storage/index/inverted/clucene/:
- LuceneBackedTextAnalyzer puts a CLucene analyzer behind TextAnalyzer, for the
  consumers that switch before their analyzers do. Each value gets a new reader
  through the index's char filter and a new CLucene stream, as a query reads a
  value today (the SNII writer re-initializes one reader instead); terms are
  copied into the batch, and a CLuceneError comes out as a doris::Exception. It
  goes once every analyzer is native.
- LuceneAnalyzerBridge puts a TextAnalyzer behind lucene::analysis::Analyzer,
  for the V1/V2 writers. reusableTokenStream() rebinds one stream per scalar
  value, tokenStream() makes an owned stream per array element, reset() starts
  the value as SDocumentsWriter::invertField() expects, and terms go out with
  setTextNoCopy(), empty ones included, because those writers give an empty
  term a position.

One difference turned up and is pinned by a test: on the V1/V2 array path,
ICUAnalyzer::tokenStream() resets the stream and invertField() resets it
again, and ICUTokenizer, which reads its reader in reset(), finds it consumed:
array elements of an icu index are indexed as nothing. The bridge starts each
element once and indexes them. No table can have such an index, because the FE
accepts no parser and no analyzer on an array column
(InvertedIndexUtil.checkInvertedIndexParser).

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh --run --filter='TokenBatchTest.*:PerTokenStreamTest.*:LuceneBackedTextAnalyzerTest.*:LuceneAnalyzerBridgeTest.*:LuceneBridgeWriterTest.*:AnalysisGolden*:AnalysisDifferential*:AnalysisWriteGolden*:AnnWriteGolden*:SniiWriterGoldenBytes*:SniiGoldenCorpus*:InvertedIndexWriterTest.*'
      passes on ASAN, with no leak report.
    - TokenBatchTest: 10000 copied terms over many arena blocks keep their
      bytes; clear() keeps the token array and the largest block, and a batch
      that fits allocates nothing; release_oversized() leaves at most 64 KiB
      after 10000 tokens or a 1 MiB term, and keeps an ordinary batch.
    - PerTokenStreamTest: batches of at most 256 tokens; a filter making two
      tokens out of one; a filter dropping every token returns false, never an
      empty batch; a filter holding a 300-character token across two batches;
      next_token() is not called after it returned false; reset() in the middle
      of a value does not replay it; reserved_bytes() adds up the chain and
      reset() releases an oversized input batch.
    - expect_text_stream_contract(), a checker later components reuse: false
      only with an empty batch and again after, no token for an empty value with
      a null or a non-null pointer, a reused stream equal to a new one, and a
      reset after the first batch. It passes on the test streams, on a scripted
      CLucene analyzer and on the standard analyzer through
      LuceneBackedTextAnalyzer.
    - AnalysisGolden.LuceneBackedTextAnalyzerMatchesTheFrozenDigests: all 122
      analyzer configurations through LuceneBackedTextAnalyzer, with a new
      stream per line and with one stream for all lines, match
      analysis_golden.txt (3172 jobs).
    - AnalysisGolden.EightThreadsShareOneTextAnalyzer: eight threads share each
      configuration's analyzer on mixed_zh_en and invalid_utf8, half with a new
      stream per line, and all match the frozen digests.
    - AnalysisWriteGolden.LuceneAnalyzerBridgeWritesTheSameV2Index: the
      production V2 writer with its analyzer behind the bridge writes the
      frozen string-column digests of chinese, ik, icu and the custom chain,
      and the same array-column index as the analyzer itself, except the icu
      case above.
    - LuceneBridgeWriterTest: a doris::Exception from reset() or next_batch()
      reaches add_values() and add_array_values() as
      INVERTED_INDEX_ANALYZER_ERROR through the CLucene frames.
    - analysis_golden.txt and ann_write_golden.txt unchanged.
      build-support/check-build-hygiene.sh passes with the same 31 + 51
      exceptions. build-support/run-clang-tidy.sh reports no warning on the
      changed lines.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yzer interface

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The SNII text analysis is moving off CLucene. The previous
change added the CLucene-free TextAnalyzer interface and two adapters with no
production caller. This one moves every consumer onto the interface, so the
analyzers can follow one at a time. The analyzers themselves still run on
CLucene behind LuceneBackedTextAnalyzer, except the gram family, which moves now
because the SNII writer depends on its stream.

- AnalyzerProvider::get_analyzer(), InvertedIndexAnalyzerCtx::analyzer,
  InvertedIndexAnalyzer::create_analyzer() and the IndexPolicyMgr getters hand
  out a TextAnalyzerPtr. index_policy_mgr.h and inverted_index_parser.h no longer
  name a CLucene type, and no AnalyzerPtr is left outside the analysis layer.
- The index-level char filter (char_filter_type and friends) moves from the
  readers each consumer built (create_reader) into the analyzer:
  create_analyzer() and create_analyzer_provider() put it in front of whatever
  they build, builtin, custom or normalizer, so every consumer reads every value
  through it exactly as before. IndexPolicyMgr's unused outer_char_filter_map
  parameter goes.
- get_analyse_result() has one loop: a reusable form over a stream and a batch,
  and a one-shot form over an analyzer. analyze() and normalize() use it.
- The SNII writer keeps one stream and one batch for all its rows, made in
  init() and destroyed by finish() and close_on_error(). The two lanes
  (_plain_lane_token_stream, _finish_plain_lane_row) and _char_string_reader go;
  the memory report covers the stream and the batch of every analyzer
  (_report_analysis_capacity), not only the gram tokenizer's buffers. The solved
  gram density is set directly on the stream, which replaces
  _pending_density_permille.
- GramTokenizer is a native TextTokenizer that hands out its grams as views, and
  GramTextAnalyzer is the analyzer of a gram-family policy, which the FE allows
  no filter. The CLucene version stays as LuceneGramTokenizer for a CLucene
  chain, which only a policy created before the FE check (a gram tokenizer plus
  a filter) can still build. GramExtractor and GramTokenizer release a buffer
  above 64 KiB at the next row instead of keeping it until the segment ends.
- MATCH without an index and tokenize() tokenize a block with one stream and
  one batch.
- The V1/V2 writer reaches its analyzer through LuceneAnalyzerBridge and hands
  it plain readers.
- create_builtin_analyzer() and normalize() turn a CLuceneError, such as a
  missing dictionary file, into an INVERTED_INDEX_ANALYZER_ERROR exception, so
  the CLuceneError catches of analyze(), the SNII writer, the scoring collector,
  the search functions and tokenize() go. function_search.cpp keeps the one
  around the whole evaluation, which covers index reads too.
- The analyzer benchmark drives TextAnalyzers built through the production
  entry points: "row/" with a stream per row, "reuse/" with one stream.

### Release note

None

### Check List (For Author)

- Test: Unit Test, Regression test, Manual test
    - ./run-be-ut.sh --run with the analysis, golden, gram, SNII, MATCH,
      tokenize, search, inverted index reader/writer, policy and collector
      suites passes on ASAN (3945 tests).
    - analysis_golden.txt only gains lines: four frozen write/v2-array digests
      of an ARRAY<STRING> column on V2, added at 5271950, whose bridge
      path wrote them first; there the icu one already read 282 postings,
      digest 118e86a496cf6071, as here. The other digests are unchanged.
    - The benchmark's digest mode over all six corpora gives the same tokens
      as the T1 baseline binary in all 5,240,060 rows; row/ and reuse/ run.
    - New tests: the index char filter is part of create_analyzer() and
      create_analyzer_provider(); a missing chinese dictionary fails SNII
      init(), analyze() and tokenize() with INVERTED_INDEX_ANALYZER_ERROR;
      after a 2 MiB row the SNII batch is under 64 KiB and the writer charges
      exactly the stream's and the batch's capacity; finish() and
      close_on_error() destroy the stream; the solved density is on the
      writer's GramTokenizer; the native GramTokenizer gives the CLucene
      tokenizer's grams and releases a long row's scratch at the next row.
    - Fixed while widening the run: the golden write tests and the adapter
      tests now set up ExecEnv's tmp file directory themselves
      (test::ensure_tmp_file_dirs()), since some earlier test may have
      cleared it.
    - ./run-regression-test.sh --run -d inverted_index_p0 on an ASAN cluster
      of this build: 232 of 252 suites pass on the first run. The other 20
      need debug points (18) or room under the FE's limit of 100 index
      policies, which the earlier suites fill (2); all 20 pass on a fresh
      cluster with enable_debug_points = true. The BE logged no sanitizer
      error.
    - build-support/run-clang-tidy.sh reports no warning on the changed
      lines. build-support/check-build-hygiene.sh passes, with the CLucene
      exception tables down from 31 + 51 to 29 + 46 entries: the SNII writer,
      the compaction eligibility check, analyzer_provider.h and
      inverted_index_parser.h no longer reach or name CLucene.
- Behavior changed: No. Two differences are only reachable outside SQL or on
  an error path: a V1/V2 ARRAY index with the icu parser now indexes its
  elements (before, ICUAnalyzer::tokenStream() and invertField() both reset the
  stream and the second reset found the reader consumed), but the FE accepts no
  parser on an array column; and a CLuceneError raised while tokenize() or MATCH
  without an index analyzes a value now reports INVERTED_INDEX_ANALYZER_ERROR.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ream interface

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The SNII text analysis is moving off CLucene. The two previous
changes added the TextAnalyzer interface and moved every consumer onto it, with
the analyzers still CLucene ones behind LuceneBackedTextAnalyzer. This change
moves the components themselves, all in one step because the two sets of base
classes cannot be mixed in one chain: the 11 registered tokenizers (keyword,
empty, standard, ngram, edge_ngram, char_group, basic, icu, pinyin, ik_smart,
ik_max_word) and kuromoji, the 6 token filters, the 3 char filters, their
factories and the analyzers built from them. english, standard, unicode and
chinese are CLucene analyzers, not Doris components, and stay behind
LuceneBackedTextAnalyzer until their own tasks.

- TokenizerFactory::create() const returns a TextTokenizer,
  TokenFilterFactory::create() const takes and returns a TextTokenStream, and
  CharFilterFactory::create() const returns a TextCharFilter, a text-to-text
  function run in reset() instead of a CLucene Reader.
- ChainAnalyzer is char filters (CharFilteredStream), a tokenizer and token
  filters. A custom analyzer and a custom normalizer are ChainAnalyzers, and
  create_builtin_analyzer() builds the basic, icu, ik and kuromoji parsers as
  preset chains with the same lowercasing and dictionaries as before. The index
  char filter is a CharFilteredAnalyzer around whatever the index analyzes with;
  create_reader() is gone and filter_characters() runs the native char_replace.
- Components that make one token at a time derive from PerTokenTokenizer and
  PerTokenFilter and keep their state machines. Terms that stay valid until the
  next value, views of it or per-value storage, go into the batch without a copy.
- The source byte offsets added with apache#67918 are removed: nothing outside the
  analysis layer read them. Their only effect on terms, strict handling of
  invalid UTF-8 below a pinyin filter with ignore_pinyin_offset=false, stays as
  set_strict_utf8(): icu, ngram and edge_ngram throw, ik folds rune by rune.
  PositionCapability had no reader either and goes.
- Every component reports what it keeps from one value to the next
  (reserved_bytes()) and releases buffers above 64 KiB at the next value. basic,
  ik, pinyin and kuromoji kept a long value's buffers until the stream died,
  which was harmless with a stream per value and is not with the reused stream
  of the SNII writer.
- The IK core reads the value through a TextCursor (the same 4096-byte window and
  read semantics), throws doris::Exception instead of CLuceneError, and decodes
  runes with analysis/common/rune.h, jieba's rules byte for byte, instead of the
  CLucene submodule's Unicode.hpp. StandardTokenizerImpl reads through the same
  cursor. The ik tokenizer factory builds its Configuration once instead of for
  every stream.
- Deleted: analysis/reader.h (ReaderPtr moves to clucene/lucene_reader.h for the
  V1/V2 writer), token_stream.h, DorisTokenizer, DorisTokenFilter,
  DorisCharFilter, LuceneGramTokenizer, and the BasicAnalyzer, ICUAnalyzer,
  IKAnalyzer and KuromojiAnalyzer classes. match.cpp had CLucene's _CLFINALLY
  only through the old headers; it uses Defer.

### Release note

None

### Check List (For Author)

- Test: Unit Test, Regression test, Manual test
    - ./run-be-ut.sh --run with T3's suites plus the component, query engine,
      kuromoji, ik and icu suites passes on ASAN: 4121 tests, 6 skipped.
    - The new component_stream_test runs every tokenizer, filter and char
      filter through the stream contract (kuromoji in kuromoji_viterbi_test,
      on the dictionary that test builds); checks that after a 2 MiB value and
      a short one each keeps at most 1 MiB, exactly what its members hold (read
      with -fno-access-control); checks strict UTF-8 (icu, ngram and edge_ngram
      throw, ik folds rune by rune, a pinyin filter with ignore_pinyin_offset
      false turns it on upstream, the other tokenizers ignore it); and checks
      that a value NFKC_CF folds to nothing makes no token.
    - analysis_golden.txt and ann_write_golden.txt are unchanged, and
      ANALYSIS_GOLDEN_FULL=1 (every code point through every configuration)
      passes. The benchmark's digest mode gives the same tokens as the T1
      baseline binary in all 5,240,060 rows of the six corpora and fourteen
      analyzers, kuromoji on the real dictionary included.
    - Throughput with be/benchmark/analyzer_ab.py: five alternating rounds,
      the median of the per-round ratios on each of six corpora and their
      geometric mean; an A/A of the T3 binary stays within 0.98 to 1.03.
      Against T3, reuse/ (one stream for all values) and row/ (a stream per
      value): basic 2.49 and 2.01, custom basic+lowercase 2.48 and 2.00,
      keyword+lowercase 1.88 and 1.54, standard+asciifolding+lowercase 1.57
      and 1.24, standard+lowercase 1.76 and 1.51,
      standard+word_delimiter+lowercase 2.10 and 1.70, icu 1.35 and 1.18,
      ik_max_word 1.17 and 1.02, ik_smart 1.17 and 1.01, kuromoji 1.01 and
      1.01. Against the T1 baseline binary: basic 1.82 and 1.73, custom basic
      1.83 and 1.72, keyword 1.15 and 1.40, standard+asciifolding 1.23 and
      1.09, standard 1.41 and 1.34, standard+word_delimiter 1.79 and 1.57,
      icu 1.14 and 1.12, ik_max_word and ik_smart 0.99 and 0.99, kuromoji 0.99
      and 0.99; the lowest single corpus is 0.96 (ik on agent_input).
    - ./run-regression-test.sh --run -d inverted_index_p0 on an ASAN cluster
      of this build with debug points: 249 of 252 suites pass. Two hit the
      FE's limit of 100 index policies, which the earlier suites fill, and
      test_drop_index_with_format_v2 lists the index file as soon as the FE
      alter job finishes, while the BE received the drop-index task 23 and 28
      seconds later in two runs. All three pass on a fresh cluster, and the BE
      logged no sanitizer error.
    - build-support/check-build-hygiene.sh passes with the CLucene exception
      tables down from 29 + 46 to 13 + 12 entries; analyzer.cpp is the only
      file of analysis/ left in them. build-support/run-clang-tidy.sh reports
      no warning on the changed lines.
    - The rewritten tests drop only assertions on deleted features: token
      offsets and source byte offsets (keyword 6, ngram 2, icu tokenizer 8,
      pinyin tokenizer 50, ik 2, lowercase 4, icu normalizer filter 7, icu
      normalizer char filter 7 macros, pinyin filter offsets in 27 tests and
      its provenance accessors in 10), the token type of lowercase (2,
      TokenRef has none), and char_filter_test.cpp, whose 3 assertions tested
      the reader methods of the deleted DorisCharFilter.
- Behavior changed: No. The terms and positions of every analyzer are unchanged
  (above). An analyzer error now comes from the component as a doris::Exception
  instead of through the CLuceneError conversion, with the same
  INVERTED_INDEX_ANALYZER_ERROR code and a slightly different message.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lassifier

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The english parser still ran CLucene's SimpleTokenizer<char>
behind LuceneBackedTextAnalyzer, and the basic tokenizer walked the value byte
by byte into a token list. Both now classify the value 64 bytes at a time and
cut it at the edges of the letter-and-digit bits.

- tokenizer/byte_classifier.h: classify_block() turns 64 bytes into a bitmap
  of ASCII letters and digits, a bitmap of the bytes that can start a
  multi-byte character and, when lowercasing, a copy with A-Z folded. It is a
  plain loop the compiler vectorizes on every target, the byte masks turned
  into bits by simd::bytes32_mask_to_bits32_mask as the Doris filters turn
  theirs. classify_tail() copies the last bytes through a stack buffer, so
  nothing past the value is read.
- simd::bytes32_mask_to_bits32_mask gets a NEON branch; aarch64 took its
  scalar loop. It compiles for aarch64 but has not run: no aarch64 host or
  emulator was at hand, so BitsTest still has to pass on an ARM machine.
- EnglishTokenizer replaces SimpleAnalyzer<char> for the english parser and an
  unknown one: the runs of ASCII letters and digits, cut every 255 bytes,
  lowercased into a copy that keeps the offsets of the value, or views of the
  value without lower_case. The chain has no stop words: SimpleAnalyzer<char>
  never read the ones set_stopwords() gave it.
- BasicTokenizer runs the same block loop and decodes a character only at a
  byte that can start one, the only bytes the scalar loop made a token of
  besides the runs; a run is cut to 255 bytes and the rest dropped, as before.
  It lowercases its own tokens, so the builtin basic parser drops its
  lowercase filter, which changed nothing else in them: the CJK characters it
  keeps have no case. With extra_chars it keeps the scalar loop.
- normalize() lowercases the text of a builtin parser with the same ASCII
  function the tokenizers use. The std::tolower it replaces changed only A-Z
  as well: the BE never calls setlocale.
- rowid_read_ahead.cpp includes storage/iterators.h for StorageReadOptions; it
  compiled only through another file of its unity batch, and the new source
  moved it to a batch without that file.

### Release note

None

### Check List (For Author)

- Test: Unit Test, Manual test
    - BE UT under ASAN: the analysis suites with the new ones, 3396 of 3402
      tests pass; the 6 skipped are the opt-in code point scan, four gram
      report tests and the vector width check, which runs in the build
      without AVX2 below. The wider set of analysis, index and search suites:
      4152 of 4159 pass, 7 skipped. The code point scan passes with
      ANALYSIS_GOLDEN_FULL=1. New tests:
      ByteClassifierTest (every byte value at every position, random blocks,
      every tail length, a guard page after the value), EnglishTokenizerTest
      (CLucene's SimpleAnalyzer<char> run beside it on random bytes, block
      edges, the 255-byte cuts, batches around 256 tokens, views without
      lowercase, a guard page), BasicTokenizerTest (the same against the
      scalar loop), BitsTest for the mask conversion, and ComponentStreamTest
      for the buffers both release after a long value. After the last change,
      push_chinese_char forced inline, the tokenizer, component and golden
      suites ran again: 60 pass, 2 skipped.
    - The analysis golden files are unchanged, and the benchmark's digest mode
      gives the same tokens as the T1 baseline binary in all 5,240,060 rows of
      the six corpora and fourteen analyzers.
    - -Rpass=loop-vectorize with the RELEASE flags: the classification loop
      is vectorized at width 32 with AVX2 and at width 16 with SSE4.2 alone,
      in all four instantiations (a block or a tail, with or without the
      lowered copy).
    - A second ASAN UT build with USE_AVX2=0 in its own CMAKE_BUILD_DIR
      (USE_AVX2=0 in its CMakeCache.txt) runs the same 3402 tests: 3397 pass,
      ByteClassifierTest.BuiltWithTheExpectedVectorWidth among them, which
      asserts that __AVX2__ is not defined there.
    - Throughput of the benchmark binary of this commit, with
      be/benchmark/analyzer_ab.py: five alternating rounds, the median of the
      per-round ratios on each of six corpora and their geometric mean.
      Against the T1 baseline binary, reuse/ (one stream for all values) and
      row/ (a stream per value): english 3.78 and 3.12, no corpus below 1.80
      (row/http_request); basic 7.42 and 7.74, no corpus below 4.27
      (reuse/ja_wiki); custom basic+lowercase 2.40 and 2.37. Against T4:
      english 4.81 and 3.91, basic 4.05 and 4.45, custom basic+lowercase 1.31
      and 1.38, and standard, which this change does not touch, 1.00 and
      0.99. push_chinese_char is forced inline: out of line, its call for
      every lead byte made basic 5-8% slower on ja_wiki and zh_wiki. A binary
      with the same machine code for both tokenizers at other alignments
      differs by up to 8% on one corpus of row/english (geomean 0.96), so a
      difference of that size is code layout.
    - check-build-hygiene.sh passes with the CLucene exception tables
      unchanged; clang-format 16 (run_clang_format.py) leaves the changed
      files as they are; run-clang-tidy.sh reports nothing on the changed
      lines.
- Behavior changed: No. Every token of english and basic is the same as
  before (above).
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able

### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The builtin standard and unicode parsers still ran CLucene's
standard95 StandardTokenizer behind LuceneBackedTextAnalyzer, and the custom
standard tokenizer ran the JFlex scanner StandardTokenizerImpl one code point
at a time through a buffer it refills and shifts. Both now run one flat table
of the same DFA over the value.

- tokenizer/standard/uax29_table.{h,cpp}: Uax29Table lays the scanner's ZZ_*
  tables out at first use: 59 states by 29 classes with one byte per
  transition, and a compact copy of the two-level character map, so that a
  step is one load and no table is copied by hand. CLucene's standard95
  scanner has the same tables entry for entry. longest_match() is the JFlex
  longest match, over a window its caller defines.
- tokenizer/utf8_validator.h: the UTF-8 rule of the builtin parsers, ported
  from CLucene's StringUtil::validate_utf8, which accepts overlong forms, and
  the decoder of CLucene's scanner that goes with it.
- Uax29Tokenizer, the builtin standard and unicode parsers: a value strict
  UTF-8 accepts (simdutf) passes, the lenient rule decides the others, and a
  value it refuses has no token, as before. The window (a token is scanned
  while it is shorter than 255 bytes), the drop of a longer token without a
  gap, the ASCII lowercase of WORD tokens and the 33 stop words, also only
  for WORD tokens, follow the old tokenizer. Lowercasing folds the whole value
  into a copy and scans the copy: A-Z and a-z are one class of the DFA and
  every accepting state reached through a letter is WORD, so the tokens are
  the same; Uax29TableTest checks both on the table. Stop words are looked up
  as packed integers after a length test, not in an unordered_set.
- StandardTokenizer, the custom standard tokenizer: a valid UTF-8 value runs
  through the same table with the scanner's window, max_token_length bytes
  from each token start: a character that does not fit ends the token, and
  one that does not fit an empty window ends the value, as the scanner's
  refill did. Its terms are views of the value. Any other value still runs
  through StandardTokenizerImpl, whose reaction to invalid bytes depends on
  where its buffer refills; those terms are copied, since a refill moves the
  buffer.
- analyzer.cpp builds the standard and unicode parsers on Uax29Tokenizer;
  lucene_builtin_analyzer stays for chinese.
- storage/utils.cpp no longer opens ErrorCode for whatever follows it in a
  unity batch: the new sources moved it into a batch with version_graph.cpp,
  and gen_cpp/types.pb.h, first included there, then saw two LIMIT_REACH.

### Release note

None

### Check List (For Author)

- Test: Unit Test, Manual test
    - BE UT under ASAN: the analysis, index and search suites with the T5 and
      T6 ones, 4177 of 4184 tests pass; the 7 skipped are opt-in reports and
      generators, the opt-in code point scan and the vector width check of the
      build without AVX2. The code point scan passes with
      ANALYSIS_GOLDEN_FULL=1. AnalysisDifferential, which runs both standard
      parsers beside standard95 and the custom tokenizer beside
      StandardTokenizerImpl for max_token_length 1, 2, 3, 4, 5, 16, 255, 256,
      1000 and 16383, on the golden families and on random lines, passes.
      New tests: Uax29TableTest (the table against ZZ_* entry by entry, the
      class of every code point, every first character accepted, the proof
      that lowercasing the whole value changes only WORD tokens),
      Utf8ValidatorTest (CLucene's validate_utf8 on every value of up to three
      bytes and on random bytes, the decoder against CLucene's
      string_to_wstring on every accepted sequence), Uax29TokenizerTest
      (standard95 beside it on random values for every lower_case and stop
      words setting, the 255-byte window and drop, overlong and invalid
      values, the 33 stop words with their prefixes, suffixes and case
      variants, batches around 256 tokens, a guard page) and
      CustomStandardTokenizerTest (the scanner beside it on random values for
      the same ten max_token_length values, the cut at the window, the end of
      the value at a character longer than the window, the copied terms of an
      invalid value with max_token_length=8, the views of a valid one).
      LuceneBackedTextAnalyzerTest now wraps CLucene's standard analyzer
      itself instead of the standard parser.
    - The analysis golden files are unchanged, and the benchmark's digest mode
      gives the same tokens as the T1 baseline binary in all 5,240,060 rows of
      the six corpora and fourteen analyzers, the builtin standard parser and
      three custom standard chains among them.
    - Throughput of the benchmark binary of this change, with
      be/benchmark/analyzer_ab.py: five alternating rounds, the median of the
      per-round ratios on each of six corpora and their geometric mean.
      Against the T1 baseline binary, reuse/ (one stream for all values) and
      row/ (a stream per value): the builtin standard parser 1.82 and 1.79, no
      corpus below 1.52 (prose_en); unicode is the same analyzer. The custom
      standard chains, whose lowercase filter is still per token: standard +
      lowercase 1.84 and 1.78, + asciifolding + lowercase 1.51 and 1.33,
      + word_delimiter + lowercase 2.06 and 1.78. Against T5: standard 2.13
      and 2.06 (it no longer goes through the CLucene adapter), the custom
      chains 1.13 to 1.32.
    - check-build-hygiene.sh passes with the CLucene exception tables
      unchanged (analyzer.cpp keeps CLucene for chinese); clang-format 16
      (run_clang_format.py) leaves the changed files as they are;
      run-clang-tidy.sh reports nothing on the changed lines.
- Behavior changed: No. Every token of standard, unicode and the custom
  standard tokenizer is the same as before (above).
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: The chinese parser was the last builtin parser still on a
CLucene analyzer: LanguageBasedAnalyzer behind LuceneBackedTextAnalyzer, over
the copy of cppjieba in CLucene's contribs-lib. jieba's segmenters now live in
the BE as analysis/tokenizer/jieba/, ported without a change in how they cut,
and the parser runs them through a tokenizer of the new stream interface.
This commit only moves the code; a later one changes its data layout.

- analysis/tokenizer/jieba/: trie.h, dict_trie.h, mp_segment.h, hmm_model.h,
  hmm_segment.h, mix_segment.h, full_segment.h, pre_filter.h, segment_base.h,
  unicode.h and jieba.h from cppjieba, string_util.h from limonp, with
  cppjieba's and limonp's namespaces replaced by
  doris::segment_v2::inverted_index::jieba, so that doris_be_test links both
  copies without merging them. No namespace std, XLOG or XCHECK: a dictionary,
  user dictionary or model file that cannot be read, or has an illegal line,
  throws INVERTED_INDEX_ANALYZER_ERROR instead of aborting the process.
  Decoding comes from analysis/common/rune.h, which ik already uses. The
  keyword extractor, POS tagger, query segment, logging and std extensions are
  not brought over, so idf.utf8 is no longer read; it stays in the package.
  The asserts on dictionary data are DCHECKs, active in the same builds.
- JiebaTokenizer: copies the value, cuts it with MixSegment and the HMM in the
  coarse mode or with FullSegment in the fine mode, and hands the words out in
  batches, cut to 255 bytes. With lower_case, a word whose first byte is ASCII
  is lowercased where it lies when it is handed out, as the old tokenizer did
  to its buffer, so a word sees the lowercasing of the words before it. The
  fine mode's words overlap: it hands out a run of ASCII letters and digits
  whole and then the runs of letters and of digits in it, and jieba reads a
  stray continuation byte and the byte after it as one character, so a run
  can start with a byte that is not ASCII, keep its capitals, and be lowered
  by the next word. A word that still has a capital after its turn is
  therefore copied into the batch; the other terms are views of the copy of
  the value. The dictionaries are loaded once per directory, and a failed
  load is tried again by the next analyzer.
- analyzer.cpp builds the chinese parser on JiebaTokenizer, and with
  lucene_builtin_analyzer and the CLuceneError catch gone it includes no
  CLucene: it leaves the CLucene exception tables of check-header-deps.py.
- LICENSE.txt, dist/LICENSE-dist.txt, dist/licenses/LICENSE-cppjieba.txt,
  dist/licenses/LICENSE-limonp.txt and .licenserc.yaml carry the MIT terms of
  the derived files.

### Release note

None

### Check List (For Author)

- Test: Unit Test, Manual test
    - BE UT under ASAN: the analysis, index and search suites with the T5 to
      T7 ones, 4191 of 4198 tests pass; the 7 skipped are opt-in reports and
      generators, the opt-in code point scan and the vector width check of the
      build without AVX2. The code point scan passes with
      ANALYSIS_GOLDEN_FULL=1. JiebaTokenizerTest runs the parser beside
      LanguageBasedAnalyzer in both modes, with and without lowercase, on
      random values with invalid bytes, on every word of jieba.dict.utf8 and
      user.dict.utf8 (64 to a value, half with raised letters) and on
      mixed-case, unknown and undecodable words. A death test does the same on
      a dictionary of its own, with words such as 卡拉OK, Q版本 and words of a
      supplementary plane, in a process of its own because CLucene's jieba
      keeps the first dictionary it loads. Other new cases: 你好\xE4\xB8 makes
      no token, the first byte decides the lowercase, supplementary-plane
      characters and user words, loading without idf.utf8, a failed load tried
      again, eight threads on one analyzer, batches around 256 tokens, the
      stream contract, and the buffers released after a long value.
    - Mutation: lowercasing the words of a batch in reverse order fails
      AnalysisDifferential and JiebaTokenizerTest's random values, both on
      the shipped dictionaries, where a stray continuation byte starts a run
      of letters that keeps its capitals, and the batching test. The source
      was restored before the final runs.
    - The analysis golden files are unchanged, and the benchmark's digest mode
      gives the same tokens as the T1 baseline binary in all 5,240,060 rows,
      chinese_coarse and chinese_fine among them.
    - The port was also compared with the original headers directly, outside
      the BE build: 3,408,272 calls of cut and cut_all with 0 differences, on
      the shipped dictionaries (every byte and pairs and triples of chosen
      bytes, 300k random sentences with invalid bytes, every dictionary word
      alone and joined to the next, about 70 MB of corpora) and on edited ones
      (an undecodable word, user dictionaries of 1 to 4 columns, CRLF); 15
      broken files fail on both sides.
    - check-build-hygiene.sh passes, with analyzer.cpp out of both CLucene
      exception tables (12+11 now); clang-format 16 leaves the changed files
      as they are; run-clang-tidy.sh reports nothing on the changed lines.
- Behavior changed: No. Every token of the chinese parser is the same as
  before (above). A missing dictionary file still fails the analyzer, now
  with INVERTED_INDEX_ANALYZER_ERROR from the BE.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
### What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary: jieba, moved into the BE unchanged in the previous commit,
still walked a trie of hash maps behind pointers, kept a 20-byte RuneStr and a
DAG entry with an inline vector per character, and looked separators, stop
words and HMM emissions up in hash sets and maps. The chinese parser now cuts
over flat arrays built once from the same dictionaries, with every decision
the old segmenters made kept, so the tokens do not change.

- flat_dict.h: FlatDict, built from the loaded DictTrie, HMMModel and stop
  word file, which are dropped afterwards. The trie is breadth-first arrays
  (child_begin, key, weight, has_value), so the children of a node are
  consecutive and a node's count is the difference of two child_begin
  entries; the first rune has a table of 0x10000 entries, supplementary first
  runes are searched for among the root's children, up to 8 children are
  scanned and more are bisected. User single-rune words are a bitmap with a
  set for supplementary runes, the emission probabilities a rune-major table
  of 0x10000 x 4 with a map for supplementary runes, and stop words of up to
  8 bytes an open-addressing table keyed by their bytes, longer ones a sorted
  vector. The pointer trie is never built: a stable sort keeps the last unit
  of each word, which is what inserting the units in order kept.
- segmenter.h: the mix and full cuts of MPSegment, HMMSegment, MixSegment,
  FullSegment and PreFilter over a JiebaScratch (jieba_scratch.h): runes as
  code point and byte offset arrays, the DAG walked straight into the DP or
  the full cut in its old order instead of being stored, one column of
  Viterbi weights. Kept as they decide tokens: the lenient decoding of
  rune.h, no token for a value that does not decode, the DP adding weights in
  the old order and keeping the first maximum (strict >), MAX_WORD_LENGTH
  512, FullSegment's word_len carried from one rune to the next, the whole
  alphanumeric run before its runs of letters and digits, user single-rune
  words, stop words dropped before lowercasing. The separators are compared
  directly: space, \t, \n, U+FF0C and U+3002, all SPECIAL_SEPARATORS held.
- jieba.h loads the files and keeps only the FlatDict; JiebaTokenizer owns a
  JiebaScratch, releases it above the high water mark at reset() like its
  other buffers and counts it in reserved_bytes(). next_batch() is unchanged.
- trie.h, mp_segment.h, hmm_segment.h, mix_segment.h, full_segment.h,
  pre_filter.h and segment_base.h are gone; LICENSE.txt and .licenserc.yaml
  list the derived files that remain.

### Release note

The chinese parser tokenizes about 3.2 times as fast in the coarse mode and
2.3 times as fast in the fine mode, and its dictionaries take less memory.

### Check List (For Author)

- Test: Unit Test, Manual test
    - BE UT under ASAN: the analysis, index and search suites with the T5 to
      T7 ones, 4191 of 4198 tests pass and the 7 skipped are the opt-in ones;
      the code point scan passes with ANALYSIS_GOLDEN_FULL=1. JiebaTokenizerTest
      passes unchanged: the live differential with LanguageBasedAnalyzer on
      random values, every dictionary word, mixed-case, unknown and
      undecodable words, and on a dictionary of its own in a death test.
      ComponentStreamTest counts the scratch among the bytes a long value
      leaves. IndexFileWriterTest.CacheEvictionTest failed once in a run of the
      wider suite list and passes on its own: it depends on what earlier tests
      leave in a shared searcher cache, not on this change.
    - The analysis golden files are unchanged, and the benchmark's digest mode
      gives the same tokens as the T1 baseline binary in all 5,240,060 rows,
      chinese_coarse and chinese_fine among them.
    - The new code was also compared with the original CLucene headers
      directly, outside the BE build and with the real jieba_tokenizer.cpp:
      4,208,058 cuts and 7,364,072 tokenizer runs (both modes, lowercase on
      and off, the scratch reused and fresh) on every byte and pairs and
      triples of chosen bytes, 300k random sentences with invalid bytes, every
      dictionary word alone and joined to the next, long values, all corpora,
      and edited dictionaries (an undecodable main word, duplicate main words,
      supplementary-plane user words including words of 512 and 513 runes,
      supplementary-plane emissions, CRLF files, 12k stop words): 0
      mismatches, in a release and in an ASAN+UBSan build. 9 of 10 deliberate
      mutations of the new code were caught; the tenth swaps E and S on a tie
      at the last rune, which ends a word either way.
    - Throughput of the benchmark binary of this change with
      be/benchmark/analyzer_ab.py: five alternating rounds, the median of the
      per-round ratios on each corpus and their geometric mean, reuse/ and
      row/. Against the T1 baseline: coarse 3.62 and 3.23 (zh_wiki 2.85 and
      2.70, ja_wiki 3.18 and 2.93, the four mixed corpora 3.97 and 3.47), fine
      2.44 and 2.28 (zh_wiki 2.48 and 2.41, ja_wiki 2.66 and 2.54, mixed 2.37
      and 2.18). Against the previous commit: coarse 3.54 and 3.10, fine 2.43
      and 2.19, so the move itself kept the old speed.
    - Memory of loading the shipped dictionaries, each in a process of its own
      with glibc malloc: the old cppjieba::Jieba with its five files +132.3 MB
      of RSS after loading (+145 MB at the peak); the new holder +40.5 MB
      right after loading and +11.2 MB after malloc_trim (+51 MB at the peak),
      the difference being freed loader memory that glibc keeps and the BE's
      jemalloc returns on decay. Loading takes 0.19 s instead of 0.51 s.
    - check-build-hygiene.sh passes; clang-format 16 leaves the changed files
      as they are; run-clang-tidy.sh reports nothing on the changed lines.
- Behavior changed: No. Every token of the chinese parser is the same as
  before (above).
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant