com.threewks.thundr.search.gae.BaseGaeSearchService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of thundr-gae Show documentation
Show all versions of thundr-gae Show documentation
A thundr module enabling thundr for use on GAE (Google App Engine)
/*
* This file is a component of thundr, a software library from 3wks.
* Read more: http://3wks.github.io/thundr/
* Copyright (C) 2015 3wks,
*
* 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.
*/
package com.threewks.thundr.search.gae;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import org.apache.commons.lang3.StringUtils;
import com.atomicleopard.expressive.Cast;
import com.atomicleopard.expressive.EList;
import com.atomicleopard.expressive.ETransformer;
import com.atomicleopard.expressive.Expressive;
import com.atomicleopard.expressive.transform.CollectionTransformer;
import com.google.appengine.api.search.*;
import com.google.appengine.api.search.Document.Builder;
import com.google.appengine.api.search.Field.FieldType;
import com.threewks.thundr.logger.Logger;
import com.threewks.thundr.search.IdTextSearchService;
import com.threewks.thundr.search.IndexOperation;
import com.threewks.thundr.search.Is;
import com.threewks.thundr.search.OrderComponent;
import com.threewks.thundr.search.QueryComponent;
import com.threewks.thundr.search.Result;
import com.threewks.thundr.search.SearchException;
import com.threewks.thundr.search.TextSearchService;
import com.threewks.thundr.search.gae.mediator.FieldMediator;
import com.threewks.thundr.search.gae.mediator.FieldMediatorSet;
import com.threewks.thundr.search.gae.meta.IndexType;
import com.threewks.thundr.search.gae.meta.IndexTypeLookup;
import com.threewks.thundr.search.gae.meta.SearchMetadata;
import com.threewks.thundr.search.gae.naming.IndexNamingStrategy;
import com.threewks.thundr.transformer.TransformerManager;
import jodd.bean.BeanUtil;
/**
* A common base class for {@link TextSearchService} and {@link IdTextSearchService} implementations that
* use the Appengine Full Text Search API.
*
* @param
* @param
*
* @see GaeSearchService
* @see IdGaeSearchService
*/
public abstract class BaseGaeSearchService implements SearchExecutor> {
private static final int IntLow = Integer.MIN_VALUE + 1;
private static final int IntHigh = Integer.MAX_VALUE;
private static final Date DateLow = new Date(0);
private static final Date DateHigh = new Date(Long.MAX_VALUE);
private static final String StringLow = "";
private static final String StringHigh = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
private static final Map IsSymbols = createIsSymbolMap();
protected Class type;
protected SearchMetadata metadata;
protected FieldMediatorSet fieldMediators;
protected TransformerManager transformerManager;
protected IndexTypeLookup indexTypeLookup;
protected IndexNamingStrategy indexNamingStrategy;
protected String indexName;
protected BaseGaeSearchService(Class type, SearchConfig searchConfig, IndexNamingStrategy indexNamingStrategy) {
this(type, new SearchMetadata(type, searchConfig.getIndexTypeLookup()), searchConfig, indexNamingStrategy);
}
protected BaseGaeSearchService(Class type, Class keyType, SearchConfig searchConfig, IndexNamingStrategy indexNamingStrategy) {
this(type, new SearchMetadata(type, keyType, searchConfig.getIndexTypeLookup()), searchConfig, indexNamingStrategy);
}
protected BaseGaeSearchService(Class type, SearchMetadata metadata, SearchConfig searchConfig, IndexNamingStrategy indexNamingStrategy) {
this.type = type;
this.fieldMediators = searchConfig.getFieldMediators();
this.transformerManager = searchConfig.getTransformerManager();
this.indexTypeLookup = searchConfig.getIndexTypeLookup();
this.indexNamingStrategy = indexNamingStrategy;
this.indexName = indexNamingStrategy.getName(type);
this.metadata = metadata;
}
public boolean hasIndexableFields() {
return metadata.hasIndexableFields();
}
protected IndexOperation index(T object, K id) {
Map data = metadata.getData(object);
Document document = buildDocument(id, data);
Future putAsync = getIndex().putAsync(document);
return new IndexOperation(putAsync);
}
protected IndexOperation index(Collection objects) {
if (Expressive.isEmpty(objects)) {
return new IndexOperation(null);
}
List documents = new ArrayList(objects.size());
for (T t : objects) {
Map data = metadata.getData(t);
K id = metadata.getId(t);
Document document = buildDocument(id, data);
documents.add(document);
}
return new IndexOperation(getIndex().putAsync(documents));
}
protected IndexOperation index(Map objects) {
if (Expressive.isEmpty(objects)) {
return new IndexOperation(null);
}
List documents = new ArrayList(objects.size());
for (Map.Entry entry : objects.entrySet()) {
Map data = metadata.getData(entry.getValue());
Document document = buildDocument(entry.getKey(), data);
documents.add(document);
}
return new IndexOperation(getIndex().putAsync(documents));
}
protected IndexOperation removeById(K id) {
String stringId = convert(id, String.class);
Future deleteAsync = getIndex().deleteAsync(stringId);
return new IndexOperation(deleteAsync);
}
protected IndexOperation removeById(Iterable ids) {
List stringIds = convert(ids, String.class);
Future deleteAsync = getIndex().deleteAsync(stringIds);
return new IndexOperation(deleteAsync);
}
protected int removeAll() {
int count = 0;
Index index = getIndex();
GetRequest request = GetRequest.newBuilder().setReturningIdsOnly(true).setLimit(200).build();
GetResponse response = index.getRange(request);
// can only delete documents in blocks of 200 so we need to iterate until they're all gone
while (!response.getResults().isEmpty()) {
List ids = new ArrayList();
for (Document document : response) {
ids.add(document.getId());
}
index.delete(ids);
count += ids.size();
response = index.getRange(request);
}
return count;
}
protected SearchImpl search() {
return new SearchImpl(this);
}
@Override
public Result createSearchResult(SearchImpl searchRequest) {
String queryString = buildQueryString(searchRequest.query());
SortOptions.Builder sortOptions = buildSortOptions(searchRequest.order());
QueryOptions.Builder queryOptions = QueryOptions.newBuilder();
Integer limit = searchRequest.limit();
int offset = 0;
if (limit != null) {
offset = searchRequest.offset() == null ? 0 : searchRequest.offset();
int effectiveLimit = limit + offset;
if (effectiveLimit > 1000) {
Logger.warn("Currently the Google Search API does not support queries with a limit over 1000. With an offset of %d and a limit of %d, you have an effective limit of %d", offset, limit, effectiveLimit);
}
limit = effectiveLimit;
/* Note, this can't be more than 1000 (Crashes) */
queryOptions = queryOptions.setLimit(limit);
}
if (searchRequest.accuracy() != null) {
queryOptions.setNumberFoundAccuracy(searchRequest.accuracy());
}
queryOptions.setSortOptions(sortOptions);
Query query = Query.newBuilder().setOptions(queryOptions).build(queryString);
Future> searchAsync = getIndex().searchAsync(query);
return new ResultImpl(this, searchAsync, searchRequest.offset());
}
protected SortOptions.Builder buildSortOptions(List order) {
SortOptions.Builder sortOptions = SortOptions.newBuilder();
for (OrderComponent sort : order) {
String fieldName = getEncodedFieldName(sort.getField());
SortExpression.Builder expression = SortExpression.newBuilder().setExpression(fieldName);
expression = expression.setDirection(sort.isAscending() ? SortExpression.SortDirection.ASCENDING : SortExpression.SortDirection.DESCENDING);
IndexType indexType = metadata.getIndexType(sort.getField());
if (IndexType.SmallDecimal == indexType || IndexType.BigDecimal == indexType) {
expression = expression.setDefaultValueNumeric(sort.isDescending() ? IntLow : IntHigh);
} else if (IndexType.Date == indexType) {
expression = expression.setDefaultValueDate(sort.isDescending() ? DateLow : DateHigh);
} else {
expression = expression.setDefaultValue(sort.isDescending() ? StringLow : StringHigh);
}
sortOptions = sortOptions.addSortExpression(expression);
}
return sortOptions;
}
protected String buildQueryString(List queryComponents) {
List stringQueryComponents = new ArrayList<>();
for (QueryComponent queryComponent : queryComponents) {
String fragmentString = convertQueryComponentToQueryFragment(queryComponent);
stringQueryComponents.add(fragmentString);
}
return StringUtils.join(stringQueryComponents, " ");
}
protected String convertQueryComponentToQueryFragment(QueryComponent queryComponent) {
if (!queryComponent.isFieldedQuery()) {
return queryComponent.getQuery();
}
String field = this.getEncodedFieldName(queryComponent.getField());
if (field == null) {
throw new SearchException("Unable to build query string - there is no field named '%s' on %s", queryComponent.getField(), type.getSimpleName());
}
String operation = IsSymbols.get(queryComponent.getIs());
if (queryComponent.isCollectionQuery()) {
List values = convertValuesToString(field, queryComponent.getCollectionValue());
String stringValue = StringUtils.join(values, " OR ");
return String.format("%s:(%s)", field, stringValue);
} else {
String value = convertValueToString(field, queryComponent.getValue());
return String.format("%s%s%s", field, operation, value);
}
}
protected Document buildDocument(K id, Map fields) {
String stringId = convert(id, String.class);
Builder documentBuilder = Document.newBuilder();
documentBuilder.setId(stringId);
for (Map.Entry fieldData : fields.entrySet()) {
Object value = fieldData.getValue();
String fieldName = fieldData.getKey();
for (Object object : getCollectionValues(value)) {
try {
Field field = buildField(metadata, fieldName, object);
documentBuilder.addField(field);
} catch (Exception e) {
throw new SearchException(e, "Failed to add field '%s' with value '%s' to document with id '%s': %s", fieldName, value.toString(), id, e.getMessage());
}
}
}
return documentBuilder.build();
}
Field buildField(SearchMetadata metadata, String field, Object value) {
com.google.appengine.api.search.Field.Builder fieldBuilder = Field.newBuilder().setName(metadata.getEncodedFieldName(field));
IndexType indexType = metadata.getIndexType(field);
FieldMediator fieldMediator = fieldMediators.get(indexType);
F normalised = fieldMediator.normalise(transformerManager, value);
fieldMediator.setValue(fieldBuilder, normalised);
return fieldBuilder.build();
}
@SuppressWarnings("unchecked")
R convert(O input, Class type) {
Class inputType = (Class) input.getClass();
if (inputType == type) {
return (R) input;
}
ETransformer transformer = transformerManager.getTransformerSafe(inputType, type);
return transformer.from(input);
}
@SuppressWarnings("unchecked")
List convert(Iterable inputs, Class type) {
Iterator iterator = inputs.iterator();
if (!iterator.hasNext()) {
return Collections.emptyList();
}
List results = new ArrayList();
ETransformer transformer = null;
for (O t : inputs) {
if (transformer == null) {
Class inputType = (Class) t.getClass();
transformer = transformerManager.getTransformerSafe(inputType, type);
}
results.add(transformer.from(t));
}
return results;
}
/**
* We treat all values as collections.
* Nulls are treated as an empty collection,
* Non-collections are treated as a collection of length 1
*
* @param value
* @return
*/
@SuppressWarnings("unchecked")
private Collection