@@ -677,11 +677,78 @@ public void testTrailingCommaInVectorRejected() throws IOException {
677677 assertThat (ex .getMessage (), containsString ("trailing or consecutive commas" ));
678678 }
679679
680+ // ── Alias with multiple backing indices ───────────────────────────────
681+ // vectorSearch() accepts an alias as `table=`. When the alias points at multiple backing
682+ // indices, planning must accept the alias string instead of treating it as a wildcard or
683+ // multi-target. Execution correctness over compatible knn_vector mappings is a separate
684+ // concern covered by k-NN-enabled tests/follow-up; these tests lock in planning acceptance
685+ // only, via _explain on the default no-kNN cluster.
686+
687+ @ Test
688+ public void testExplainOverAliasWithMultipleBackingIndices () throws IOException {
689+ // Create two indices with identical keyword mappings (no knn_vector, since the plugin is
690+ // not installed) and a shared alias. We only assert the planner accepts the alias; whether
691+ // k-NN accepts the alias at execution is a separate concern tested on a k-NN-enabled
692+ // cluster.
693+ // Randomized names so a stale alias/index left by an aborted prior run of this class does
694+ // not shadow a fresh setup — a concrete risk on local reruns.
695+ String suffix = java .util .UUID .randomUUID ().toString ().replace ("-" , "" ).substring (0 , 8 );
696+ String idx1 = "vector_alias_backing_1_" + suffix ;
697+ String idx2 = "vector_alias_backing_2_" + suffix ;
698+ String alias = "vector_alias_combined_" + suffix ;
699+ try {
700+ createSimpleIndex (idx1 );
701+ createSimpleIndex (idx2 );
702+ addToAlias (idx1 , alias );
703+ addToAlias (idx2 , alias );
704+
705+ String explain =
706+ explainQuery (
707+ "SELECT v._id FROM vectorSearch(table='"
708+ + alias
709+ + "', field='embedding', vector='[1.0, 2.0]', option='k=5') AS v" );
710+
711+ assertThat (explain , containsString ("VectorSearchIndexScan" ));
712+ assertThat (explain , containsString (alias ));
713+ } finally {
714+ // Deleting the backing indices removes the alias automatically, but delete the alias
715+ // first for robustness against partial setup failures.
716+ deleteAliasIfExists (alias );
717+ deleteIndexIfExists (idx1 );
718+ deleteIndexIfExists (idx2 );
719+ }
720+ }
721+
722+ private void createSimpleIndex (String indexName ) throws IOException {
723+ Request create = new Request ("PUT" , "/" + indexName );
724+ create .setJsonEntity ("{\" mappings\" :{\" properties\" :{\" state\" :{\" type\" :\" keyword\" }}}}" );
725+ client ().performRequest (create );
726+ }
727+
728+ private void addToAlias (String indexName , String aliasName ) throws IOException {
729+ Request req = new Request ("POST" , "/_aliases" );
730+ req .setJsonEntity (
731+ "{\" actions\" :[{\" add\" :{\" index\" :\" "
732+ + indexName
733+ + "\" ,\" alias\" :\" "
734+ + aliasName
735+ + "\" }}]}" );
736+ client ().performRequest (req );
737+ }
738+
680739 private void deleteIndexIfExists (String indexName ) {
681740 try {
682741 client ().performRequest (new Request ("DELETE" , "/" + indexName ));
683742 } catch (IOException ignored ) {
684743 // Index does not exist, which is fine.
685744 }
686745 }
746+
747+ private void deleteAliasIfExists (String aliasName ) {
748+ try {
749+ client ().performRequest (new Request ("DELETE" , "/_all/_alias/" + aliasName ));
750+ } catch (IOException ignored ) {
751+ // Alias does not exist, which is fine.
752+ }
753+ }
687754}
0 commit comments