Showing posts with label Lucene-Solr. Show all posts
Showing posts with label Lucene-Solr. Show all posts

How Solr Create Collection - Learn Solr Code


Test Code to create collections
MiniSolrCloudCluster cluster = new MiniSolrCloudCluster(4 /*numServers*/, testBaseDir, solrXml, JettyConfig.builder().setContext("/solr").build());
cluster.createCollection(collectionName, 2/*numShards*/, 2/*replicationFactor*/, "cie-default", null);

CollectionsHandler
CollectionsHandler.handleRequestBody(SolrQueryRequest, SolrQueryResponse)

CollectionAction action = CollectionAction.get(a); // CollectionAction .CREATE(true)
CollectionOperation operation = CollectionOperation.get(action); //CollectionOperation .CREATE_OP(CREATE)

Map result = operation.call(req, rsp, this);
Return a mpa like this:
{name=collectionName, fromApi=true, replicationFactor=2, collection.configName=configName, numShards=2, stateFormat=2}

ZkNodeProps props = new ZkNodeProps(result);
if (operation.sendToOCPQueue) handleResponse(operation.action.toLower(), props, rsp, operation.timeOut);

CollectionsHandler. handleResponse
QueueEvent event = coreContainer.getZkController() .getOverseerCollectionQueue() .offer(Utils.toJSON(m), timeout);

This uses DistributedQueue.offer(byte[] data, long timeout) to add a task to /overseer/collection-queue-work/qnr-numbers.

It uses LatchWatcher to wait until this task is processed.

Overseer and OverseerCollectionProcessor

OverseerCollectionProcessor.processMessage(ZkNodeProps, String operation /*create*/)
OverseerCollectionProcessor.processMessage(ZkNodeProps, String)
{
  "name":"collectionName",
  "fromApi":"true",
  "replicationFactor":"2",
  "collection.configName":"configName",
  "numShards":"2",
  "stateFormat":"2",
  "operation":"create"}
  
OverseerCollectionProcessor.createCollection(ClusterState, ZkNodeProps, NamedList)

  ClusterStateMutator.getShardNames(numSlices, shardNames);
   positionVsNodes = identifyNodes(clusterState, nodeList, message, shardNames, repFactor); // round-robin if rule not set

  createConfNode(configName, collectionName, isLegacyCloud);
    Overseer.getInQueue(zkStateReader.getZkClient()).offer(Utils.toJSON(message));
// This message will be processed by ClusterStateUpdater
// wait for a while until we do see the collection in the clusterState

  for (Map.Entry e : positionVsNodes.entrySet()) {
  if (isLegacyCloud) {
    shardHandler.submit(sreq, sreq.shards[0], sreq.params);
  } else {
    coresToCreate.put(coreName, sreq);
  }
}

This will send http call and be handled by CoreAdminHandler.handleRequestBody.

ClusterStateUpdater
  // if there were any errors while processing
  // the state queue, items would have been left in the
  // work queue so let's process those first
  byte[] data = workQueue.peek();
  boolean hadWorkItems = data != null;
  while (data != null)  {
    final ZkNodeProps message = ZkNodeProps.load(data);
    clusterState = processQueueItem(message, clusterState, zkStateWriter, false, null);
    workQueue.poll(); // poll-ing removes the element we got by peek-ing
    data = workQueue.peek();
  }

ClusterStateUpdater.processQueueItem

  zkWriteCommand = processMessage(clusterState, message, operation);
  stats.success(operation);

  clusterState = zkStateWriter.enqueueUpdate(clusterState, zkWriteCommand, callback);
processMessage

  case CREATE:
    return new ClusterStateMutator(getZkStateReader()).createCollection(clusterState, message);

overseer.ClusterStateMutator.createCollection(ClusterState, ZkNodeProps)

How DistributedUpdateProcessor Works - Learning Solr Code


Case Study
Case 1: Update request is first sent to a follower (can be any node)
I: The coordinator nodes receives the add request:
DistributedUpdateProcessor.processAdd(AddUpdateCommand)

DistribPhase phase =
    DistribPhase.parseParam(req.getParams().get(DISTRIB_UPDATE_PARAM));

DISTRIB_FROM_COLLECTION is null
phrase is None

DistributedUpdateProcessor.setupRequest(String, SolrInputDocument, String)

ClusterState cstate = zkController.getClusterState();
DocCollection coll = cstate.getCollection(collection);
Slice slice = coll.getRouter().getTargetSlice(id, doc, route, req.getParams(), coll);
String shardId = slice.getName();

In which shard this doc should store

Replica leaderReplica = zkController.getZkStateReader().getLeaderRetry(
    collection, shardId);
isLeader = leaderReplica.getName().equals(
    req.getCore().getCoreDescriptor().getCloudDescriptor()
        .getCoreNodeName());

Whether I am the leader that should store the doc: false

2. Forward to the leader that should store the doc
// I need to forward onto the leader...
nodes = new ArrayList<>(1);

3. DistributedUpdateProcessor.processAdd(AddUpdateCommand)
params.set(DISTRIB_UPDATE_PARAM,
           (isLeader || isSubShardLeader ?
            DistribPhase.FROMLEADER.toString() :
            DistribPhase.TOLEADER.toString())); ==> TOLEADER
params.set(DISTRIB_FROM, ZkCoreNodeProps.getCoreUrl(
    zkController.getBaseUrl(), req.getCore().getName()));
cmdDistrib.distribAdd(cmd, nodes, params, false, replicationTracker);

II: The leader receives the request:
org.apache.solr.update.processor.UpdateRequestProcessorChain.createProcessor(SolrQueryRequest, SolrQueryResponse)
final String distribPhase = req.getParams().get(DistributingUpdateProcessorFactory.DISTRIB_UPDATE_PARAM);
=TOLEADER
skipToDistrib true
// skip anything that doesn't have the marker interface - UpdateRequestProcessorFactory.RunAlways

org.apache.solr.update.processor.DistributedUpdateProcessor.processAdd(AddUpdateCommand)
DistribPhase phase = TOLEADER
String fromCollection = updateCommand.getReq().getParams().get(DISTRIB_FROM_COLLECTION);
null
if (isLeader || isSubShardLeader) {
          // that means I want to forward onto my replicas...
          // so get the replicas...
          forwardToLeader = false;
}
nodes = follower nodes

2. The leader adds the doc locally first
boolean dropCmd = false;
if (!forwardToLeader) {    // forwardToLeader false
  dropCmd = versionAdd(cmd); // usually return false
}

doLocalAdd(cmd);
----
private void doLocalAdd(AddUpdateCommand cmd) throws IOException {
  super.processAdd(cmd);
}

if (willDistrib) { // true
  cmd.solrDoc = clonedDoc;
}

3. The leader forwards the add request to its followers
update.distrib=FROMLEADER&distrib.from=http://192.168.1.12:8983/solr/pie_search_shard1_replica2/
params = new ModifiableSolrParams(filterParams(req.getParams()));
params.set(DISTRIB_UPDATE_PARAM,
           (isLeader || isSubShardLeader ?
            DistribPhase.FROMLEADER.toString() :
            DistribPhase.TOLEADER.toString()));
