WooCommerce search mechanism
Search relevance

Does your WooCommerce search find Herrenschuhe for Schuhe?

German compound nouns expose what product search really stores. See how decompounding, filter order, and an asymmetric analyzer make a base noun reach the right products.

A German shopper types Schuhe. Your catalog contains Herrenschuhe, Kinderschuhe, and Fußballschuhe. Whether those products come back is decided before the query is run—when the search engine cuts each title into tokens.

The 60-second model

  1. Compound matching is a tokenization problem, not a filter-UI feature.
  2. A generic search analyzer can find fewer German products than SQL substring matching.
  3. Decompounding belongs at index time; the shopper’s query stays narrow.
  4. Filter order decides whether the split works, and a wrong order can fail silently.
  5. The final section gives you a test you can run on your own catalog.
The mechanism

Three engines can store one German word in three different ways

Search succeeds only when the query token intersects the tokens stored for the product.

The word Herrenschuhe is a single orthographic word made of Herren and Schuhe. German builds nouns this way constantly, especially in product catalogs: Winterjacke, Waschmaschinenzubehör, Damenmantel, Sicherheitsschuhe, and Kühlschrankdichtung.

A SQL condition such as LIKE '%schuhe%' can find the substring inside Herrenschuhe. It did not understand the compound; it happened to scan across it. A generic tokenizing analyzer instead stores one whole token. The query Schuhe produces a different token, so the two sets never intersect.

A German analyzer with decompounding stores the whole word plus useful subwords. The query can then reach the product through a real token match that can be scored, boosted, and combined with structured filters.

EngineHerrenschuhe becomesDoes Schuhe match?Main trade-off
SQL substring matchRaw text, no tokensYes, as a substringNo language analysis or controlled relevance
Generic analyzerherrenschuhNoCan rank matches, but this match disappeared
German decompounding chainherrenschuh, herren, schuhYes, as a tokenRequires curated artifacts and reindexing to change them

A better search engine can return fewer German products when its analyzer is wrong.

The engine is not the language model; the analyzer chain is
Inside the analyzer

Hyphenation proposes the split; the dictionary decides which subwords survive

Useful decompounding is a controlled language artifact, not every possible substring.

Elasticsearch offers a dictionary decompounder and a hyphenation decompounder. The first tries substrings against a word list and can emit noise. The second uses a hyphenation grammar to propose linguistically plausible split points, then retains the candidates that exist in a curated dictionary.

The current WPXFacets configuration uses:

"de_decompound": {
  "type": "hyphenation_decompounder",
  "hyphenation_patterns_path": "analysis/de_DR.xml",
  "word_list_path": "analysis/de_words.txt",
  "only_longest_match": true,
  "min_subword_size": 4
}

Both files must exist in $ES_PATH_CONF/analysis/ on every node. Elasticsearch refuses to create an index when a named analysis file is absent. The analyzer profile and server provisioning are therefore one deliverable, not two.

The artifacts must remain reproducible and licensable

The current de-v1 artifact set is produced by a committed build pipeline: the OFFO FOP v1.2 de_DR.xml grammar, a normalized word list derived from igerman98, plus version, licensing, and checksum records. A second build produced byte-identical output.

The license summary still needs its publication-day review. The operational rule is already clear: a handed-over node, backup, or future self-hosted edition would require a fresh distribution-compliance check.

Filter order

Normalization must happen before the dictionary sees the word

The same filters in the wrong sequence can silently turn German decompounding off.

The German index chain currently runs in this order:

Normalization must produce the dictionary form before decompounding runs.

german_normalization runs before de_decompound. The word list is normalized with the same filter at build time: ä becomes a, ö becomes o, and ß becomes ss. Reverse those positions and the decompounder compares incompatible forms. It returns no useful split and raises no obvious error.

asciifolding follows decompounding. It can still make Café reachable from Cafe without changing the representation that the German dictionary expects. The word delimiter leads the chain because the verified fixture showed that A55 survives intact as a55 and also produces a and 55, without changing the compound outputs.

The verified index-time chain stems Herren to herr and Schuhe to schuh, while preserving A55 as a55 and also emitting a and 55.
Index/search asymmetry

Expand the index, but keep the shopper’s query narrow

Index-time decompounding creates paths to a product; query-time decompounding throws away the specificity a shopper typed.

Index:  word delimiter → lowercase → German normalization
        → decompound → ASCII folding → light German stemmer

Search: lowercase → German normalization → ASCII folding
        → light German stemmer

de_decompound is deliberately absent from the search analyzer. If a query for Winterjacke were split into winter and jacke, it could retrieve Winterhose, Winterstiefel, and every other winter product. Recall would expand by sacrificing the precision of the original term.

The rule is asymmetric because the two sides have different jobs. Splitting stored product text adds legitimate ways to reach that product. Splitting the shopper’s query broadens an explicit request. The fixture suite locks this difference in so a future cleanup cannot make the two chains symmetrical by accident.

Stopword removal can erase valid product queries

The German, Romanian, and neutral profiles do not remove stopwords. A German stop list contains terms such as man, war, will, die, so, and also. In a mixed-language catalog, those are ordinary queries. An analyzed query can collapse to no tokens and return no products without an obvious error.

Verified behavior

The test suite records both the working compounds and the known miss

Passing examples establish token and matching behavior; the failing example keeps the current dictionary boundary visible.

