Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased 3.x]
### Added
- Add getWrappedScorer method to ProfileScorer for plugin access to wrapped scorers ([#20548](https://github.com/opensearch-project/OpenSearch/issues/20548))
- Add ProfilingWrapper interface for plugin access to delegates in profiling decorators ([#20607](https://github.com/opensearch-project/OpenSearch/pull/20607))
- Support expected cluster name with validation in CCS Sniff mode ([#20532](https://github.com/opensearch-project/OpenSearch/pull/20532))
- Choose the best performing node when writing with append-only index ([#20065](https://github.com/opensearch-project/OpenSearch/pull/20065))
- Add security policy to allow `accessUnixDomainSocket` in `transport-grpc` module ([#20463](https://github.com/opensearch-project/OpenSearch/pull/20463))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
*
* @opensearch.internal
*/
final class ProfileScorer extends Scorer {
final class ProfileScorer extends Scorer implements ProfilingWrapper<Scorer> {

private final Scorer scorer;

Expand Down Expand Up @@ -81,7 +81,8 @@ final class ProfileScorer extends Scorer {
* @return the underlying wrapped scorer
* @see ProfileCollector#getDelegate()
*/
public Scorer getWrappedScorer() {
@Override
public Scorer getDelegate() {
return scorer;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.search.profile.query;

import org.opensearch.common.annotation.PublicApi;

/**
* Generic interface for profiling wrappers that decorate search components with timing instrumentation
* while preserving access to the original (delegate) component.
* <p>
* When search profiling is enabled, OpenSearch wraps scorers, collectors, and other components in
* profiling decorators that are package-private. This interface provides a public contract for plugins
* to detect and unwrap such wrappers, accessing the underlying component without requiring reflection
* or knowledge of package-private implementation classes.
* <p>
* Example usage in a plugin to unwrap a profiled scorer:
* <pre>{@code
* if (scorer instanceof ProfilingWrapper) {
* Scorer delegate = ((ProfilingWrapper<Scorer>) scorer).getDelegate();
* // access custom scorer methods on the delegate
* }
* }</pre>
*
* @param <T> the type of the wrapped component (e.g., {@link org.apache.lucene.search.Scorer},
* {@link org.apache.lucene.search.Collector})
* @opensearch.api
*/
@PublicApi(since = "3.6.0")
public interface ProfilingWrapper<T> {

/**
* Returns the underlying delegate component that this profiling wrapper decorates.
*
* @return the original component that was wrapped for profiling
*/
T getDelegate();
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorable;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Weight;
import org.opensearch.search.profile.ProfileMetricUtil;
import org.opensearch.test.OpenSearchTestCase;

import java.io.IOException;
import java.util.Collection;

public class ProfileScorerTests extends OpenSearchTestCase {

Expand Down Expand Up @@ -102,14 +104,71 @@ public void testPropagateMaxScore() throws IOException {
assertEquals(42f, profileScorer.getMaxScore(DocIdSetIterator.NO_MORE_DOCS), 0f);
}

public void testGetWrappedScorer() throws IOException {
public void testGetDelegate() throws IOException {
Query query = new MatchAllDocsQuery();
Weight weight = query.createWeight(new IndexSearcher(new MultiReader()), ScoreMode.TOP_SCORES, 1f);
FakeScorer fakeScorer = new FakeScorer(weight);
QueryProfileBreakdown profile = new QueryProfileBreakdown(ProfileMetricUtil.getDefaultQueryProfileMetrics());
ProfileScorer profileScorer = new ProfileScorer(fakeScorer, profile);

// Verify getWrappedScorer returns the original scorer
assertSame(fakeScorer, profileScorer.getWrappedScorer());
// Verify getDelegate returns the original scorer
assertSame(fakeScorer, profileScorer.getDelegate());
}

@SuppressWarnings("unchecked")
public void testImplementsProfilingWrapper() throws IOException {
Query query = new MatchAllDocsQuery();
Weight weight = query.createWeight(new IndexSearcher(new MultiReader()), ScoreMode.TOP_SCORES, 1f);
FakeScorer fakeScorer = new FakeScorer(weight);
QueryProfileBreakdown profile = new QueryProfileBreakdown(ProfileMetricUtil.getDefaultQueryProfileMetrics());
ProfileScorer profileScorer = new ProfileScorer(fakeScorer, profile);

// ProfileScorer implements ProfilingWrapper<Scorer>, allowing plugins to detect and unwrap
// profiling wrappers using instanceof without reflection
assertTrue("ProfileScorer should implement ProfilingWrapper", profileScorer instanceof ProfilingWrapper);
ProfilingWrapper<Scorer> wrapper = (ProfilingWrapper<Scorer>) profileScorer;
assertSame("ProfilingWrapper.getDelegate() should return the original scorer", fakeScorer, wrapper.getDelegate());
}

@SuppressWarnings("unchecked")
public void testUnwrapNestedProfilingScorerFromScorableReference() throws IOException {
// Simulate nested profiling: a scorer wrapped in two ProfileScorer layers.
// This tests that a plugin can iteratively unwrap through multiple profiling
// layers via a generic Scorable reference (as received in LeafCollector.setScorer())
Query query = new MatchAllDocsQuery();
Weight weight = query.createWeight(new IndexSearcher(new MultiReader()), ScoreMode.TOP_SCORES, 1f);
FakeScorer fakeScorer = new FakeScorer(weight);
QueryProfileBreakdown innerProfile = new QueryProfileBreakdown(ProfileMetricUtil.getDefaultQueryProfileMetrics());
QueryProfileBreakdown outerProfile = new QueryProfileBreakdown(ProfileMetricUtil.getDefaultQueryProfileMetrics());

// Create nested profiling: FakeScorer -> innerProfileScorer -> outerProfileScorer
ProfileScorer innerProfileScorer = new ProfileScorer(fakeScorer, innerProfile);
ProfileScorer outerProfileScorer = new ProfileScorer(innerProfileScorer, outerProfile);

// Plugin only sees a Scorable reference
Scorable scorable = outerProfileScorer;

// Unwrap first layer
assertTrue("Outer Scorable should be detected as ProfilingWrapper", scorable instanceof ProfilingWrapper);
Scorer firstUnwrap = ((ProfilingWrapper<Scorer>) scorable).getDelegate();
assertNotSame("First unwrap should not be the original scorer", fakeScorer, firstUnwrap);
assertTrue("First unwrap should still be a ProfilingWrapper (inner ProfileScorer)", firstUnwrap instanceof ProfilingWrapper);

// Unwrap second layer to reach the original scorer
Scorer secondUnwrap = ((ProfilingWrapper<Scorer>) firstUnwrap).getDelegate();
assertSame("Second unwrap should be the original FakeScorer", fakeScorer, secondUnwrap);
assertFalse("Original scorer should not implement ProfilingWrapper", secondUnwrap instanceof ProfilingWrapper);
}

public void testGetChildren_delegatesToWrappedScorer() throws IOException {
Query query = new MatchAllDocsQuery();
Weight weight = query.createWeight(new IndexSearcher(new MultiReader()), ScoreMode.TOP_SCORES, 1f);
FakeScorer fakeScorer = new FakeScorer(weight);
QueryProfileBreakdown profile = new QueryProfileBreakdown(ProfileMetricUtil.getDefaultQueryProfileMetrics());
ProfileScorer profileScorer = new ProfileScorer(fakeScorer, profile);

// getChildren() should delegate to the wrapped scorer's getChildren()
Collection<Scorer.ChildScorable> children = profileScorer.getChildren();
assertEquals("FakeScorer has no children, so ProfileScorer should return empty collection", 0, children.size());
}
Comment thread
owaiskazi19 marked this conversation as resolved.
}
Loading