params.set(DISTRIB_FROM, ZkCoreNodeProps.getCoreUrl(
    zkController.getBaseUrl(), req.getCore().getName()));

if (replicationTracker != null && minRf > 1)
  params.set(UpdateRequest.MIN_REPFACT, String.valueOf(minRf));

cmdDistrib.distribAdd(cmd, nodes, params, false, replicationTracker);

III: Followers receives the request:
1. org.apache.solr.update.processor.UpdateRequestProcessorChain.createProcessor(SolrQueryRequest, SolrQueryResponse)
FROMLEADER
final String distribPhase = req.getParams().get(DistributingUpdateProcessorFactory.DISTRIB_UPDATE_PARAM); //FROMLEADER
final boolean skipToDistrib = distribPhase != null; // true

2.
org.apache.solr.update.processor.DistributedUpdateProcessor.processAdd(AddUpdateCommand)
if (DistribPhase.FROMLEADER == phase && !couldIbeSubShardLeader(coll)) {
  if (req.getCore().getCoreDescriptor().getCloudDescriptor().isLeader()) {
    // locally we think we are leader but the request says it came FROMLEADER
    // that could indicate a problem, let the full logic below figure it out
  } else {
    isLeader = false;     // we actually might be the leader, but we don't want leader-logic for these types of updates anyway.
    forwardToLeader = false;
    return nodes;
  }
}

return empty nodes
if (!forwardToLeader) {
  dropCmd = versionAdd(cmd);
}

Case 2: The update request is sent to leader which should store this doc
DistributedUpdateProcessor.setupRequest(String, SolrInputDocument, String)
DistribPhase phase: none

Replica leaderReplica = zkController.getZkStateReader().getLeaderRetry(
    collection, shardId);
isLeader = leaderReplica.getName().equals(
    req.getCore().getCoreDescriptor().getCloudDescriptor()
        .getCoreNodeName());
true

if (isLeader || isSubShardLeader) {
          // that means I want to forward onto my replicas...
          // so get the replicas...
          forwardToLeader = false;
}
nodes = followers

It will forward the request to its followers with params:
update.distrib=FROMLEADER&distrib.from=the_leader_node_url

org.apache.solr.update.processor.DistributedUpdateProcessor.processAdd(AddUpdateCommand)
if (!forwardToLeader) { // false
  dropCmd = versionAdd(cmd);
}
It will add to its local at this stage

// It doesn't forward this request to itself again, so no stage update.distrib=TOLEADER

Case 3: The add request is sent to a leader which should not own this doc
Case 4: The add request is sent to a leader which should not own this doc
The coordinator node will forward the add request to the leader of the shard that should store the request

DistributedUpdateProcessor.setupRequest(String, SolrInputDocument, String)
ClusterState cstate = zkController.getClusterState();
DocCollection coll = cstate.getCollection(collection);
Slice slice = coll.getRouter().getTargetSlice(id, doc, route, req.getParams(), coll);
String shardId = slice.getName();

decide which shard this doc belongs to
return nodes - the leader that should store the doc

org.apache.solr.update.processor.DistributedUpdateProcessor.processAdd(AddUpdateCommand)
update.distrib=TOLEADER&distrib.from=this_node_url

cmdDistrib.distribAdd(cmd, nodes, params, false, replicationTracker);

Case 5: Send multiple docs in one command to a follower
XMLLoader.processUpdate(SolrQueryRequest, UpdateRequestProcessor, XMLStreamReader)

while (true) {
  if ("doc".equals(currTag)) {
    if(addCmd != null) {
      log.trace("adding doc...");
      addCmd.clear();
      addCmd.solrDoc = readDoc(parser);
      processor.processAdd(addCmd);
    } else {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unexpected tag without an tag surrounding it.");
    }
  }
}


It calls processAdd for each doc.

Related Code
UpdateRequestProcessorFactory.RunAlways

UpdateRequestProcessorChain.createProcessor(SolrQueryRequest, SolrQueryResponse)

UpdateRequestProcessorChain.init(PluginInfo)
if the chain includes the RunUpdateProcessorFactory, but does not include an implementation of the DistributingUpdateProcessorFactory interface, then an instance of DistributedUpdateProcessorFactory will be injected immediately prior to the RunUpdateProcessorFactory.
if (0 <= runIndex && 0 == numDistrib) {
  // by default, add distrib processor immediately before run
  DistributedUpdateProcessorFactory distrib
    = new DistributedUpdateProcessorFactory();
  distrib.init(new NamedList());
  list.add(runIndex, distrib);

}

DistribPhase phase = DistribPhase.parseParam(req.getParams().get(DISTRIB_UPDATE_PARAM))

boolean isOnCoordinateNode = (phase == null || phase == DistribPhase.NONE);

Making Child Documents Working with Spring-data-solr


The Problem
We use spring-data-solr in our project - as we like its conversion feature which can convert string to enum, entity to json data and etc, and vice versa, and recently we need use Solr's nested documents feature which spring-data-solr doesn't support.

Issues in Spring-data-solr
SolrInputDocument class contains a Map _fields AND List _childDocuments.

Spring-data-solr converts java entity class to SolrDocument. It provides two converters: MappingSolrConverter and SolrJConverter.

MappingSolrConverter converts the entity to a Map: MappingSolrConverter.write(Object, Map, SolrPersistentEntity)

SolrJConverter uses solr's DocumentObjectBinder to convert entity to SolrInputDocument,
it will convert field that is annotated with @Field(child = true) to child documents.
- This also means that spring-data-solt's convert features will not work with SolrJConverter

BUT SolrJConverter still just thinks SolrInputDocument is a map and add all into the destination: Map sink
- SolrJConverter.write(Object, Map)

After this, the child documents is discarded.

The Fix
We still want to use spring-data-solr's conversion functions - partly because we don't want to rewrite everything to use SolrJ directly.

So when save to solr: we uses spring-data-solr's MappingSolrConverter to convert parent entity as solrInputDocument, then convert child entities as solrInputDocuments and add them into parent's solrInputDocument.

When read from solr, we read the SolrDocument as parent entity, then read its child documents as child entities and add them into parent entity.
public class ParentEntity {
  @Field(child = true)
  private List<ChildEntity> children;
}
@Autowired
protected SolrClient solrClient;

// we add our own converters into MappingSolrConverter
// for more, please check 
// http://lifelongprogrammer.blogspot.com/2015/09/mix-spring-data-solr-and-solrj-in-solr.html
@Autowired
protected MyMappingSolrConverter solrConverter;

public void save(@Nonnull final ParentEntity parentEntity) {
    final SolrInputDocument solrInputDocument = solrConverter.createAndWrite(parentEntity);
    daddChildDocuemnts(parentEntity, solrInputDocument);
    try {
        solrClient.add(getCollection(), solrInputDocument);
        solrClient.commit(getCollection());
    } catch (SolrServerException | IOException e) {
        throw new BusinessException(e, "failed to save " + parentEntity);
    }
}

