Incorrect filter results for `display_name` in `metaobjects` query

We’ve noticed an issue with the display_name filter when querying Metaobjects using the GraphQL API.

When filtering with display_name, the query returns multiple results, including entries with similar but different values. For example, given two Metaobject entries with a field values “漢字えー” and “漢字びい” and the field is set as display name, the following query:

metaobjects(type: "...", first: 2, query: "display_name:'漢字えー'") { ... }

unexpectedly returns both entries, instead of only the exact match for “漢字えー”.

This issue seems to specifically affect non-Latin characters, suggesting that the search behavior might be performing partial matching or normalization in a way that impacts languages like Japanese.

Could you confirm whether this is intended behavior or a known issue? Any insights on how display_name queries handle non-Latin text would be appreciated.

This is the code that reproduces the bug (as custom apps for node/typescript).

import "@shopify/shopify-api/adapters/node";
import { shopifyApi, ApiVersion } from "@shopify/shopify-api";
import dotenv from "dotenv";

const METAOBJECT_DEFINITION_TYPE = "metaobject_for_test_query_by_display_name";
const METAOBJECT_FIELD_KEY = "the_name";
const METAOBJECT_TEST_NAMES = {
  "kanji-a": "漢字えー",
  "kanji-b": "漢字びい",
};

dotenv.config();
const SHOPIFY_STORE: string = process.env.SHOPIFY_STORE ?? "";
const SHOPIFY_SECRET_KEY: string = process.env.SHOPIFY_SECRET_KEY ?? "";
const SHOPIFY_ACCESS_TOKEN: string = process.env.SHOPIFY_ACCESS_TOKEN ?? "";

const shopify = shopifyApi({
  apiSecretKey: SHOPIFY_SECRET_KEY,
  apiVersion: ApiVersion.January25,
  isCustomStoreApp: true,
  adminApiAccessToken: SHOPIFY_ACCESS_TOKEN,
  isEmbeddedApp: false,
  hostName: SHOPIFY_STORE,
});

const customAppSession = shopify.session.customAppSession(SHOPIFY_STORE);
const graphQLClient = new shopify.clients.Graphql({ session: customAppSession });

/** Check if the metaobject definition exists */
async function checkMetaobjectDefinitionExists(): Promise<string | undefined> {
  const response = await graphQLClient.request(
    `query {
      metaobjectDefinitionByType(type: "${METAOBJECT_DEFINITION_TYPE}") { id }
    }`,
  );
  console.log(`Checking if metaobject definition ${METAOBJECT_DEFINITION_TYPE} exists: ` + JSON.stringify(response?.data?.metaobjectDefinitionByType, null, 2));
  return response?.data?.metaobjectDefinitionByType?.id;
}

/** Create the metaobject definition */
async function createMetaobjectDefinition(): Promise<string | undefined> {
  const response = await graphQLClient.request(
    `mutation ($definition: MetaobjectDefinitionCreateInput!) {
      metaobjectDefinitionCreate(definition: $definition) {
        metaobjectDefinition { id }
        userErrors { field message code }
      }
    }`, {
    variables: {
      "definition": {
        "type": METAOBJECT_DEFINITION_TYPE,
        "displayNameKey": METAOBJECT_FIELD_KEY,
        "fieldDefinitions": [
          {
            "key": METAOBJECT_FIELD_KEY,
            "type": "single_line_text_field",
          }
        ]
      }
    }
  }
  );
  console.log(`Creating metaobject definition ${METAOBJECT_DEFINITION_TYPE}: ` + JSON.stringify(response?.data?.metaobjectDefinitionCreate, null, 2));
  return response?.data?.metaobjectDefinitionCreate?.metaobjectDefinition?.id;
}

/** Check if a metaobject entry exists */
async function checkMetaobjectExists(handle: string, name: string): Promise<string | undefined> {
  const response = await graphQLClient.request(
    `query ($handle: MetaobjectHandleInput!) {
      metaobjectByHandle(handle: $handle) { id }
    }`, {
    variables: {
      "handle": {
        "handle": handle,
        "type": METAOBJECT_DEFINITION_TYPE,
      }
    }
  }
  );
  console.log(`Checking if metaobject ${handle}, ${name} exists: ` + JSON.stringify(response?.data?.metaobjectByHandle, null, 2));
  return response?.data?.metaobjectByHandle?.id;
}

