You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by sa...@apache.org on 2014/04/03 01:09:09 UTC

svn commit: r1584211 - in /lucene/dev/trunk: lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/ solr/ solr/core/src/java/org/apache/solr/rest/schema/analysis/ solr/core/src/test-files/solr/collection1/conf/ solr/core/src/test/org/apach...

Author: sarowe
Date: Wed Apr  2 23:09:08 2014
New Revision: 1584211

URL: http://svn.apache.org/r1584211
Log:
SOLR-5654: Create a synonym filter factory that is (re)configurable, and capable of reporting its configuration, via REST API

Added:
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/rest/schema/analysis/ManagedSynonymFilterFactory.java   (with props)
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymFilterFactory.java   (with props)
    lucene/dev/trunk/solr/example/solr/collection1/conf/_schema_analysis_synonyms_english.json   (with props)
Modified:
    lucene/dev/trunk/lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java
    lucene/dev/trunk/solr/CHANGES.txt
    lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml
    lucene/dev/trunk/solr/example/solr/collection1/conf/schema.xml

Modified: lucene/dev/trunk/lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java?rev=1584211&r1=1584210&r2=1584211&view=diff
==============================================================================
--- lucene/dev/trunk/lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java (original)
+++ lucene/dev/trunk/lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java Wed Apr  2 23:09:08 2014
@@ -158,7 +158,7 @@ public class SynonymFilterFactory extend
   /**
    * Load synonyms with the given {@link SynonymMap.Parser} class.
    */