protected void daddChildDocuemnts(@Nonnull final ParentEntity parentEntity,
        @Nonnull final SolrInputDocument solrInputDocument) {
    solrInputDocument.addChildDocuments(parentEntity.getChildren().stream()
            .map(child -> solrConverter.createAndWrite(child)).collect(Collectors.toList()));
}

public List<T> querySolr(final SolrParams query) {
    try {
        final QueryResponse response = solrClient.query(getCollection(), query);
        return convertFromSolrDocs(response.getResults());
    } catch (final Exception e) {
        throw new BusinessException("data retrieve failed." + query);
    }
}
/*
 * Also return child documents in solr response as ChildEntity if it exists
 */
protected List<ParentEntity> convertFromSolrDocs(final SolrDocumentList docList) {
    List<ParentEntity> result = new ArrayList<>();
    if (docList != null) {
        result = docList.stream().map(solrDoc -> {
            final ParentEntity parentEntity = solrConverter.read(ParentEntity.class, solrDoc);
            final List<SolrDocument> childDocs = solrDoc.getChildDocuments();
            if (childDocs != null) {
                ParentEntity.setChildren(
                        childDocs.stream().map(childSolrDoc -> solrConverter.read(ChildEntity.class, solrDoc))
                                .collect(Collectors.toList()));
            }

            return parentEntity;
        }).collect(Collectors.toList());
    }

    return result;
}
Related
Mix Spring Data Solr and SolrJ in Solr Cloud 5
SolrJ: Support Converter and make it easier to extend DocumentObjectBinder

Script to Setup SolrCloud Environment


Senario
Here is the script how to create SolrCloud environment in local dev setup: a collection with numShards=2&replicationFactor=3 on 3 separate (local) nodes.

vms_solr_init creates folders: example/cloud/node{1,2,3}/solr, copy solr.xml and zoo.cfg to these folders, starts the server and creates the collection using admin collection apis.

Other scripts which start/stop are easier to implement.
The implementation
function solr_init()
{
  cd $SOLR_HOME
  mkdir -p $SOLR_NODE1_REL_HOME
  mkdir -p $SOLR_NODE2_REL_HOME
  mkdir -p $SOLR_NODE3_REL_HOME

  cp $SOLR_HOME/server/solr/solr.xml $SOLR_HOME/server/solr/zoo.cfg $SOLR_NODE1_REL_HOME
  cp $SOLR_HOME/server/solr/solr.xml $SOLR_HOME/server/solr/zoo.cfg $SOLR_NODE2_REL_HOME
  cp $SOLR_HOME/server/solr/solr.xml $SOLR_HOME/server/solr/zoo.cfg $SOLR_NODE3_REL_HOME

  solr_start
  data_solr_create
}

function solr_start() {
  if [[ `solr_pid` ]]
  then
    echo "solr is already running...";
  else
    echo "Starting solr-cloud... $SOLR_NODE1_PORT, $SOLR_NODE2_PORT, $SOLR_NODE3_PORT";
    $SOLR_HOME/bin/solr start -cloud -Dsolr.ltr.enabled=true -s "$SOLR_NODE1_REL_HOME" -p $SOLR_NODE1_PORT -h $SOLR_HOSTNAME;
    $SOLR_HOME/bin/solr start -cloud -Dsolr.ltr.enabled=true -s "$SOLR_NODE2_REL_HOME" -p $SOLR_NODE2_PORT -z $SOLR_ZKHOST -h $SOLR_HOSTNAME;
    $SOLR_HOME/bin/solr start -cloud -Dsolr.ltr.enabled=true -s "$SOLR_NODE3_REL_HOME" -p $SOLR_NODE3_PORT -z $SOLR_ZKHOST -h $SOLR_HOSTNAME;
  fi
}

function solr_stop() {
  echo "Stopping solr-cloud...";
  $SOLR_HOME/bin/solr stop -all;
}

function solr_restart() {
  echo "Restarting solr-cloud...";
  solr_stop && solr_start
}

function solr_pid() {
  pgrep -f "solr-6.4.0/server";
}

function data_solr_create() {
  # Go to the solr config directory
  currdir=`pwd`;
  cd "$WS/resource/solr";

  # Retrieve list of collections
  collections_list=`curl -s -v -X GET  -H 'Content-type:application/json' "$SOLR_NODE1_PORT/admin/collections?action=LIST&wt=json" | jq '.collections | join(" ")' `;

  # Create/update schema
  mv solrconfig solrconfig.old.`datetimestamp`;
  unzip -d solrconfig solr-core-config.zip;

  # create myCollection
  cp myCollection_solrconfig.xml solrconfig/conf/solrconfig.xml;
  cp myCollection_schema.xml solrconfig/conf/schema.xml;
  $SOLR_HOME/server/scripts/cloud-scripts/zkcli.sh -zkhost "$SOLR_ZKHOST" -cmd upconfig -confname myCollection -confdir "solrconfig/conf/";
  if grep -q "myCollection" <<< $collections_list; then
    curl -s -v -X GET "$SOLR_NODE1_ENDPOINT/admin/collections?action=RELOAD&name=myCollection";
    echo "Updated myCollection";
  else
    curl -s -v -X GET "$SOLR_NODE1_ENDPOINT/admin/collections?action=CREATE&name=myCollection&numShards=2&collection.configName=myCollection&replicationFactor=3&maxShardsPerNode=2";
    echo "Created myCollection";
  fi

  rm -rf solrconfig;
  cd $currdir;
}

Java APIs to Get and Update Solr Configuration Files


User Case
SolrCloud stores its configuration files (for example: elevate.xml) in Zookeeper. Usually we need APIs that clients(for example UI) can call to get these configuration files or update.

Related: Build Web Service APIs to Update Solr's Managed Resources (stop words, synonyms)

The Implementation
We use SolrJ's SolrZkClient APIs to get data, make znode, and set znode's data.

public String getConfigData(final String filePath) {
    final ZkStateReader zkStateReader = getZKReader(getSolrClient());
    final String path = normalizeConfigPath(filePath);
    final SolrZkClient zkClient = zkStateReader.getZkClient();
    try {
        return new String(zkClient.getData(path, null, null, true));
    } catch (KeeperException | InterruptedException e) {
        throw new BusinessException(ErrorCode.data_access_error, e, "Failed to get " + path);
    }
}
public void setConfigData(final String filePath, final String data, final boolean createPath,
        final boolean reloadCollection) {
    Validate.notNull(filePath);
    Validate.notNull(data);
    final ZkStateReader zkStateReader = getZKReader(getSolrClient());
    final String path = normalizeConfigPath(filePath);
    final SolrZkClient zkClient = zkStateReader.getZkClient();
    try {
        if (createPath) {
            zkClient.makePath(path, false, true);
        }
        zkClient.setData(path, data.getBytes(), true);

        if (reloadCollection) {
            reloadCollection();
        }
    } catch (KeeperException | InterruptedException e) {
        throw new BusinessException(ErrorCode.data_access_error, e, "Failed to get " + path);
    }
}