/** Create a metaobject entry */
async function createMetaobject(handle: string, name: string): Promise<string | undefined> {
  const response = await graphQLClient.request(
    `mutation ($metaobject: MetaobjectCreateInput!) {
      metaobjectCreate(metaobject: $metaobject) {
        metaobject { id }
        userErrors { field message code }
      }
    }`, {
    variables: {
      "metaobject": {
        "handle": handle,
        "type": METAOBJECT_DEFINITION_TYPE,
        "fields": [{
          key: METAOBJECT_FIELD_KEY, value: name,
        }]
      }
    }
  }
  );
  console.log(`Creating metaobject ${handle}, ${name}: ` + JSON.stringify(response?.data?.metaobjectCreate, null, 2));
  return response?.data?.metaobjectCreate?.metaobject?.id;
}

/** Fetch metaobject entries by display_name */
async function searchMetaobjectsByDisplayName(displayName: string): Promise<string[]> {
  const response = await graphQLClient.request(
    `query {
      metaobjects(type:"${METAOBJECT_DEFINITION_TYPE}", first:5, query:"display_name:'${displayName}'") {
        nodes {
          id
          theField: field(key:"${METAOBJECT_FIELD_KEY}"){ value }
        }
      }
    }`
  );
  console.log(`Searching metaobjects by display_name ${displayName}: ` + JSON.stringify(response?.data?.metaobjects, null, 2));
  return response?.data?.metaobjects?.nodes.map((node: any) => {
    return node.theField.value;
  });
}

async function test() {
  // Create metaobject definition if it does not exist.
  let metaobjectDefinitionId = await checkMetaobjectDefinitionExists();
  if (!metaobjectDefinitionId) {
    metaobjectDefinitionId = await createMetaobjectDefinition();
    if (!metaobjectDefinitionId) {
      console.log("Failed to create metaobject definition.");
      return;
    }
  }

  // Create metaobject entries.
  let objectCreated = false;
  for (const [handle, name] of Object.entries(METAOBJECT_TEST_NAMES)) {
    let metaobjectId = await checkMetaobjectExists(handle, name);
    if (!metaobjectId) {
      metaobjectId = await createMetaobject(handle, name);
      if (!metaobjectId) {
        console.log("Failed to create metaobject.");
        return;
      }
      objectCreated = true;
    }
  }

  // Sleep for 3 seconds to wait for the metaobject index to be updated.
  if (objectCreated) {
    console.log("Sleep 3 seconds to wait for the metaobject index to be updated.");
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }

  // Test if each search result by display_name is identical.
  console.log("==== Tests ====");
  for (const [handle, name] of Object.entries(METAOBJECT_TEST_NAMES)) {
    const result = await searchMetaobjectsByDisplayName(name);
    if (result.length !== 1) {
      console.log(`[NG] Searching display_name "${name}" results in ${result.length} entries: ${result.join(", ")}`);
      continue;
    }
    if (result[0] !== name) {
      console.log(`[NG] Searching display_name "${name}" results in a wrong entry: ${result.join(", ")}`);
      continue;
    }
    console.log(`[OK] Searching display_name "${name}" returns the correct result.`);
  }
}

test();

Here is the result (of the problematic part).

Searching metaobjects by display_name 漢字えー: {
  "nodes": [
    {
      "id": "gid://shopify/Metaobject/95075893544",
      "theField": {
        "value": "漢字えー"
      }
    },
    {
      "id": "gid://shopify/Metaobject/95075926312",
      "theField": {
        "value": "漢字びい"
      }
    }
  ]
}
[NG] Searching display_name "漢字えー" results in 2 entries: 漢字えー, 漢字びい

Yes, the display_name filter does not perform an exact match. Instead, it acts as a partial match and returns all Metaobjects where the display_name contains the queried value. This behavior has been the same since Metaobjects were first introduced. If you need an exact match, you’ll need to filter the results manually after retrieving them or use IDs or handles instead.

We ran into the exact same issue last year, We needed to delete a specific Metaobject and assumed the display_name filter would return only one result. Instead, it returned multiple items, and we accidentally deleted them all. It took us some time to fix this because we initially expected the filter to return a single result.

1 Like

Thank you for your reply. I’m going to filter the results of metaobjects query to avoid the problem.

However, I’m wondering why the above cases of “漢字えー” and “漢字びい” are returning results that are not the result of “partial matching”, but rather the result of so-called “ambiguous search”. I think that the non-Latin character tokenizer is behaving a little strangely.