Performing faceted search with elastic search repositories using spring data? - spring

I am in the need of performing faceted search using elastic search repositories developed using spring data.
One of the repositories which I have created are
public interface EmployeeSearchRepository extends ElasticsearchRepository<Employee, Long> {
}
it does provide a method called search with a signature:
FacetedPage<Employee> search(QueryBuilder query, Pageable pageable);
but the getFacets method of the FacetedPage returns null. How can I query to generate the facets?

I have the same problem and it seems that it is not implemented (yet).
If you look at DefaultResultMapper.mapResults() it calls response.getFacets() which is always null.
Note that facets are deprecated in elasticsearch and you should use aggregations instead. So maybe the contributors of the project are refactoring it?
I worked this around by writing my own results mapper class which extends the DefaultResultMapper but also converts the aggregations to FacetResults.
SomethingResultsMapper:
#Override
public <T> FacetedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
FacetedPage<T> facetedPage = super.mapResults(response, clazz, pageable);
//Process Aggregations.
if (response.getAggregations() != null) {
for (Aggregation aggregations : response.getAggregations().asList()) {
final Filter filterAggregations = (Filter) aggregations;
for (Aggregation filterAgg : filterAggregations.getAggregations().asList()) {
if (filterAgg instanceof Terms) {
final Terms aggTerm = (Terms) filterAgg;
if (!aggTerm.getBuckets().isEmpty()) {
facetedPage.getFacets().add(processTermAggregation(aggTerm));
}
} else if (filterAgg instanceof Nested) {
final Nested nestedAgg = (Nested) filterAgg;
for (Aggregation aggregation : nestedAgg.getAggregations().asList()) {
final Terms aggTerm = (Terms) aggregation;
if (!aggTerm.getBuckets().isEmpty()) {
facetedPage.getFacets().add(processTermAggregation(aggTerm));
}
}
} else {
throw new IllegalArgumentException("Aggregation type not (yet) supported: " + filterAgg.getClass().getName());
}
}
}
}
return facetedPage;
}
private FacetResult processTermAggregation(final Terms aggTerm) {
long total = 0;
List<Term> terms = new ArrayList<>();
List<Terms.Bucket> buckets = aggTerm.getBuckets();
for (Terms.Bucket bucket : buckets) {
terms.add(new Term(bucket.getKey(), (int) bucket.getDocCount()));
total += bucket.getDocCount();
}
return new FacetTermResult(aggTerm.getName(), FacetConfig.fromAggregationTerm(aggTerm.getName()).getLabel(),
terms, total, aggTerm.getSumOfOtherDocCounts(), aggTerm.getDocCountError());
}
Then i created a custom Spring data repository (see the docs) and defined a custom method where i provide my SomethingResultsMapper:
#Override
public FacetedPage<Something> searchSomething(final SearchQuery searchQuery) {
return elasticsearchTemplate.queryForPage(searchQuery, Something.class, new SomethingResultsMapper());
}
EDIT: I think this one is being fixed by https://jira.spring.io/browse/DATAES-211

Related

Simple aggregation is getting failed in javaelasticsearch 8.0+ client

I have got a simple method that performs simple terms aggregation using elastic search8.0
I am able to do it using RestHighLevelClient but with ElasticsearchClient I am getting empty buckets.
can someone please help me to resolve
public void aggregate(ElasticsearchClient client) throws ElasticsearchException, IOException {
String field = "loglevel";
Map<String, Long> buckets = new HashMap<String, Long>();
SearchResponse<SspDevLog> response = client.search(fn -> fn
.aggregations("loglevel", a -> a.terms(v-> v.field(field))), SspDevLog.class);
Map<String, Aggregate> aggrs = response.aggregations();
for(Map.Entry<String, Aggregate> entry : aggrs.entrySet()) {
Aggregate aggregate = entry.getValue();
StringTermsAggregate sterms = aggregate.sterms();
Buckets<StringTermsBucket> sbuckets = sterms.buckets();
List<StringTermsBucket> bucArr = sbuckets.array();
for(StringTermsBucket bucObj : bucArr) {
buckets.put(bucObj.key(), bucObj.docCount());
}
}
System.out.println(buckets);
}

