org.broadleafcommerce.core.search.service.solr.SolrIndexServiceImpl Maven / Gradle / Ivy
/*
* #%L
* BroadleafCommerce Framework
* %%
* Copyright (C) 2009 - 2013 Broadleaf Commerce
* %%
* Licensed 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.
* #L%
*/
package org.broadleafcommerce.core.search.service.solr;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrInputDocument;
import org.broadleafcommerce.common.exception.ExceptionHelper;
import org.broadleafcommerce.common.exception.ServiceException;
import org.broadleafcommerce.common.extension.ExtensionResultStatusType;
import org.broadleafcommerce.common.locale.domain.Locale;
import org.broadleafcommerce.common.locale.service.LocaleService;
import org.broadleafcommerce.common.util.BLCCollectionUtils;
import org.broadleafcommerce.common.util.StopWatch;
import org.broadleafcommerce.common.util.TransactionUtils;
import org.broadleafcommerce.common.util.TypedTransformer;
import org.broadleafcommerce.common.web.BroadleafRequestContext;
import org.broadleafcommerce.core.catalog.dao.ProductDao;
import org.broadleafcommerce.core.catalog.domain.Product;
import org.broadleafcommerce.core.catalog.service.dynamic.DynamicSkuActiveDatesService;
import org.broadleafcommerce.core.catalog.service.dynamic.DynamicSkuPricingService;
import org.broadleafcommerce.core.catalog.service.dynamic.SkuActiveDateConsiderationContext;
import org.broadleafcommerce.core.catalog.service.dynamic.SkuPricingConsiderationContext;
import org.broadleafcommerce.core.search.dao.CatalogStructure;
import org.broadleafcommerce.core.search.dao.FieldDao;
import org.broadleafcommerce.core.search.dao.SolrIndexDao;
import org.broadleafcommerce.core.search.domain.Field;
import org.broadleafcommerce.core.search.domain.solr.FieldType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Resource;
/**
* Responsible for building and rebuilding the Solr index
*
* @author Andre Azzolini (apazzolini)
* @author Jeff Fischer
*/
@Service("blSolrIndexService")
public class SolrIndexServiceImpl implements SolrIndexService {
private static final Log LOG = LogFactory.getLog(SolrIndexServiceImpl.class);
protected final Object LOCK_OBJECT = new Object();
protected boolean IS_LOCKED = false;
@Value("${solr.index.errorOnConcurrentReIndex}")
protected boolean errorOnConcurrentReIndex = false;
@Value("${solr.index.product.pageSize}")
protected int pageSize;
@Resource(name = "blProductDao")
protected ProductDao productDao;
@Resource(name = "blFieldDao")
protected FieldDao fieldDao;
@Resource(name = "blLocaleService")
protected LocaleService localeService;
@Resource(name = "blSolrHelperService")
protected SolrHelperService shs;
@Resource(name = "blSolrSearchServiceExtensionManager")
protected SolrSearchServiceExtensionManager extensionManager;
@Resource(name = "blTransactionManager")
protected PlatformTransactionManager transactionManager;
@Resource(name = "blSolrIndexDao")
protected SolrIndexDao solrIndexDao;
public static String ATTR_MAP = "productAttributes";
@Override
public void performCachedOperation(SolrIndexCachedOperation.CacheOperation cacheOperation) throws ServiceException {
try {
CatalogStructure cache = new CatalogStructure();
SolrIndexCachedOperation.setCache(cache);
cacheOperation.execute();
} finally {
if (LOG.isInfoEnabled()) {
LOG.info("Cleaning up Solr index cache from memory - size approx: " + getCacheSizeInMemoryApproximation(SolrIndexCachedOperation.getCache()) + " bytes");
}
SolrIndexCachedOperation.clearCache();
}
}
protected int getCacheSizeInMemoryApproximation(CatalogStructure structure) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(structure);
oos.close();
return baos.size();
} catch (IOException e) {
throw ExceptionHelper.refineException(e);
}
}
@Override
public boolean isReindexInProcess() {
synchronized (LOCK_OBJECT) {
return IS_LOCKED;
}
}
@Override
public void rebuildIndex() throws ServiceException, IOException {
synchronized (LOCK_OBJECT) {
if (IS_LOCKED) {
if (errorOnConcurrentReIndex) {
throw new IllegalStateException("More than one thread attempting to concurrently reindex Solr.");
} else {
LOG.warn("There is more than one thread attempting to concurrently "
+ "reindex Solr. Failing additional threads gracefully. Check your configuration.");
return;
}
} else {
IS_LOCKED = true;
}
}
try {
LOG.info("Rebuilding the solr index...");
StopWatch s = new StopWatch();
LOG.info("Deleting the reindex core prior to rebuilding the index");
deleteAllDocuments();
Object[] pack = saveState();
try {
final Long numProducts = productDao.readCountAllActiveProducts();
if (LOG.isDebugEnabled()) {
LOG.debug("There are " + numProducts + " total products");
}
performCachedOperation(new SolrIndexCachedOperation.CacheOperation() {
@Override
public void execute() throws ServiceException {
int page = 0;
while ((page * pageSize) < numProducts) {
buildIncrementalIndex(page, pageSize);
page++;
}
}
});
optimizeIndex(SolrContext.getReindexServer());
} finally {
restoreState(pack);
}
// Swap the active and the reindex cores
shs.swapActiveCores();
LOG.info(String.format("Finished building index in %s", s.toLapString()));
} finally {
synchronized (LOCK_OBJECT) {
IS_LOCKED = false;
}
}
}
/**
*
* This method deletes all of the documents from {@link SolrContext#getReindexServer()}
*
* @throws ServiceException if there was a problem removing the documents
* @deprecated use {@link #deleteAllReindexCoreDocuments()} instead
*/
@Deprecated
protected void deleteAllDocuments() throws ServiceException {
deleteAllReindexCoreDocuments();
}
/**
*
* This method deletes all of the documents from {@link SolrContext#getReindexServer()}
*
* @throws ServiceException if there was a problem removing the documents
*/
protected void deleteAllReindexCoreDocuments() throws ServiceException {
try {
String deleteQuery = shs.getNamespaceFieldName() + ":(\"" + shs.getCurrentNamespace() + "\")";
LOG.debug("Deleting by query: " + deleteQuery);
SolrContext.getReindexServer().deleteByQuery(deleteQuery);
SolrContext.getReindexServer().commit();
} catch (Exception e) {
throw new ServiceException("Could not delete documents", e);
}
}
protected void buildIncrementalIndex(int page, int pageSize) throws ServiceException {
buildIncrementalIndex(page, pageSize, true);
}
@Override
public void buildIncrementalIndex(int page, int pageSize, boolean useReindexServer) throws ServiceException {
if (SolrIndexCachedOperation.getCache() == null) {
LOG.warn("Consider using SolrIndexService.performCachedOperation() in combination with " +
"SolrIndexService.buildIncrementalIndex() for better caching performance during solr indexing");
}
TransactionStatus status = TransactionUtils.createTransaction("readProducts",
TransactionDefinition.PROPAGATION_REQUIRED, transactionManager, true);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Building index - page: [%s], pageSize: [%s]", page, pageSize));
}
StopWatch s = new StopWatch();
boolean cacheOperationManaged = false;
try {
CatalogStructure cache = SolrIndexCachedOperation.getCache();
if (cache != null) {
cacheOperationManaged = true;
} else {
cache = new CatalogStructure();
SolrIndexCachedOperation.setCache(cache);
}
List products = readAllActiveProducts(page, pageSize);
List productIds = BLCCollectionUtils.collectList(products, new TypedTransformer() {
@Override
public Long transform(Object input) {
return ((Product) input).getId();
}
});
solrIndexDao.populateCatalogStructure(productIds, SolrIndexCachedOperation.getCache());
List fields = fieldDao.readAllProductFields();
List locales = getAllLocales();
Collection documents = new ArrayList();
for (Product product : products) {
SolrInputDocument doc = buildDocument(product, fields, locales);
//If someone overrides the buildDocument method and determines that they don't want a product
//indexed, then they can return null. If the document is null it does not get added to
//to the index.
if (doc != null) {
documents.add(doc);
}
}
logDocuments(documents);
if (!CollectionUtils.isEmpty(documents)) {
SolrServer server = useReindexServer ? SolrContext.getReindexServer() : SolrContext.getServer();
server.add(documents);
server.commit();
}
TransactionUtils.finalizeTransaction(status, transactionManager, false);
} catch (SolrServerException e) {
TransactionUtils.finalizeTransaction(status, transactionManager, true);
throw new ServiceException("Could not rebuild index", e);
} catch (IOException e) {
TransactionUtils.finalizeTransaction(status, transactionManager, true);
throw new ServiceException("Could not rebuild index", e);
} catch (RuntimeException e) {
TransactionUtils.finalizeTransaction(status, transactionManager, true);
throw e;
} finally {
if (!cacheOperationManaged) {
SolrIndexCachedOperation.clearCache();
}
}
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Built index - page: [%s], pageSize: [%s] in [%s]", page, pageSize, s.toLapString()));
}
}
/**
* This method to read all active products will be slow if you have a large catalog. In this case, you will want to
* read the products in a different manner. For example, if you know the fields that will be indexed, you can configure
* a DAO object to only load those fields. You could also use a JDBC based DAO for even faster access. This default
* implementation is only suitable for small catalogs.
*
* @return the list of all active products to be used by the index building task
*/
protected List readAllActiveProducts() {
return productDao.readAllActiveProducts();
}
/**
* This method to read active products utilizes paging to improve performance over {@link #readAllActiveProducts()}.
* While not optimal, this will reduce the memory required to load large catalogs.
*
* It could still be improved for specific implementations by only loading fields that will be indexed or by accessing
* the database via direct JDBC (instead of Hibernate).
*
* @return the list of all active products to be used by the index building task
* @since 2.2.0
*/
protected List readAllActiveProducts(int page, int pageSize) {
return productDao.readAllActiveProducts(page, pageSize);
}
@Override
public List getAllLocales() {
return localeService.findAllLocales();
}
@Override
public SolrInputDocument buildDocument(final Product product, List fields, List locales) {
final SolrInputDocument document = new SolrInputDocument();
attachBasicDocumentFields(product, document);
// Keep track of searchable fields added to the index. We need to also add the search facets if
// they weren't already added as a searchable field.
List addedProperties = new ArrayList();
for (Field field : fields) {
try {
// Index the searchable fields
if (field.getSearchable()) {
List searchableFieldTypes = shs.getSearchableFieldTypes(field);
for (FieldType sft : searchableFieldTypes) {
Map propertyValues = getPropertyValues(product, field, sft, locales);
// Build out the field for every prefix
for (Entry entry : propertyValues.entrySet()) {
String prefix = entry.getKey();
prefix = StringUtils.isBlank(prefix) ? prefix : prefix + "_";
String solrPropertyName = shs.getPropertyNameForFieldSearchable(field, sft, prefix);
Object value = entry.getValue();
document.addField(solrPropertyName, value);
addedProperties.add(solrPropertyName);
}
}
}
// Index the faceted field type as well
FieldType facetType = field.getFacetFieldType();
if (facetType != null) {
Map propertyValues = getPropertyValues(product, field, facetType, locales);
// Build out the field for every prefix
for (Entry entry : propertyValues.entrySet()) {
String prefix = entry.getKey();
prefix = StringUtils.isBlank(prefix) ? prefix : prefix + "_";
String solrFacetPropertyName = shs.getPropertyNameForFieldFacet(field, prefix);
Object value = entry.getValue();
if (!addedProperties.contains(solrFacetPropertyName)) {
document.addField(solrFacetPropertyName, value);
}
}
}
} catch (Exception e) {
LOG.error("Could not get value for property[" + field.getQualifiedFieldName() + "] for product id["
+ product.getId() + "]", e);
}
}
return document;
}
/**
* Adds the ID, category, and explicitCategory fields for the product to the document
*
* @param product
* @param document
*/
protected void attachBasicDocumentFields(Product product, SolrInputDocument document) {
boolean cacheOperationManaged = false;
try {
CatalogStructure cache = SolrIndexCachedOperation.getCache();
if (cache != null) {
cacheOperationManaged = true;
} else {
cache = new CatalogStructure();
SolrIndexCachedOperation.setCache(cache);
solrIndexDao.populateCatalogStructure(Arrays.asList(product.getId()), SolrIndexCachedOperation.getCache());
}
// Add the namespace and ID fields for this product
document.addField(shs.getNamespaceFieldName(), shs.getCurrentNamespace());
document.addField(shs.getIdFieldName(), shs.getSolrDocumentId(document, product));
document.addField(shs.getProductIdFieldName(), product.getId());
extensionManager.getProxy().attachAdditionalBasicFields(product, document, shs);
// The explicit categories are the ones defined by the product itself
if (cache.getParentCategoriesByProduct().containsKey(product.getId())) {
for (Long categoryId : cache.getParentCategoriesByProduct().get(product.getId())) {
document.addField(shs.getExplicitCategoryFieldName(), shs.getCategoryId(categoryId));
String categorySortFieldName = shs.getCategorySortFieldName(shs.getCategoryId(categoryId));
String displayOrderKey = categoryId + "-" + shs.getProductId(product.getId());
BigDecimal displayOrder = cache.getDisplayOrdersByCategoryProduct().get(displayOrderKey);
if (displayOrder == null) {
displayOrderKey = categoryId + "-" + product.getId();
displayOrder = cache.getDisplayOrdersByCategoryProduct().get(displayOrderKey);
}
if (document.getField(categorySortFieldName) == null) {
document.addField(categorySortFieldName, displayOrder);
}
// This is the entire tree of every category defined on the product
buildFullCategoryHierarchy(document, cache, categoryId, new HashSet());
}
}
} finally {
if (!cacheOperationManaged) {
SolrIndexCachedOperation.clearCache();
}
}
}
/**
* Walk the category hierarchy upwards, adding a field for each level to the solr document
*
* @param document the solr document for the product
* @param cache the catalog structure cache
* @param categoryId the current category id
*/
protected void buildFullCategoryHierarchy(SolrInputDocument document, CatalogStructure cache, Long categoryId, Set indexedParents) {
Long catIdToAdd = shs.getCategoryId(categoryId);
Collection