public void reloadCollection() {
    try {
        final CollectionAdminRequest.Reload reload = new CollectionAdminRequest.Reload();
        reload.setCollectionName(getSolrClient().getDefaultCollection());
        final CollectionAdminResponse response = reload.process(getSolrClient());
        logger.info(MessageFormat.format("reload collection: {0} rsp: {1}", getSolrClient().getDefaultCollection(),
                response));
        final int status = response.getStatus();
        if (status != 0) {
            throw new BusinessException(ErrorCode.data_access_error,
                    "Failed to reload collection, status: " + status);
        }
    } catch (SolrServerException | IOException e) {
        throw new BusinessException(ErrorCode.data_access_error,
                "Failed to reload collection: " + getSolrClient().getDefaultCollection());
    }
}

public static ZkStateReader getZKReader(final CloudSolrClient solrClient) {
    final ZkStateReader zkReader = solrClient.getZkStateReader();
    if (zkReader == null) {
        // This only happens when we first time call solrClient to do anything
        // Usually we will call solrClient to do something during abolition starts: such as
        // healthCheck, so in most cases, its already connected.
        solrClient.connect();
    }
    return solrClient.getZkStateReader();
}
/**
 * @param filePath
 * @return add prefix to make elevate.xml - /configs/myCollection/elevate.xml
 */
private String normalizeConfigPath(final String filePath) {
    return ZkStateReader.CONFIGS_ZKNODE + "/" + getSolrClient().getDefaultCollection() + "/" + filePath;
}

Resources
Build Web Service APIs to Update Solr's Managed Resources (stop words, synonyms)

Java APIs to Build Solr Suggester and Get Suggestion


User Case
Usually we provide Rest APIs to manage Solr, same for suggestor.
This article focuses on how to programmatically build Solr suggester and get suggestions using java code.

The implementation
Please check the end of the article for Solr configuration files.

Build Suggester
In Solr, after we add docs to Solr, we call suggest?suggest.build=true to build the suggestor to make them available for autocompletion.

The only trick here is the suggest.build request doesn't build suggester for all cores in the collection, BUT only builds suggester to the core that receives the request.

We need get all replicas urls of the collection, add them into shards parameter, and also add shards.qt=/suggest:
shards=127.0.0.1:4567/solr/myCollection_shard1_replica3,127.0.0.1:4565/solr/myCollection_shard1_replica2,127.0.0.1:4566/solr/myCollection_shard1_replica1,127.0.0.1:4567/solr/myCollection_shard2_replica3,127.0.0.1:4566/solr/myCollection_shard2_replica1/,127.0.0.1:4565/solr/myCollection_shard2_replica2&shards.qt=/suggest

public void buildSuggester() {
    final SolrQuery solrQuery = new SolrQuery();
    final List<String> urls = getAllSolrCoreUrls(getSolrClient());

    solrQuery.setRequestHandler("/suggest").setParam("suggest.build", "true")
            .setParam(ShardParams.SHARDS, COMMA_JOINER.join(urls))
            .setParam(ShardParams.SHARDS_QT, "/suggest");
    try {
        final QueryResponse queryResponse = getSolrClient().query(solrQuery);
        final int status = queryResponse.getStatus();
        if (status >= 300) {
            throw new BusinessException(ErrorCode.data_access_error,
                    MessageFormat.format("Failed to build suggestions: status: {0}", status));
        }
    } catch (SolrServerException | IOException e) {
        throw new BusinessException(ErrorCode.data_access_error, e, "Failed to build suggestions");
    }
}
public static List<String> getAllSolrCoreUrls(final CloudSolrClient solrClient) {
    final ZkStateReader zkReader = getZKReader(solrClient);
    final ClusterState clusterState = zkReader.getClusterState();

    final Collection<Slice> slices = clusterState.getSlices(solrClient.getDefaultCollection());
    if (slices.isEmpty()) {
        throw new BusinessException(ErrorCode.data_access_error, "No slices");
    }
    return slices.stream().map(slice -> slice.getReplicas()).flatMap(replicas -> replicas.stream())
            .map(replica -> replica.getCoreUrl()).collect(Collectors.toList());
}

private static ZkStateReader getZKReader(final CloudSolrClient solrClient) {
    final ZkStateReader zkReader = solrClient.getZkStateReader();
    if (zkReader == null) {
        // This only happens when we first time call solrClient to do anything
        // Usually we will call solrClient to do something during abolition starts: such as
        // healthCheck, so in most cases, its already connected.
        solrClient.connect();
    }
    return solrClient.getZkStateReader();
}

Get Suggestions


public Set<SearchSuggestion> getSuggestions(final String prefix, final int limit) {
   final Set<SearchSuggestion> result = new LinkedHashSet<>(limit);
   try {
       final SolrQuery solrQuery = new SolrQuery().setRequestHandler("/suggest").setParam("suggest.q", prefix)
               .setParam("suggest.count", String.valueOf(limit)).setParam(CommonParams.TIME_ALLOWED,
                       mergedConfig.getConfigByNameAsString("search.suggestions.time_allowed.millSeconds"));
       // context filters
       solrQuery.setParam("suggest.cfq", getContextFilters());
       final QueryResponse queryResponse = getSolrClient().query(solrQuery);
       if (queryResponse != null) {
           final SuggesterResponse suggesterResponse = queryResponse.getSuggesterResponse();
           final Map<String, List<Suggestion>> map = suggesterResponse.getSuggestions();
           final List<Suggestion> infixSuggesters = map.get("infixSuggester");
           if (infixSuggesters != null) {
               for (final Suggestion suggester : infixSuggesters) {
                   if (result.size() < limit) {
                       result.add(new SearchSuggestion().setText(suggester.getTerm())
                               .setHighlightedText(replaceTagB(suggester.getTerm())));
                   } else {
                       break;
                   }
               }
           }
       }
       logger.info(
               MessageFormat.format("User: {0}, query: {1}, limit: {2}, result: {3}", user, query, limit, result));
       return result;
   } catch (final Exception e) {
       throw new BusinessException(ErrorCode.data_access_error, e, "Failed to get suggestions for " + query);
   }
}
private static final Pattern TAGB_PATTERN = Pattern.compile("<b>|</b>");
public static String replaceTagB(String input)
{
    return TAGB_PATTERN.matcher(input).replaceAll("");
}

Schema.xml
We define textSuggest and suggesterContextField, copy fields which are shown in the autocompletion to textSuggest field, and copy filter fields such as zipCodes, genres to suggesterContextField.

Solr suggester supports filters on multiple fields, all we just need copy all these filter fields to suggesterContextField.


<field name="suggester" type="textSuggest" indexed="true"
  stored="true" multiValued="true" />
<field name="suggesterContextField" type="string" indexed="true" stored="true"
  multiValued="true" />

<copyField source="seriesTitle" dest="suggester" />
<copyField source="programTitle" dest="suggester" />

