Skip to content

production.py unconditionally overwrites an explicitly configured SEARCH_ENGINE, making non-Elasticsearch backends unreachable #38991

Description

@blarghmatey

Summary

lms/envs/production.py and cms/envs/production.py overwrite SEARCH_ENGINE with
search.elastic.ElasticSearchEngine whenever certain feature flags are on — even when the
operator has explicitly set SEARCH_ENGINE to something else in LMS_CFG / CMS_CFG. This
makes the documented Typesense backend (and Meilisearch, and any custom engine) unreachable on
any deployment that has courseware search enabled.

Affected versions

Present and identical on all three branches checked on 2026-08-14:

  • master @ 0c243f5a7
  • release/verawood @ 259473c5d
  • release/ulmo

Details

CMScms/envs/production.py (master :283-285, verawood :286-288):

if ENABLE_COURSEWARE_INDEX or ENABLE_LIBRARY_INDEX:
    # Use ElasticSearch for the search engine
    SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"

LMSlms/envs/production.py (master :322-330, verawood :338-346):

if (
   ENABLE_COURSEWARE_SEARCH or
   ENABLE_DASHBOARD_SEARCH or
   ENABLE_COURSE_DISCOVERY or
   ENABLE_TEAMS
   ):
    # Use ElasticSearch as the search engine herein
    SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"

Both run after the YAML config has been applied to the module namespace
(vars().update(...) over _YAML_TOKENS, cms/envs/production.py:70), so an explicitly
configured SEARCH_ENGINE is silently discarded.

The FeaturesProxy interaction makes this hard to work around

Since FEATURES = FeaturesProxy(globals()) (cms/envs/production.py:35,
lms/envs/production.py:39), and FeaturesProxy.__setitem__ writes straight into the
settings module's namespace:

def __setitem__(self, key, value):
    warnings.warn(...)
    self.ns[key] = value

...the YAML FEATURES ingest loop (cms/envs/production.py:154-155) writes those keys as
module globals. So setting ENABLE_COURSEWARE_INDEX under FEATURES: in CMS_CFG is
indistinguishable from setting the top-level name, and there is no way to enable courseware
indexing without also triggering the SEARCH_ENGINE override.

Minimal reproduction using the real proxy class:

from openedx.core.lib.features_setting_proxy import FeaturesProxy

ns = {'ENABLE_COURSEWARE_INDEX': False, 'ENABLE_LIBRARY_INDEX': False,
      'SEARCH_ENGINE': 'search.typesense.TypesenseEngine'}
FEATURES = FeaturesProxy(ns)

for feature, value in {'ENABLE_COURSEWARE_INDEX': True}.items():   # production.py:154-155
    FEATURES[feature] = value

if ns['ENABLE_COURSEWARE_INDEX'] or ns['ENABLE_LIBRARY_INDEX']:    # production.py:283
    ns['SEARCH_ENGINE'] = 'search.elastic.ElasticSearchEngine'

print(ns['SEARCH_ENGINE'])   # search.elastic.ElasticSearchEngine

Impact

An operator following
Use Typesense search backend
and setting SEARCH_ENGINE: search.typesense.TypesenseEngine in LMS_CFG/CMS_CFG will find
that:

  • the LMS reads from Elasticsearch as soon as any of ENABLE_COURSEWARE_SEARCH,
    ENABLE_DASHBOARD_SEARCH, ENABLE_COURSE_DISCOVERY or ENABLE_TEAMS is on — i.e. in
    virtually every real deployment;
  • the CMS writes to Elasticsearch as soon as courseware indexing is enabled.

There is no warning or log line; the configured value is simply gone.

Suggested fix

Only apply the Elasticsearch default when SEARCH_ENGINE was not explicitly configured, e.g.

if (ENABLE_COURSEWARE_INDEX or ENABLE_LIBRARY_INDEX) and 'SEARCH_ENGINE' not in _YAML_TOKENS:
    SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"

or drop the override entirely and let SEARCH_ENGINE default in common.py, which would be
consistent with how other backends are selected.

I'm happy to open a PR for whichever shape maintainers prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions