chrevanthreddy commented on code in PR #20107:
URL: https://github.com/apache/hudi/pull/20107#discussion_r4384291881


##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/analysis/HoodieVectorSearchPlanBuilder.scala:
##########
@@ -17,365 +17,473 @@
 
 package org.apache.spark.sql.hudi.analysis
 
+import org.apache.hudi.SparkAdapterSupport
+import org.apache.hudi.client.common.HoodieSparkEngineContext
+import org.apache.hudi.common.config.HoodieMetadataConfig
+import org.apache.hudi.common.config.TypedProperties
+import org.apache.hudi.common.index.vector.{ScoredVectorPostingMatch, 
VectorDistanceMetric, VectorIndexArbiter, VectorIndexMdtSearchUtils, 
VectorIndexMetadataCache, VectorIndexOptions, VectorPostingMatch, 
VectorStalePolicy}
+import org.apache.hudi.common.model.{FileSlice, HoodieIndexDefinition, 
HoodieRecord}
 import org.apache.hudi.common.schema.HoodieSchema
-
-import org.apache.spark.sql.{AnalysisException, DataFrame, SparkSession}
+import org.apache.hudi.common.table.{HoodieTableMetaClient, 
TableSchemaResolver}
+import org.apache.hudi.common.table.timeline.{HoodieTimeline, 
InstantComparison}
+import 
org.apache.hudi.common.table.timeline.InstantComparison.compareTimestamps
+import org.apache.hudi.common.table.view.HoodieTableFileSystemView
+import org.apache.hudi.data.HoodieJavaRDD
+import org.apache.hudi.hadoop.fs.HadoopFSUtils
+import org.apache.hudi.metadata.{HoodieTableMetadata, HoodieTableMetadataUtil}
+
+import org.apache.spark.api.java.JavaSparkContext
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.{AnalysisException, DataFrame, Row, SparkSession}
+import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.parser.ParseException
+import 
org.apache.spark.sql.catalyst.plans.logical.HoodieVectorSearchTableValuedFunction
 import 
org.apache.spark.sql.catalyst.plans.logical.HoodieVectorSearchTableValuedFunction.{DistanceMetric,
 SearchAlgorithm}
 import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
 import org.apache.spark.sql.expressions.Window
 import org.apache.spark.sql.functions.{broadcast, col, 
monotonically_increasing_id, row_number}
 import org.apache.spark.sql.hudi.command.exception.HoodieAnalysisException
-import org.apache.spark.sql.types.{ArrayType, ByteType, DataType, DoubleType, 
FloatType}
+import org.apache.spark.sql.types.{ArrayType, ByteType, DataType, DoubleType, 
FloatType, Metadata, StructField, StructType}
+import org.apache.spark.util.LongAccumulator
+import org.slf4j.LoggerFactory
 
+import scala.collection.JavaConverters._
+import scala.collection.mutable.ArrayBuffer
 import scala.util.{Failure, Success, Try}
 
 /**
- * Extension point for vector search algorithms. Each implementation provides
- * the Spark logical plan for single-query and batch-query KNN search.
- *
- * To add a new algorithm (e.g. RowMatrix, HNSW):
- *  1. Create an object extending this trait
- *  2. Add a value to [[SearchAlgorithm]]
- *  3. Register the mapping in 
[[HoodieVectorSearchPlanBuilder.resolveAlgorithm]]
+ * IVF + RaBitQ vector search backed by native metadata-table posting prefix 
lookups.
  *
- * Implementations can use the shared validation helpers on
- * [[HoodieVectorSearchPlanBuilder]] (validateEmbeddingColumn, 
validateBatchDimensions, etc.)
- * and the raw distance functions on [[VectorDistanceUtils]].
- *
- * The output schema contract:
- *  - Single-query: all corpus columns (minus the embedding column) + 
`_hudi_distance: Double`
- *  - Batch-query: all corpus columns (minus the embedding column) + clashing 
query columns
- *    (prefixed with `_hudi_query_`) + `_hudi_distance: Double` + 
`_hudi_query_index: Long`
- *  - Results are ordered by `_hudi_distance` ascending (lower = more similar)
- *  - `_hudi_query_index` is an opaque grouping identifier (not a sequential 
index). Values may be
- *    large non-contiguous numbers because they are generated by 
`monotonically_increasing_id()`.
+ * The algorithm keeps Spark SQL out of the posting scan: it probes centroids, 
converts
+ * the selected clusters to MDT posting prefixes, reads posting rows with 
HFile-friendly
+ * prefix lookups, reduces with RaBitQ approximate scores before 
record-location lookup,
+ * then exact-reranks the fetched Hudi rows.
  */
