Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2092,7 +2092,7 @@ public List<Contentlet> search(String query, int limit, int offset, String sortB
SearchHits hits = indexSearch(query, limit, offset, sortBy);
List<String> inodes=new ArrayList<>();
for(SearchHit h : hits){
inodes.add(Try.of(()->h.sourceAsMap().get("inode").toString()).getOrNull());
inodes.add(Try.of(()->h.getSourceAsMap().get("inode").toString()).getOrNull());
}
return findContentlets(inodes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1683,17 +1683,17 @@ public List<ContentletSearch> searchIndex(String luceneQuery, int limit, int off
final com.dotcms.content.index.domain.SearchHits searchHits = contentFactory.indexSearch(queryWithPermissions, limit,
offset, sortBy);
final PaginatedArrayList<ContentletSearch> list = new PaginatedArrayList<>();
list.setTotalResults(searchHits.totalHits().value());
list.setTotalResults(searchHits.getTotalHits().value());

for (final com.dotcms.content.index.domain.SearchHit searchHit : searchHits.hits()) {
for (final com.dotcms.content.index.domain.SearchHit searchHit : searchHits.getHits()) {
try {
final Map<String, Object> sourceMap = searchHit.sourceAsMap();
final Map<String, Object> sourceMap = searchHit.getSourceAsMap();
list.add(ImmutableContentletSearch.builder()
.id(searchHit.id())
.index(searchHit.index())
.id(searchHit.getId())
.index(searchHit.getIndex())
.identifier(sourceMap.get("identifier").toString())
.inode(sourceMap.get("inode").toString())
.score(searchHit.score())
.score(searchHit.getScore())
.build());
} catch (Exception e) {
Logger.error(this, e.getMessage(), e);
Expand Down Expand Up @@ -2400,12 +2400,12 @@ private List<Contentlet> getRelatedChildren(final Contentlet contentlet, final R
respectFrontendRoles, limit, offset, null);
}

if (response.hits().hits().isEmpty()) {
if (response.hits().getHits().isEmpty()) {
return result;
}

for (final com.dotcms.content.index.domain.SearchHit sh : response.hits()) {
final Map<String, Object> sourceMap = sh.sourceAsMap();
final Map<String, Object> sourceMap = sh.getSourceAsMap();
if (sourceMap.get(relationshipName) != null) {
List<String> relatedIdentifiers = ((ArrayList<String>) sourceMap.get(
relationshipName));
Expand Down Expand Up @@ -2493,9 +2493,9 @@ private List<Contentlet> getRelatedParents(final Contentlet contentlet, final Re
respectFrontendRoles, limit, offset, null);
}

if (!response.hits().hits().isEmpty()) {
if (!response.hits().getHits().isEmpty()) {
for (final com.dotcms.content.index.domain.SearchHit sh : response.hits()) {
final Map<String, Object> sourceMap = sh.sourceAsMap();
final Map<String, Object> sourceMap = sh.getSourceAsMap();
final String identifier = (String) sourceMap.get("identifier");
if (identifier != null && !relatedMap.containsKey(identifier)) {
final Contentlet mappedContentlet = findContentletByIdentifierAnyLanguage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ class ESContentletScrollImpl implements IndexContentletScroll {

// Convert to domain SearchHits
final SearchHits searchHits = SearchHits.from(esSearchHits);
this.totalHits = Objects.requireNonNull(searchHits.totalHits()).value();
this.totalHits = Objects.requireNonNull(searchHits.getTotalHits()).value();

// Convert hits to ContentletSearch
final List<ContentletSearch> results = getContentletSearchFromSearchHits(searchHits);
this.hasMoreResults = (searchHits.hits() != null && !searchHits.hits().isEmpty());
this.hasMoreResults = (searchHits.getHits() != null && !searchHits.getHits().isEmpty());

Logger.debug(this.getClass(),
() -> String.format("Scroll initialized: scrollId=%s, totalHits=%d, firstBatchSize=%d",
Expand Down Expand Up @@ -144,7 +144,7 @@ public List<ContentletSearch> nextBatch() throws DotDataException {
// Convert to domain SearchHits
final SearchHits searchHits = SearchHits.from(esSearchHits);
final List<ContentletSearch> results = getContentletSearchFromSearchHits(searchHits);
this.hasMoreResults = (searchHits.hits() != null && !searchHits.hits().isEmpty());
this.hasMoreResults = (searchHits.getHits() != null && !searchHits.getHits().isEmpty());

Logger.debug(this.getClass(),
() -> String.format("Scroll next batch: batchSize=%d, hasMore=%b",
Expand Down Expand Up @@ -192,17 +192,17 @@ public void close() {

private List<ContentletSearch> getContentletSearchFromSearchHits(final SearchHits searchHits) {
PaginatedArrayList<ContentletSearch> list=new PaginatedArrayList<>();
list.setTotalResults(searchHits.totalHits().value());
list.setTotalResults(searchHits.getTotalHits().value());

for (SearchHit sh : searchHits.hits()) {
for (SearchHit sh : searchHits.getHits()) {
try{
Map<String, Object> sourceMap = sh.sourceAsMap();
Map<String, Object> sourceMap = sh.getSourceAsMap();
list.add(ImmutableContentletSearch.builder()
.id(sh.id())
.index(sh.index())
.id(sh.getId())
.index(sh.getIndex())
.identifier(sourceMap.get("identifier").toString())
.inode(sourceMap.get("inode").toString())
.score(sh.score())
.score(sh.getScore())
.build());
}
catch(Exception e){
Expand Down
152 changes: 152 additions & 0 deletions dotCMS/src/main/java/com/dotcms/content/index/domain/Aggregation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.dotcms.content.index.domain;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.immutables.value.Value;

/**
* Vendor-neutral representation of a single named aggregation result.
*
* <p>Mirrors the shape that Velocity templates relied on before the Elasticsearch → OpenSearch
* migration, where {@code $results.aggregations.<name>} returned an
* {@code org.elasticsearch.search.aggregations.Aggregation}. Templates then walked
* {@code .buckets} (for {@code terms} aggregations) or {@code .getHits().getHits()} (for the
* {@code top_hits} metric aggregation).</p>
*
* <p>The set of aggregations is exposed as a plain {@code Map<String, Aggregation>} (Velocity
* resolves {@code $aggregations.content_types} through {@code Map#get}), so this is the only new
* type required — there is no separate container class. Use {@link #from(org.elasticsearch.search.aggregations.Aggregations)}
* / {@link #fromOS(Map)} to build that map from a vendor response.</p>
*
* <p>Factory methods are the only places where vendor imports are allowed in this file.</p>
*
* @see AggregationBucket
*/
@Value.Immutable
public interface Aggregation extends Iterable<AggregationBucket> {

/** The aggregation name as declared in the query (e.g. {@code content_types}). */
String getName();

/** The vendor-reported aggregation type (e.g. {@code sterms}, {@code lterms}, {@code top_hits}). */
@Value.Default
default String getType() {
return "unknown";
}

/** Buckets for multi-bucket ({@code terms}) aggregations; empty for metric aggregations. */
@Value.Default
default List<AggregationBucket> getBuckets() {
return Collections.emptyList();
}

/** Hits for the {@code top_hits} metric aggregation; {@code null} for other aggregation types. */
@Nullable
SearchHits getHits();

/** Iterate the buckets directly: {@code #foreach($bucket in $agg)}. */
@Override
default Iterator<AggregationBucket> iterator() {
return getBuckets().iterator();
}

static ImmutableAggregation.Builder builder() {
return ImmutableAggregation.builder();
}

// -------------------------------------------------------------------------
// ES factories
// -------------------------------------------------------------------------

/** Maps the full set of Elasticsearch aggregations to a {@code name -> Aggregation} map. */
static Map<String, Aggregation> from(
final org.elasticsearch.search.aggregations.Aggregations esAggs) {
if (esAggs == null) {
return Collections.emptyMap();
}
final Map<String, Aggregation> map = new LinkedHashMap<>();
for (final org.elasticsearch.search.aggregations.Aggregation agg : esAggs.asList()) {
map.put(agg.getName(), fromSingle(agg));
}
return map;
}

private static Aggregation fromSingle(final org.elasticsearch.search.aggregations.Aggregation esAgg) {
final ImmutableAggregation.Builder builder = builder()
.name(esAgg.getName())
.type(esAgg.getType());

if (esAgg instanceof org.elasticsearch.search.aggregations.bucket.terms.Terms) {
final org.elasticsearch.search.aggregations.bucket.terms.Terms terms =
(org.elasticsearch.search.aggregations.bucket.terms.Terms) esAgg;
builder.buckets(terms.getBuckets().stream()
.map(AggregationBucket::from)
.collect(Collectors.toList()));
} else if (esAgg instanceof org.elasticsearch.search.aggregations.metrics.TopHits) {
final org.elasticsearch.search.aggregations.metrics.TopHits topHits =
(org.elasticsearch.search.aggregations.metrics.TopHits) esAgg;
builder.hits(SearchHits.from(topHits.getHits()));
}

return builder.build();
}

// -------------------------------------------------------------------------
// OS factories
// -------------------------------------------------------------------------

/** Maps the full set of OpenSearch aggregations to a {@code name -> Aggregation} map. */
static Map<String, Aggregation> fromOS(
final Map<String, org.opensearch.client.opensearch._types.aggregations.Aggregate> osAggs) {
if (osAggs == null || osAggs.isEmpty()) {
return Collections.emptyMap();
}
final Map<String, Aggregation> map = new LinkedHashMap<>();
for (final Map.Entry<String, org.opensearch.client.opensearch._types.aggregations.Aggregate> entry
: osAggs.entrySet()) {
final Aggregation aggregation = fromSingleOS(entry.getKey(), entry.getValue());
if (aggregation != null) {
map.put(entry.getKey(), aggregation);
}
}
return map;
}

@Nullable
private static Aggregation fromSingleOS(final String name,
final org.opensearch.client.opensearch._types.aggregations.Aggregate agg) {

final ImmutableAggregation.Builder builder = builder().name(name);

if (agg.isSterms()) {
return builder.type("sterms")
.buckets(agg.sterms().buckets().array().stream()
.map(AggregationBucket::fromOS)
.collect(Collectors.toList()))
.build();
} else if (agg.isLterms()) {
return builder.type("lterms")
.buckets(agg.lterms().buckets().array().stream()
.map(AggregationBucket::fromOS)
.collect(Collectors.toList()))
.build();
} else if (agg.isDterms()) {
return builder.type("dterms")
.buckets(agg.dterms().buckets().array().stream()
.map(AggregationBucket::fromOS)
.collect(Collectors.toList()))
.build();
} else if (agg.isTopHits()) {
return builder.type("top_hits")
.hits(SearchHits.from(agg.topHits().hits()))
.build();
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.dotcms.content.index.domain;

import java.util.List;
import java.util.stream.Collectors;
import java.util.Collections;
import java.util.Map;
import javax.annotation.Nullable;
import org.immutables.value.Value;

/**
Expand All @@ -10,6 +11,12 @@
* <p>Replaces direct use of {@code org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket}
* and {@code org.opensearch.client.opensearch._types.aggregations.StringTermsBucket} in
* application code.</p>
*
* <p>In addition to the fluent neutral accessors ({@link #key()} / {@link #docCount()}), this type
* exposes the legacy ES-style getters ({@link #getKey()}, {@link #getKeyAsString()},
* {@link #getKeyAsNumber()}, {@link #getDocCount()}) and the nested {@link #getAggregations()} so
* that Velocity templates written against the old Elasticsearch {@code Terms.Bucket} API keep
* working unchanged after the OpenSearch migration.</p>
*/
@Value.Immutable
public interface AggregationBucket {
Expand All @@ -20,6 +27,54 @@ public interface AggregationBucket {
/** Number of documents in this bucket. */
long docCount();

/**
* Sub-aggregations nested under this bucket (e.g. a {@code top_hits} or a nested {@code terms}).
* Empty when the bucket has no sub-aggregations.
*/
@Value.Default
default Map<String, Aggregation> subAggregations() {
return Collections.emptyMap();
}

// -------------------------------------------------------------------------
// Legacy / Velocity-friendly getters (mirror the ES Terms.Bucket API shape)
// -------------------------------------------------------------------------

default String getKey() {
return key();
}

default String getKeyAsString() {
return key();
}

/**
* The key parsed as a {@link Number}, mirroring {@code Terms.Bucket#getKeyAsNumber()}.
* Returns {@code null} when the key is not numeric (so {@code $!{bucket.getKeyAsNumber()}}
* renders empty rather than throwing).
*/
@Nullable
default Number getKeyAsNumber() {
try {
return Long.valueOf(key());
} catch (final NumberFormatException longMiss) {
try {
return Double.valueOf(key());
} catch (final NumberFormatException doubleMiss) {
return null;
}
}
}

default long getDocCount() {
return docCount();
}

/** Nested sub-aggregations, mirroring {@code Terms.Bucket#getAggregations()}. */
default Map<String, Aggregation> getAggregations() {
return subAggregations();
}

static ImmutableAggregationBucket.Builder builder() {
return ImmutableAggregationBucket.builder();
}
Expand All @@ -28,43 +83,47 @@ static ImmutableAggregationBucket.Builder builder() {
// ES factories
// -------------------------------------------------------------------------

/** Creates a bucket from an Elasticsearch terms bucket. */
/** Creates a bucket from an Elasticsearch terms bucket, including its sub-aggregations. */
static AggregationBucket from(
final org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket esBucket) {
return builder()
.key(esBucket.getKeyAsString())
.docCount(esBucket.getDocCount())
.subAggregations(Aggregation.from(esBucket.getAggregations()))
.build();
}

// -------------------------------------------------------------------------
// OS factories
// -------------------------------------------------------------------------

/** Creates a bucket from an OpenSearch string-terms bucket. */
/** Creates a bucket from an OpenSearch string-terms bucket, including its sub-aggregations. */
static AggregationBucket fromOS(
final org.opensearch.client.opensearch._types.aggregations.StringTermsBucket osBucket) {
return builder()
.key(osBucket.key())
.docCount(osBucket.docCount())
.subAggregations(Aggregation.fromOS(osBucket.aggregations()))
.build();
}

/** Creates a bucket from an OpenSearch long-terms bucket. */
/** Creates a bucket from an OpenSearch long-terms bucket, including its sub-aggregations. */
static AggregationBucket fromOS(
final org.opensearch.client.opensearch._types.aggregations.LongTermsBucket osBucket) {
return builder()
.key(String.valueOf(osBucket.key()))
.docCount(osBucket.docCount())
.subAggregations(Aggregation.fromOS(osBucket.aggregations()))
.build();
}

/** Creates a bucket from an OpenSearch double-terms bucket. */
/** Creates a bucket from an OpenSearch double-terms bucket, including its sub-aggregations. */
static AggregationBucket fromOS(
final org.opensearch.client.opensearch._types.aggregations.DoubleTermsBucket osBucket) {
return builder()
.key(String.valueOf(osBucket.key()))
.docCount(osBucket.docCount())
.subAggregations(Aggregation.fromOS(osBucket.aggregations()))
.build();
}
}
Loading
Loading