Skip to content

Commit d0897a2

Browse files
ninjaAB-5joehan
andauthored
Fix the __name__ normalization for vector index (#9407)
* fix the __name__ normalization for vector index * update CHANGELOG * fix lint of CHANGELOG --------- Co-authored-by: Joe Hanley <joehanley@google.com>
1 parent 6dd9a2c commit d0897a2

File tree

3 files changed

+342
-4
lines changed

3 files changed

+342
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
- Fix the `__name__` normalization of vector indexes for Firestore standard
2+
edition databases.
13
- Fixed an issue where the emulator would fail to start when using `firebase-functions` v7+ (#9401).
24
- Added `functions.list_functions` as a MCP tool (#9369)
35
- Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185)

src/firestore/api.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,27 @@ export class FirestoreApi {
3030
*/
3131
public static processIndex(index: Spec.Index): Spec.Index {
3232
// Per https://firebase.google.com/docs/firestore/query-data/index-overview#default_ordering_and_the_name_field
33-
// this matches the direction of the last non-name field in the index.
34-
const fields = index.fields;
33+
// this matches the direction of the last non-name non-vector field in the index.
34+
let fields = index.fields;
35+
const suffixOrder: types.Order = FirestoreApi.lastIndexFieldOrder(fields);
36+
const nameSuffix = { fieldPath: "__name__", order: suffixOrder } as types.IndexField;
37+
3538
const lastField = index.fields?.[index.fields.length - 1];
39+
if (lastField.vectorConfig) {
40+
// lastField is vector field, refer to the second from last field
41+
const vectorField = lastField;
42+
fields = fields.slice(0, -1);
43+
44+
if (fields.length === 0 || fields?.[fields.length - 1].fieldPath !== "__name__") {
45+
fields.push(nameSuffix);
46+
}
47+
fields.push(vectorField);
48+
return {
49+
...index,
50+
fields,
51+
};
52+
}
3653
if (lastField?.fieldPath !== "__name__") {
37-
const defaultDirection = index.fields?.[index.fields.length - 1]?.order;
38-
const nameSuffix = { fieldPath: "__name__", order: defaultDirection } as types.IndexField;
3954
fields.push(nameSuffix);
4055
}
4156
return {
@@ -44,6 +59,16 @@ export class FirestoreApi {
4459
};
4560
}
4661

62+
public static lastIndexFieldOrder(fields: types.IndexField[]): types.Order {
63+
let lastIndexFieldOrder: types.Order = types.Order.ASCENDING;
64+
for (const field of fields) {
65+
if (field.order) {
66+
lastIndexFieldOrder = field.order;
67+
}
68+
}
69+
return lastIndexFieldOrder;
70+
}
71+
4772
/**
4873
* Deploy an index specification to the specified project.
4974
* @param options the CLI options.

src/firestore/indexes.spec.ts

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,78 @@ describe("Normalize __name__ field for database indexes", () => {
909909
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
910910
});
911911

912+
it("No-op if exists __name__ field as the last field with default sort order, with array contains", () => {
913+
const mockSpecIndex = {
914+
collectionGroup: "collection",
915+
queryScope: "COLLECTION",
916+
fields: [
917+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
918+
{ fieldPath: "__name__", order: API.Order.ASCENDING },
919+
],
920+
} as Spec.Index;
921+
922+
const result = FirestoreApi.processIndex(mockSpecIndex);
923+
924+
expect(result.fields).to.have.length(2);
925+
expect(result.fields[0].fieldPath).to.equal("arr");
926+
expect(result.fields[1].fieldPath).to.equal("__name__");
927+
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
928+
});
929+
930+
it("No-op if exists __name__ field as the last field with default sort order, with vector field", () => {
931+
const mockSpecIndex = {
932+
collectionGroup: "collection",
933+
queryScope: "COLLECTION",
934+
fields: [
935+
{ fieldPath: "foo", order: API.Order.ASCENDING },
936+
{ fieldPath: "__name__", order: API.Order.ASCENDING },
937+
{
938+
fieldPath: "vector",
939+
vectorConfig: {
940+
dimension: 100,
941+
flat: {},
942+
},
943+
},
944+
],
945+
} as Spec.Index;
946+
947+
const result = FirestoreApi.processIndex(mockSpecIndex);
948+
949+
expect(result.fields).to.have.length(3);
950+
expect(result.fields[0].fieldPath).to.equal("foo");
951+
expect(result.fields[1].fieldPath).to.equal("__name__");
952+
expect(result.fields[2].fieldPath).to.equal("vector");
953+
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
954+
});
955+
956+
it("No-op if exists __name__ field as the last field with default sort order, with array contains and vector field", () => {
957+
const mockSpecIndex = {
958+
collectionGroup: "collection",
959+
queryScope: "COLLECTION",
960+
fields: [
961+
{ fieldPath: "foo", order: API.Order.ASCENDING },
962+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
963+
{ fieldPath: "__name__", order: API.Order.ASCENDING },
964+
{
965+
fieldPath: "vector",
966+
vectorConfig: {
967+
dimension: 100,
968+
flat: {},
969+
},
970+
},
971+
],
972+
} as Spec.Index;
973+
974+
const result = FirestoreApi.processIndex(mockSpecIndex);
975+
976+
expect(result.fields).to.have.length(4);
977+
expect(result.fields[0].fieldPath).to.equal("foo");
978+
expect(result.fields[1].fieldPath).to.equal("arr");
979+
expect(result.fields[2].fieldPath).to.equal("__name__");
980+
expect(result.fields[3].fieldPath).to.equal("vector");
981+
expect(result.fields[2].order).to.equal(API.Order.ASCENDING);
982+
});
983+
912984
it("No-op if exists __name__ field as the last field with non-default sort order", () => {
913985
const mockSpecIndex = {
914986
collectionGroup: "collection",
@@ -927,6 +999,78 @@ describe("Normalize __name__ field for database indexes", () => {
927999
expect(result.fields[1].order).to.equal(API.Order.DESCENDING);
9281000
});
9291001

1002+
it("No-op if exists __name__ field as the last field with default sort order, with array contains", () => {
1003+
const mockSpecIndex = {
1004+
collectionGroup: "collection",
1005+
queryScope: "COLLECTION",
1006+
fields: [
1007+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1008+
{ fieldPath: "__name__", order: API.Order.ASCENDING },
1009+
],
1010+
} as Spec.Index;
1011+
1012+
const result = FirestoreApi.processIndex(mockSpecIndex);
1013+
1014+
expect(result.fields).to.have.length(2);
1015+
expect(result.fields[0].fieldPath).to.equal("arr");
1016+
expect(result.fields[1].fieldPath).to.equal("__name__");
1017+
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
1018+
});
1019+
1020+
it("No-op if exists __name__ field as the last field with default sort order, with vector field", () => {
1021+
const mockSpecIndex = {
1022+
collectionGroup: "collection",
1023+
queryScope: "COLLECTION",
1024+
fields: [
1025+
{ fieldPath: "foo", order: API.Order.ASCENDING },
1026+
{ fieldPath: "__name__", order: API.Order.DESCENDING },
1027+
{
1028+
fieldPath: "vector",
1029+
vectorConfig: {
1030+
dimension: 100,
1031+
flat: {},
1032+
},
1033+
},
1034+
],
1035+
} as Spec.Index;
1036+
1037+
const result = FirestoreApi.processIndex(mockSpecIndex);
1038+
1039+
expect(result.fields).to.have.length(3);
1040+
expect(result.fields[0].fieldPath).to.equal("foo");
1041+
expect(result.fields[1].fieldPath).to.equal("__name__");
1042+
expect(result.fields[2].fieldPath).to.equal("vector");
1043+
expect(result.fields[1].order).to.equal(API.Order.DESCENDING);
1044+
});
1045+
1046+
it("No-op if exists __name__ field as the last field with default sort order, with array contains and vector field", () => {
1047+
const mockSpecIndex = {
1048+
collectionGroup: "collection",
1049+
queryScope: "COLLECTION",
1050+
fields: [
1051+
{ fieldPath: "foo", order: API.Order.ASCENDING },
1052+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1053+
{ fieldPath: "__name__", order: API.Order.DESCENDING },
1054+
{
1055+
fieldPath: "vector",
1056+
vectorConfig: {
1057+
dimension: 100,
1058+
flat: {},
1059+
},
1060+
},
1061+
],
1062+
} as Spec.Index;
1063+
1064+
const result = FirestoreApi.processIndex(mockSpecIndex);
1065+
1066+
expect(result.fields).to.have.length(4);
1067+
expect(result.fields[0].fieldPath).to.equal("foo");
1068+
expect(result.fields[1].fieldPath).to.equal("arr");
1069+
expect(result.fields[2].fieldPath).to.equal("__name__");
1070+
expect(result.fields[3].fieldPath).to.equal("vector");
1071+
expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
1072+
});
1073+
9301074
it("should attach __name__ suffix with the default order if not exists, ascending", () => {
9311075
const mockSpecIndex = {
9321076
collectionGroup: "collection",
@@ -942,6 +1086,102 @@ describe("Normalize __name__ field for database indexes", () => {
9421086
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
9431087
});
9441088

1089+
it("should attach __name__ suffix with the default order if not exists, ascending, with array contains", () => {
1090+
const mockSpecIndex = {
1091+
collectionGroup: "collection",
1092+
queryScope: "COLLECTION",
1093+
fields: [
1094+
{ fieldPath: "foo", order: API.Order.ASCENDING },
1095+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1096+
],
1097+
} as Spec.Index;
1098+
1099+
const result = FirestoreApi.processIndex(mockSpecIndex);
1100+
1101+
expect(result.fields).to.have.length(3);
1102+
expect(result.fields[0].fieldPath).to.equal("foo");
1103+
expect(result.fields[1].fieldPath).to.equal("arr");
1104+
expect(result.fields[2].fieldPath).to.equal("__name__");
1105+
expect(result.fields[2].order).to.equal(API.Order.ASCENDING);
1106+
});
1107+
1108+
it("should attach __name__ suffix with the default order if not exists, ascending, with vector field", () => {
1109+
const mockSpecIndex = {
1110+
collectionGroup: "collection",
1111+
queryScope: "COLLECTION",
1112+
fields: [
1113+
{ fieldPath: "foo", order: API.Order.ASCENDING },
1114+
{
1115+
fieldPath: "vector",
1116+
vectorConfig: {
1117+
dimension: 100,
1118+
flat: {},
1119+
},
1120+
},
1121+
],
1122+
} as Spec.Index;
1123+
1124+
const result = FirestoreApi.processIndex(mockSpecIndex);
1125+
1126+
expect(result.fields).to.have.length(3);
1127+
expect(result.fields[0].fieldPath).to.equal("foo");
1128+
expect(result.fields[1].fieldPath).to.equal("__name__");
1129+
expect(result.fields[2].fieldPath).to.equal("vector");
1130+
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
1131+
});
1132+
1133+
it("should attach __name__ suffix with the default order if not exists, with array contains and vector field, default to ascending", () => {
1134+
const mockSpecIndex = {
1135+
collectionGroup: "collection",
1136+
queryScope: "COLLECTION",
1137+
fields: [
1138+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1139+
{
1140+
fieldPath: "vector",
1141+
vectorConfig: {
1142+
dimension: 100,
1143+
flat: {},
1144+
},
1145+
},
1146+
],
1147+
} as Spec.Index;
1148+
1149+
const result = FirestoreApi.processIndex(mockSpecIndex);
1150+
1151+
expect(result.fields).to.have.length(3);
1152+
expect(result.fields[0].fieldPath).to.equal("arr");
1153+
expect(result.fields[1].fieldPath).to.equal("__name__");
1154+
expect(result.fields[2].fieldPath).to.equal("vector");
1155+
expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
1156+
});
1157+
1158+
it("should attach __name__ suffix with the default order if not exists, with index field with ascending order, array contains and vector field", () => {
1159+
const mockSpecIndex = {
1160+
collectionGroup: "collection",
1161+
queryScope: "COLLECTION",
1162+
fields: [
1163+
{ fieldPath: "foo", order: API.Order.ASCENDING },
1164+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1165+
{
1166+
fieldPath: "vector",
1167+
vectorConfig: {
1168+
dimension: 100,
1169+
flat: {},
1170+
},
1171+
},
1172+
],
1173+
} as Spec.Index;
1174+
1175+
const result = FirestoreApi.processIndex(mockSpecIndex);
1176+
1177+
expect(result.fields).to.have.length(4);
1178+
expect(result.fields[0].fieldPath).to.equal("foo");
1179+
expect(result.fields[1].fieldPath).to.equal("arr");
1180+
expect(result.fields[2].fieldPath).to.equal("__name__");
1181+
expect(result.fields[3].fieldPath).to.equal("vector");
1182+
expect(result.fields[2].order).to.equal(API.Order.ASCENDING);
1183+
});
1184+
9451185
it("should attach __name__ suffix with the default order if not exists, descending", () => {
9461186
const mockSpecIndex = {
9471187
collectionGroup: "collection",
@@ -960,6 +1200,77 @@ describe("Normalize __name__ field for database indexes", () => {
9601200
expect(result.fields[2].fieldPath).to.equal("__name__");
9611201
expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
9621202
});
1203+
1204+
it("should attach __name__ suffix with the default order if not exists, descending, with array contains", () => {
1205+
const mockSpecIndex = {
1206+
collectionGroup: "collection",
1207+
queryScope: "COLLECTION",
1208+
fields: [
1209+
{ fieldPath: "foo", order: API.Order.DESCENDING },
1210+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1211+
],
1212+
} as Spec.Index;
1213+
1214+
const result = FirestoreApi.processIndex(mockSpecIndex);
1215+
1216+
expect(result.fields).to.have.length(3);
1217+
expect(result.fields[0].fieldPath).to.equal("foo");
1218+
expect(result.fields[1].fieldPath).to.equal("arr");
1219+
expect(result.fields[2].fieldPath).to.equal("__name__");
1220+
expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
1221+
});
1222+
1223+
it("should attach __name__ suffix with the default order if not exists, ascending, with vector field", () => {
1224+
const mockSpecIndex = {
1225+
collectionGroup: "collection",
1226+
queryScope: "COLLECTION",
1227+
fields: [
1228+
{ fieldPath: "foo", order: API.Order.DESCENDING },
1229+
{
1230+
fieldPath: "vector",
1231+
vectorConfig: {
1232+
dimension: 100,
1233+
flat: {},
1234+
},
1235+
},
1236+
],
1237+
} as Spec.Index;
1238+
1239+
const result = FirestoreApi.processIndex(mockSpecIndex);
1240+
1241+
expect(result.fields).to.have.length(3);
1242+
expect(result.fields[0].fieldPath).to.equal("foo");
1243+
expect(result.fields[1].fieldPath).to.equal("__name__");
1244+
expect(result.fields[2].fieldPath).to.equal("vector");
1245+
expect(result.fields[1].order).to.equal(API.Order.DESCENDING);
1246+
});
1247+
1248+
it("should attach __name__ suffix with the default order if not exists, with index field with descending order, array contains and vector field", () => {
1249+
const mockSpecIndex = {
1250+
collectionGroup: "collection",
1251+
queryScope: "COLLECTION",
1252+
fields: [
1253+
{ fieldPath: "foo", order: API.Order.DESCENDING },
1254+
{ fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
1255+
{
1256+
fieldPath: "vector",
1257+
vectorConfig: {
1258+
dimension: 100,
1259+
flat: {},
1260+
},
1261+
},
1262+
],
1263+
} as Spec.Index;
1264+
1265+
const result = FirestoreApi.processIndex(mockSpecIndex);
1266+
1267+
expect(result.fields).to.have.length(4);
1268+
expect(result.fields[0].fieldPath).to.equal("foo");
1269+
expect(result.fields[1].fieldPath).to.equal("arr");
1270+
expect(result.fields[2].fieldPath).to.equal("__name__");
1271+
expect(result.fields[3].fieldPath).to.equal("vector");
1272+
expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
1273+
});
9631274
});
9641275

9651276
describe("IndexSorting", () => {

0 commit comments

Comments
 (0)