fix: expose public FTS paths in remote index listings (#4194)

Remote `list_indices()` exposes physical FTS paths such as
`docs.item.content`, while index creation, queries, and native table
listings use `docs.content`. Normalize FTS columns to the public path
after parsing the server response, using the same field-ID-based
conversion as native tables.

Keep the physical paths on the wire: existing clients need them to
resolve the Arrow schema. Cover both legacy responses that fetch index
statistics and enriched responses that already include the index type.
This commit is contained in:
Xuanwo
2026-09-16 15:08:56 +08:00
committed by GitHub
parent 3b37ea2c7a
commit 34ab278a5b
+52 -3
View File
@@ -43,8 +43,8 @@ use crate::table::{
use crate::table::{AnyQuery, Filter, Predicate, PreprocessingOutput, TableStatistics};
use crate::utils::background_cache::BackgroundCache;
use crate::utils::{
MaxBatchLengthStream, TimeoutStream, resolve_arrow_field_path, resolve_arrow_fts_field_path,
supported_btree_data_type, supported_vector_data_type,
MaxBatchLengthStream, TimeoutStream, public_fts_field_path_by_id, resolve_arrow_field_path,
resolve_arrow_fts_field_path, supported_btree_data_type, supported_vector_data_type,
};
use crate::{DistanceType, Error};
use crate::{
@@ -2042,7 +2042,19 @@ impl<S: HttpSend + 'static> RemoteTable<S> {
}
let results = futures::future::try_join_all(futures).await?;
Ok(results.into_iter().flatten().collect())
let mut indices: Vec<IndexConfig> = results.into_iter().flatten().collect();
let lance_schema = lance_core::datatypes::Schema::try_from(schema.as_ref())?;
for index in &mut indices {
if index.index_type == IndexType::FTS {
// The wire format uses physical paths for schema resolution. Match
// native tables by exposing list-transparent paths to callers.
for column in &mut index.columns {
let field_id = lance_schema.field_id(column)?;
*column = public_fts_field_path_by_id(&lance_schema, field_id)?;
}
}
}
Ok(indices)
}
}
@@ -6984,6 +6996,43 @@ mod tests {
assert_eq!(indices, expected);
}
#[rstest]
#[case::legacy(false)]
#[case::enriched(true)]
#[tokio::test]
async fn test_list_indices_fts_public_list_path(#[case] enriched: bool) {
let schema = nested_index_schema();
let table = Table::new_with_handler("my_table", move |request| {
let body = match request.url().path() {
"/v1/table/my_table/describe/" => describe_response(&schema),
"/v1/table/my_table/index/list/" => serde_json::json!({
"indexes": [{
"index_name": "docs_idx",
"columns": ["docs.item.content"],
"index_type": enriched.then_some("FTS"),
}],
})
.to_string(),
"/v1/table/my_table/index/docs_idx/stats/" => {
assert!(!enriched, "enriched responses must not fetch index stats");
serde_json::json!({
"num_indexed_rows": 1,
"num_unindexed_rows": 0,
"index_type": "FTS",
})
.to_string()
}
path => panic!("Unexpected path: {path}"),
};
http::Response::builder().status(200).body(body).unwrap()
});
let indices = table.list_indices().await.unwrap();
assert_eq!(indices.len(), 1);
assert_eq!(indices[0].index_type, IndexType::FTS);
assert_eq!(indices[0].columns, vec!["docs.content"]);
}
#[tokio::test]
async fn test_list_indices_nested_field_paths() {
let schema = nested_index_schema();