Skip to content

Add CaseFoldingFilter with flat lookup table - #16371

Open
costin wants to merge 10 commits into
apache:mainfrom
costin:lucene/case-folding-table
Open

Add CaseFoldingFilter with flat lookup table#16371
costin wants to merge 10 commits into
apache:mainfrom
costin:lucene/case-folding-table

Conversation

@costin

@costin costin commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Add a token filter that applies Unicode simple case folding to normalize text for case-insensitive matching, without requiring the ICU analysis plugin.

Performs simple case folding for ~1200 BMP codepoints, which differs from Character.toLowerCase() such as: Greek final sigma (ς→σ), micro sign to Greek mu (µ→μ) and others. For ASCII text, folding is identical to lowercasing. Supplementary codepoints (> U+FFFF) fall back to Character.toLowerCase().

Uses a flat char[65536] lookup table (128 KB) lazily initialized on first use (once per JVM), built from ~1200 {codepoint, folded} pairs (which get discarded after initialization) generated from ICU4J via ./gradlew :lucene:core:generateFoldTable

Inspired by #14389.

Benchmark

AMD EPYC (c5a.2xlarge), JDK 25, 256 tokens x 8 chars
Baseline is Character.toLowerCase()

Distribution Baseline (ops/ms) FoldTable (ops/ms) Speedup
english 0.0739 0.0774 1.05x
german 0.0699 0.0882 1.26x
turkish 0.0565 0.1044 1.85x
russian 0.0137 0.1676 12.23x
greek 0.0136 0.1649 12.14x
armenian 0.0138 0.1673 12.13x
japanese 0.0180 0.1653 9.16x
cherokee 0.0101 0.1676 16.62x
mixed 0.0178 0.1046 5.86x

@github-actions

Copy link
Copy Markdown
Contributor

This PR has not had activity in the past 2 weeks, labeling it as stale. If the PR is waiting for review, notify the dev@lucene.apache.org list. Thank you for your contribution!

@github-actions github-actions Bot added the Stale label Jul 23, 2026
@rmuir

rmuir commented Jul 23, 2026

Copy link
Copy Markdown
Member

Uses a flat char[65536] lookup table (128 KB) lazily initialized on first use (once per JVM), built from ~1200 {codepoint, folded} pairs (which get discarded after initialization) generated from ICU4J via ./gradlew :lucene:core:generateFoldTable

I think we've gone thru this before with the ASCIIFoldingFilter, and the most efficient way is to just tableize basic ascii (first 128) and do a switch statement for the remainder. You dont need to build explicit table, just return toLowerCase() for value < 128. its already using two-stage tables behind the scenes.

Otherwise you blow away too much cache with the big lookup table. It might look good in a microbenchmark but not in an overall indexer run. Plus the data is pretty sparse (most of the codepoints dont have case), using a big lookup table is overkill.

@github-actions github-actions Bot removed the Stale label Jul 25, 2026
costin added 3 commits July 26, 2026 09:25
Replace 128KB flat lookup table with a generated switch statement.
~200 codepoints differ between foldCase and toLowerCase.
@costin

costin commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback.
I've reworked the code and used the generated switch arppacoh from #14389.

@rmuir

rmuir commented Jul 28, 2026

Copy link
Copy Markdown
Member

yeah this is looking good to me. sorry for dropping the ball on the previous PR, thanks for getting all this together

@uschindler

uschindler commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

How does this differ from the GenerateCaseFolding.groovy script next to this one? Do they relate to each other? The naming is a bit strange.....

The GenerateCaseFolding.groovy also has a wrong comment on the generated file (tells wrong command, maybe fix this while doing this PR).

@uschindler

Copy link
Copy Markdown
Contributor

Ah it was inspired by the other one. Maybe rename the groovy files a bit to make clear which is doing what. It's not so obvious. ... and please fix the comment on the generated file by the other one! Thanks!

@uschindler uschindler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok, but please do the file renames :-)

Fix stale regeneration command in generated file headers.
Add SimpleCaseFolding.java and CaseFolding.java to .gitattributes.
Comment thread lucene/analysis/icu/build.gradle Outdated
setupIcuDependencies(project)

tasks.register("generateUnicodeProps", {
tasks.register("generateCaseFolding", {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's wrong. There's also a UnicodeProps script (that I created long ago).

I think there might be a task missing. Can you try it out and check that all files are regenerated when you delete the hash?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another idea on the names, one of these is doing expansion and the other is doing compression/folding.

we might want to eventually fix the java file in o.a.l.u.automaton to make this clear too. Its named CaseFolding.java but it isn't doing any folding, it is doing expansion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the task names and renamed them to CaseExpansion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for that cleanup: I think it makes it a lot less confusing!

@rmuir

rmuir commented Jul 28, 2026

Copy link
Copy Markdown
Member

How does this differ from the GenerateCaseFolding.groovy script next to this one? Do they relate to each other? The naming is a bit strange.....

The GenerateCaseFolding.groovy also has a wrong comment on the generated file (tells wrong command, maybe fix this while doing this PR).

I would propose we consider renaming the .java and groovy files to be CaseExpansion for the stuff in automaton.

The difference (this is just conceptual and of course its not doing A/a but only cornercases):

  • The automaton data "expands" an A to [A, a].
  • The filter here is different and "folds" A and a to a.

costin added 2 commits July 28, 2026 20:20
Rename tasks to regenerateCaseFolding and regenerateSimpleCaseFolding
so they integrate with the checksum framework and are picked up by
`gradlew regenerate`. The previous generateUnicodeProps task lost its
checksum tracking during a build refactoring (b39e9bc).
Regenerate CaseFolding.java with current ICU 78.3.
@costin
costin requested a review from uschindler July 28, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants