Skip to content

Commit a09d541

Browse files
committed
⚡ InvertedIndex - more performance and better behavior
1 parent b2459cd commit a09d541

File tree

2 files changed

+327
-17
lines changed

2 files changed

+327
-17
lines changed

src/lang.spec.ts

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,6 @@ describe("Lang.InvertedIndexMap", () => {
571571

572572
// Query people with age 30
573573
const result = peopleIndex.query({ age: 30 });
574-
console.log(result);
575574
expect(result.sort((a, b) => a.id.localeCompare(b.id))).toEqual(
576575
[alice, charlie].sort((a, b) => a.id.localeCompare(b.id)),
577576
);
@@ -773,4 +772,203 @@ describe("Lang.InvertedIndexMap", () => {
773772
flexIndex.add(updated);
774773
expect(flexIndex.query({ value: 123 })).toEqual([updated]);
775774
});
775+
776+
test("queryIndexSet behavior with filtered fields", () => {
777+
interface Product {
778+
id: string;
779+
category: string;
780+
price: number;
781+
stock: number;
782+
}
783+
784+
// Only index category and price fields
785+
const idx = new Lang.InvertedIndexMap<Product>(
786+
(r) => r.id,
787+
new Set(["category", "price"]),
788+
);
789+
790+
const products = [
791+
{ id: "p1", category: "electronics", price: 100, stock: 5 },
792+
{ id: "p2", category: "books", price: 20, stock: 10 },
793+
{ id: "p3", category: "electronics", price: 200, stock: 3 },
794+
{ id: "p4", category: "books", price: 15, stock: 8 },
795+
];
796+
797+
products.forEach((p) => idx.add(p));
798+
799+
// Test querying indexed fields
800+
const electronicsSet = idx.queryIndexSet({ category: "electronics" });
801+
expect(electronicsSet.size).toBe(2);
802+
expect([...electronicsSet].map((i) => products[i].id).sort()).toEqual(
803+
["p1", "p3"].sort(),
804+
);
805+
806+
// Test querying unindexed fields (should return empty set)
807+
const stockSet = idx.queryIndexSet({ stock: 5 });
808+
expect(stockSet.size).toBe(4);
809+
810+
// Test querying combination of indexed and unindexed fields (unindexed fields are ignored)
811+
const mixedSet = idx.queryIndexSet({ category: "books", stock: 10 });
812+
expect(mixedSet.size).toBe(2); // Should match p2 and p4 as they are in "books" category
813+
814+
// Test querying empty object returns set of all indices
815+
const allSet = idx.queryIndexSet({});
816+
expect(allSet.size).toBe(4);
817+
818+
// Test querying non-existent values
819+
const emptySet = idx.queryIndexSet({ category: "nonexistent" });
820+
expect(emptySet.size).toBe(0);
821+
822+
// Test querying multiple indexed fields
823+
const booksUnder20Set = idx.queryIndexSet({
824+
category: "books",
825+
price: 15,
826+
});
827+
expect([...booksUnder20Set].map((i) => products[i].id)).toEqual(["p4"]);
828+
});
829+
830+
test("queryIndexSet handles updates correctly", () => {
831+
interface User {
832+
id: string;
833+
role: string;
834+
active: boolean;
835+
}
836+
837+
const idx = new Lang.InvertedIndexMap<User>((r) => r.id);
838+
839+
// Add initial users
840+
const users = [
841+
{ id: "u1", role: "admin", active: true },
842+
{ id: "u2", role: "user", active: true },
843+
{ id: "u3", role: "user", active: false },
844+
];
845+
846+
users.forEach((u) => idx.add(u));
847+
848+
// Initial queries
849+
let activeUsers = idx.queryIndexSet({ active: true });
850+
expect(activeUsers.size).toBe(2);
851+
852+
// Update a user
853+
idx.add({ id: "u2", role: "admin", active: false });
854+
855+
// Check updated queries
856+
activeUsers = idx.queryIndexSet({ active: true });
857+
expect(activeUsers.size).toBe(1);
858+
859+
const adminUsers = idx.queryIndexSet({ role: "admin" });
860+
expect(adminUsers.size).toBe(2);
861+
862+
// Test combined queries after update
863+
const activeAdmins = idx.queryIndexSet({ role: "admin", active: true });
864+
expect(activeAdmins.size).toBe(1);
865+
});
866+
867+
test("queryIndexSet performance with large datasets", () => {
868+
interface Record {
869+
id: string;
870+
type: string;
871+
value: number;
872+
}
873+
874+
const idx = new Lang.InvertedIndexMap<Record>((r) => r.id);
875+
const types = ["A", "B", "C", "D"];
876+
const values = [10, 20, 30, 40, 50];
877+
878+
// Add 1000 records
879+
for (let i = 0; i < 1000; i++) {
880+
idx.add({
881+
id: `r${i}`,
882+
type: types[i % types.length],
883+
value: values[i % values.length],
884+
});
885+
}
886+
887+
// Measure time for single field query
888+
const start1 = performance.now();
889+
const typeASet = idx.queryIndexSet({ type: "A" });
890+
const duration1 = performance.now() - start1;
891+
expect(typeASet.size).toBe(250); // 1000/4 records
892+
expect(duration1).toBeLessThan(50); // Should be fast
893+
894+
// Measure time for multiple field query
895+
const start2 = performance.now();
896+
const typeAValue10Set = idx.queryIndexSet({ type: "A", value: 10 });
897+
const duration2 = performance.now() - start2;
898+
expect(typeAValue10Set.size).toBe(50); // 250/5 records
899+
expect(duration2).toBeLessThan(50); // Should still be fast
900+
});
901+
902+
test("query respects fieldsToIdx constructor parameter", () => {
903+
interface Product {
904+
id: string;
905+
name: string;
906+
category: string;
907+
price: number;
908+
inStock: boolean;
909+
}
910+
911+
const productsIndex = new Lang.InvertedIndexMap<Product>(
912+
(r) => r.id,
913+
new Set(["category", "price"]), // Only index category and price
914+
);
915+
916+
const products = [
917+
{
918+
id: "p1",
919+
name: "Laptop",
920+
category: "electronics",
921+
price: 999,
922+
inStock: true,
923+
},
924+
{
925+
id: "p2",
926+
name: "Phone",
927+
category: "electronics",
928+
price: 599,
929+
inStock: false,
930+
},
931+
{ id: "p3", name: "Book", category: "books", price: 29, inStock: true },
932+
{
933+
id: "p4",
934+
name: "Tablet",
935+
category: "electronics",
936+
price: 399,
937+
inStock: true,
938+
},
939+
];
940+
941+
products.forEach((p) => productsIndex.add(p));
942+
943+
// Should work for indexed fields
944+
expect(productsIndex.query({ category: "electronics" }).length).toBe(3);
945+
expect(productsIndex.query({ price: 599 }).length).toBe(1);
946+
expect(
947+
productsIndex.query({ category: "electronics", price: 999 }).length,
948+
).toBe(1);
949+
950+
// Should ignore non-indexed fields
951+
expect(productsIndex.query({ name: "Laptop" }).length).toBe(4);
952+
expect(productsIndex.query({ inStock: true }).length).toBe(4);
953+
954+
// Should ignore non-indexed fields when combined with indexed fields
955+
expect(
956+
productsIndex.query({ category: "electronics", name: "Laptop" }).length,
957+
).toBe(3);
958+
expect(productsIndex.query({ price: 599, inStock: false }).length).toBe(1);
959+
960+
// Should handle updates correctly for indexed fields
961+
productsIndex.add({
962+
id: "p2",
963+
name: "Phone Updated",
964+
category: "accessories",
965+
price: 499,
966+
inStock: true,
967+
});
968+
969+
expect(productsIndex.query({ category: "electronics" }).length).toBe(2);
970+
expect(productsIndex.query({ category: "accessories" }).length).toBe(1);
971+
expect(productsIndex.query({ price: 599 }).length).toBe(0);
972+
expect(productsIndex.query({ price: 499 }).length).toBe(1);
973+
});
776974
});

0 commit comments

Comments
 (0)