<copyField source="zipCodes" dest="suggesterContextField" />
<copyField source="genres" dest="suggesterContextField" />
SolrConfig.xml
We can add multiple suggester implementations to searchComponent. Another very useful is FileDictionaryFactory which allows us to using an external file that contains suggest entries. We may use it in future.


<searchComponent name="suggest" class="solr.SuggestComponent">
  <lst name="suggester">
    <str name="name">infixSuggester</str>
    <str name="lookupImpl">BlendedInfixLookupFactory</str>
    <str name="dictionaryImpl">DocumentDictionaryFactory</str>
    <str name="blenderType">position_linear</str>
    <str name="field">suggester</str>
    <str name="contextField">suggesterContextField</str>
    <str name="minPrefixChars">4</str>
    <str name="suggestAnalyzerFieldType">textSuggest</str>
    <str name="indexPath">infix_suggestions</str>
    <str name="highlight">true</str>
    <str name="buildOnStartup">false</str>
    <str name="buildOnCommit">false</str>
  </lst>
</searchComponent>

<requestHandler name="/suggest" class="solr.SearchHandler"
  >
  <lst name="defaults">
    <str name="suggest">true</str>
    <str name="suggest.dictionary">infixSuggester</str>
    <str name="suggest.onlyMorePopular">true</str>
    <str name="suggest.count">10</str>
    <str name="suggest.collate">true</str>
  </lst>
  <arr name="components">
    <str>suggest</str>
  </arr>
</requestHandler>

Resources
Solr Suggester

Build Web Service APIs to Update Solr's Managed Resources (stop words, synonyms)


User Case
Solr provides Rest API to update managed resources such as stop words, synonyms and etc. But usually we will wrap it and provide Rest api in our application layer, so admin can do solr admin operation in UI.

Also usually we use CloudSolrClient and prefer to use solrj api over make directly rest api to solr server - as usually our application only knows address of zookeeper  servers, not address of solr servers.

The Implementation
We first create generic APIs: getManagedResource, addManagedResource and deleteManagedResource. Then we call them to manage stop words, synonyms.

We use spring-data-solr's SolrJsonRequest in getManagedResource and addManagedResource which can help parse json's response.

deleteManagedResource is more complex - we can't use solrJ directly
as org.apache.solr.client.solrj.SolrRequest.METHOD only supports GET, POST, PUT not DELETE.

Here I use CloudSolrClient's Apache HttpClient to send HttpDelete, use ZkStateReader and ClusterState to get one address of live solr nodes.

The Rest APIs just call these methods.

public String getManagedResource(final String path) {
    try {
        final SolrJsonRequest request = new SolrJsonRequest(METHOD.GET, path);
        return request.process(this.getSolrClient()).getJsonResponse();
    } catch (SolrServerException | IOException e) {
        throw new MyServerException(e, "Failed to get " + path);
    }
}
public void addManagedResource(final String path, final Object content, final boolean reloadCollection) {
    final SolrJsonRequest request = new SolrJsonRequest(METHOD.PUT, path);
    request.addContentToStream(content);
    try {
        final SolrJsonResponse response = request.process(this.getSolrClient());
        final int status = response.getStatus();
        logger.info(MessageFormat.format("add resource: {0}, status: {1}, result: {2}", path, status,
                response.getJsonResponse()));
        if (status != 0) {
            throw new MyServerException(ErrorCode.data_access_error,
                    MessageFormat.format("Failed to add resource, path: {0}, status: {1}", path, status));
        }
        if (reloadCollection) {
            reloadCollection();
        }
    } catch (SolrServerException | IOException | InterruptedException e) {
        throw new MyServerException(e, "Failed to add resource: " + path);
    }
}
public void deleteManagedResource(@Nonnull final List<String> paths, final boolean reloadCollection) {
    try {
        Preconditions.checkNotNull(paths);
        final String solrUrl = getOneSolrServerUrl(getSolrClient());
        final List<String> done = new ArrayList<>(paths.size());
        for (final String path : paths) {
            final HttpDelete request = new HttpDelete(solrUrl + path);

            final HttpResponse response = getSolrClient().getHttpClient().execute(request);
            final String entity = EntityUtils.toString(response.getEntity());
            logger.info(MessageFormat.format("delete path: {0}, result: {1}", path, entity));
            final ObjectMapper objectMapper = Util.createFailSafeObjectmapper();
            final Map<String, Object> resultMap = objectMapper.readValue(entity,
                    objectMapper.getTypeFactory().constructMapLikeType(Map.class, String.class, Object.class));
            final Map<String, Object> responseHeader = (Map<String, Object>) resultMap.get("responseHeader");
            if (responseHeader != null) {
                final int status = Integer.valueOf(responseHeader.get("status").toString());
                // ignore 404 which means it's already deleted
                if (status != 0 && status != 404) {
                    throw new MyServerException(MessageFormat.format(
                            "Failed to delete path: {0}, status: {1}, already deleted: {2}", path, status, done));
                }
            }
            done.add(path);
        }
        if (reloadCollection) {
            this.reloadCollection();
        }
    } catch (IOException | SolrServerException | InterruptedException e) {
        throw new MyServerException(ErrorCode.data_access_error, e, "Failed to delete path: " + paths);
    }
}

public String getSynonyms(final String language) {
    return getManagedResource("/schema/analysis/synonyms/" + language);
}
public void addSynonyms(final String language, final List<Object> synonymes, final boolean reloadCollection) {
    for(Object synonyms: synonymes){
        addManagedResource("/schema/analysis/synonyms/" + language, synonyms, reloadCollection);
    }
}
public void deleteSynonyms(final String language, final List<String> synonyms, final boolean reloadCollection) {
    if (CollectionUtils.isNotEmpty(synonyms)) {
        deleteManagedResource(synonyms.stream().map(synonym -> {
            return MessageFormat.format("/schema/analysis/synonyms/{0}/{1}", language,
                    Util.encodeAsUtf8(synonym));
        }).collect(Collectors.toList()), reloadCollection);
    }
}

public void addStopWords(final String language, final List<String> stopWords, final boolean reloadCollection) {
    addManagedResource("/schema/analysis/stopwords/" + language, stopWords, reloadCollection);
}
public String getStopWords(final String language) {
    return getManagedResource("/schema/analysis/stopwords/" + language);
}
public void deleteStopWords(final String language, final List<String> stopWords, final boolean reloadCollection) {
    if (CollectionUtils.isNotEmpty(stopWords)) {
        deleteManagedResource(stopWords.stream().map(synonym -> {
            return MessageFormat.format("/schema/analysis/stopwords/{0}/{1}", language,
                    Util.encodeAsUtf8(synonym));
        }).collect(Collectors.toList()), reloadCollection);
    }
}


public static String getOneSolrServerUrl(CloudSolrClient solrClient)
{
    final ZkStateReader zkReader =solrClient.getZkStateReader();
    final ClusterState clusterState = zkReader.getClusterState();
    final SetString> liveNodes = clusterState.getLiveNodes();
    
    if (liveNodes.isEmpty()) {
        throw new MyServerException(ErrorCode.data_access_error, "No lobe nodes");
    }
    return zkReader.getBaseUrlForNodeName(new TreeSet<>(liveNodes).iterator().next()) + "/" + solrClient.getDefaultCollection();   
}

