public function save($index, $name, $param, $force = false)
    {
        $updated  = false;
        $existing = $this->load($index);

        // Directly compare array structures, should be enough
        if (!$existing || $force || $param !== $existing) {
            // This was updated, then we really need to save it
            $updated = true;
        }

        // Adds 'node_access' table ACL system replica into elastic index
        // mapping so that we can query it later, this will only replicate
        // grants for the view operation, here is the process:
        //   - it identifies each (realm, gid) group for the node as a raw
        //     string such as "REALM:GID"
        //   - it stores a string term vector of those
        //   - of course, it only stores those with 1 as value for grant_view
        //   - in all queries, a nice alteration will be done to force it to
        //     have a filter query that does a term exact match in the vector
        //     for all user grants
        $param['mapping']['node']['properties']['node_access']['type'] = 'string';

        $this
            ->db
            ->merge('ucms_search_index')
            ->key(['index_key' => $index])
            ->fields(['name' => $name, 'data' => serialize($param)])
            ->execute()
        ;

        $this->clearDefinitionCache();
        $this->moduleHandler->invokeAll(self::HOOK_DEF_SAVE, [$index, $param, $updated, !$existing]);

        $this->indexer();
        $this->nodeIndexerChain->addIndexer(
            $index,
            new NodeIndexer(
                $index,
                $this->client,
                $this->db,
                $this->entityManager,
                $this->moduleHandler,
                $this->eventDispatcher,
                $this->getIndexRealname($index),
                $this->preventBulkUsage
            ));

        if ($updated) {
            $this->clear($index);
        }
    }