Skip to content

Commit cc3cae8

Browse files
authored
drop chunking (#2847)
1 parent cabd1f2 commit cc3cae8

1 file changed

Lines changed: 31 additions & 70 deletions

File tree

tika-langdetect/tika-langdetect-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupLanguageDetector.java

Lines changed: 31 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,20 @@
4141
* trained on Wikipedia (primary corpus) with MADLAD supplements for thin languages.
4242
* <p>
4343
* Text is buffered via {@link #addText(char[], int, int)} up to
44-
* {@link CharSoupFeatureExtractor#MAX_TEXT_LENGTH} characters. At {@link #detectAll()} time,
45-
* the buffer is evaluated in independent {@value #CHUNK_SIZE}-character chunks.
46-
* Each chunk runs the full preprocessing pipeline (truncate → strip URLs/emails →
47-
* NFC normalize → extract bigram features → score via raw logits). If the first
48-
* chunk produces high entropy (indicating junk, code, or non-language content),
49-
* the next chunk is tried. The result from the chunk with the lowest entropy
50-
* is returned. This avoids polluting the language signal with leading junk while
51-
* keeping the implementation simple and predictable.
44+
* {@link CharSoupFeatureExtractor#MAX_TEXT_LENGTH} characters (configurable
45+
* via {@link #setMaxLength(int)}). At {@link #detectAll()} the entire buffer
46+
* is fed through the full preprocessing pipeline (strip URLs/emails →
47+
* NFC normalize → extract bigram features) and scored once. The verdict
48+
* is the argmax of the (group-collapsed) logits over the whole input.
5249
* </p>
5350
* <p>
5451
* Inference uses raw logits throughout — no softmax distribution is ever computed.
55-
* Confidence is based on the <em>margin</em> between the top two logits after
56-
* confusable-group collapsing: {@code sigmoid(top_logit − second_logit)}.
57-
* This is invariant to the number of classes and provides a stable confidence
58-
* signal from short snippets up to full documents. Per-class {@code rawScore}
59-
* is {@code sigmoid(logit_c − best_competitor_logit)}: the winner gets a value
60-
* above 0.5, all others below.
52+
* Confidence is based on the <em>margin</em> between the top two logits after
53+
* confusable-group collapsing: {@code sigmoid(top_logit − second_logit)}.
54+
* This is invariant to the number of classes and provides a stable confidence
55+
* signal from short snippets up to full documents. Per-class {@code rawScore}
56+
* is {@code sigmoid(logit_c − best_competitor_logit)}: the winner gets a value
57+
* above 0.5, all others below.
6158
* </p>
6259
*/
6360
@TikaComponent(name = "charsoup-language-detector")
@@ -73,36 +70,13 @@ public class CharSoupLanguageDetector extends LanguageDetector implements SelfCo
7370
private static final String MODEL_RESOURCE =
7471
"/org/apache/tika/langdetect/charsoup/langdetect-20260320.bin";
7572

76-
/**
77-
* Size (in chars) of each independent chunk evaluated during detection.
78-
* If the first chunk yields high entropy (junk/code), the next chunk
79-
* is tried, and so on, until a confident result is found or the buffer
80-
* is exhausted. Each chunk is preprocessed and evaluated independently
81-
* so that junk in one chunk does not pollute the signal in the next.
82-
*/
83-
private static final int CHUNK_SIZE = 5_000;
84-
8573
/**
8674
* Buffer length at which {@link #hasEnoughText()} returns true.
87-
* One chunk is more than sufficient for reliable language detection;
88-
* this is set to two chunks so the detector has a fallback if the
89-
* first chunk is junk.
90-
*/
91-
private static final int ENOUGH_TEXT_LENGTH = CHUNK_SIZE * 2;
92-
93-
/**
94-
* Maximum entropy (in bits) for a chunk to be considered "confident
95-
* enough" to return. If a chunk's collapsed-distribution entropy
96-
* exceeds this threshold, the detector moves on to the next chunk.
97-
* <p>
98-
* Typical values:
99-
* <ul>
100-
* <li>&lt; 1.0 — clean, single-language text</li>
101-
* <li>1.0–3.0 — confusable language or short text</li>
102-
* <li>&gt; 3.5 — likely junk (code, OCR garbage, binary, etc.)</li>
103-
* </ul>
75+
* 10,000 characters is comfortably above the saturation point where
76+
* the bigram-NB model has full discriminative signal on typical
77+
* prose; streaming callers can stop feeding once this is reached.
10478
*/
105-
private static final float ENTROPY_THRESHOLD = 3.5f;
79+
private static final int ENOUGH_TEXT_LENGTH = 10_000;
10680

10781
/**
10882
* Confusable language groups — languages within the same group are nearly
@@ -721,40 +695,27 @@ public boolean hasEnoughText() {
721695
@Override
722696
public List<LanguageResult> detectAll() {
723697
String text = buffer.toString();
724-
if (text.isEmpty()) {
698+
// Cheap empty/whitespace-only short-circuit so callers see the
699+
// explicit NULL result instead of model-bias logits computed from
700+
// an empty feature vector.
701+
if (text.isBlank()) {
725702
lastEntropy = Float.NaN;
726703
return Collections.singletonList(LanguageResult.NULL);
727704
}
728705

729-
int len = text.length();
730-
float[] bestLogits = null;
731-
float bestEntropy = Float.MAX_VALUE;
732-
String bestChunk = null;
706+
// Single full-buffer extraction. The feature extractor is
707+
// whitespace-invariant (only letter-letter / sentinel-letter /
708+
// letter-sentinel bigrams are emitted) and bounded internally
709+
// at MAX_TEXT_LENGTH; the caller's buffer is already bounded
710+
// by setMaxLength(...) so the work here is linear in
711+
// min(buffer.length, MAX_TEXT_LENGTH).
733712
int[] features = new int[extractor.getNumBuckets()];
734-
735-
for (int start = 0; start < len; start += CHUNK_SIZE) {
736-
int end = Math.min(start + CHUNK_SIZE, len);
737-
String chunk = text.substring(start, end);
738-
739-
extractor.extractAndCount(chunk, features);
740-
float[] logits = model.predictLogits(features);
741-
logits = applyScriptGate(logits, chunk, classScript);
742-
float[] collapsed = collapseGroups(logits, groupIndices);
743-
744-
float entropy = entropyFromLogits(collapsed);
745-
746-
if (entropy < bestEntropy) {
747-
bestEntropy = entropy;
748-
bestLogits = collapsed;
749-
bestChunk = chunk;
750-
}
751-
752-
if (entropy < ENTROPY_THRESHOLD) {
753-
break;
754-
}
755-
}
756-
757-
return buildResults(bestLogits, bestEntropy);
713+
extractor.extractAndCount(text, features);
714+
float[] logits = model.predictLogits(features);
715+
logits = applyScriptGate(logits, text, classScript);
716+
float[] collapsed = collapseGroups(logits, groupIndices);
717+
float entropy = entropyFromLogits(collapsed);
718+
return buildResults(collapsed, entropy);
758719
}
759720

760721
/**

0 commit comments

Comments
 (0)