Using in-memory Embedded Solr


The problem
We store data in solr cloud. As application evolves from single-suppose message application to multiple-tenant message applications, there is much more traffic to our application. 

The traffic is high, but the data is small. - As we don't have many active messages at specific time.

To boost search performance, we decide to use in memory embeddedSolr. 


The Solution
Admin application still use CloudSolrRepositery to write data into solr cloud.

Client-facing application periodically deletes expired data from embeddedSolr and copies (only) updated/new data from solr cloud to embeddedSolr. 

We change server code to use the EmbeddedSolrRepositery. - So there is only little change to existing code.

DataSyncService.copyMessagesFromSolrCloudToEmbeddedSolr deletes expired data from embedded Solr and then copies (only) updated/new data from solr cloud to embeddedSolr. - it ignores data that already exists(with same id and _version_ values).

It's already be called in the ContextLoaderListener so it copies all data from solrcloud to embedded Solr before application startup finishes.

It's also a scheduled task - it will be called periodically.
Here we use SchedulingConfigurer - not @Scheduled because we want to make the interval configurable and changeable. - @Scheduled only supports read value from property file, but doesn't support to call bean method.

Also admin application can change the configuration to enable/disable embedded solr and change the frequency of sync.

Talk is cheap. Show me the code.
@Service(MessageCloudRepository.NAME)
public class MessageCloudRepository extends AbstractMessageRepository {
    public static final String NAME = "MessageCloudRepository";
    @Autowired
    @Qualifier(RestCommonsAppConfig.BEAN_SOLR_CLOUD)
    private SolrClient cloudSolrServer;
    @Override
    public SolrClient getSolrServer() {
        return cloudSolrServer;
    }
}

@Service(MessageEmbeddedRepository.NAME)
public class MessageEmbeddedRepository extends AbstractMessageRepository {
    public static final String NAME = "MessageEmbeddedRepository";
    @Autowired
    @Qualifier(RestCommonsAppConfig.BEAN_EMBEDDED_MESSAGE_)
    private SolrClient embeddedSolrServer;

    @Override
    public SolrClient getSolrServer() {
        return embeddedSolrServer;
    }
}

@Service
public class DataSyncService {
    @Autowired
    @Qualifier(MessageEmbeddedRepository.NAME)
    private IMessageRepository embeddedRepository;
    @Autowired
    private @Qualifier(MessageCloudRepository.NAME) IMessageRepository cloudRepository;
    @Autowired
    private IConfigService configService;

    public void copyMessagesFromSolrCloudToEmbeddedSolr() {
        if (!configService.isEmbeddedMessageSolrEnabled()) {
            return;
        }
        deleteExpiredDataFromEmbeddedSolr();

        final String query = ""; // the query to get new active data
        final SolrQuery solrQuery = new SolrQuery(query);
        // but add filter to ignore data already in embeddedSolr with same id and _version_
        ignoreExistingData(solrQuery);

        final List<Future<List<Message>>> messagesFutures = cloudRepository.findAllAsync(solrQuery);
        if (CollectionUtils.isNotEmpty(messagesFutures)) {
            for (final Future<List<Message>> messagesFuture : messagesFutures) {
                try {
                    final List<Message> messages = messagesFuture.get().stream().map(message -> {
                        message.setVersionFromSolrCloud(message.getVersion());
                        return message;
                    }).collect(Collectors.toList());
                    embeddedRepository.saveWithoutCommit(messages);
                } catch (InterruptedException | ExecutionException e) {
                    logger.error("Failed to copy data from solr cloud to embedded solr.", e);
                }
            }

            embeddedRepository.hardCommit();
        }
    }
    /**
     * ignore data that is already in embedded solr and with same id and _version_.
     */
    protected void ignoreExistingData(final SolrQuery solrQuery) {
        final SolrQuery existingDataQuery = new SolrQuery("*:*").setFields(Abstract.FIELD_ID,
                Message.FIELD_VERSION_FROM_SOLR_CLOUD);

        final Iterable<Message> existingMessages = embeddedRepository.findAllSync(existingDataQuery);
        // NOT ((id:id1 AND _version_:v1) OR (id:id2 AND _version_:v2))
        final Iterator<Message> it = existingMessages.iterator();
        if (it.hasNext()) {
            final StringBuilder sb = new StringBuilder();
            while (it.hasNext()) {
                final Message message = it.next();
                sb.append(MessageFormat.format("({0}:{1} AND {2}:{3,number,#})", Abstract.FIELD_ID,
                        message.getId(), AbstractSolrDocument.FIELD_VERSION_,
                        message.getVersionFromSolrCloud()));

                if (it.hasNext()) {
                    sb.append(SolrUtil.SEPERATOR_OR);
                }
            }
            solrQuery.addFilterQuery(MessageFormat.format("{0}({1})", SolrUtil.NOT, sb.toString()));
        }
    }
}

@Configuration
@EnableScheduling
public class ScheduledTaskConfig implements SchedulingConfigurer {
    @Autowired
    private DataSyncService dataSyncService;
    @Autowired
    private IConfigService configService;
    @Bean(destroyMethod = "shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(10);
    }
    @Override
    public void configureTasks(final ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(new Runnable() {
            @Override
            public void run() {
                dataSyncService.copyMessagesFromSolrCloudToEmbeddedSolr();
            }
        }, new Trigger() {
            @Override
            public Date nextExecutionTime(final TriggerContext triggerContext) {
                final Calendar nextExecutionTime = new GregorianCalendar();
                final Date lastActualExecutionTime = triggerContext.lastActualExecutionTime();
                nextExecutionTime.setTime(lastActualExecutionTime != null ? lastActualExecutionTime : new Date());

                nextExecutionTime.add(Calendar.MILLISECOND,
                        configService.getSimpleConfig().extractSyncMessageToEmbeddedSolrIntervalInMill());
                return nextExecutionTime.getTime();
            }
        });
    }
}

Solr - Create custom data transformer to remove fields


Overview
Create custom data transformer to remove fields and remove field from json data in Solr.

The Problem
We store campaign message in Solr. One type of campaign is voucher. We return this user's voucher and other data based on user's accountId.

To support this, we add one searchable field: accountIds which includes all accountIds for this campaign. Add another field: details field which is a json string (mapping to java class) and non-searchable. It includes vouchers properties - a mapping from accountId to voucherCode.
-- We choose this approach to be consistent with existing data and make server code simpler.

accountIds and details.vouchers fields are big, and when return to client, actually we only need c this user's voucherCode.

The Solution
Excluding one field
We can build only fl to only include all fields except accountIds.  - This is kind of cumbersome, and every time we add a new field, we have to change the fl in SolrQuery.

[SOLR-3191] field exclusion from fl is promising, but it's not merged into Solr release.

