xiangfu0 commented on code in PR #20075:
URL: https://github.com/apache/pinot/pull/20075#discussion_r4406642551


##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/TextScoreTransformFunction.java:
##########
@@ -0,0 +1,145 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.pinot.core.operator.transform.function;
+
+import it.unimi.dsi.fastutil.ints.Int2FloatMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.pinot.core.operator.ColumnContext;
+import org.apache.pinot.core.operator.blocks.ValueBlock;
+import org.apache.pinot.core.operator.transform.TransformResultMetadata;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.index.reader.MultiColumnTextIndexReader;
+import org.apache.pinot.segment.spi.index.reader.TextIndexReader;
+import org.apache.pinot.segment.spi.index.reader.TextScoreStats;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+
+
+/// Implements `TEXT_SCORE(column, 'search query'[, 'options'[, 
'globalStats']])`: the relevance score of each
+/// document for a text search query, read from the column's text index 
(Lucene BM25 by default). Documents that do
+/// not match the query score `0`.
+///
+/// The query is parsed exactly like `TEXT_MATCH` with the same arguments, so
+/// `TEXT_SCORE(col, q) > 0` holds exactly for the documents `TEXT_MATCH(col, 
q)` accepts (BM25 scores of matching
+/// documents are strictly positive; constant-score queries such as wildcards 
score `1`).
+///
+/// The index is searched once per segment, on the first block, and the doc id 
to score map is reused for the
+/// remaining blocks of that segment. By default scores use per-segment term 
statistics (IDF, average field length),
+/// so they rank documents correctly within a segment but are only 
approximately comparable across segments.
+///
+/// With the optional 4th argument, the serialized [TextScoreStats] returned by
+/// `TEXT_SCORE_STATS(column, 'search query'[, 'options'])` for the same 
query, every segment scores with those
+/// corpus-wide statistics instead, so equal documents get equal scores in 
every segment (two-step search, like
+/// Elasticsearch's `dfs_query_then_fetch`). An empty string means no global 
statistics. Pass `''` as options to use
+/// the default parser with global statistics.
+///
+/// Not thread-safe: like other transform functions, an instance serves one 
segment on one thread.
+public class TextScoreTransformFunction extends BaseTransformFunction {
+  public static final String FUNCTION_NAME = "textScore";
+
+  private TextIndexReader _textIndexReader;
+  /// Set only when the column is served by a multi-column text index.
+  @Nullable
+  private String _multiColumnName;
+  private String _searchQuery;
+  @Nullable
+  private String _options;
+  @Nullable
+  private TextScoreStats _globalStats;
+  @Nullable
+  private Int2FloatMap _scores;
+
+  @Override
+  public String getName() {
+    return FUNCTION_NAME;
+  }
+
+  @Override
+  public void init(List<TransformFunction> arguments, Map<String, 
ColumnContext> columnContextMap) {
+    super.init(arguments, columnContextMap);
+    int numArguments = arguments.size();
+    if (numArguments < 2 || numArguments > 4) {
+      throw new IllegalArgumentException("Expected 2 to 4 arguments for 
TEXT_SCORE: "
+          + "TEXT_SCORE(column, 'searchQuery'[, 'options'[, 'globalStats']])");
+    }
+
+    TransformFunction columnArg = arguments.get(0);
+    if (!(columnArg instanceof IdentifierTransformFunction && 
columnArg.getResultMetadata().isSingleValue())) {
+      throw new IllegalArgumentException("The first argument of TEXT_SCORE 
must be a single-valued column");
+    }
+    String columnName = ((IdentifierTransformFunction) 
columnArg).getColumnName();
+    DataSource dataSource = columnContextMap.get(columnName).getDataSource();
+    TextIndexReader textIndexReader = dataSource != null ? 
dataSource.getTextIndex() : null;
+    if (textIndexReader == null && dataSource != null) {
+      textIndexReader = dataSource.getMultiColumnTextIndex();

Review Comment:
   **MAJOR:** On consuming segments this resolves to 
`MultiColumnRealtimeLuceneTextIndex`. That class doesn't override the new 
`getDocScores(column, ...)` / `getTextScoreStats(column, ...)`, so it falls 
through to the SPI defaults that throw `UnsupportedOperationException`. As a 
result, `TEXT_SCORE` and `TEXT_SCORE_STATS` fail on any realtime table with 
`multiColumnTextIndexConfig` whenever a consuming segment is queried.
   
   I reproduced it locally at this SHA: `getDocIds("title","usb")` returns 
`{0}`, but `getDocScores` throws *"Relevance scoring is not supported by text 
index reader: MultiColumnRealtimeLuceneTextIndex"*. Could you implement both 
methods there, mirroring `RealtimeLuceneTextIndex` (searcher pool plus 
stored-doc-id translation), and add a test? None of the new tests cover a 
multi-column consuming segment.



##########
pinot-core/src/main/java/org/apache/pinot/core/plan/AggregationPlanNode.java:
##########
@@ -89,9 +90,42 @@ public Operator<AggregationResultsBlock> run() {
       return new EmptyAggregationOperator(_queryContext, 
_indexSegment.getSegmentMetadata().getTotalDocs());
     }
 
+    Operator<AggregationResultsBlock> textScoreStatsOperator = 
buildTextScoreStatsOperator();
+    if (textScoreStatsOperator != null) {
+      return textScoreStatsOperator;
+    }
+
     return _queryContext.hasFilteredAggregations() ? 
buildFilteredAggOperator() : buildNonFilteredAggOperator();
   }
 
+  /// `TEXT_SCORE_STATS` reads corpus statistics straight from the text index, 
without scanning documents. The
+  /// statistics describe the whole segment (the filter is not applied, like 
Elasticsearch's DFS phase), so the query is

Review Comment:
   **MINOR / design:** Skipping the filter also skips filters Pinot adds on its 
own. For a hybrid table, the broker's time-boundary predicate is ignored, so 
docs in segments that straddle the boundary are counted from both the offline 
and realtime sides. For upsert tables, superseded rows (the validDocIds filter) 
still count toward `docCount`, `docFreq` and `avgdl`. If that's intended, could 
the function docs say so?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to