On August 10, 2026, the chain was checked on an isolated node with inline _analyze fixtures, the files loaded from that node, and a temporary index built from the plugin’s real product mapping and queried through its real query builder. Temporary indexes were deleted after each run and did not touch a live product index.

QueryAsserted resultWhat it provesCurrent caveat
SchuheFinds Herrenschuhe, Fußballschuhe, and KinderschuheThe base noun reaches several compoundsMatch presence, not ranked quality
Herren / KinderFinds the corresponding compoundBoth sides of useful splits remain searchableFixture catalog is intentionally small
WinterjackeReturns Winterjacke onlyThe query analyzer does not decompoundDoes not measure relevance across a retail catalog
Cafe / A55Finds Café Mantel / Herrenschuhe A55Folding and SKU-shaped tokens surviveOnly the asserted forms are established
FußballDoes not find FußballschuheThe current dictionary has a known boundaryCause has not yet been isolated

The plausible explanation for the final miss is the interaction between only_longest_match and the words available around that split point. That is an inference, not a measured finding. It requires dictionary work against a real German catalog before it can be closed.

What the fixture does not prove

It verifies tokens and matching, not result ordering. No German golden-query set, MRR, nDCG@10, or before/after retail comparison exists yet. The English phone demo is an interface example, not evidence of German relevance.

Live tuning

Dictionary changes require a new index; synonyms can change without one

The reindex boundary determines which relevance improvements can be made routinely on a live catalog.

Analyzers are frozen when an index is created. Changing the decompounding dictionary, filter order, or analyzer profile means building a new index and reindexing every product. That is a scheduled operation on a large catalog, not an inline edit.

A synonym set deployed through the Elasticsearch Synonyms API can reload the search analyzers without closing the index or reindexing. Synonyms are therefore the better tool for knowledge learned continually: brand variants, regional terms such as Brötchen, Semmel, and Schrippe, Anglicisms, and recurring zero-result terms.

There are implementation boundaries. A graph-producing word delimiter cannot precede synonym_graph while synonym rules are parsed, so the search analyzer drops that delimiter. Synonyms also reach the main text fields immediately, while some dynamic meta and taxonomy fields pick them up only after the next full sync.

Boundaries

One analyzer profile cannot honestly solve a multilingual catalog

A language-aware German index is useful precisely because it does not pretend the same rules are correct for every language.

An Elasticsearch field has one analyzer chain. In a genuinely multilingual catalog, whichever language owns that chain makes the other languages accept its stemming and normalization rules. WPXFacets currently resolves a profile from the WordPress locale and falls back to a neutral chain—no stemming and no stopwords—when a multilingual installation is detectable.

German locales including de, de_AT, and de_CH resolve to the German profile. Installations whose catalog language cannot be inferred have a validated profile filter. The cleaner long-term model is one searchable product document per language, with language as a required constraint and a corresponding analyzer. That remains a design direction, not current WPXFacets behavior.

The plugin can declare the artifact version expected by an index, but Elasticsearch does not expose an analysis file for checksum verification. The authoritative check is behavioral: ask the cluster to analyze a known compound and compare the emitted tokens.

Five-minute acceptance test

Ask your own index what it stores before changing boosts or fuzziness

Two Analyze API calls and one negative test separate a tokenization problem from a ranking problem.

If your search is SQL-based, inspect the generated condition first. A leading-wildcard substring condition may find the compound without providing language analysis or controlled relevance. The exact current WooCommerce SQL behavior still needs its publication review before the article makes that implementation claim final.

If you already run Elasticsearch, ask the product field what it does with the compound:

GET /<your-index>/_analyze
{
  "field": "post_title",
  "text": "Herrenschuhe"
}

Then analyze the shopper’s query through the search analyzer:

GET /<your-index>/_analyze
{
  "analyzer": "<your-search-analyzer>",
  "text": "Schuhe"
}

Compare the token lists. If the query token does not appear in the tokens stored for the title, field boosts and fuzziness are working one layer too late. Then run the negative test:

GET /<your-index>/_analyze
{
  "analyzer": "<your-search-analyzer>",
  "text": "Winterjacke"
}

If the query returns separate winter and jacke tokens, the search-time chain is decompounding and precision may leak. Finish with ten real compounds from your best-selling categories and write down the shorter terms shoppers actually use. That catalog-specific list is more useful than a generic vendor benchmark.

Your acceptance condition

The base query token appears in the indexed compound’s token set, while a specific compound query remains intact at search time. Record both the positive and negative examples so future dictionary changes cannot move the boundary silently.

Conclusion

The fix is an inspectable analysis chain, not a faster search box

German compounds show why product discovery depends on how language is represented before any ranking formula runs.

A search engine added without a German analyzer can reduce recall compared with the naive substring matching it replaces. Decompounding belongs at index time, the query should stay narrow, and filter order is part of the behavior—not an implementation detail that can be rearranged freely.

The current fixtures establish token and matching correctness for a small set of compounds and keep one failure visible. They do not establish ranked relevance across a real German retail catalog. That next claim needs a dedicated demo shop and versioned golden queries.

Does the query token exist in the product tokens your engine actually stored?

Ask this before tuning boosts, fuzziness, or synonyms
Research

Sources and evidence

Analyzer behavior and Elasticsearch defaults can change. Recheck these sources against the exact version you run.

Explore WPXFacets

Local filtering or managed Elasticsearch

Start with the local WordPress backend or choose managed, dedicated Elasticsearch with a fallback path—without changing the way you configure facets.

View plans and pricing