So we create a data transformer which supports the following params:
removeFields - what fields to remove
Example: removeFields=accountIds,field1,field2

removeOthersVoucher - enable the feature if it's true
If removeOthersVoucher is true:
If accountId is empty, then remove all voucherCodes from details field.
If accountId is not empty, then remove all voucherCodes except accountId's voucher.

How to uses it
fl=*,[removeFeilds]&removeFields=accountIds&removeOthersVoucher=true&accountId=account1

Writing Custom Data Transformer
We use Jackson ObjectMapper to deserialize details field from String to  Map<String, Object>.

public class  MyTransformerFactory extends TransformerFactory {
    protected static Logger logger = LoggerFactory.getLogger(MyTransformerFactory.class);
    private boolean enabled = false;

    @Override
    public void init(@SuppressWarnings("rawtypes") final NamedList args) {
        try {
            super.init(args);
            if (args != null) {
                final SolrParams params = SolrParams.toSolrParams(args);
                enabled = SolrParams.toSolrParams(args).getBool("enabled", true);
            }
        } catch (final Exception e) {
            logger.error("MyTransformerFactory init failed", e);
        }
    }

    @Override
    public DocTransformer create(final String field, final SolrParams params, final SolrQueryRequest req) {
        final SolrParams reqParams = req.getParams();
        final String removeFields = reqParams.get("removeFields");
        final boolean removeOthersVoucher = reqParams.getBool("removeOthersVoucher", false);
        final String accountId = reqParams.get("accountId");
        if (!enabled || (removeFields == null && !removeOthersVoucher)) {
            return null;
        }
        return new  MyTransformer(removeFields, removeOthersVoucher, accountId);
    }

    private static class  MyTransformer extends DocTransformer {
        private static final String FIELD_DETAILS = "details";
        private static final String DETAIL_VOUCHER_CODES = "voucherCodes";

        private static ObjectMapper objectMapper = new ObjectMapper();
        private static Splitter splitter = Splitter.on(",").trimResults();

        private final String removeFields;
        private final boolean removeOthersVoucher;
        private final String accountId;

        public  MyTransformer(final String removeFields, final boolean removeOthersVoucher, final String accountId) {
            this.removeFields = removeFields;
            this.removeOthersVoucher = removeOthersVoucher;
            this.accountId = accountId;
        }
        @Override
        public String getName() {
            return  MyTransformer.class.getSimpleName();
        }

        @Override
        public void transform(final SolrDocument doc, final int docid) throws IOException {
            if (removeFields != null) {
                final Iterable<String> it = splitter.split(removeFields);
                for (final String removeField : it) {
                    doc.removeFields(removeField);
                }
            }
            try {
                if (removeOthersVoucher) {
                    removeOthersVoucher(doc);
                }
            } catch (final Exception e) {
                // ignore it if there is exception
                logger.error("MyTransformer transform failed", e);
            }
        }

        protected void removeOthersVoucher(final SolrDocument doc)
                throws IOException, JsonParseException, JsonMappingException, JsonProcessingException {
            final String detailsObj = getFieldValue(doc, FIELD_DETAILS);
            if (detailsObj == null) {
                return;
            }

            final Map<String, Object> detailsMap = objectMapper.readValue(detailsObj.toString(),
                    TypeFactory.defaultInstance().constructMapType(Map.class, String.class, Object.class));
            if (detailsMap == null) {
                return;
            }
            final Object voucherCodesObj = detailsMap.get(DETAIL_VOUCHER_CODES);
            if (!(voucherCodesObj instanceof HashMap)) {
                return;
            }
            final Map<String, String> voucherCodesMap = (Map<String, String>) voucherCodesObj;
            final String voucherCode = voucherCodesMap.get(accountId);

            final Map<String, String> myVoucherMap = new HashMap<String, String>();
            if (voucherCode != null) {
                myVoucherMap.put(accountId, voucherCode);
            }
            detailsMap.put(DETAIL_VOUCHER_CODES, myVoucherMap);

            doc.setField(FIELD_DETAILS, objectMapper.writeValueAsString(detailsMap));
        }
    }

    public static String getFieldValue(final SolrDocument doc, final String field) {
        final List<String> rst = new ArrayList<String>();
        final Object obj = doc.get(field);
        getFieldvalues(doc, rst, obj);

        if (rst.isEmpty()) {
            return null;
        }
        return rst.get(0);
    }

    public static void getFieldvalues(final SolrDocument doc, final List<String> rst, final Object obj) {
        if (obj == null) {
            return;
        }
        if (obj instanceof org.apache.lucene.document.Field) {
            final org.apache.lucene.document.Field field = (Field) obj;
            final String oldValue = field.stringValue();
            if (oldValue != null) {
                rst.add(oldValue);
            }
        } else if (obj instanceof IndexableField) {
            final IndexableField field = (IndexableField) obj;
            final String oldValue = field.stringValue();
            if (oldValue != null) {
                rst.add(oldValue);
            }
        } else if (obj instanceof Collection) {
            final Collection colls = (Collection) obj;
            for (final Object newObj : colls) {
                getFieldvalues(doc, rst, newObj);
            }
        } else {
            logger.error(MessageFormat.format("type: {0}", obj.getClass()));
            rst.add(obj.toString());
        }
    }
}
Add Transformer into solrConfig.xml

<lib dir="../../../lib" regex="lifelongprogrammer-solr-extension-jar-with-dependencies.jar" />

<transformer name="removeFeilds" class="com.lifelongprogrammer.solr.MyTransformerFactory">
  <bool name="enabled">true</bool>
</transformer>

pom.xml - Build solr-extension jar
We declare scope of solr-core as provided and use maven-assembly-plugin to build jar-with-dependencies.

<build>
  <finalName>lifelongprogrammer-solr-extension</finalName>
  <plugins>
    <plugin>
      <artifactId>maven-assembly-plugin</artifactId>
      <version>2.6</version>
      <configuration>
        <descriptorRefs>
          <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
      </configuration>
      <executions>
        <execution>
          <id>make-assembly</id>
          <phase>package</phase>
          <goals>
            <goal>single</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

<dependencies>
  <dependency>
    <groupId>org.apache.solr</groupId>
    <artifactId>solr-core</artifactId>
    <version>5.2.0</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.7.4</version>
  </dependency>
</dependencies>

Solr - Tips and Tricks


Admin UI
http://127.0.0.1:8983/solr/#/~cloud?view=tree
bin/solr help
bin/solr status
bin/solr healthcheck
bin/solr stop -all
(bin/solr start -cloud -s example/cloud/node1/solr -p 8983 -h 127.0.0.1)  && (bin/solr start -cloud -s example/cloud/node2/solr -p 7574 -z 127.0.0.1:9983 -h 127.0.0.1) && (bin/solr start -cloud -s example/cloud/node3/solr -p 6463 -z 127.0.0.1:9983 -h 127.0.0.1)

Delete docs:
change *;* to your query
update?commit=true&stream.body=<delete><query>*:*</query></delete>

