diff --git a/app/(reference-data)/biochem/compounds/[id]/page.tsx b/app/(reference-data)/biochem/compounds/[id]/page.tsx
index 8d2a1b94..f09aebf6 100644
--- a/app/(reference-data)/biochem/compounds/[id]/page.tsx
+++ b/app/(reference-data)/biochem/compounds/[id]/page.tsx
@@ -467,9 +467,6 @@ export default function CompoundDetailPage() {
- {cpd.ontology && cpd.ontology !== 'class:null|context:null' && (
- {cpd.ontology}
- )}
{cpd.source ?? 'N/A'}
diff --git a/app/(reference-data)/biochem/reactions/[id]/page.tsx b/app/(reference-data)/biochem/reactions/[id]/page.tsx
index ff9be792..3012e99a 100644
--- a/app/(reference-data)/biochem/reactions/[id]/page.tsx
+++ b/app/(reference-data)/biochem/reactions/[id]/page.tsx
@@ -426,13 +426,6 @@ export default function ReactionDetailPage() {
- {rxn.ontology && rxn.ontology !== 'class:null|context:null|step:null' && (
-
-
- {rxn.ontology}
-
-
- )}
diff --git a/lib/api/biochem.ts b/lib/api/biochem.ts
index 60a887dc..5e8aa274 100644
--- a/lib/api/biochem.ts
+++ b/lib/api/biochem.ts
@@ -12,6 +12,7 @@
import {
CPD_IMG_BASE,
MODELSEED_API_URL,
+ SOLR_COMPOUNDS_COLLECTION,
solrCorpusEndpoint,
} from './config';
import { hasNestedSchema, parentDocTypeFilter } from './solrSchema';
@@ -114,7 +115,6 @@ export interface Reaction {
ec_numbers: string[];
is_obsolete: string;
is_transport: boolean;
- ontology: string;
pathways: string[];
notes: string[];
abbreviation?: string;
@@ -146,7 +146,6 @@ export interface Compound {
deltagerr: number;
abbreviation: string;
aliases: string[];
- ontology: string;
inchikey?: string;
smiles?: string;
is_cofactor?: boolean;
@@ -461,7 +460,13 @@ function buildQuickSearchClause(
if (nestedStoichiometryQuickSearch) {
const wildcard = usePrefixOnly ? `${token}*` : `*${token}*`;
fieldClauses.push(
- `({!parent which="${parentDocTypeFilter('reactions')}" v="doc_type:stoichiometry AND (compound:${wildcard} OR participant_name:${wildcard} OR participant_aliases:${wildcard} OR aliases:${wildcard})"})`,
+ // Participant aliases are NOT denormalised onto the stoichiometry children.
+ // They are read from the compounds core at query time with a cross-core
+ // join, so editing a compound's aliases takes effect without reposting
+ // every reaction that uses it. fromIndex must name the compounds core for
+ // THIS environment -- compounds_staging from reactions_staging -- or the
+ // clause silently resolves against the wrong dataset instead of erroring.
+ `({!parent which="${parentDocTypeFilter('reactions')}" v="doc_type:stoichiometry AND (compound:${wildcard} OR participant_name:${wildcard} OR {!join from=id to=compound fromIndex=${SOLR_COMPOUNDS_COLLECTION}}aliases:${wildcard})"})`,
);
}
if (nestedThermoEvidenceQuickSearch) {
@@ -519,11 +524,7 @@ function buildSolrUrl(collection: BiochemCollection, opts: SolrQueryOpts = {}):
url += `&fq=${encodeURIComponent(fq)}`;
}
- // Filter out ontology field for compounds (Solr compounds_staging has no ontology field)
- const filterItems = (filterModel?.items ?? []).filter(item => {
- const field = toSolrField(String(item.field ?? ''));
- return !(collection === 'compounds' && field === 'ontology');
- });
+ const filterItems = filterModel?.items ?? [];
const filterClauses = filterItems
.map((item) => buildFilterClause(item))
@@ -799,8 +800,6 @@ function matchesFilterItem(
const field = toSolrField(String(item.field ?? ''));
if (!field || !operator) return true;
- /* Solr compounds_staging has no ontology query field — REST payloads typically omit it too. */
- if (endpoint === 'compounds' && field === 'ontology') return true;
const rawField = doc[field];
const fieldValue = normalizeFieldValue(rawField);
@@ -1157,7 +1156,7 @@ const RXN_SEARCH_FIELDS_NESTED = RXN_SEARCH_FIELDS.filter((field) => field !== '
const RXN_VISIBLE = [
'name', 'id', 'definition', 'reversibility', 'thermo_evidence',
'stoichiometry', 'status', 'aliases', 'ec_numbers', 'is_obsolete',
- 'is_transport', 'ontology', 'pathways', 'notes',
+ 'is_transport', 'pathways', 'notes',
];
// Solr child transformers only return fields also present in the parent `fl` list.
// Include the stored stoichiometry fields needed by Equation highlighting before
@@ -1165,15 +1164,11 @@ const RXN_VISIBLE = [
const RXN_VISIBLE_NESTED = [
...RXN_VISIBLE,
'compound', 'coefficient', 'compartment', 'is_reactant', 'participant_name',
- 'participant_aliases', 'aliases', 'grade', 'doc_type', '_nest_path_',
+ 'grade', 'doc_type', '_nest_path_',
'[child childFilter="doc_type:stoichiometry OR doc_type:thermo_evidence OR doc_type:thermo-evidence" limit=200]',
];
-/**
- * Compound quick-search fields — must exist on Solr `compounds_staging`.
- * Note: Solr does not expose an `ontology` field on compounds; querying it yields 400
- * ("undefined field ontology") and breaks the whole quick-search clause.
- */
+/** Compound quick-search fields — every entry must exist on the Solr core. */
const CPD_SEARCH_FIELDS = ['id', 'name', 'formula', 'synonyms', 'aliases'];
/** Compound visible fields matching Solr `compounds_staging` stored fields (see fl=). */
diff --git a/tests/unit/api/biochem-rest-filtering.test.ts b/tests/unit/api/biochem-rest-filtering.test.ts
index e569b678..e69805a8 100644
--- a/tests/unit/api/biochem-rest-filtering.test.ts
+++ b/tests/unit/api/biochem-rest-filtering.test.ts
@@ -98,24 +98,6 @@ describe('biochem REST path local filter/sort/pagination', () => {
expect(String(fetchMock.mock.calls[0]?.[0] ?? '')).toContain('limit=5000');
});
- it('ignores ontology column filters on compound REST payloads (Solr has no ontology field)', async () => {
- const fetchMock = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => [{ id: 'cpd00001', name: 'ATP', formula: '', aliases: [] }],
- });
- vi.stubGlobal('fetch', fetchMock);
-
- const biochem = await import('@/lib/api/biochem');
- const res = await biochem.getCompoundsFromModelseedApi({
- limit: 10,
- filterModel: {
- items: [{ field: 'ontology', operator: 'contains', value: 'nope-miss' }],
- quickFilterValues: [],
- },
- });
- expect(res.docs).toHaveLength(1);
- });
-
it('refines quick search across compound fields locally (OR across searchFields)', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
diff --git a/tests/unit/api/biochem.test.ts b/tests/unit/api/biochem.test.ts
index bb102e7b..f982041b 100644
--- a/tests/unit/api/biochem.test.ts
+++ b/tests/unit/api/biochem.test.ts
@@ -118,7 +118,7 @@ describe('getCompounds Solr query shape', () => {
vi.restoreAllMocks();
});
- it('quick search must not reference ontology (undefined field on compounds_staging)', async () => {
+ it('quick search only references fields that exist on the compounds core', async () => {
const biochemApi = await loadBiochemApi();
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(() =>
Promise.resolve(
@@ -143,7 +143,14 @@ describe('getCompounds Solr query shape', () => {
const qRaw = u.searchParams.get('q');
expect(qRaw).toBeTruthy();
const q = decodeURIComponent(qRaw ?? '');
- expect(q).not.toMatch(/\bontology\b/i);
+ // Every field: token in q must be one the core actually defines. Solr answers an
+ // undefined field in q with a hard 400, and a 400 carries no CORS header, so the
+ // browser reports a CORS failure and points at the wrong layer. Three fields have
+ // reached production this way.
+ const COMPOUND_CORE_FIELDS = ['id', 'name', 'formula', 'synonyms', 'aliases'];
+ const queried = [...q.matchAll(/(\w+):/g)].map((mm) => mm[1]);
+ const unknown = [...new Set(queried)].filter((f) => !COMPOUND_CORE_FIELDS.includes(f));
+ expect(unknown).toEqual([]);
expect(q).toContain('cpd05323');
expect(q).toMatch(/formula|aliases|name|id/);
});
diff --git a/tests/unit/api/biochemStoichiometry.test.ts b/tests/unit/api/biochemStoichiometry.test.ts
index e2a544db..85bef740 100644
--- a/tests/unit/api/biochemStoichiometry.test.ts
+++ b/tests/unit/api/biochemStoichiometry.test.ts
@@ -124,9 +124,9 @@ describe('Solr stoichiometry support', () => {
expect(nestedUrl.searchParams.get('fl')).toBe([
'name', 'id', 'definition', 'reversibility', 'thermo_evidence',
'stoichiometry', 'status', 'aliases', 'ec_numbers', 'is_obsolete',
- 'is_transport', 'ontology', 'pathways', 'notes',
+ 'is_transport', 'pathways', 'notes',
'compound', 'coefficient', 'compartment', 'is_reactant', 'participant_name',
- 'participant_aliases', 'aliases', 'grade', 'doc_type', '_nest_path_',
+ 'grade', 'doc_type', '_nest_path_',
'[child childFilter="doc_type:stoichiometry OR doc_type:thermo_evidence OR doc_type:thermo-evidence" limit=200]',
].join(','));
expect(nested.docs[0].participants).toEqual([{
@@ -150,7 +150,7 @@ describe('Solr stoichiometry support', () => {
expect(reaction.participants).toHaveLength(1);
await api.getReactions({ filterModel: { items: [], quickFilterValues: ['cpd05331'] } });
const legacyListUrl = new URL(dataUrl(fetchMock));
- expect(legacyListUrl.searchParams.get('fl')).toBe('name,id,definition,reversibility,thermo_evidence,stoichiometry,status,aliases,ec_numbers,is_obsolete,is_transport,ontology,pathways,notes');
+ expect(legacyListUrl.searchParams.get('fl')).toBe('name,id,definition,reversibility,thermo_evidence,stoichiometry,status,aliases,ec_numbers,is_obsolete,is_transport,pathways,notes');
expect(legacyListUrl.searchParams.get('q')).toBe('(id:*cpd05331* OR name:*cpd05331* OR definition:*cpd05331* OR reversibility:*cpd05331* OR status:*cpd05331* OR ec_numbers:*cpd05331* OR aliases:*cpd05331* OR pathways:*cpd05331* OR stoichiometry:*cpd05331* OR notes:*cpd05331*)');
expect(legacyListUrl.searchParams.get('sort')).toBe('id asc');
await api.getReactions({ filterModel: { items: [], quickFilterValues: ['Glucoraphanin'] } });
@@ -176,8 +176,8 @@ describe('Solr stoichiometry support', () => {
expect(nestedQuery).toContain('name:*Glucoraphanin*');
expect(nestedQuery).toContain('definition:*Glucoraphanin*');
expect(nestedQuery).not.toContain('stoichiometry:*');
- expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*Glucoraphanin* OR participant_name:*Glucoraphanin* OR participant_aliases:*Glucoraphanin* OR aliases:*Glucoraphanin*)"})');
- expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*cpd05331* OR participant_name:*cpd05331* OR participant_aliases:*cpd05331* OR aliases:*cpd05331*)"})');
+ expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*Glucoraphanin* OR participant_name:*Glucoraphanin* OR {!join from=id to=compound fromIndex=compounds_staging}aliases:*Glucoraphanin*)"})');
+ expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*cpd05331* OR participant_name:*cpd05331* OR {!join from=id to=compound fromIndex=compounds_staging}aliases:*cpd05331*)"})');
expect(nestedQuery).toContain(') AND (');
await api.findReactionsForCompound('cpd00002');
expect(new URL(dataUrl(fetchMock)).searchParams.get('q')).toContain('{!parent which="doc_type:reaction" v="doc_type:stoichiometry AND compound:cpd00002"}');
@@ -237,7 +237,6 @@ describe('reaction request regression matrix', () => {
await api.getReactions({ filterModel: { items: [], quickFilterValues: ['RegistryAlias'] } });
const query = new URL(dataUrl(fetchMock)).searchParams.get('q') ?? '';
- expect(query).toContain('participant_aliases:*RegistryAlias*');
expect(query).toContain('aliases:*RegistryAlias*');
expect(query).toContain('participant_name:*RegistryAlias*');
expect(query).toContain('compound:*RegistryAlias*');