Get Aggregate Information from Elasticsearch using Spring-data-elasticsearch, ElasticsearchRepository

I would like to get aggregate results from ES like avgSize (avg of a field with name 'size'), totalhits for documents that match a term, and some other aggregates in future, for which I don't think ElasticsearchRepository has any methods to call. I built Query and Aggregate Builders as below. I want to use my Repository interface but I am not sure of what should the return ObjectType be ? Should it be a document type in my DTOs ? Also I have seen examples where the searchQueryis passed directly to ElasticsearchTemplate but then what is the point of having Repository interface that extends ElasticsearchRepository
Repository Interface
public interface CCFilesSummaryRepository extends ElasticsearchRepository<DataReferenceSummary, UUID> {
}
Elastic configuration
#Configuration
#EnableElasticsearchRepositories(basePackages = "com.xxx.repository.es")
public class ElasticConfiguration {
#Bean
public ElasticsearchOperations elasticsearchTemplate() throws UnknownHostException {
return new ElasticsearchTemplate(elasticsearchClient());
}
#Bean
public Client elasticsearchClient() throws UnknownHostException {
Settings settings = Settings.builder().put("cluster.name", "elasticsearch").build();
TransportClient client = new PreBuiltTransportClient(settings);
client.addTransportAddress(new TransportAddress(InetAddress.getLocalHost(), 9200));
return client;
}
}
Service Method
public DataReferenceSummary createSummary(final DataSet dataSet) {
try {
QueryBuilder queryBuilder = QueryBuilders.matchQuery("type" , dataSet.getDataSetCreateRequest().getContentType());
AvgAggregationBuilder avgAggregationBuilder = AggregationBuilders.avg("avg_size").field("size");
ValueCountAggregationBuilder valueCountAggregationBuilder = AggregationBuilders.count("total_references")
.field("asset_id");
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.addAggregation(avgAggregationBuilder)
.addAggregation(valueCountAggregationBuilder)
.build();
return ccFilesSummaryRepository.search(searchQuery).iterator().next();
} catch (Exception e){
e.printStackTrace();
}
return null;
}
DataReferernceSummary is just a POJO for now and for which I am getting an error during my build that says Unable to build Bean CCFilesSummaryRepository, illegalArgumentException DataReferernceSummary. is not a amanged Object
First DataReferenceSummary must be a class annotated with #Document.
In Spring Data Elasticsearch 3.2.0 (the current version) you need to define the repository return type as AggregatedPage<DataReferenceSummary>, the returned object will contain the aggregations.
From the upcoming version 4.0 on, you will have to define the return type as SearchHits<DataReferenceSummary> and find the aggregations in this returned object.

Is it correct that we use both, MassIndexer and manual indexing from Hibernate Search in our application?