-  private SynonymMap loadSynonyms(ResourceLoader loader, String cname, boolean dedup, Analyzer analyzer) throws IOException, ParseException {
+  protected SynonymMap loadSynonyms(ResourceLoader loader, String cname, boolean dedup, Analyzer analyzer) throws IOException, ParseException {
     CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
         .onMalformedInput(CodingErrorAction.REPORT)
         .onUnmappableCharacter(CodingErrorAction.REPORT);

Modified: lucene/dev/trunk/solr/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/CHANGES.txt?rev=1584211&r1=1584210&r2=1584211&view=diff
==============================================================================
--- lucene/dev/trunk/solr/CHANGES.txt (original)
+++ lucene/dev/trunk/solr/CHANGES.txt Wed Apr  2 23:09:08 2014
@@ -170,7 +170,9 @@ New Features
 * SOLR-5829: Allow ExpandComponent to accept query and filter query parameters
   (Joel Bernstein)
 
-
+* SOLR-5654: Create a synonym filter factory that is (re)configurable, and
+  capable of reporting its configuration, via REST API.
+  (Tim Potter via Steve Rowe)
 
 Bug Fixes
 ----------------------

Added: lucene/dev/trunk/solr/core/src/java/org/apache/solr/rest/schema/analysis/ManagedSynonymFilterFactory.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/rest/schema/analysis/ManagedSynonymFilterFactory.java?rev=1584211&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/rest/schema/analysis/ManagedSynonymFilterFactory.java (added)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/rest/schema/analysis/ManagedSynonymFilterFactory.java Wed Apr  2 23:09:08 2014
@@ -0,0 +1,349 @@
+package org.apache.solr.rest.schema.analysis;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.io.Reader;
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.synonym.SynonymFilterFactory;
+import org.apache.lucene.analysis.synonym.SynonymMap;
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.lucene.util.CharsRef;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.rest.BaseSolrResource;
+import org.apache.solr.rest.ManagedResource;
+import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
+import org.restlet.data.Status;
+import org.restlet.resource.ResourceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TokenFilterFactory and ManagedResource implementation for 
+ * doing CRUD on synonyms using the REST API.
+ */
+public class ManagedSynonymFilterFactory extends BaseManagedTokenFilterFactory {
+  
+  public static final Logger log = LoggerFactory.getLogger(ManagedSynonymFilterFactory.class);
+  
+  public static final String SYNONYM_MAPPINGS = "synonymMappings";
+  public static final String IGNORE_CASE_INIT_ARG = "ignoreCase";
+  
+  /**
+   * ManagedResource implementation for synonyms, which are so specialized that
+   * it makes sense to implement this class as an inner class as it has little 
+   * application outside the SynonymFilterFactory use cases.
+   */
+  public static class SynonymManager extends ManagedResource 
+      implements ManagedResource.ChildResourceSupport
+  {
+
+    // TODO: Maybe hold this using a SoftReference / WeakReference to
+    // reduce memory in case the set of synonyms is large and the JVM 
+    // is running low on memory?
+    protected Map<String,Set<String>> synonymMappings;
+    
+    public SynonymManager(String resourceId, SolrResourceLoader loader, StorageIO storageIO)
+        throws SolrException {
+      super(resourceId, loader, storageIO);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void onManagedDataLoadedFromStorage(NamedList<?> managedInitArgs, Object managedData)
+        throws SolrException
+    {
+      NamedList<Object> initArgs = (NamedList<Object>)managedInitArgs;
+      
+      String format = (String)initArgs.get("format");
+      if (format != null && !"solr".equals(format)) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid format "+
+           format+"! Only 'solr' is supported.");
+      }
+      
+      // the default behavior is to not ignore case, 
+      // so if not supplied, then install the default
+      if (initArgs.get(IGNORE_CASE_INIT_ARG) == null) {
+        initArgs.add(IGNORE_CASE_INIT_ARG, Boolean.FALSE);
+      }
+      boolean ignoreCase = getIgnoreCase(managedInitArgs);
+      synonymMappings = new TreeMap<>();
+      if (managedData != null) {
+        Map<String,Object> storedSyns = (Map<String,Object>)managedData;
+        for (String key : storedSyns.keySet()) {
+          // give the nature of our JSON parsing solution, we really have
+          // no guarantees on what is in the file
+          Object mapping = storedSyns.get(key);
+          if (!(mapping instanceof List)) {
+            throw new SolrException(ErrorCode.SERVER_ERROR, 
+                "Invalid synonym file format! Expected a list of synonyms for "+key+
+                " but got "+mapping.getClass().getName());
+          }
+                    
+          // if we're configured to ignoreCase, then we build the mappings with all lower           
+          List<String> vals = (List<String>)storedSyns.get(key);
+          Set<String> sortedVals = new TreeSet<>();
+          if (ignoreCase) {
+            for (String next : vals) {
+              sortedVals.add(applyCaseSetting(ignoreCase, next));
+            }
+          } else {
+            sortedVals.addAll(vals);
+          }
+          
+          synonymMappings.put(applyCaseSetting(ignoreCase, key), sortedVals);
+        }
+      }
+      
+      log.info("Loaded {} synonym mappings for {}", synonymMappings.size(), getResourceId());      
+    }    
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected Object applyUpdatesToManagedData(Object updates) {
+      if (!(updates instanceof Map)) {
+        throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST,
+          "Unsupported data format (" + updates.getClass().getName() + "); expected a JSON object (Map)!");
+      }
+      boolean ignoreCase = getIgnoreCase();      
+      boolean madeChanges = false;
+      Map<String,Object> jsonMap = (Map<String,Object>)updates;
+      for (String term : jsonMap.keySet()) {
+        
+        term = applyCaseSetting(ignoreCase, term);
+        
+        Set<String> output = synonymMappings.get(term); 
+        
+        Object val = jsonMap.get(term);
+        if (val instanceof String) {
+          String strVal = applyCaseSetting(ignoreCase, (String)val);
+          
+          if (output == null) {
+            output = new TreeSet<>();
+            synonymMappings.put(term, output);
+          }
+                    
+          if (output.add(strVal)) {
+            madeChanges = true;
+          }
+        } else if (val instanceof List) {
+          List<String> vals = (List<String>)val;
+          
+          if (output == null) {
+            output = new TreeSet<>();
+            synonymMappings.put(term, output);
+          }
+          
+          for (String nextVal : vals) {
+            if (output.add(applyCaseSetting(ignoreCase, nextVal))) {
+              madeChanges = true;
+            }
+          }          
+          
+        } else {
+          throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "Unsupported value "+val+
+              " for "+term+"; expected single value or a JSON array!");
+        }
+      }
+          
+      return madeChanges ? synonymMappings : null;
+    }
+    
+    /**
+     * Handles a change in the ignoreCase setting for synonyms, which requires
+     * a full rebuild of the synonymMappings.
+     */
+    @Override
+    protected boolean updateInitArgs(NamedList<?> updatedArgs) {
+      if (updatedArgs == null || updatedArgs.size() == 0) {
+        return false;
+      }
+      boolean currentIgnoreCase = getIgnoreCase(managedInitArgs);
+      boolean updatedIgnoreCase = getIgnoreCase(updatedArgs);
+      if (currentIgnoreCase == true && updatedIgnoreCase == false) {
+        throw new SolrException(ErrorCode.BAD_REQUEST,
+            "Changing a managed word set's ignoreCase arg from true to false is not permitted.");
+      } else if (currentIgnoreCase == false && updatedIgnoreCase == true) {
+        // ignore case policy changed ... rebuild the map
+        Map<String,Set<String>> rebuild = new TreeMap<>();
+        for (String curr : synonymMappings.keySet()) {
+          Set<String> newMappings = new TreeSet<>();
+          for (String next : synonymMappings.get(curr)) {
+            newMappings.add(applyCaseSetting(updatedIgnoreCase, next));
+          }
+          rebuild.put(applyCaseSetting(updatedIgnoreCase, curr), newMappings);
+        }
+        synonymMappings = rebuild;
+      }
+      
+      return super.updateInitArgs(updatedArgs);
+    }
+    
+    protected String applyCaseSetting(boolean ignoreCase, String str) {
+      return (ignoreCase && str != null) ? str.toLowerCase(Locale.ROOT) : str;
+    }
+    
+    public boolean getIgnoreCase() {
+      return getIgnoreCase(managedInitArgs);
+    }
+
+    public boolean getIgnoreCase(NamedList<?> initArgs) {
+      Boolean ignoreCase = initArgs.getBooleanArg(IGNORE_CASE_INIT_ARG);
+      // ignoreCase = false by default
+      return null == ignoreCase ? false : ignoreCase;
+    }
+    
+    @Override
+    public void doGet(BaseSolrResource endpoint, String childId) {
+      SolrQueryResponse response = endpoint.getSolrResponse();
+      if (childId != null) {
+        boolean ignoreCase = getIgnoreCase();
+        String key = applyCaseSetting(ignoreCase, childId);
+        Set<String> output = synonymMappings.get(key);
+        if (output == null) {
+          throw new SolrException(ErrorCode.NOT_FOUND,
+              String.format(Locale.ROOT, "%s not found in %s", key, getResourceId()));
+        }
+        response.add(key, output);
+      } else {
+        response.add(SYNONYM_MAPPINGS, buildMapToStore(synonymMappings));      
+      }
+    }  
+
+    @Override
+    public synchronized void doDeleteChild(BaseSolrResource endpoint, String childId) {
+      boolean ignoreCase = getIgnoreCase();
+      String key = applyCaseSetting(ignoreCase, childId);
+      Set<String> output = synonymMappings.get(key);
+      if (output == null)
+        throw new SolrException(ErrorCode.NOT_FOUND, 
+            String.format(Locale.ROOT, "%s not found in %s", key, getResourceId()));
+      
+      synonymMappings.remove(key);
+      storeManagedData(synonymMappings);
+      log.info("Removed synonym mappings for: {}", key);      
+    }
+  }
+  
+  /**
+   * Custom SynonymMap.Parser implementation that provides synonym
+   * mappings from the managed JSON in this class during SynonymMap
+   * building.
+   */
+  private class ManagedSynonymParser extends SynonymMap.Parser {
+
+    SynonymManager synonymManager;
+    
+    public ManagedSynonymParser(SynonymManager synonymManager, boolean dedup, Analyzer analyzer) {
+      super(dedup, analyzer);
+      this.synonymManager = synonymManager;
+    }
+
+    /**
+     * Add the managed synonyms and their mappings into the SynonymMap builder.
+     */
+    @Override
+    public void parse(Reader in) throws IOException, ParseException {
+      for (String term : synonymManager.synonymMappings.keySet()) {
+        for (String mapping : synonymManager.synonymMappings.get(term)) {
+          add(new CharsRef(term), new CharsRef(mapping), false);
+        }
+      }      
+    }    
+  }
+  
+  protected SynonymFilterFactory delegate;
+          
+  public ManagedSynonymFilterFactory(Map<String,String> args) {
+    super(args);    
+  }
+
+  @Override
+  public String getResourceId() {
+    return "/schema/analysis/synonyms/"+handle;
+  }  
+    
+  protected Class<? extends ManagedResource> getManagedResourceImplClass() {
+    return SynonymManager.class;
+  }
+
+  /**
+   * Called once, during core initialization, to initialize any analysis components
+   * that depend on the data managed by this resource. It is important that the
+   * analysis component is only initialized once during core initialization so that
+   * text analysis is consistent, especially in a distributed environment, as we
+   * don't want one server applying a different set of stop words than other servers.
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public void onManagedResourceInitialized(NamedList<?> initArgs, final ManagedResource res) 
+      throws SolrException
+  {    
+    NamedList<Object> args = (NamedList<Object>)initArgs;    
+    args.add("synonyms", getResourceId());
+    args.add("expand", "false");
+    args.add("format", "solr");
+    
+    Map<String,String> filtArgs = new HashMap<>();
+    for (Map.Entry<String,?> entry : args) {
+      filtArgs.put(entry.getKey(), entry.getValue().toString());
+    }
+    // create the actual filter factory that pulls the synonym mappings
+    // from synonymMappings using a custom parser implementation
+    delegate = new SynonymFilterFactory(filtArgs) {
+      @Override
+      protected SynonymMap loadSynonyms
+          (ResourceLoader loader, String cname, boolean dedup, Analyzer analyzer)
+          throws IOException, ParseException {
+
+        ManagedSynonymParser parser =
+            new ManagedSynonymParser((SynonymManager)res, dedup, analyzer);
+        // null is safe here because there's no actual parsing done against a input Reader
+        parser.parse(null);
+        return parser.build(); 
+      }
+    };
+    try {
+      delegate.inform(res.getResourceLoader());
+    } catch (IOException e) {
+      throw new SolrException(ErrorCode.SERVER_ERROR, e);
+    }    
+  }
+    
+  @Override
+  public TokenStream create(TokenStream input) {    
+    if (delegate == null)
+      throw new IllegalStateException(this.getClass().getName()+
+          " not initialized correctly! The SynonymFilterFactory delegate was not initialized.");
+    
+    return delegate.create(input);
+  }
+}

Modified: lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml?rev=1584211&r1=1584210&r2=1584211&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml (original)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema-rest.xml Wed Apr  2 23:09:08 2014
@@ -457,6 +457,7 @@
        <analyzer>
         <tokenizer class="solr.StandardTokenizerFactory"/>
         <filter class="solr.ManagedStopFilterFactory" managed="english" />
+        <filter class="solr.ManagedSynonymFilterFactory" managed="english" />
       </analyzer>
     </fieldtype>
 

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymFilterFactory.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymFilterFactory.java?rev=1584211&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymFilterFactory.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/rest/schema/analysis/TestManagedSynonymFilterFactory.java Wed Apr  2 23:09:08 2014
@@ -0,0 +1,179 @@
+package org.apache.solr.rest.schema.analysis;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.solr.util.RestTestBase;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.noggit.JSONUtil;
+import org.restlet.ext.servlet.ServerServlet;
+
+public class TestManagedSynonymFilterFactory extends RestTestBase {
+  
+  private static File tmpSolrHome;
+
+  /**
+   * Setup to make the schema mutable
+   */
+  @Before
+  public void before() throws Exception {
+    tmpSolrHome = new File(dataDir + File.separator + TestManagedStopFilterFactory.class.getSimpleName()
+                          + System.currentTimeMillis());
+    FileUtils.copyDirectory(new File(TEST_HOME()), tmpSolrHome.getAbsoluteFile());
+
+    final SortedMap<ServletHolder,String> extraServlets = new TreeMap<>();
+    final ServletHolder solrRestApi = new ServletHolder("SolrSchemaRestApi", ServerServlet.class);
+    solrRestApi.setInitParameter("org.restlet.application", "org.apache.solr.rest.SolrSchemaRestApi");
+    extraServlets.put(solrRestApi, "/schema/*");
+
+    System.setProperty("managed.schema.mutable", "true");
+    System.setProperty("enable.update.log", "false");
+    createJettyAndHarness(tmpSolrHome.getAbsolutePath(), "solrconfig-managed-schema.xml", "schema-rest.xml",
+                          "/solr", true, extraServlets);
+  }
+
+  @After
+  private void after() throws Exception {
+    jetty.stop();
+    jetty = null;
+    FileUtils.deleteDirectory(tmpSolrHome);
+    System.clearProperty("managed.schema.mutable");
+    System.clearProperty("enable.update.log");
+  }
+  
+  @Test
+  public void testManagedSynonyms() throws Exception {
+    // this endpoint depends on at least one field type containing the following
+    // declaration in the schema-rest.xml:
+    // 
+    //   <filter class="solr.ManagedSynonymFilterFactory" managed="english" />
+    //      
+    String endpoint = "/schema/analysis/synonyms/english";
+    
+    assertJQ(endpoint, 
+             "/synonymMappings/initArgs/ignoreCase==false",
+             "/synonymMappings/managedMap=={}");
+      
+    // put a new mapping into the synonyms
+    Map<String,List<String>> syns = new HashMap<>();
+    syns.put("happy", Arrays.asList("glad","cheerful","joyful"));    
+    assertJPut(endpoint, 
+               JSONUtil.toJSON(syns),
+               "/responseHeader/status==0");
+    
+    assertJQ(endpoint, 
+             "/synonymMappings/managedMap/happy==['cheerful','glad','joyful']");
+
+    // request to a specific mapping
+    assertJQ(endpoint+"/happy", 
+             "/happy==['cheerful','glad','joyful']");
+    
+    // does not exist
+    assertJQ(endpoint+"/sad", 
+             "/error/code==404");
+    
+    // verify the user can update the ignoreCase initArg
+    assertJPut(endpoint, 
+               json("{ 'initArgs':{ 'ignoreCase':true } }"), 
+               "responseHeader/status==0");
+
+    assertJQ(endpoint, 
+             "/synonymMappings/initArgs/ignoreCase==true");
+    
+    syns = new HashMap<>();
+    syns.put("sad", Arrays.asList("unhappy"));    
+    syns.put("SAD", Arrays.asList("Unhappy"));    
+    assertJPut(endpoint, 
+               JSONUtil.toJSON(syns),
+               "/responseHeader/status==0");
+    
+    assertJQ(endpoint, 
+             "/synonymMappings/managedMap/sad==['unhappy']");
+    
+    // verify delete works
+    assertJDelete(endpoint+"/sad",
+                  "/responseHeader/status==0");
+    
+    assertJQ(endpoint, 
+        "/synonymMappings/managedMap=={'happy':['cheerful','glad','joyful']}");
+    
+    // should fail with 404 as foo doesn't exist
+    assertJDelete(endpoint+"/foo",
+                  "/error/code==404");
+    
+    // verify that a newly added synonym gets expanded on the query side after core reload
+    
+    String newFieldName = "managed_en_field";
+    // make sure the new field doesn't already exist
+    assertQ("/schema/fields/" + newFieldName + "?indent=on&wt=xml",
+            "count(/response/lst[@name='field']) = 0",
+            "/response/lst[@name='responseHeader']/int[@name='status'] = '404'",
+            "/response/lst[@name='error']/int[@name='code'] = '404'");
+
+    // add the new field
+    assertJPut("/schema/fields/" + newFieldName, json("{'type':'managed_en'}"),
+               "/responseHeader/status==0");
+
+    // make sure the new field exists now
+    assertQ("/schema/fields/" + newFieldName + "?indent=on&wt=xml",
+            "count(/response/lst[@name='field']) = 1",
+            "/response/lst[@name='responseHeader']/int[@name='status'] = '0'");
+
+    assertU(adoc(newFieldName, "I am a happy test today but yesterday I was angry", "id", "5150"));
+    assertU(commit());
+
+    assertQ("/select?q=" + newFieldName + ":angry",
+            "/response/lst[@name='responseHeader']/int[@name='status'] = '0'",
+            "/response/result[@name='response'][@numFound='1']",
+            "/response/result[@name='response']/doc/str[@name='id'][.='5150']");    
+    
+    // add a mapping that will expand a query for "mad" to match docs with "angry"
+    syns = new HashMap<>();
+    syns.put("mad", Arrays.asList("angry"));    
+    assertJPut(endpoint, 
+               JSONUtil.toJSON(syns),
+               "/responseHeader/status==0");
+    
+    assertJQ(endpoint, 
+        "/synonymMappings/managedMap/mad==['angry']");
+
+    // should not match as the synonym mapping between mad and angry does not    
+    // get applied until core reload
+    assertQ("/select?q=" + newFieldName + ":mad",
+        "/response/lst[@name='responseHeader']/int[@name='status'] = '0'",
+        "/response/result[@name='response'][@numFound='0']");    
+    
+    restTestHarness.reload();
+
+    // now query for mad and we should see our test doc
+    assertQ("/select?q=" + newFieldName + ":mad",
+        "/response/lst[@name='responseHeader']/int[@name='status'] = '0'",
+        "/response/result[@name='response'][@numFound='1']",
+        "/response/result[@name='response']/doc/str[@name='id'][.='5150']");    
+  }
+}

Added: lucene/dev/trunk/solr/example/solr/collection1/conf/_schema_analysis_synonyms_english.json
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/collection1/conf/_schema_analysis_synonyms_english.json?rev=1584211&view=auto
==============================================================================
--- lucene/dev/trunk/solr/example/solr/collection1/conf/_schema_analysis_synonyms_english.json (added)
+++ lucene/dev/trunk/solr/example/solr/collection1/conf/_schema_analysis_synonyms_english.json Wed Apr  2 23:09:08 2014
@@ -0,0 +1,11 @@
+{
+  "initArgs":{
+    "ignoreCase":true,
+    "format":"solr"
+  },
+  "managedMap":{
+    "GB":["GiB","Gigabyte"],
+    "happy":["glad","joyful"],
+    "TV":["Television"]
+  }
+}

Modified: lucene/dev/trunk/solr/example/solr/collection1/conf/schema.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/collection1/conf/schema.xml?rev=1584211&r1=1584210&r2=1584211&view=diff
==============================================================================
--- lucene/dev/trunk/solr/example/solr/collection1/conf/schema.xml (original)
+++ lucene/dev/trunk/solr/example/solr/collection1/conf/schema.xml Wed Apr  2 23:09:08 2014
@@ -453,6 +453,7 @@
       <analyzer>
         <tokenizer class="solr.StandardTokenizerFactory"/>
         <filter class="solr.ManagedStopFilterFactory" managed="english" />
+        <filter class="solr.ManagedSynonymFilterFactory" managed="english" />
       </analyzer>
     </fieldType>