diff --git a/grails-app/services/org/grails/plugins/elasticsearch/ElasticSearchService.groovy b/grails-app/services/org/grails/plugins/elasticsearch/ElasticSearchService.groovy index 6460c9e5..eb125d50 100644 --- a/grails-app/services/org/grails/plugins/elasticsearch/ElasticSearchService.groovy +++ b/grails-app/services/org/grails/plugins/elasticsearch/ElasticSearchService.groovy @@ -231,11 +231,11 @@ public class ElasticSearchService implements GrailsApplicationAware { LOG.debug("Deleting all instances of ${scm.domainClass}") } - // The index is splitted to avoid out of memory exception - def count = scm.domainClass.clazz.count() ?: 0 - int nbRun = Math.ceil(count / maxRes) - scm.domainClass.clazz.withNewSession {session -> + // The index is splitted to avoid out of memory exception + def count = scm.domainClass.clazz.count() ?: 0 + int nbRun = Math.ceil(count / maxRes) + for(int i = 0; i < nbRun; i++) { scm.domainClass.clazz.withCriteria { firstResult(i * maxRes) diff --git a/src/groovy/org/grails/plugins/elasticsearch/conversion/marshall/DeepDomainClassMarshaller.groovy b/src/groovy/org/grails/plugins/elasticsearch/conversion/marshall/DeepDomainClassMarshaller.groovy index d22d3d9c..1a84ac16 100644 --- a/src/groovy/org/grails/plugins/elasticsearch/conversion/marshall/DeepDomainClassMarshaller.groovy +++ b/src/groovy/org/grails/plugins/elasticsearch/conversion/marshall/DeepDomainClassMarshaller.groovy @@ -15,7 +15,7 @@ class DeepDomainClassMarshaller extends DefaultMarshaller { if (!scm) { throw new IllegalStateException("Domain class ${domainClass} is not searchable.") } - for (GrailsDomainClassProperty prop in domainClass.persistantProperties) { + for (GrailsDomainClassProperty prop in domainClass.properties) { def propertyMapping = scm.getPropertyMapping(prop.name) if (!propertyMapping) { continue diff --git a/src/groovy/org/grails/plugins/elasticsearch/mapping/SearchableClassMapping.groovy b/src/groovy/org/grails/plugins/elasticsearch/mapping/SearchableClassMapping.groovy index 03dd83fe..7c1fb78f 100644 --- a/src/groovy/org/grails/plugins/elasticsearch/mapping/SearchableClassMapping.groovy +++ b/src/groovy/org/grails/plugins/elasticsearch/mapping/SearchableClassMapping.groovy @@ -29,9 +29,14 @@ public class SearchableClassMapping { private boolean root = true; private boolean all = true; + /** Identifier properties used for indexing */ + private List identityProperties; + private String identitySeparator = ':'; + public SearchableClassMapping(GrailsDomainClass domainClass, Collection propertiesMapping) { this.domainClass = domainClass; this.propertiesMapping = propertiesMapping; + this.identityProperties = [domainClass.identifier.name]; } public SearchableClassPropertyMapping getPropertyMapping(String propertyName) { @@ -59,6 +64,22 @@ public class SearchableClassMapping { return domainClass; } + public List getIdentityProperties() { + return identityProperties; + } + + public void setIdentityProperties(List propertyNames) { + this.identityProperties = propertyNames; + } + + public String getIdentitySeparator() { + return identitySeparator; + } + + public void setIdentitySeparator(String separator) { + this.identitySeparator = separator; + } + /** * Validate searchable class mapping. * @param contextHolder context holding all known searchable mappings. diff --git a/src/java/org/grails/plugins/elasticsearch/conversion/unmarshall/DomainClassUnmarshaller.java b/src/java/org/grails/plugins/elasticsearch/conversion/unmarshall/DomainClassUnmarshaller.java index 2682cc64..427722be 100644 --- a/src/java/org/grails/plugins/elasticsearch/conversion/unmarshall/DomainClassUnmarshaller.java +++ b/src/java/org/grails/plugins/elasticsearch/conversion/unmarshall/DomainClassUnmarshaller.java @@ -59,12 +59,20 @@ public Collection buildResults(SearchHits hits) { LOG.warn("Unknown SearchHit: " + hit.id() + "#" + hit.type() + ", domain class name: "); continue; } - String domainClassName = scm.getDomainClass().getFullName(); - GrailsDomainClassProperty identifier = scm.getDomainClass().getIdentifier(); - Object id = typeConverter.convertIfNecessary(hit.id(), identifier.getType()); GroovyObject instance = (GroovyObject) scm.getDomainClass().newInstance(); - instance.setProperty(identifier.getName(), id); + + // The id stored in the index may be a composition of multiple fields. Decompose it and seat each of the + // values contained there in (excluding non-persistant properties as they're likely transient or read-only) + String[] idValues = hit.id().split(scm.getIdentitySeparator()); + for (int i = 0; i < idValues.length; i++) { + String propertyName = scm.getIdentityProperties().get(i); + GrailsDomainClassProperty property = scm.getDomainClass().getPropertyByName(propertyName); + if (property.isPersistent()) { + Object value = typeConverter.convertIfNecessary(idValues[i], property.getType()); + instance.setProperty(property.getName(), value); + } + } /*def mapContext = elasticSearchContextHolder.getMappingContext(domainClass.propertyName)?.propertiesMapping*/ Map rebuiltProperties = new HashMap(); diff --git a/src/java/org/grails/plugins/elasticsearch/index/IndexRequestQueue.java b/src/java/org/grails/plugins/elasticsearch/index/IndexRequestQueue.java index 2f6d7892..b16cb8c8 100644 --- a/src/java/org/grails/plugins/elasticsearch/index/IndexRequestQueue.java +++ b/src/java/org/grails/plugins/elasticsearch/index/IndexRequestQueue.java @@ -15,6 +15,8 @@ */ package org.grails.plugins.elasticsearch.index; +import groovy.lang.GroovyObject; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsHibernateUtil; import org.codehaus.groovy.grails.orm.hibernate.support.HibernatePersistenceContextInterceptor; @@ -94,13 +96,8 @@ public void setSessionFactory(SessionFactory sessionFactory) { } public void addIndexRequest(Object instance) { - addIndexRequest(instance, null); - } - - public void addIndexRequest(Object instance, Serializable id) { synchronized (this) { - IndexEntityKey key = id == null ? new IndexEntityKey(instance) : - new IndexEntityKey(id.toString(), GrailsHibernateUtil.unwrapIfProxy(instance).getClass()); + IndexEntityKey key = new IndexEntityKey(instance); indexRequests.put(key, GrailsHibernateUtil.unwrapIfProxy(instance)); } } @@ -384,7 +381,13 @@ class IndexEntityKey implements Serializable { if (scm == null) { throw new IllegalArgumentException("Class " + clazz + " is not a searchable domain class."); } - this.id = (InvokerHelper.invokeMethod(instance, "ident", null)).toString(); + + // Set the id based on the fields configured in the indexId searchable property + List idValues = new ArrayList(); + for (String property : scm.getIdentityProperties()) { + idValues.add(InvokerHelper.getGroovyObjectProperty((GroovyObject)instance, property).toString()); + } + this.id = StringUtils.join(idValues, scm.getIdentitySeparator()); } public String getId() { diff --git a/src/java/org/grails/plugins/elasticsearch/mapping/SearchableDomainClassMapper.java b/src/java/org/grails/plugins/elasticsearch/mapping/SearchableDomainClassMapper.java index 4e1c65dc..d02e5196 100644 --- a/src/java/org/grails/plugins/elasticsearch/mapping/SearchableDomainClassMapper.java +++ b/src/java/org/grails/plugins/elasticsearch/mapping/SearchableDomainClassMapper.java @@ -28,12 +28,19 @@ class SearchableDomainClassMapper extends GroovyObjectSupport { /** * Options applied to searchable class itself */ - public static final Set CLASS_MAPPING_OPTIONS = new HashSet(Arrays.asList("all", "root", "only", "except")); + public static final Set CLASS_MAPPING_OPTIONS = new HashSet(Arrays.asList("all", "root", "only", "except", "indexId")); /** * Searchable property name */ public static final String SEARCHABLE_PROPERTY_NAME = "searchable"; + /** + * Mapping properties used with 'indexId' to allow for a custom stored key + */ + public static final String INDEX_ID_PROPERTIES_NAME = "properties"; + public static final String INDEX_ID_SEPARATOR_NAME = "separator"; + public static final Set INDEX_ID_MAPPING_OPTIONS = new HashSet(Arrays.asList(INDEX_ID_PROPERTIES_NAME, INDEX_ID_SEPARATOR_NAME)); + /** * Class mapping properties */ @@ -46,6 +53,7 @@ class SearchableDomainClassMapper extends GroovyObjectSupport { private GrailsApplication grailsApplication; private Object only; private Object except; + private Object indexId; private ConfigObject esConfig; @@ -78,10 +86,19 @@ public void setExcept(Object except) { this.except = except; } + public void setIndexId(Object indexId) { + this.indexId = indexId; + } + public void root(Boolean rootFlag) { this.root = rootFlag; } + + public void indexId(Object indexId) { + this.indexId = indexId; + } + /** * @return searchable domain class mapping */ @@ -169,9 +186,56 @@ public SearchableClassMapping buildClassMapping() { SearchableClassMapping scm = new SearchableClassMapping(grailsDomainClass, customMappedProperties.values()); scm.setRoot(root); + + // Override the default properties to use as _id in the index + if (indexId != null) { + Map indexIdMap = buildIndexIdMapping(); + scm.setIdentityProperties((List)indexIdMap.get(INDEX_ID_PROPERTIES_NAME)); + if (indexIdMap.containsKey(INDEX_ID_SEPARATOR_NAME)) { + scm.setIdentitySeparator(indexIdMap.get(INDEX_ID_SEPARATOR_NAME).toString()); + } + } + return scm; } + /** + * Examines the indexId property, and converts it into a Map keyed by the values in INDEX_ID_MAPPING_OPTIONS + * @return A Map with the custom indexId definition + */ + private Map buildIndexIdMapping() { + if ((indexId != null) && !root) { + throw new IllegalArgumentException("'indexId' was used on non-root '" + grailsDomainClass.getPropertyName() + "#searchable': indexId may only apply to root searchable classes"); + } + + Object args = indexId; + + Map indexIdMapping = null; + if (args instanceof String) { + indexIdMapping = Collections.singletonMap(INDEX_ID_PROPERTIES_NAME, Collections.singletonList(args)); + } + else if (args instanceof Collection) { + indexIdMapping = Collections.singletonMap(INDEX_ID_PROPERTIES_NAME, new ArrayList((Collection) args)); + } + else if (args instanceof Map) { + indexIdMapping = new HashMap(); + for (Object key : ((Map)args).keySet()) { + if (!INDEX_ID_MAPPING_OPTIONS.contains(key.toString())) { + throw new IllegalArgumentException("'" + key + "' is not a valid attribute for 'indexId'"); + } + indexIdMapping.put(key.toString(), ((Map)args).get(key)); + } + if (!indexIdMapping.containsKey(INDEX_ID_PROPERTIES_NAME)) { + throw new IllegalArgumentException("'indexId' must contain a '" + INDEX_ID_PROPERTIES_NAME + "' attribute"); + } + } + else { + throw new IllegalArgumentException("Unsupported 'indexId' argument: " + args); + } + + return indexIdMapping; + } + private Set getInheritedProperties(GrailsDomainClass domainClass) { // check which properties belong to this domain class ONLY Set inheritedProperties = new HashSet(); @@ -213,6 +277,7 @@ public void buildHashMapMapping(LinkedHashMap map, GrailsDomainClass domainClass // Support old searchable-plugin syntax ([only: ['category', 'title']] or [except: 'createdAt']) only = map.containsKey("only") ? map.get("only") : null; except = map.containsKey("except") ? map.get("except") : null; + indexId = map.containsKey("indexId") ? map.get("indexId") : null; buildMappingFromOnlyExcept(domainClass, inheritedProperties); }