Recently, I joined a project that is using Hibernate Search.
I suspect we have a glitch in our app that causes ignoring newly indexed data by other background job due to using FullTextEntityManager in 2 places:
1) While performing the search of target data from UI, we use MassIndexer to index the data at first search request, and all subsequent search requests will not cause reindexing:
private final AtomicBoolean initialized = new AtomicBoolean(false);
...
public FullTextQuery buildTransactionSearchQuery(SearchRequestDTO request) {
final FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
final Query expression = buildTransactionSearchExpression(request.getFilter(), fullTextEntityManager);
final FullTextQuery query = fullTextEntityManager.createFullTextQuery(expression, Transaction.class);
return query;
}
...
private FullTextEntityManager getFullTextEntityManager() {
final FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
if (initialized.get()) {
return fullTextEntityManager;
} else {
synchronized (initialized) {
if (!initialized.getAndSet(true)) {
try {
fullTextEntityManager.createIndexer().startAndWait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return fullTextEntityManager;
}
}
}
2) In the background job:
#Scheduled(initialDelay = 1_000, fixedDelay = 5_000)
private void indexAuditValues() {
Instant previousRunTime = ...; // assume data is set
Instant currentTime = ...;
int page = 0;
boolean hasMore = true;
while (hasMore) {
hasMore = hsIndexingService.indexAuditValues(previousRunTime, currentTime, page++);
}
}
#Transactional(readOnly = true)
public boolean indexAuditValues(Instant previousRunTime, Instant currentTime, int page) {
PageRequest pageRequest = return new PageRequest(page, batchSize, Sort.Direction.ASC, AUDIT_VALUE_SORT_COLUMN);
Page<AuditValue> pageResults = auditValueRepository.findByAuditTransactionLastModifiedDateBetween(previousRunTime, currentTime, pageRequest);
FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
List<AuditValue> content = pageResults.getContent();
content.forEach(fullTextEntityManager::index); // here we do index the data
return pageResults.hasNext();
}
private FullTextEntityManager getFullTextEntityManager() {
return Search.getFullTextEntityManager(entityManager);
}
Recently, our users reported that the new data doesn't appear on the search page, can it be possible due to using 2 FullTextEntityManagers in 2 separate threads which are not synchronized? If yes, how can it be solved?
We use file Spring boot, Hibernate Search, Lucene, and store indexes in file system.
Entities are annotated with #Indexed and searchable fields are annotated with #Field.
I'm not sure it was part of your question, but I'll make it clear anyway: FullTextEntityManager can be used in two separate threads, as long as you're using a different entity manager. And if you're using Spring, it's very likely that you do. So everything is fine there.
The main problem I see in your setup is that, potentially, the two methods could execute simultaneously (if the first search query is sent before or during the first scheduled indexing). But in that case, you would rather get duplicate documents in your index than missing documents (because of the way the mass indexer works). So I don't really know what's going wrong.
I would advise to stay away from lazily executing mass indexing in the query method, and more importantly to avoid waiting for a potentially long-running operation (mass indexing) in request threads: it's a major anti-pattern.
Ideally you should only mass index when you re-deploy your application (when the customer doesn't use the application), and re-use the index after a restart. That way you never have to make requests wait for mass indexing: by the time anyone accesses the application, everything has already been indexed.
But you didn't do any of that, so I will assume you have your reasons. If you really want to reindex everything on startup, and to block search requests as long as mass indexing is not over, something like below should be safer. Maybe not flawless (it depends on your model, really: I don't know whether audit values may be updated), but safer.
1) While performing the search of target data from UI, block the request until the initial indexing is over [once again, this is a bad idea, but to each his own].
// Assuming the background job class is named "IndexInitializer"
#Autowired
IndexInitializer indexInitializer;
...
public FullTextQuery buildTransactionSearchQuery(SearchRequestDTO request) {
final FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
final Query expression = buildTransactionSearchExpression(request.getFilter(), fullTextEntityManager);
final FullTextQuery query = fullTextEntityManager.createFullTextQuery(expression, Transaction.class);
return query;
}
...
private FullTextEntityManager getFullTextEntityManager() {
indexInitializer.awaitInitialIndexing();
return Search.getFullTextEntityManager(entityManager);
}
2) In the background job, use the mass indexer on the first tick, and incremental indexing on each subsequent tick:
private final CountDownLatch initialIndexingsRemaining = new CountDownLatch(1);
public void awaitInitialIndexing() {
initialIndexingsRemaining.await();
}
#Scheduled(initialDelay = 0, fixedDelay = 5_000)
private void indexAuditValues() {
if (isInitialIndexingDone()) {
doIncrementalIndexing();
} else {
doInitialIndexing();
}
}
private boolean isInitialIndexingDone() {
return initialIndexingsRemaining.await(0, TimeUnit.NANOSECONDS);
}
private void doInitialIndexing() {
// Synchronization is only necessary here if the scheduled method may be called again before the previous execution is over. Not sure it's possible?
synchronized (this) {
if (isInitialIndexingDone()) {
return;
}
try {
fullTextEntityManager.createIndexer().startAndWait();
initialIndexingsRemaining.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void doIncrementalIndexing() {
Instant previousRunTime = ...; // assume data is set
Instant currentTime = ...;
int page = 0;
boolean hasMore = true;
while (hasMore) {
hasMore = hsIndexingService.indexAuditValues(previousRunTime, currentTime, page++);
}
}
#Transactional(readOnly = true)
public boolean indexAuditValues(Instant previousRunTime, Instant currentTime, int page) {
PageRequest pageRequest = return new PageRequest(page, batchSize, Sort.Direction.ASC, AUDIT_VALUE_SORT_COLUMN);
Page<AuditValue> pageResults = auditValueRepository.findByAuditTransactionLastModifiedDateBetween(previousRunTime, currentTime, pageRequest);
FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
List<AuditValue> content = pageResults.getContent();
content.forEach(fullTextEntityManager::index); // here we do index the data
return pageResults.hasNext();
}
private FullTextEntityManager getFullTextEntityManager() {
return Search.getFullTextEntityManager(entityManager);
}
On a side note, you could also replace your manual, periodic indexing with automatic, on-the-fly indexing: Hibernate Search will update the index automatically when entities are persisted/updated/deleted in Hibernate ORM.

Read Application Object from GemFire using Spring Data GemFire. Data stored using SpringXD's gemfire-json-server

I'm using the gemfire-json-server module in SpringXD to populate a GemFire grid with json representation of “Order” objects. I understand the gemfire-json-server module saves data in Pdx form in GemFire. I’d like to read the contents of the GemFire grid into an “Order” object in my application. I get a ClassCastException that reads:
java.lang.ClassCastException: com.gemstone.gemfire.pdx.internal.PdxInstanceImpl cannot be cast to org.apache.geode.demo.cc.model.Order
I’m using the Spring Data GemFire libraries to read contents of the cluster. The code snippet to read the contents of the Grid follows:
public interface OrderRepository extends GemfireRepository<Order, String>{
Order findByTransactionId(String transactionId);
}
How can I use Spring Data GemFire to convert data read from the GemFire cluster into an Order object?
Note: The data was initially stored in GemFire using SpringXD's gemfire-json-server-module
Still waiting to hear back from the GemFire PDX engineering team, specifically on Region.get(key), but, interestingly enough if you annotate your application domain object with...
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#type")
public class Order ... {
...
}
This works!
Under-the-hood I knew the GemFire JSONFormatter class (see here) used Jackson's API to un/marshal (de/serialize) JSON data to and from PDX.
However, the orderRepository.findOne(ID) and ordersRegion.get(key) still do not function as I would expect. See updated test class below for more details.
Will report back again when I have more information.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = GemFireConfiguration.class)
#SuppressWarnings("unused")
public class JsonToPdxToObjectDataAccessIntegrationTest {
protected static final AtomicLong ID_SEQUENCE = new AtomicLong(0l);
private Order amazon;
private Order bestBuy;
private Order target;
private Order walmart;
#Autowired
private OrderRepository orderRepository;
#Resource(name = "Orders")
private com.gemstone.gemfire.cache.Region<Long, Object> orders;
protected Order createOrder(String name) {
return createOrder(ID_SEQUENCE.incrementAndGet(), name);
}
protected Order createOrder(Long id, String name) {
return new Order(id, name);
}
protected <T> T fromPdx(Object pdxInstance, Class<T> toType) {
try {
if (pdxInstance == null) {
return null;
}
else if (toType.isInstance(pdxInstance)) {
return toType.cast(pdxInstance);
}
else if (pdxInstance instanceof PdxInstance) {
return new ObjectMapper().readValue(JSONFormatter.toJSON(((PdxInstance) pdxInstance)), toType);
}
else {
throw new IllegalArgumentException(String.format("Expected object of type PdxInstance; but was (%1$s)",
pdxInstance.getClass().getName()));
}
}
catch (IOException e) {
throw new RuntimeException(String.format("Failed to convert PDX to object of type (%1$s)", toType), e);
}
}
protected void log(Object value) {
System.out.printf("Object of Type (%1$s) has Value (%2$s)", ObjectUtils.nullSafeClassName(value), value);
}
protected Order put(Order order) {
Object existingOrder = orders.putIfAbsent(order.getTransactionId(), toPdx(order));
return (existingOrder != null ? fromPdx(existingOrder, Order.class) : order);
}
protected PdxInstance toPdx(Object obj) {
try {
return JSONFormatter.fromJSON(new ObjectMapper().writeValueAsString(obj));
}
catch (JsonProcessingException e) {
throw new RuntimeException(String.format("Failed to convert object (%1$s) to JSON", obj), e);
}
}
#Before
public void setup() {
amazon = put(createOrder("Amazon Order"));
bestBuy = put(createOrder("BestBuy Order"));
target = put(createOrder("Target Order"));
walmart = put(createOrder("Wal-Mart Order"));
}
#Test
public void regionGet() {
assertThat((Order) orders.get(amazon.getTransactionId()), is(equalTo(amazon)));
}
#Test
public void repositoryFindOneMethod() {
log(orderRepository.findOne(target.getTransactionId()));
assertThat(orderRepository.findOne(target.getTransactionId()), is(equalTo(target)));
}
#Test
public void repositoryQueryMethod() {
assertThat(orderRepository.findByTransactionId(amazon.getTransactionId()), is(equalTo(amazon)));
assertThat(orderRepository.findByTransactionId(bestBuy.getTransactionId()), is(equalTo(bestBuy)));
assertThat(orderRepository.findByTransactionId(target.getTransactionId()), is(equalTo(target)));
assertThat(orderRepository.findByTransactionId(walmart.getTransactionId()), is(equalTo(walmart)));
}
#Region("Orders")
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#type")
public static class Order implements PdxSerializable {
protected static final OrderPdxSerializer pdxSerializer = new OrderPdxSerializer();
#Id
private Long transactionId;
private String name;
public Order() {
}
public Order(Long transactionId) {
this.transactionId = transactionId;
}
public Order(Long transactionId, String name) {
this.transactionId = transactionId;
this.name = name;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public Long getTransactionId() {
return transactionId;
}
public void setTransactionId(final Long transactionId) {
this.transactionId = transactionId;
}
#Override
public void fromData(PdxReader reader) {
Order order = (Order) pdxSerializer.fromData(Order.class, reader);
if (order != null) {
this.transactionId = order.getTransactionId();
this.name = order.getName();
}
}
#Override
public void toData(PdxWriter writer) {
pdxSerializer.toData(this, writer);
}
#Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Order)) {
return false;
}
Order that = (Order) obj;
return ObjectUtils.nullSafeEquals(this.getTransactionId(), that.getTransactionId());
}
#Override
public int hashCode() {
int hashValue = 17;
hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getTransactionId());
return hashValue;
}
#Override
public String toString() {
return String.format("{ #type = %1$s, id = %2$d, name = %3$s }",
getClass().getName(), getTransactionId(), getName());
}
}
public static class OrderPdxSerializer implements PdxSerializer {
#Override
public Object fromData(Class<?> type, PdxReader in) {
if (Order.class.equals(type)) {
return new Order(in.readLong("transactionId"), in.readString("name"));
}
return null;
}
#Override
public boolean toData(Object obj, PdxWriter out) {
if (obj instanceof Order) {
Order order = (Order) obj;
out.writeLong("transactionId", order.getTransactionId());
out.writeString("name", order.getName());
return true;
}
return false;
}
}
public interface OrderRepository extends GemfireRepository<Order, Long> {
Order findByTransactionId(Long transactionId);
}
#Configuration
protected static class GemFireConfiguration {
#Bean
public Properties gemfireProperties() {
Properties gemfireProperties = new Properties();
gemfireProperties.setProperty("name", JsonToPdxToObjectDataAccessIntegrationTest.class.getSimpleName());
gemfireProperties.setProperty("mcast-port", "0");
gemfireProperties.setProperty("log-level", "warning");
return gemfireProperties;
}
#Bean
public CacheFactoryBean gemfireCache(Properties gemfireProperties) {
CacheFactoryBean cacheFactoryBean = new CacheFactoryBean();
cacheFactoryBean.setProperties(gemfireProperties);
//cacheFactoryBean.setPdxSerializer(new MappingPdxSerializer());
cacheFactoryBean.setPdxSerializer(new OrderPdxSerializer());
cacheFactoryBean.setPdxReadSerialized(false);
return cacheFactoryBean;
}
#Bean(name = "Orders")
public PartitionedRegionFactoryBean ordersRegion(Cache gemfireCache) {
PartitionedRegionFactoryBean regionFactoryBean = new PartitionedRegionFactoryBean();
regionFactoryBean.setCache(gemfireCache);
regionFactoryBean.setName("Orders");
regionFactoryBean.setPersistent(false);
return regionFactoryBean;
}
#Bean
public GemfireRepositoryFactoryBean orderRepository() {
GemfireRepositoryFactoryBean<OrderRepository, Order, Long> repositoryFactoryBean =
new GemfireRepositoryFactoryBean<>();
repositoryFactoryBean.setRepositoryInterface(OrderRepository.class);
return repositoryFactoryBean;
}
}
}
So, as you are aware, GemFire (and by extension, Apache Geode) stores JSON in PDX format (as a PdxInstance). This is so GemFire can interoperate with many different language-based clients (native C++/C#, web-oriented (JavaScript, Pyhton, Ruby, etc) using the Developer REST API, in addition to Java) and also to be able to use OQL to query the JSON data.
After a bit of experimentation, I am surprised GemFire is not behaving as I would expect. I created an example, self-contained test class (i.e. no Spring XD, of course) that simulates your use case... essentially storing JSON data in GemFire as PDX and then attempting to read the data back out as the Order application domain object type using the Repository abstraction, logical enough.
Given the use of the Repository abstraction and implementation from Spring Data GemFire, the infrastructure will attempt to access the application domain object based on the Repository generic type parameter (in this case "Order" from the "OrderRepository" definition).
However, the data is stored in PDX, so now what?
No matter, Spring Data GemFire provides the MappingPdxSerializer class to convert PDX instances back to application domain objects using the same "mapping meta-data" that the Repository infrastructure uses. Cool, so I plug that in...
#Bean
public CacheFactoryBean gemfireCache(Properties gemfireProperties) {
CacheFactoryBean cacheFactoryBean = new CacheFactoryBean();
cacheFactoryBean.setProperties(gemfireProperties);
cacheFactoryBean.setPdxSerializer(new MappingPdxSerializer());
cacheFactoryBean.setPdxReadSerialized(false);
return cacheFactoryBean;
}
You will also notice, I set the PDX 'read-serialized' property (cacheFactoryBean.setPdxReadSerialized(false);) to false in order to ensure data access operations return the domain object and not the PDX instance.
However, this had no affect on the query method. In fact, it had no affect on the following operations either...
orderRepository.findOne(amazonOrder.getTransactionId());
ordersRegion.get(amazonOrder.getTransactionId());
Both calls returned a PdxInstance. Note, the implementation of OrderRepository.findOne(..) is based on SimpleGemfireRepository.findOne(key), which uses GemfireTemplate.get(key), which just performs Region.get(key), and so is effectively the same as (ordersRegion.get(amazonOrder.getTransactionId();). The outcome should not be, especially with Region.get() and read-serialized set to false.
With the OQL query (SELECT * FROM /Orders WHERE transactionId = $1) generated from the findByTransactionId(String id), the Repository infrastructure has a bit less control over what the GemFire query engine will return based on what the caller (OrderRepository) expects (based on the generic type parameter), so running OQL statements could potentially behave differently than direct Region access using get.
Next, I went onto try modifying the Order type to implement PdxSerializable, to handle the conversion during data access operations (direct Region access with get, OQL, or otherwise). This had no affect.
So, I tried to implement a custom PdxSerializer for Order objects. This had no affect either.
The only thing I can conclude at this point is something is getting lost in translation between Order -> JSON -> PDX and then from PDX -> Order. Seemingly, GemFire needs additional type meta-data required by PDX (something like #JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#type") in the JSON data that PDXFormatter recognizes, though I am not certain it does.
Note, in my test class, I used Jackson's ObjectMapper to serialize the Order to JSON and then GemFire's JSONFormatter to serialize the JSON to PDX, which I suspect Spring XD is doing similarly under-the-hood. In fact, Spring XD uses Spring Data GemFire and is most likely using the JSON Region Auto Proxy support. That is exactly what SDG's JSONRegionAdvice object does (see here).
Anyway, I have an inquiry out to the rest of the GemFire engineering team. There are also things that could be done in Spring Data GemFire to ensure the PDX data is converted, such as making use of the MappingPdxSerializer directly to convert the data automatically on behalf of the caller if the data is indeed of type PdxInstance. Similar to how JSON Region Auto Proxying works, you could write AOP interceptor for the Orders Region to automagicaly convert PDX to an Order.
Though, I don't think any of this should be necessary as GemFire should be doing the right thing in this case. Sorry I don't have a better answer right now. Let's see what I find out.
Cheers and stay tuned!
See subsequent post for test code.

Add a custom parameter to Solr while using Spring Data Solr

Is it possible to add an additional parameter to a Solr query using Spring Data Solr that generates the following request?
"params": {
"indent": "true",
"q": "*.*",
"_": "1430295713114",
"wt": "java",
"AuthenticatedUserName": "user#domain.com"
}
I want to add a parameter needed by Apache Manifoldcf, AuthenticatedUserName and its value, alongside the other ones that are automatically populated by Spring Data Solr (q, wt).
Thank you,
V.
I managed to make it work by looking at the source code of the SolrTemplate class but I was wondering if there is a less intrusive solution.
public Page<Document> searchDocuments(DocumentSearchCriteria criteria, Pageable page) {
String[] words = criteria.getTitle().split(" ");
Criteria conditions = createSearchConditions(words);
SimpleQuery query = new SimpleQuery(conditions);
query.setPageRequest(page);
SolrQuery solrQuery = queryParsers.getForClass(query.getClass()).constructSolrQuery(query);
solrQuery.add(AUTHENTICATED_USER_NAME, criteria.getLoggedUsername());
try {
String queryString = this.queryParsers.getForClass(query.getClass()).getQueryString(query);
solrQuery.set(CommonParams.Q, queryString);
QueryResponse response = solrTemplate.getSolrServer().query(solrQuery);
List<Document> beans = convertQueryResponseToBeans(response, Document.class);
SolrDocumentList results = response.getResults();
return new SolrResultPage<>(beans, query.getPageRequest(), results.getNumFound(), results.getMaxScore());
} catch (SolrServerException e) {
log.error(e.getMessage(), e);
return new SolrResultPage<>(Collections.<Document>emptyList());
}
}
private <T> List<T> convertQueryResponseToBeans(QueryResponse response, Class<T> targetClass) {
return response != null ? convertSolrDocumentListToBeans(response.getResults(), targetClass) : Collections
.<T> emptyList();
}
public <T> List<T> convertSolrDocumentListToBeans(SolrDocumentList documents, Class<T> targetClass) {
if (documents == null) {
return Collections.emptyList();
}
return solrTemplate.getConverter().read(documents, targetClass);
}
private Criteria createSearchConditions(String[] words) {
return new Criteria("title").contains(words)
.or(new Criteria("description").contains(words))
.or(new Criteria("content").contains(words))
.or(new Criteria("resourcename").contains(words));
}

Resources