https://wiki.apache.org/solr/SearchHandler
Use invariants to lock options and overwrite values client passes.
Use appends to append options, use defaults to provide default options.

Request Paramters
distrib=false - only query current core

debugQuery
debug=query/results/timing

explainOther
debug=results&explainOther=id:MA*

Range Query inclusive: [a to b]
exclusive: {a to b} - it's not ().
mixed: [a to b} {a to b]

Negative Query
Query empty fields: -field:*
field is empty or is abc: (*:* OR -field:*) OR field:abc
(*:* -id:1) OR id:1 - return all docs
http://stackoverflow.com/questions/634765/using-or-and-not-in-solr-query
-foo is transformed by solr into (*:* -foo)
The big caveat is that Solr only checks to see if the top level query is a pure negative query!

Solr zkcli
zkcli.sh -zkhost zooServer:port  -cmd putfile /configs/solrconfig.xml solrconfig.xml
zkcli.sh -zkhost zooServer:port  -cmd get /configs/schema.xml

To get solrcloud nodes info(such as ip address)
java -classpath "*" org.apache.solr.cloud.ZkCLI -zkhost myzkhost -cmd get /clusterstate.json | grep base_url
zkcli.sh -zkhost myzkhost:port -cmd get /clusterstate.json

Rest API
solr/collection/config
solr/collection/config/requestHandler
solr/collection/schema
solr/collection/schema/version
solr/admin/collections?action=RELOAD&name=$NAME

SolrJ Field Annotation
Map Dynamic fields to fields
@Field("supplier_*")
Map> supplier;

@Field("sup_simple_*")
Map supplier_simple;

@Field("allsupplier_*")
private String[] allSuppliers;

@Field(child = true)
Child[] child;

Staring Solr
-m 2g: Start Solr with the defined value as the min (-Xms) and max (-Xmx) heap size for the JVM.

bin/solr stop -all

(bin/solr start -cloud -s example/cloud/node1/solr -p 8983 -h 127.0.0.1 -m 2g)  && (bin/solr start -cloud -s example/cloud/node2/solr -p 7574 -z 127.0.0.1:9983 -h 127.0.0.1 -m 2g) && (bin/solr start -cloud -s example/cloud/node3/solr -p 6463 -z 127.0.0.1:9983 -h 127.0.0.1 -m 2g)
-- Use -h 127.0.0.1 so solr can continue to work even ip changed.

Extending Solr
Implement the SolrCoreAware interface in custom RequestHandler to get SolrCore in inform method.

Customize and extend DocumentObjectBinder

Get solr static fields
SolrServer solrCore = new HttpSolrServer("http://{host:port}/solr/core-name");
SolrQuery query = new SolrQuery();

query.setRequestHandler("/schema/fields");
// query.add(CommonParams.QT, "/schema/fields");
QueryResponse response = solrClient.query(query);
NamedList responseHeader = response.getResponseHeader();
ArrayList fields = (ArrayList) response.getResponse().get("fields");
for (SimpleOrderedMap field : fields) {
    Object fieldName = field.get("name");

}

Solr Internals
replicationFactor

The Solr replicationFactor has nothing to do with quorum. Solr uses Zookeeper's Quorum sensing to insure that all Solr nodes have a consistent picture of the cluster.

openSearcher and hardCommit
- Soft commit always opens new searcher.
- openSearcher only makes sense for hardcommit

Use config api to change solr settings dynamically

Use JSON API, but be aware SolrJ may not work with JSON API in some cases.

Solr Facet APIs
http://yonik.com/json-facet-api/
http://yonik.com/solr-facet-functions/

Don't forget facet.mincount=1

update.distrib
=toLeader when one replica sends this doc to it's leader=
=fromLeader when the doc's leader sends it to its followers

Solr Nested Objects
Define _root_ field

Use [child] - ChildDocTransformerFactory to return child documents

admin collection apis
/admin/collections?action=CLUSTERSTATUS

curl http://localhost:8983/solr/mycollection/update -X POST -H 'Content-Type: application/json' --data-binary @atomic.json

Zookeeper
Clean ZK data - link
run java -cp zookeeper-3.4.6.jar:conf org.apache.zookeeper.server.PurgeTxnLog  ../zoo_data/ ../zoo_data/ -n 3

Access solr cloud via ssh tunnel
Create a tunnel to zookeeper and solr nodes
- But when solrJ queries zookeeper, it still returns the external solr nodes that we can't access directly
Add a conditional breakpoint at CloudSolrClient.sendRequest(SolrRequest, String)
- before  LBHttpSolrClient.Req req = new LBHttpSolrClient.Req(request, theUrlList);
theUrlList.clear();
theUrlList.add("http://localhost:18983/solr/searchItems/");
theUrlList.add("http://localhost:28983/solr/searchItems/");

return false;

Solr suggester
It supports filter on multiple fields. Just copy these fields to the contextFilterFeild.

Troubleshooting
400 Unknown Version - when run curl solr
- Maybe u need encode query parameters

Debug Solr Query
http://splainer.io/

APIs
http://localhost:8983/solr/admin/collections
?action=LIST&wt=json

solr/admin/collections?action=OVERSEERSTATUS
overseer_queue_size
overseer_work_queue_size

admin/mbeans?key=fieldCache&stats=true

Coding
Watches are one time triggers

SolrJ
*SolrClient
request(SolrRequest)
.getZkStateReader()

GenericSolrRequest 
solrClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/mbeans", params))
(NamedList<Object>) nl.findRecursive("solr-mbeans", "CACHE", "fieldCache", "stats");</Object>

Use Zookeeper client
./zkCli.sh -server localhost:9983
ls(delete) path
create path data // data can be ''
get path
stat /overseer/collection-queue-work
get /overseer/collection-queue-work/qn-0001379031
Create chroot path
create /the-chroot-path []

Solving Problem from Different Angles - Programmer Skills


Senario
Hit one issue in production: when we query solr, the parameter rows is too small. 
We need fix this problem ASAP.

One possible solution is to fix the code and make a new deployment.
Another possible solution is to change the data so the one we want to return to client listed at first.

The solution that we used is to change solrconfix xml(only we uses the solr server) to always get 100 rows, ignore rows client passes.

https://wiki.apache.org/solr/SearchHandler
-- invariants - provides param values that will be used in spite of any values provided at request time. They are a way of letting the Solr maintainer lock down the options available to Solr clients. Any params values specified here are used regardless of what values may be specified in either the query, the "defaults", or the "appends" params.

<requestHandler name="/select" class="solr.SearchHandler">
   <lst name="defaults">
     <str name="echoParams">explicit</str>
     <int name="rows">10</int>
   </lst>

   <lst name="invariants">
     <int name="rows">100</int>
   </lst>
</requestHandler>
Later we fixed the code and design issue, and removed the invariants in solrconfig.xml.

Labels

ANT (6) Algorithm (69) Algorithm Series (35) Android (7) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) JSON (7) Java (186) JavaScript (27) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) adsense (5) bat (8) regex (5) xml (5)