-trait VectorSearchAlgorithm {
+object IvfRaBitQMdtSearchAlgorithm extends VectorSearchAlgorithm with 
SparkAdapterSupport {
+  private val APPROXIMATE_CANDIDATE_FLOOR = 32
 
-  /** Human-readable name for error messages and logging. */
-  def name: String
+  private[analysis] def approximateCandidateHeapSize(k: Int): Int =
+    math.max(k, APPROXIMATE_CANDIDATE_FLOOR)
 
-  /**
-   * Build a plan that finds the k nearest corpus rows to a single query 
vector.
-   *
-   * @param spark          active SparkSession
-   * @param corpusDf       resolved corpus DataFrame (may be Hudi, Parquet, or 
temp view)
-   * @param embeddingCol   name of the array-typed embedding column in corpusDf
-   * @param queryVector    the query vector, normalized to Array[Double]
-   * @param k              number of nearest neighbors to return
-   * @param metric         distance metric (COSINE, L2, DOT_PRODUCT)
-   * @param filter         optional SQL predicate applied to the corpus before 
distance computation,
-   *                       String literals inside the predicate must use 
double quotes (e.g. {@code label = "z-axis"})
-   *                       because the filter is passed as a single-quoted TVF 
argument and
-   *                       Spark's lexer does not support {@code ''} as an 
escape sequence
-   *                       inside string literals.
-   * @param maxDistance    optional distance threshold; results exceeding this 
value are excluded
-   * @return an analyzed LogicalPlan whose output matches the single-query 
schema contract
-   */
-  def buildSingleQueryPlan(
-      spark: SparkSession,
-      corpusDf: DataFrame,
-      embeddingCol: String,
-      queryVector: Array[Double],
-      k: Int,
-      metric: DistanceMetric.Value,
-      filter: Option[String] = None,
-      maxDistance: Option[Double] = None): LogicalPlan
-
-  /**
-   * Build a plan that finds the k nearest corpus rows for each row in the 
query table.
-   *
-   * @param spark              active SparkSession
-   * @param corpusDf           resolved corpus DataFrame
-   * @param corpusEmbeddingCol name of the embedding column in corpusDf
-   * @param queryDf            resolved query DataFrame
-   * @param queryEmbeddingCol  name of the embedding column in queryDf
-   * @param k                  number of nearest neighbors per query
-   * @param metric             distance metric (COSINE, L2, DOT_PRODUCT)
-   * @param filter             optional SQL predicate applied to the corpus 
before distance
-   *                           computation, and shrinking cross-join 
cardinality. Applied to the
-   *                           corpus only — to restrict the query side, apply 
a projection or
-   *                           filter on the query table before invoking the 
TVF. See
-   *                           [[buildSingleQueryPlan]] for quoting 
requirements.
-   * @param maxDistance        optional distance threshold; results exceeding 
this value are
-   *                           excluded before per-query top-K selection, 
reducing shuffle volume.
-   * @return an analyzed LogicalPlan whose output matches the batch-query 
schema contract
-   * @note Batch mode broadcasts the query table to all executors via a 
cross-join.
-   *       This is designed for small-to-medium query sets (tens to low 
hundreds of rows).
-   *       For large query tables, memory pressure on executors may occur.
-   */
-  def buildBatchQueryPlan(
-      spark: SparkSession,
-      corpusDf: DataFrame,
-      corpusEmbeddingCol: String,
-      queryDf: DataFrame,
-      queryEmbeddingCol: String,
-      k: Int,
-      metric: DistanceMetric.Value,
-      filter: Option[String] = None,
-      maxDistance: Option[Double] = None): LogicalPlan
-}
+  import HoodieVectorSearchPlanBuilder._
 
-/**
- * Resolves [[SearchAlgorithm]] values to [[VectorSearchAlgorithm]] 
implementations
- * and provides shared validation helpers used across algorithms.
- */
-object HoodieVectorSearchPlanBuilder {
-
-  val DISTANCE_COL = "_hudi_distance"
-  private[analysis] val QUERY_ID_COL = "_hudi_query_index"
-  private[analysis] val QUERY_EMB_ALIAS = "_hudi_query_emb"
-  private[analysis] val RANK_COL = "_hudi_rank"
-  private[analysis] val QUERY_COL_PREFIX = "_hudi_query_"
-
-  /** Resolve a [[SearchAlgorithm]] enum value to its implementation. */
-  def resolveAlgorithm(algorithm: SearchAlgorithm.Value): 
VectorSearchAlgorithm = algorithm match {
-    case SearchAlgorithm.BRUTE_FORCE => BruteForceSearchAlgorithm
-    case other => throw new HoodieAnalysisException(
-      s"Unsupported search algorithm: $other")
+  private[analysis] def isLogResidentCandidate(
+      candidate: ScoredVectorPostingMatch,
+      currentBaseInstant: String,
+      sliceHasLogs: Boolean): Boolean = {
+    // A negative position is the authoritative log-resident signal: base-file 
records always carry a
+    // non-negative row position, so a candidate with position < 0 in a slice 
that still has log files
+    // lives in a log block and cannot be materialized from the base Parquet. 
The candidate's instant
+    // is NOT a reliable discriminator here -- a log delta can share the 
slice's base instant time.
+    val location = candidate.getLocation
+    sliceHasLogs && location != null && location.getPosition < 0
   }
 
-  private[analysis] def validateEmbeddingColumn(df: DataFrame, colName: 
String): Unit = {
-    val fieldOpt = df.schema.fields.find(_.name.equalsIgnoreCase(colName))
-    val field = fieldOpt.getOrElse(
-      throw new HoodieAnalysisException(
-        s"Embedding column '$colName' not found in table schema. " +
-          s"Available columns: ${df.schema.fieldNames.mkString(", ")}"))
-    field.dataType match {
-      case ArrayType(FloatType, _) | ArrayType(DoubleType, _) | 
ArrayType(ByteType, _) => // valid
-      case other =>
-        throw new HoodieAnalysisException(
-          s"Embedding column '$colName' has type $other, " +
-            "expected array<float>, array<double>, or array<byte>")
+  private[analysis] def approximateArbitratedCandidate(
+      candidate: ScoredVectorPostingMatch): Option[ScoredVectorPostingMatch] =
+    candidate.getArbiterDecision match {
+      case VectorIndexArbiter.Decision.SERVE => Some(candidate)
+      // A delta carries the updated vector code. Refreshing only its locator 
is safe and preserves
+      // the new score; a stale packed-block row carries old code and must 
never be resurrected.
+      case VectorIndexArbiter.Decision.STALE if candidate.isDelta => 
Some(candidate)
+      case _ => None
     }
+
+  private[analysis] case class FreshnessLag(firstUnmarkedInstant: String, 
lagCount: Int)
+
+  private[analysis] def completedSourceWriteTimeline(timeline: 
HoodieTimeline): HoodieTimeline =
+    timeline.getWriteTimeline.filterCompletedInstants()
+
+  private[analysis] def freshnessLag(
+      completedWriteInstants: Seq[String],
+      coveredInstant: Option[String]): Option[FreshnessLag] = {
+    val uncovered = completedWriteInstants.filter(instant => 
coveredInstant.forall(covered =>

Review Comment:
   **Fixed (bootstrap)** in `41bdd71c5b`; maintenance follow-up: 
https://github.com/chrevanthreddy/hudi/pull/5. Bootstrap uses the contiguous 
completed-write cutoff (`getContiguousCompletedWriteTimeline`), consistent with 
async index scheduling, rather than treating an inflight write as covered. The 
inflight-writer regression verifies the baseline excludes writer A, A emits its 
marker when it commits, and exact search includes both A and B.
   
   Update (2026-10-27): the maintenance follow-up no longer advances a 
per-write manifest frontier or maintains coverage barriers. Normal writes 
maintain postings and an audit marker together; strict read validation is 
opt-in/default off, with WARN the default policy in both modes. This does not 
remove the existing async-index bootstrap/catch-up lifecycle.
   
   Fixture ordering, resequenced in the maintenance follow-up: observe inflight 
partition marking in table config, wait for bootstrap-built state and the 
rewrite backup to disappear, then release A. Catch-up keeps final index 
completion behind A's commit. The test remains enabled with its original 
coverage assertions; all six production E2Es passed with retries disabled on 
Spark 3.5 and Spark 4.0 at `4097189466`. No config-reader production changes 
are included.
   



-- 
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]

Reply via email to