BRD-1150: add categoriesFlat field with split category levels - #76
Open
egidijuskunigonis wants to merge 3 commits into
Open
BRD-1150: add categoriesFlat field with split category levels#76egidijuskunigonis wants to merge 3 commits into
egidijuskunigonis wants to merge 3 commits into
Conversation
Synonym and stemmed matching never reach a leaf category: synonyms need
an exact value match against the stored path string, and the engine's
analyzer only processes the first ~20 characters, so deep levels of
'Store > Summer > Men > T-Shirts' are invisible to search.
Each adapter now also emits categoriesFlat_{locale}: the unique level
values of every category path, split on ' > '. The field is search-only.
categories and categoryDefault are untouched, so filter aggregations and
the storefront category tree stay exactly as they are. Appending levels
into categories itself was tested and rejected: level values dominate
the top-50 facet buckets and pollute the filter sidebar (see BRD-1150).
PrestaShopAdapterV2 covers PrestaShop and WooCommerce; MagentoAdapterV2
splits its hierarchical paths; ShopifyAdapter splits the taxonomy
fullName only because tags are already flat single values.
The field only becomes searchable once brad-app adds it to the mapping
and search presets and applications reindex.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
egidijuskunigonis
marked this pull request as ready for review
July 31, 2026 07:25
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Move the new Magento tests above the helpers marker and drop a narration comment from the Shopify test.
ShopifyAdapter is shared: brad-app's V1ProductTransformer calls
transform() with no locales, which routes to buildPlainFields(), and the
V1 bulk path posts the operation payload with no field whitelist. An
unsuffixed categoriesFlat there would hand a V1 tenant a field its index
never mapped, plus full checksum churn, for no benefit - brad-app's
presets define only categoriesFlat_{locale}.
Emit the field in locale mode only. V1 output is now byte-identical to
main for every platform.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Synonym matching never reaches a leaf category today: the stored value is the whole path string, and a leaf like
Sneakerscan never match"Women > Shoes > Sneakers".Each adapter now also emits a
categoriesFlat_{locale}field: the unique level values of every category path, split on>. Every level becomes an independently-analyzed value that keyword/exact clauses can hit, and that synonym expansion can reach once the field carries thesynonymsearch type (added in brad-app#538).The field is search-only.
categoriesandcategoryDefaultare byte-for-byte untouched, so filter aggregations and the storefront category tree do not change. Appending levels intocategoriesitself was tested on a real index and rejected: level values dominate the top-50 facet buckets and pollute the filter sidebar — evidence on BRD-1150.What changed
AdapterUtils::splitCategoryLevels()— shared helper: split on>(space-padded only, so a bare>inside a category name is not a delimiter), trim, dedupe preserving first-seen order.PrestaShopAdapterV2— emitscategoriesFlat_{locale}per collectedcategories_{locale}. Covers PrestaShop and WooCommerce (brad-app routes WooCommerce through this adapter in V2).MagentoAdapterV2— same, from its hierarchical paths.ShopifyAdapter— splits the taxonomyfullNameonly; tags are excluded because they are already flat single values. Emitted in locale mode only:ShopifyAdapteris the one adapter shared with the V1 sync path (brad-app'sV1ProductTransformercallstransform()with no locales, which routes tobuildPlainFields()), and the V1 bulk path posts the operation payload with no field whitelist. Keeping the field out ofbuildPlainFields()leaves V1 output byte-identical tomainfor every platform.Example:
["Store > Summer > Men > T-Shirts", "Store > Spring > Men > Shirtlings"]→categoriesFlat_{locale}=["Store", "Summer", "Men", "T-Shirts", "Spring", "Shirtlings"].Deploy notes (corrected after local end-to-end test)
DataValidatormakes the field inert until brad-app defines it. That is wrong on the path brad-app actually uses.DataValidatoris constructed only by the V1SynchronizationApiSdk(src/SynchronizationApiSdk.php:33), and it only validates —validateProduct()iterates the field configuration, never the product, so a key absent from the configuration is neither rejected nor stripped (src/Validators/DataValidator.php:24-58). The V2 path has no validator at all:Product::fromArray()sweeps every non-core key intoadditionalFieldsandjsonSerialize()writes them all straight back out (src/V2/ValueObjects/BulkOperations/Product.php:185-194,221-224), and brad-app'sV2BulkOperations::indexProductsfeeds it the stored payload verbatim.dynamic: stricton an index, so OpenSearch simply dynamically maps the extra key rather than rejecting the document. Until #538 lands, the field is absent from the pushed search configuration, and brad-search only ever queries the fields it finds inconfig.Fields(services/v2/search/query_builder_fields.go), so nothing searches, filters or facets it. It also receives OpenSearch's default dynamic mapping (text+.keywordwithignore_above: 256), not the intended analyzer chain — the field only becomes useful once #538's mapping preset is pushed and the index is rebuilt.sync:reindex-applicationsandsync:resync-applicationsboth replay storedproduct_sync_operationspayloads without re-running the adapters — a reindex alone does NOT backfill this field. The data backfill happens via the first full platform sync after the SDK bump.app/Services/ChangeDetectionService.php:16-21, fed the adapter output atapp/Jobs/CollectProductDataJob.php:169,299), so adding a field changes the checksum of every product that belongs to at least one category — in practice the entire catalogue.sync:check-and-runis scheduled hourly and fires each application's ownproduct_sync_cron, so this lands per tenant on its normal cadence, platform-wide, with no per-tenant rollout gate in front of it. Load to plan for:processProductForIncrementalSyncgoes from its documented "~5% of total" change rate to ~100%, i.e. one extraprevious_datalookup query per product, oneproduct_sync_operationsUPDATE row per product carrying both the full previous and the full current payload, and a full re-push of every document to OpenSearch. Bump the SDK in a window where that is acceptable.categories.keyword == 'sneakers'→ 0 docs) to working (categoriesFlat.keyword == 'sneakers'→ 122 docs)..synonymssubfield — the only place thesynonym_graphfilter lives is{lang}_synonym_search_analyzer(services/v2/mapping_builder.go:566-571), and that subfield is only queried for fields whose search types includesynonym(models/query_config.go:101-104,services/v2/search/query_builder_fields.go:605-606). Exact clauses target.keyword/.keyword_ascii, whose normalizers cannot apply synonyms. brad-app#538 therefore givescategoriesFlatasynonymsearch type; without it this field would deliver literal leaf matching only and BRD-1150's acceptance criterion 2 could not pass.Testing
categoriesFlat_{locale}content and thatcategories/categoryDefaultstay unchanged.ShopifyAdapterTest::testCategoriesFlatIsNotEmittedWithoutLocalespins the V1 / no-locale output to carrying no flat field at all.pint --testonmainwith the same fixers, so reformatting would add unrelated churn.Confidence: 92%
Needs human verification
synonymsearch type on this field. Do not run that check on a Polish tenant:buildSynonymSubfieldfalls back to the plain search analyzer for languages with no stemmer/stop words, and Polish is currently the only such language.