@topgunbuild/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +756 -141
- package/dist/index.d.ts +756 -141
- package/dist/index.js +1094 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1094 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4510,6 +4510,593 @@ _InvertedIndex.RETRIEVAL_COST = 50;
|
|
|
4510
4510
|
_InvertedIndex.SUPPORTED_QUERIES = ["contains", "containsAll", "containsAny", "has"];
|
|
4511
4511
|
var InvertedIndex = _InvertedIndex;
|
|
4512
4512
|
|
|
4513
|
+
// src/query/indexes/CompoundIndex.ts
|
|
4514
|
+
var _CompoundIndex = class _CompoundIndex {
|
|
4515
|
+
/**
|
|
4516
|
+
* Create a CompoundIndex.
|
|
4517
|
+
*
|
|
4518
|
+
* @param attributes - Array of attributes to index (order matters!)
|
|
4519
|
+
* @param options - Optional configuration
|
|
4520
|
+
*
|
|
4521
|
+
* @example
|
|
4522
|
+
* ```typescript
|
|
4523
|
+
* const statusAttr = simpleAttribute<Product, string>('status', p => p.status);
|
|
4524
|
+
* const categoryAttr = simpleAttribute<Product, string>('category', p => p.category);
|
|
4525
|
+
*
|
|
4526
|
+
* const compoundIndex = new CompoundIndex<string, Product>([statusAttr, categoryAttr]);
|
|
4527
|
+
* ```
|
|
4528
|
+
*/
|
|
4529
|
+
constructor(attributes, options = {}) {
|
|
4530
|
+
this.type = "compound";
|
|
4531
|
+
/** Map from composite key to set of record keys */
|
|
4532
|
+
this.data = /* @__PURE__ */ new Map();
|
|
4533
|
+
/** Set of all indexed keys */
|
|
4534
|
+
this.allKeys = /* @__PURE__ */ new Set();
|
|
4535
|
+
if (attributes.length < 2) {
|
|
4536
|
+
throw new Error("CompoundIndex requires at least 2 attributes");
|
|
4537
|
+
}
|
|
4538
|
+
this._attributes = attributes;
|
|
4539
|
+
this.separator = options.separator ?? "|";
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Get the first attribute (used for Index interface compatibility).
|
|
4543
|
+
* Note: CompoundIndex spans multiple attributes.
|
|
4544
|
+
*/
|
|
4545
|
+
get attribute() {
|
|
4546
|
+
return this._attributes[0];
|
|
4547
|
+
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Get all attributes in this compound index.
|
|
4550
|
+
*/
|
|
4551
|
+
get attributes() {
|
|
4552
|
+
return [...this._attributes];
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* Get attribute names as a combined identifier.
|
|
4556
|
+
*/
|
|
4557
|
+
get compoundName() {
|
|
4558
|
+
return this._attributes.map((a) => a.name).join("+");
|
|
4559
|
+
}
|
|
4560
|
+
getRetrievalCost() {
|
|
4561
|
+
return _CompoundIndex.RETRIEVAL_COST;
|
|
4562
|
+
}
|
|
4563
|
+
supportsQuery(queryType) {
|
|
4564
|
+
return queryType === "compound";
|
|
4565
|
+
}
|
|
4566
|
+
/**
|
|
4567
|
+
* Retrieve records matching compound query.
|
|
4568
|
+
*
|
|
4569
|
+
* @param query - Compound query with values matching each attribute
|
|
4570
|
+
* @returns ResultSet of matching keys
|
|
4571
|
+
*
|
|
4572
|
+
* @example
|
|
4573
|
+
* ```typescript
|
|
4574
|
+
* // Find products where status='active' AND category='electronics'
|
|
4575
|
+
* index.retrieve({
|
|
4576
|
+
* type: 'compound',
|
|
4577
|
+
* values: ['active', 'electronics']
|
|
4578
|
+
* });
|
|
4579
|
+
* ```
|
|
4580
|
+
*/
|
|
4581
|
+
retrieve(query) {
|
|
4582
|
+
if (query.type !== "compound") {
|
|
4583
|
+
throw new Error(`CompoundIndex only supports 'compound' query type, got: ${query.type}`);
|
|
4584
|
+
}
|
|
4585
|
+
const compoundQuery = query;
|
|
4586
|
+
const values = compoundQuery.values;
|
|
4587
|
+
if (values.length !== this._attributes.length) {
|
|
4588
|
+
throw new Error(
|
|
4589
|
+
`CompoundIndex requires ${this._attributes.length} values, got ${values.length}`
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
const compositeKey = this.buildCompositeKey(values);
|
|
4593
|
+
const keys = this.data.get(compositeKey);
|
|
4594
|
+
return new SetResultSet(
|
|
4595
|
+
keys ? new Set(keys) : /* @__PURE__ */ new Set(),
|
|
4596
|
+
_CompoundIndex.RETRIEVAL_COST
|
|
4597
|
+
);
|
|
4598
|
+
}
|
|
4599
|
+
/**
|
|
4600
|
+
* Retrieve with explicit values (convenience method).
|
|
4601
|
+
*
|
|
4602
|
+
* @param values - Values in order of index attributes
|
|
4603
|
+
* @returns ResultSet of matching keys
|
|
4604
|
+
*/
|
|
4605
|
+
retrieveByValues(...values) {
|
|
4606
|
+
return this.retrieve({ type: "compound", values });
|
|
4607
|
+
}
|
|
4608
|
+
add(key, record) {
|
|
4609
|
+
const compositeKey = this.buildCompositeKeyFromRecord(record);
|
|
4610
|
+
if (compositeKey === null) return;
|
|
4611
|
+
let keys = this.data.get(compositeKey);
|
|
4612
|
+
if (!keys) {
|
|
4613
|
+
keys = /* @__PURE__ */ new Set();
|
|
4614
|
+
this.data.set(compositeKey, keys);
|
|
4615
|
+
}
|
|
4616
|
+
keys.add(key);
|
|
4617
|
+
this.allKeys.add(key);
|
|
4618
|
+
}
|
|
4619
|
+
remove(key, record) {
|
|
4620
|
+
const compositeKey = this.buildCompositeKeyFromRecord(record);
|
|
4621
|
+
if (compositeKey === null) return;
|
|
4622
|
+
const keys = this.data.get(compositeKey);
|
|
4623
|
+
if (keys) {
|
|
4624
|
+
keys.delete(key);
|
|
4625
|
+
if (keys.size === 0) {
|
|
4626
|
+
this.data.delete(compositeKey);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
this.allKeys.delete(key);
|
|
4630
|
+
}
|
|
4631
|
+
update(key, oldRecord, newRecord) {
|
|
4632
|
+
const oldKey = this.buildCompositeKeyFromRecord(oldRecord);
|
|
4633
|
+
const newKey = this.buildCompositeKeyFromRecord(newRecord);
|
|
4634
|
+
if (oldKey === newKey) {
|
|
4635
|
+
return;
|
|
4636
|
+
}
|
|
4637
|
+
this.remove(key, oldRecord);
|
|
4638
|
+
this.add(key, newRecord);
|
|
4639
|
+
}
|
|
4640
|
+
clear() {
|
|
4641
|
+
this.data.clear();
|
|
4642
|
+
this.allKeys.clear();
|
|
4643
|
+
}
|
|
4644
|
+
getStats() {
|
|
4645
|
+
let totalEntries = 0;
|
|
4646
|
+
for (const keys of this.data.values()) {
|
|
4647
|
+
totalEntries += keys.size;
|
|
4648
|
+
}
|
|
4649
|
+
return {
|
|
4650
|
+
distinctValues: this.data.size,
|
|
4651
|
+
totalEntries,
|
|
4652
|
+
avgEntriesPerValue: this.data.size > 0 ? totalEntries / this.data.size : 0
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
/**
|
|
4656
|
+
* Get extended statistics for compound index.
|
|
4657
|
+
*/
|
|
4658
|
+
getExtendedStats() {
|
|
4659
|
+
const stats = this.getStats();
|
|
4660
|
+
return {
|
|
4661
|
+
...stats,
|
|
4662
|
+
attributeCount: this._attributes.length,
|
|
4663
|
+
attributeNames: this._attributes.map((a) => a.name),
|
|
4664
|
+
compositeKeyCount: this.data.size
|
|
4665
|
+
};
|
|
4666
|
+
}
|
|
4667
|
+
/**
|
|
4668
|
+
* Check if this compound index can answer a query on the given attributes.
|
|
4669
|
+
* Compound indexes can be used if query attributes match in prefix order.
|
|
4670
|
+
*
|
|
4671
|
+
* @param attributeNames - Attribute names being queried
|
|
4672
|
+
* @returns true if this index can answer the query
|
|
4673
|
+
*/
|
|
4674
|
+
canAnswerQuery(attributeNames) {
|
|
4675
|
+
if (attributeNames.length !== this._attributes.length) {
|
|
4676
|
+
return false;
|
|
4677
|
+
}
|
|
4678
|
+
for (let i = 0; i < attributeNames.length; i++) {
|
|
4679
|
+
if (attributeNames[i] !== this._attributes[i].name) {
|
|
4680
|
+
return false;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return true;
|
|
4684
|
+
}
|
|
4685
|
+
/**
|
|
4686
|
+
* Build composite key from array of values.
|
|
4687
|
+
*/
|
|
4688
|
+
buildCompositeKey(values) {
|
|
4689
|
+
return values.map((v) => this.encodeValue(v)).join(this.separator);
|
|
4690
|
+
}
|
|
4691
|
+
/**
|
|
4692
|
+
* Build composite key from record by extracting attribute values.
|
|
4693
|
+
* Returns null if any attribute value is undefined.
|
|
4694
|
+
*/
|
|
4695
|
+
buildCompositeKeyFromRecord(record) {
|
|
4696
|
+
const values = [];
|
|
4697
|
+
for (const attr of this._attributes) {
|
|
4698
|
+
const value = attr.getValue(record);
|
|
4699
|
+
if (value === void 0) {
|
|
4700
|
+
return null;
|
|
4701
|
+
}
|
|
4702
|
+
values.push(value);
|
|
4703
|
+
}
|
|
4704
|
+
return this.buildCompositeKey(values);
|
|
4705
|
+
}
|
|
4706
|
+
/**
|
|
4707
|
+
* Encode value for composite key.
|
|
4708
|
+
* Handles common types and escapes separator.
|
|
4709
|
+
*/
|
|
4710
|
+
encodeValue(value) {
|
|
4711
|
+
if (value === null) return "__null__";
|
|
4712
|
+
if (value === void 0) return "__undefined__";
|
|
4713
|
+
const str = String(value);
|
|
4714
|
+
return str.replace(
|
|
4715
|
+
new RegExp(this.escapeRegex(this.separator), "g"),
|
|
4716
|
+
`\\${this.separator}`
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
4719
|
+
/**
|
|
4720
|
+
* Escape regex special characters.
|
|
4721
|
+
*/
|
|
4722
|
+
escapeRegex(str) {
|
|
4723
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4724
|
+
}
|
|
4725
|
+
};
|
|
4726
|
+
/** Retrieval cost (lower than individual indexes combined) */
|
|
4727
|
+
_CompoundIndex.RETRIEVAL_COST = 20;
|
|
4728
|
+
var CompoundIndex = _CompoundIndex;
|
|
4729
|
+
function isCompoundIndex(index) {
|
|
4730
|
+
return index.type === "compound";
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
// src/query/indexes/lazy/LazyHashIndex.ts
|
|
4734
|
+
var LazyHashIndex = class {
|
|
4735
|
+
constructor(attribute, options = {}) {
|
|
4736
|
+
this.attribute = attribute;
|
|
4737
|
+
this.type = "hash";
|
|
4738
|
+
this.isLazy = true;
|
|
4739
|
+
/** Underlying hash index (created on first query) */
|
|
4740
|
+
this.innerIndex = null;
|
|
4741
|
+
/** Pending records before materialization */
|
|
4742
|
+
this.pendingRecords = /* @__PURE__ */ new Map();
|
|
4743
|
+
/** Track if index has been built */
|
|
4744
|
+
this.built = false;
|
|
4745
|
+
this.onProgress = options.onProgress;
|
|
4746
|
+
this.progressBatchSize = options.progressBatchSize ?? 1e3;
|
|
4747
|
+
}
|
|
4748
|
+
get isBuilt() {
|
|
4749
|
+
return this.built;
|
|
4750
|
+
}
|
|
4751
|
+
get pendingCount() {
|
|
4752
|
+
return this.pendingRecords.size;
|
|
4753
|
+
}
|
|
4754
|
+
getRetrievalCost() {
|
|
4755
|
+
return 30;
|
|
4756
|
+
}
|
|
4757
|
+
supportsQuery(queryType) {
|
|
4758
|
+
return ["equal", "in", "has"].includes(queryType);
|
|
4759
|
+
}
|
|
4760
|
+
retrieve(query) {
|
|
4761
|
+
if (!this.built) {
|
|
4762
|
+
this.materialize();
|
|
4763
|
+
}
|
|
4764
|
+
return this.innerIndex.retrieve(query);
|
|
4765
|
+
}
|
|
4766
|
+
add(key, record) {
|
|
4767
|
+
if (this.built) {
|
|
4768
|
+
this.innerIndex.add(key, record);
|
|
4769
|
+
} else {
|
|
4770
|
+
this.pendingRecords.set(key, record);
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
remove(key, record) {
|
|
4774
|
+
if (this.built) {
|
|
4775
|
+
this.innerIndex.remove(key, record);
|
|
4776
|
+
} else {
|
|
4777
|
+
this.pendingRecords.delete(key);
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
update(key, oldRecord, newRecord) {
|
|
4781
|
+
if (this.built) {
|
|
4782
|
+
this.innerIndex.update(key, oldRecord, newRecord);
|
|
4783
|
+
} else {
|
|
4784
|
+
this.pendingRecords.set(key, newRecord);
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
clear() {
|
|
4788
|
+
if (this.built) {
|
|
4789
|
+
this.innerIndex.clear();
|
|
4790
|
+
}
|
|
4791
|
+
this.pendingRecords.clear();
|
|
4792
|
+
}
|
|
4793
|
+
getStats() {
|
|
4794
|
+
if (this.built) {
|
|
4795
|
+
return this.innerIndex.getStats();
|
|
4796
|
+
}
|
|
4797
|
+
return {
|
|
4798
|
+
distinctValues: 0,
|
|
4799
|
+
totalEntries: this.pendingRecords.size,
|
|
4800
|
+
avgEntriesPerValue: 0
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
/**
|
|
4804
|
+
* Force materialization of the index.
|
|
4805
|
+
* Called automatically on first query.
|
|
4806
|
+
*/
|
|
4807
|
+
materialize(progressCallback) {
|
|
4808
|
+
if (this.built) return;
|
|
4809
|
+
const callback = progressCallback ?? this.onProgress;
|
|
4810
|
+
const total = this.pendingRecords.size;
|
|
4811
|
+
this.innerIndex = new HashIndex(this.attribute);
|
|
4812
|
+
let processed = 0;
|
|
4813
|
+
for (const [key, record] of this.pendingRecords) {
|
|
4814
|
+
this.innerIndex.add(key, record);
|
|
4815
|
+
processed++;
|
|
4816
|
+
if (callback && processed % this.progressBatchSize === 0) {
|
|
4817
|
+
const progress = Math.round(processed / total * 100);
|
|
4818
|
+
callback(this.attribute.name, progress, processed, total);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
if (callback && total > 0) {
|
|
4822
|
+
callback(this.attribute.name, 100, total, total);
|
|
4823
|
+
}
|
|
4824
|
+
this.pendingRecords.clear();
|
|
4825
|
+
this.built = true;
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* Get the underlying HashIndex (for testing/debugging).
|
|
4829
|
+
* Returns null if not yet materialized.
|
|
4830
|
+
*/
|
|
4831
|
+
getInnerIndex() {
|
|
4832
|
+
return this.innerIndex;
|
|
4833
|
+
}
|
|
4834
|
+
};
|
|
4835
|
+
|
|
4836
|
+
// src/query/indexes/lazy/LazyNavigableIndex.ts
|
|
4837
|
+
var LazyNavigableIndex = class {
|
|
4838
|
+
constructor(attribute, comparator, options = {}) {
|
|
4839
|
+
this.attribute = attribute;
|
|
4840
|
+
this.type = "navigable";
|
|
4841
|
+
this.isLazy = true;
|
|
4842
|
+
/** Underlying navigable index (created on first query) */
|
|
4843
|
+
this.innerIndex = null;
|
|
4844
|
+
/** Pending records before materialization */
|
|
4845
|
+
this.pendingRecords = /* @__PURE__ */ new Map();
|
|
4846
|
+
/** Track if index has been built */
|
|
4847
|
+
this.built = false;
|
|
4848
|
+
this.comparator = comparator;
|
|
4849
|
+
this.onProgress = options.onProgress;
|
|
4850
|
+
this.progressBatchSize = options.progressBatchSize ?? 1e3;
|
|
4851
|
+
}
|
|
4852
|
+
get isBuilt() {
|
|
4853
|
+
return this.built;
|
|
4854
|
+
}
|
|
4855
|
+
get pendingCount() {
|
|
4856
|
+
return this.pendingRecords.size;
|
|
4857
|
+
}
|
|
4858
|
+
getRetrievalCost() {
|
|
4859
|
+
return 40;
|
|
4860
|
+
}
|
|
4861
|
+
supportsQuery(queryType) {
|
|
4862
|
+
return ["equal", "in", "has", "gt", "gte", "lt", "lte", "between"].includes(queryType);
|
|
4863
|
+
}
|
|
4864
|
+
retrieve(query) {
|
|
4865
|
+
if (!this.built) {
|
|
4866
|
+
this.materialize();
|
|
4867
|
+
}
|
|
4868
|
+
return this.innerIndex.retrieve(query);
|
|
4869
|
+
}
|
|
4870
|
+
add(key, record) {
|
|
4871
|
+
if (this.built) {
|
|
4872
|
+
this.innerIndex.add(key, record);
|
|
4873
|
+
} else {
|
|
4874
|
+
this.pendingRecords.set(key, record);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
remove(key, record) {
|
|
4878
|
+
if (this.built) {
|
|
4879
|
+
this.innerIndex.remove(key, record);
|
|
4880
|
+
} else {
|
|
4881
|
+
this.pendingRecords.delete(key);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
update(key, oldRecord, newRecord) {
|
|
4885
|
+
if (this.built) {
|
|
4886
|
+
this.innerIndex.update(key, oldRecord, newRecord);
|
|
4887
|
+
} else {
|
|
4888
|
+
this.pendingRecords.set(key, newRecord);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
clear() {
|
|
4892
|
+
if (this.built) {
|
|
4893
|
+
this.innerIndex.clear();
|
|
4894
|
+
}
|
|
4895
|
+
this.pendingRecords.clear();
|
|
4896
|
+
}
|
|
4897
|
+
getStats() {
|
|
4898
|
+
if (this.built) {
|
|
4899
|
+
return this.innerIndex.getStats();
|
|
4900
|
+
}
|
|
4901
|
+
return {
|
|
4902
|
+
distinctValues: 0,
|
|
4903
|
+
totalEntries: this.pendingRecords.size,
|
|
4904
|
+
avgEntriesPerValue: 0
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4907
|
+
/**
|
|
4908
|
+
* Force materialization of the index.
|
|
4909
|
+
* Called automatically on first query.
|
|
4910
|
+
*/
|
|
4911
|
+
materialize(progressCallback) {
|
|
4912
|
+
if (this.built) return;
|
|
4913
|
+
const callback = progressCallback ?? this.onProgress;
|
|
4914
|
+
const total = this.pendingRecords.size;
|
|
4915
|
+
this.innerIndex = new NavigableIndex(this.attribute, this.comparator);
|
|
4916
|
+
let processed = 0;
|
|
4917
|
+
for (const [key, record] of this.pendingRecords) {
|
|
4918
|
+
this.innerIndex.add(key, record);
|
|
4919
|
+
processed++;
|
|
4920
|
+
if (callback && processed % this.progressBatchSize === 0) {
|
|
4921
|
+
const progress = Math.round(processed / total * 100);
|
|
4922
|
+
callback(this.attribute.name, progress, processed, total);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
if (callback && total > 0) {
|
|
4926
|
+
callback(this.attribute.name, 100, total, total);
|
|
4927
|
+
}
|
|
4928
|
+
this.pendingRecords.clear();
|
|
4929
|
+
this.built = true;
|
|
4930
|
+
}
|
|
4931
|
+
/**
|
|
4932
|
+
* Get the underlying NavigableIndex (for testing/debugging).
|
|
4933
|
+
* Returns null if not yet materialized.
|
|
4934
|
+
*/
|
|
4935
|
+
getInnerIndex() {
|
|
4936
|
+
return this.innerIndex;
|
|
4937
|
+
}
|
|
4938
|
+
/**
|
|
4939
|
+
* Get the minimum indexed value.
|
|
4940
|
+
* Forces materialization if not built.
|
|
4941
|
+
*/
|
|
4942
|
+
getMinValue() {
|
|
4943
|
+
if (!this.built) {
|
|
4944
|
+
this.materialize();
|
|
4945
|
+
}
|
|
4946
|
+
return this.innerIndex.getMinValue();
|
|
4947
|
+
}
|
|
4948
|
+
/**
|
|
4949
|
+
* Get the maximum indexed value.
|
|
4950
|
+
* Forces materialization if not built.
|
|
4951
|
+
*/
|
|
4952
|
+
getMaxValue() {
|
|
4953
|
+
if (!this.built) {
|
|
4954
|
+
this.materialize();
|
|
4955
|
+
}
|
|
4956
|
+
return this.innerIndex.getMaxValue();
|
|
4957
|
+
}
|
|
4958
|
+
};
|
|
4959
|
+
|
|
4960
|
+
// src/query/indexes/lazy/LazyInvertedIndex.ts
|
|
4961
|
+
var LazyInvertedIndex = class {
|
|
4962
|
+
constructor(attribute, pipeline, options = {}) {
|
|
4963
|
+
this.attribute = attribute;
|
|
4964
|
+
this.type = "inverted";
|
|
4965
|
+
this.isLazy = true;
|
|
4966
|
+
/** Underlying inverted index (created on first query) */
|
|
4967
|
+
this.innerIndex = null;
|
|
4968
|
+
/** Pending records before materialization */
|
|
4969
|
+
this.pendingRecords = /* @__PURE__ */ new Map();
|
|
4970
|
+
/** Track if index has been built */
|
|
4971
|
+
this.built = false;
|
|
4972
|
+
this.pipeline = pipeline ?? TokenizationPipeline.simple();
|
|
4973
|
+
this.onProgress = options.onProgress;
|
|
4974
|
+
this.progressBatchSize = options.progressBatchSize ?? 1e3;
|
|
4975
|
+
}
|
|
4976
|
+
get isBuilt() {
|
|
4977
|
+
return this.built;
|
|
4978
|
+
}
|
|
4979
|
+
get pendingCount() {
|
|
4980
|
+
return this.pendingRecords.size;
|
|
4981
|
+
}
|
|
4982
|
+
getRetrievalCost() {
|
|
4983
|
+
return 50;
|
|
4984
|
+
}
|
|
4985
|
+
supportsQuery(queryType) {
|
|
4986
|
+
return ["contains", "containsAll", "containsAny", "has"].includes(queryType);
|
|
4987
|
+
}
|
|
4988
|
+
retrieve(query) {
|
|
4989
|
+
if (!this.built) {
|
|
4990
|
+
this.materialize();
|
|
4991
|
+
}
|
|
4992
|
+
return this.innerIndex.retrieve(query);
|
|
4993
|
+
}
|
|
4994
|
+
add(key, record) {
|
|
4995
|
+
if (this.built) {
|
|
4996
|
+
this.innerIndex.add(key, record);
|
|
4997
|
+
} else {
|
|
4998
|
+
this.pendingRecords.set(key, record);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
remove(key, record) {
|
|
5002
|
+
if (this.built) {
|
|
5003
|
+
this.innerIndex.remove(key, record);
|
|
5004
|
+
} else {
|
|
5005
|
+
this.pendingRecords.delete(key);
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
update(key, oldRecord, newRecord) {
|
|
5009
|
+
if (this.built) {
|
|
5010
|
+
this.innerIndex.update(key, oldRecord, newRecord);
|
|
5011
|
+
} else {
|
|
5012
|
+
this.pendingRecords.set(key, newRecord);
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
clear() {
|
|
5016
|
+
if (this.built) {
|
|
5017
|
+
this.innerIndex.clear();
|
|
5018
|
+
}
|
|
5019
|
+
this.pendingRecords.clear();
|
|
5020
|
+
}
|
|
5021
|
+
getStats() {
|
|
5022
|
+
if (this.built) {
|
|
5023
|
+
return this.innerIndex.getStats();
|
|
5024
|
+
}
|
|
5025
|
+
return {
|
|
5026
|
+
distinctValues: 0,
|
|
5027
|
+
totalEntries: this.pendingRecords.size,
|
|
5028
|
+
avgEntriesPerValue: 0
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
/**
|
|
5032
|
+
* Get extended statistics for full-text index.
|
|
5033
|
+
* Forces materialization if not built.
|
|
5034
|
+
*/
|
|
5035
|
+
getExtendedStats() {
|
|
5036
|
+
if (!this.built) {
|
|
5037
|
+
this.materialize();
|
|
5038
|
+
}
|
|
5039
|
+
return this.innerIndex.getExtendedStats();
|
|
5040
|
+
}
|
|
5041
|
+
/**
|
|
5042
|
+
* Force materialization of the index.
|
|
5043
|
+
* Called automatically on first query.
|
|
5044
|
+
*/
|
|
5045
|
+
materialize(progressCallback) {
|
|
5046
|
+
if (this.built) return;
|
|
5047
|
+
const callback = progressCallback ?? this.onProgress;
|
|
5048
|
+
const total = this.pendingRecords.size;
|
|
5049
|
+
this.innerIndex = new InvertedIndex(this.attribute, this.pipeline);
|
|
5050
|
+
let processed = 0;
|
|
5051
|
+
for (const [key, record] of this.pendingRecords) {
|
|
5052
|
+
this.innerIndex.add(key, record);
|
|
5053
|
+
processed++;
|
|
5054
|
+
if (callback && processed % this.progressBatchSize === 0) {
|
|
5055
|
+
const progress = Math.round(processed / total * 100);
|
|
5056
|
+
callback(this.attribute.name, progress, processed, total);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
if (callback && total > 0) {
|
|
5060
|
+
callback(this.attribute.name, 100, total, total);
|
|
5061
|
+
}
|
|
5062
|
+
this.pendingRecords.clear();
|
|
5063
|
+
this.built = true;
|
|
5064
|
+
}
|
|
5065
|
+
/**
|
|
5066
|
+
* Get the underlying InvertedIndex (for testing/debugging).
|
|
5067
|
+
* Returns null if not yet materialized.
|
|
5068
|
+
*/
|
|
5069
|
+
getInnerIndex() {
|
|
5070
|
+
return this.innerIndex;
|
|
5071
|
+
}
|
|
5072
|
+
/**
|
|
5073
|
+
* Get the tokenization pipeline.
|
|
5074
|
+
*/
|
|
5075
|
+
getPipeline() {
|
|
5076
|
+
return this.pipeline;
|
|
5077
|
+
}
|
|
5078
|
+
/**
|
|
5079
|
+
* Check if a specific token exists in the index.
|
|
5080
|
+
* Forces materialization if not built.
|
|
5081
|
+
*/
|
|
5082
|
+
hasToken(token) {
|
|
5083
|
+
if (!this.built) {
|
|
5084
|
+
this.materialize();
|
|
5085
|
+
}
|
|
5086
|
+
return this.innerIndex.hasToken(token);
|
|
5087
|
+
}
|
|
5088
|
+
/**
|
|
5089
|
+
* Get the number of documents for a specific token.
|
|
5090
|
+
* Forces materialization if not built.
|
|
5091
|
+
*/
|
|
5092
|
+
getTokenDocumentCount(token) {
|
|
5093
|
+
if (!this.built) {
|
|
5094
|
+
this.materialize();
|
|
5095
|
+
}
|
|
5096
|
+
return this.innerIndex.getTokenDocumentCount(token);
|
|
5097
|
+
}
|
|
5098
|
+
};
|
|
5099
|
+
|
|
4513
5100
|
// src/query/resultset/IntersectionResultSet.ts
|
|
4514
5101
|
var IntersectionResultSet = class {
|
|
4515
5102
|
/**
|
|
@@ -5063,6 +5650,8 @@ var IndexRegistry = class {
|
|
|
5063
5650
|
constructor() {
|
|
5064
5651
|
/** Indexes grouped by attribute name */
|
|
5065
5652
|
this.attributeIndexes = /* @__PURE__ */ new Map();
|
|
5653
|
+
/** Compound indexes (Phase 9.03) - keyed by sorted attribute names */
|
|
5654
|
+
this.compoundIndexes = /* @__PURE__ */ new Map();
|
|
5066
5655
|
/** Fallback index for full scan (optional) */
|
|
5067
5656
|
this.fallbackIndex = null;
|
|
5068
5657
|
}
|
|
@@ -5073,6 +5662,10 @@ var IndexRegistry = class {
|
|
|
5073
5662
|
* @param index - Index to register
|
|
5074
5663
|
*/
|
|
5075
5664
|
addIndex(index) {
|
|
5665
|
+
if (isCompoundIndex(index)) {
|
|
5666
|
+
this.addCompoundIndex(index);
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5076
5669
|
const attrName = index.attribute.name;
|
|
5077
5670
|
let indexes = this.attributeIndexes.get(attrName);
|
|
5078
5671
|
if (!indexes) {
|
|
@@ -5083,6 +5676,15 @@ var IndexRegistry = class {
|
|
|
5083
5676
|
indexes.push(index);
|
|
5084
5677
|
}
|
|
5085
5678
|
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Register a compound index (Phase 9.03).
|
|
5681
|
+
*
|
|
5682
|
+
* @param index - Compound index to register
|
|
5683
|
+
*/
|
|
5684
|
+
addCompoundIndex(index) {
|
|
5685
|
+
const key = this.makeCompoundKey(index.attributes.map((a) => a.name));
|
|
5686
|
+
this.compoundIndexes.set(key, index);
|
|
5687
|
+
}
|
|
5086
5688
|
/**
|
|
5087
5689
|
* Remove an index from the registry.
|
|
5088
5690
|
*
|
|
@@ -5090,6 +5692,9 @@ var IndexRegistry = class {
|
|
|
5090
5692
|
* @returns true if index was found and removed
|
|
5091
5693
|
*/
|
|
5092
5694
|
removeIndex(index) {
|
|
5695
|
+
if (isCompoundIndex(index)) {
|
|
5696
|
+
return this.removeCompoundIndex(index);
|
|
5697
|
+
}
|
|
5093
5698
|
const attrName = index.attribute.name;
|
|
5094
5699
|
const indexes = this.attributeIndexes.get(attrName);
|
|
5095
5700
|
if (!indexes) {
|
|
@@ -5105,6 +5710,16 @@ var IndexRegistry = class {
|
|
|
5105
5710
|
}
|
|
5106
5711
|
return true;
|
|
5107
5712
|
}
|
|
5713
|
+
/**
|
|
5714
|
+
* Remove a compound index (Phase 9.03).
|
|
5715
|
+
*
|
|
5716
|
+
* @param index - Compound index to remove
|
|
5717
|
+
* @returns true if index was found and removed
|
|
5718
|
+
*/
|
|
5719
|
+
removeCompoundIndex(index) {
|
|
5720
|
+
const key = this.makeCompoundKey(index.attributes.map((a) => a.name));
|
|
5721
|
+
return this.compoundIndexes.delete(key);
|
|
5722
|
+
}
|
|
5108
5723
|
/**
|
|
5109
5724
|
* Get all indexes for an attribute.
|
|
5110
5725
|
*
|
|
@@ -5175,6 +5790,50 @@ var IndexRegistry = class {
|
|
|
5175
5790
|
const indexes = this.getIndexes(attributeName);
|
|
5176
5791
|
return indexes.filter((index) => index.supportsQuery(queryType)).sort((a, b) => a.getRetrievalCost() - b.getRetrievalCost());
|
|
5177
5792
|
}
|
|
5793
|
+
// ========================================
|
|
5794
|
+
// Phase 9.03: Compound Index Methods
|
|
5795
|
+
// ========================================
|
|
5796
|
+
/**
|
|
5797
|
+
* Find a compound index that covers the given attribute names (Phase 9.03).
|
|
5798
|
+
* The compound index must cover ALL the attributes (exact match or superset).
|
|
5799
|
+
*
|
|
5800
|
+
* @param attributeNames - Array of attribute names to search for
|
|
5801
|
+
* @returns Matching compound index or null
|
|
5802
|
+
*/
|
|
5803
|
+
findCompoundIndex(attributeNames) {
|
|
5804
|
+
if (attributeNames.length < 2) {
|
|
5805
|
+
return null;
|
|
5806
|
+
}
|
|
5807
|
+
const key = this.makeCompoundKey(attributeNames);
|
|
5808
|
+
const exactMatch = this.compoundIndexes.get(key);
|
|
5809
|
+
if (exactMatch) {
|
|
5810
|
+
return exactMatch;
|
|
5811
|
+
}
|
|
5812
|
+
return null;
|
|
5813
|
+
}
|
|
5814
|
+
/**
|
|
5815
|
+
* Check if a compound index exists for the given attributes (Phase 9.03).
|
|
5816
|
+
*
|
|
5817
|
+
* @param attributeNames - Array of attribute names
|
|
5818
|
+
* @returns true if a compound index exists
|
|
5819
|
+
*/
|
|
5820
|
+
hasCompoundIndex(attributeNames) {
|
|
5821
|
+
return this.findCompoundIndex(attributeNames) !== null;
|
|
5822
|
+
}
|
|
5823
|
+
/**
|
|
5824
|
+
* Get all compound indexes (Phase 9.03).
|
|
5825
|
+
*
|
|
5826
|
+
* @returns Array of all compound indexes
|
|
5827
|
+
*/
|
|
5828
|
+
getCompoundIndexes() {
|
|
5829
|
+
return Array.from(this.compoundIndexes.values());
|
|
5830
|
+
}
|
|
5831
|
+
/**
|
|
5832
|
+
* Create a compound key from attribute names (sorted for consistency).
|
|
5833
|
+
*/
|
|
5834
|
+
makeCompoundKey(attributeNames) {
|
|
5835
|
+
return [...attributeNames].sort().join("+");
|
|
5836
|
+
}
|
|
5178
5837
|
/**
|
|
5179
5838
|
* Set a fallback index for queries without a suitable index.
|
|
5180
5839
|
* Typically a FallbackIndex that performs full scan.
|
|
@@ -5205,6 +5864,9 @@ var IndexRegistry = class {
|
|
|
5205
5864
|
index.add(key, record);
|
|
5206
5865
|
}
|
|
5207
5866
|
}
|
|
5867
|
+
for (const compoundIndex of this.compoundIndexes.values()) {
|
|
5868
|
+
compoundIndex.add(key, record);
|
|
5869
|
+
}
|
|
5208
5870
|
}
|
|
5209
5871
|
/**
|
|
5210
5872
|
* Notify all indexes of a record update.
|
|
@@ -5220,6 +5882,9 @@ var IndexRegistry = class {
|
|
|
5220
5882
|
index.update(key, oldRecord, newRecord);
|
|
5221
5883
|
}
|
|
5222
5884
|
}
|
|
5885
|
+
for (const compoundIndex of this.compoundIndexes.values()) {
|
|
5886
|
+
compoundIndex.update(key, oldRecord, newRecord);
|
|
5887
|
+
}
|
|
5223
5888
|
}
|
|
5224
5889
|
/**
|
|
5225
5890
|
* Notify all indexes of a record removal.
|
|
@@ -5234,6 +5899,9 @@ var IndexRegistry = class {
|
|
|
5234
5899
|
index.remove(key, record);
|
|
5235
5900
|
}
|
|
5236
5901
|
}
|
|
5902
|
+
for (const compoundIndex of this.compoundIndexes.values()) {
|
|
5903
|
+
compoundIndex.remove(key, record);
|
|
5904
|
+
}
|
|
5237
5905
|
}
|
|
5238
5906
|
/**
|
|
5239
5907
|
* Clear all indexes.
|
|
@@ -5245,6 +5913,9 @@ var IndexRegistry = class {
|
|
|
5245
5913
|
index.clear();
|
|
5246
5914
|
}
|
|
5247
5915
|
}
|
|
5916
|
+
for (const compoundIndex of this.compoundIndexes.values()) {
|
|
5917
|
+
compoundIndex.clear();
|
|
5918
|
+
}
|
|
5248
5919
|
}
|
|
5249
5920
|
/**
|
|
5250
5921
|
* Get total number of registered indexes.
|
|
@@ -5254,6 +5925,7 @@ var IndexRegistry = class {
|
|
|
5254
5925
|
for (const indexes of this.attributeIndexes.values()) {
|
|
5255
5926
|
count += indexes.length;
|
|
5256
5927
|
}
|
|
5928
|
+
count += this.compoundIndexes.size;
|
|
5257
5929
|
return count;
|
|
5258
5930
|
}
|
|
5259
5931
|
/**
|
|
@@ -5266,9 +5938,17 @@ var IndexRegistry = class {
|
|
|
5266
5938
|
type: index.type,
|
|
5267
5939
|
stats: index.getStats()
|
|
5268
5940
|
}));
|
|
5941
|
+
for (const compoundIndex of this.compoundIndexes.values()) {
|
|
5942
|
+
indexStats.push({
|
|
5943
|
+
attribute: compoundIndex.compoundName,
|
|
5944
|
+
type: "compound",
|
|
5945
|
+
stats: compoundIndex.getStats()
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5269
5948
|
return {
|
|
5270
|
-
totalIndexes: indexes.length,
|
|
5949
|
+
totalIndexes: indexes.length + this.compoundIndexes.size,
|
|
5271
5950
|
indexedAttributes: this.getIndexedAttributes().length,
|
|
5951
|
+
compoundIndexes: this.compoundIndexes.size,
|
|
5272
5952
|
indexes: indexStats
|
|
5273
5953
|
};
|
|
5274
5954
|
}
|
|
@@ -5402,9 +6082,10 @@ var QueryOptimizer = class {
|
|
|
5402
6082
|
* Strategy: Find child with lowest cost, use as base, filter with rest.
|
|
5403
6083
|
*
|
|
5404
6084
|
* CQEngine "smallest first" strategy:
|
|
5405
|
-
* 1.
|
|
5406
|
-
* 2.
|
|
5407
|
-
* 3.
|
|
6085
|
+
* 1. Check for CompoundIndex covering all eq children (Phase 9.03)
|
|
6086
|
+
* 2. Sort children by merge cost
|
|
6087
|
+
* 3. Use intersection if multiple indexes available
|
|
6088
|
+
* 4. Apply remaining predicates as filters
|
|
5408
6089
|
*/
|
|
5409
6090
|
optimizeAnd(query) {
|
|
5410
6091
|
if (!query.children || query.children.length === 0) {
|
|
@@ -5413,6 +6094,10 @@ var QueryOptimizer = class {
|
|
|
5413
6094
|
if (query.children.length === 1) {
|
|
5414
6095
|
return this.optimizeNode(query.children[0]);
|
|
5415
6096
|
}
|
|
6097
|
+
const compoundStep = this.tryCompoundIndex(query.children);
|
|
6098
|
+
if (compoundStep) {
|
|
6099
|
+
return compoundStep;
|
|
6100
|
+
}
|
|
5416
6101
|
const childSteps = query.children.map((child) => this.optimizeNode(child));
|
|
5417
6102
|
const sortedWithIndex = childSteps.map((step, index) => ({ step, originalIndex: index })).sort((a, b) => this.estimateCost(a.step) - this.estimateCost(b.step));
|
|
5418
6103
|
const sortedSteps = sortedWithIndex.map((s) => s.step);
|
|
@@ -5437,6 +6122,71 @@ var QueryOptimizer = class {
|
|
|
5437
6122
|
}
|
|
5438
6123
|
return { type: "intersection", steps: indexedSteps };
|
|
5439
6124
|
}
|
|
6125
|
+
/**
|
|
6126
|
+
* Try to use a CompoundIndex for an AND query (Phase 9.03).
|
|
6127
|
+
*
|
|
6128
|
+
* Returns a compound index scan step if:
|
|
6129
|
+
* 1. All children are simple 'eq' queries
|
|
6130
|
+
* 2. A CompoundIndex exists covering all queried attributes
|
|
6131
|
+
*
|
|
6132
|
+
* @param children - Children of the AND query
|
|
6133
|
+
* @returns IndexScanStep using CompoundIndex, or null if not applicable
|
|
6134
|
+
*/
|
|
6135
|
+
tryCompoundIndex(children) {
|
|
6136
|
+
const eqQueries = [];
|
|
6137
|
+
const otherQueries = [];
|
|
6138
|
+
for (const child of children) {
|
|
6139
|
+
if (isSimpleQuery(child) && child.type === "eq") {
|
|
6140
|
+
eqQueries.push(child);
|
|
6141
|
+
} else {
|
|
6142
|
+
otherQueries.push(child);
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
if (eqQueries.length < 2) {
|
|
6146
|
+
return null;
|
|
6147
|
+
}
|
|
6148
|
+
const attributeNames = eqQueries.map((q) => q.attribute);
|
|
6149
|
+
const compoundIndex = this.indexRegistry.findCompoundIndex(attributeNames);
|
|
6150
|
+
if (!compoundIndex) {
|
|
6151
|
+
return null;
|
|
6152
|
+
}
|
|
6153
|
+
const values = this.buildCompoundValues(compoundIndex, eqQueries);
|
|
6154
|
+
if (!values) {
|
|
6155
|
+
return null;
|
|
6156
|
+
}
|
|
6157
|
+
const compoundStep = {
|
|
6158
|
+
type: "index-scan",
|
|
6159
|
+
index: compoundIndex,
|
|
6160
|
+
query: { type: "compound", values }
|
|
6161
|
+
};
|
|
6162
|
+
if (otherQueries.length > 0) {
|
|
6163
|
+
const filterPredicate = otherQueries.length === 1 ? otherQueries[0] : { type: "and", children: otherQueries };
|
|
6164
|
+
return { type: "filter", source: compoundStep, predicate: filterPredicate };
|
|
6165
|
+
}
|
|
6166
|
+
return compoundStep;
|
|
6167
|
+
}
|
|
6168
|
+
/**
|
|
6169
|
+
* Build values array for compound index query in correct attribute order.
|
|
6170
|
+
*
|
|
6171
|
+
* @param compoundIndex - The compound index to use
|
|
6172
|
+
* @param eqQueries - Array of 'eq' queries
|
|
6173
|
+
* @returns Values array in compound index order, or null if mismatch
|
|
6174
|
+
*/
|
|
6175
|
+
buildCompoundValues(compoundIndex, eqQueries) {
|
|
6176
|
+
const attributeNames = compoundIndex.attributes.map((a) => a.name);
|
|
6177
|
+
const values = [];
|
|
6178
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
6179
|
+
for (const q of eqQueries) {
|
|
6180
|
+
queryMap.set(q.attribute, q.value);
|
|
6181
|
+
}
|
|
6182
|
+
for (const attrName of attributeNames) {
|
|
6183
|
+
if (!queryMap.has(attrName)) {
|
|
6184
|
+
return null;
|
|
6185
|
+
}
|
|
6186
|
+
values.push(queryMap.get(attrName));
|
|
6187
|
+
}
|
|
6188
|
+
return values;
|
|
6189
|
+
}
|
|
5440
6190
|
/**
|
|
5441
6191
|
* Optimize OR query.
|
|
5442
6192
|
* Strategy: Union of all child results with deduplication.
|
|
@@ -5993,7 +6743,9 @@ var MEMORY_OVERHEAD_ESTIMATES = {
|
|
|
5993
6743
|
/** Navigable index overhead per record */
|
|
5994
6744
|
navigable: 32,
|
|
5995
6745
|
/** Inverted index overhead per record (depends on token count) */
|
|
5996
|
-
inverted: 48
|
|
6746
|
+
inverted: 48,
|
|
6747
|
+
/** Compound index overhead per record (includes composite key) */
|
|
6748
|
+
compound: 40
|
|
5997
6749
|
};
|
|
5998
6750
|
|
|
5999
6751
|
// src/query/adaptive/QueryPatternTracker.ts
|
|
@@ -6010,6 +6762,7 @@ function parseStatsKey(key) {
|
|
|
6010
6762
|
var QueryPatternTracker = class {
|
|
6011
6763
|
constructor(options = {}) {
|
|
6012
6764
|
this.stats = /* @__PURE__ */ new Map();
|
|
6765
|
+
this.compoundStats = /* @__PURE__ */ new Map();
|
|
6013
6766
|
this.queryCounter = 0;
|
|
6014
6767
|
this.samplingRate = options.samplingRate ?? TRACKING_SAMPLE_RATE;
|
|
6015
6768
|
this.maxTrackedPatterns = options.maxTrackedPatterns ?? 1e3;
|
|
@@ -6056,6 +6809,94 @@ var QueryPatternTracker = class {
|
|
|
6056
6809
|
});
|
|
6057
6810
|
}
|
|
6058
6811
|
}
|
|
6812
|
+
/**
|
|
6813
|
+
* Record a compound (AND) query execution for pattern tracking (Phase 9.03).
|
|
6814
|
+
*
|
|
6815
|
+
* @param attributes - Array of attribute names being queried together
|
|
6816
|
+
* @param executionTime - Query execution time in milliseconds
|
|
6817
|
+
* @param resultSize - Number of results returned
|
|
6818
|
+
* @param hasCompoundIndex - Whether a compound index was used
|
|
6819
|
+
*/
|
|
6820
|
+
recordCompoundQuery(attributes, executionTime, resultSize, hasCompoundIndex) {
|
|
6821
|
+
if (attributes.length < 2) return;
|
|
6822
|
+
this.queryCounter++;
|
|
6823
|
+
if (this.samplingRate > 1 && this.queryCounter % this.samplingRate !== 0) {
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
const sortedAttrs = [...attributes].sort();
|
|
6827
|
+
const compoundKey = sortedAttrs.join("+");
|
|
6828
|
+
const existing = this.compoundStats.get(compoundKey);
|
|
6829
|
+
const now = Date.now();
|
|
6830
|
+
if (existing) {
|
|
6831
|
+
existing.queryCount++;
|
|
6832
|
+
existing.totalCost += executionTime;
|
|
6833
|
+
existing.averageCost = existing.totalCost / existing.queryCount;
|
|
6834
|
+
existing.lastQueried = now;
|
|
6835
|
+
existing.hasCompoundIndex = hasCompoundIndex;
|
|
6836
|
+
} else {
|
|
6837
|
+
if (this.compoundStats.size >= this.maxTrackedPatterns) {
|
|
6838
|
+
this.evictOldestCompound();
|
|
6839
|
+
}
|
|
6840
|
+
this.compoundStats.set(compoundKey, {
|
|
6841
|
+
attributes: sortedAttrs,
|
|
6842
|
+
compoundKey,
|
|
6843
|
+
queryCount: this.samplingRate,
|
|
6844
|
+
// Adjust for sampling
|
|
6845
|
+
totalCost: executionTime * this.samplingRate,
|
|
6846
|
+
averageCost: executionTime,
|
|
6847
|
+
lastQueried: now,
|
|
6848
|
+
hasCompoundIndex
|
|
6849
|
+
});
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
/**
|
|
6853
|
+
* Get all compound query statistics (Phase 9.03).
|
|
6854
|
+
*
|
|
6855
|
+
* @returns Array of compound query statistics, sorted by query count descending
|
|
6856
|
+
*/
|
|
6857
|
+
getCompoundStatistics() {
|
|
6858
|
+
this.pruneStaleCompound();
|
|
6859
|
+
return Array.from(this.compoundStats.values()).sort((a, b) => b.queryCount - a.queryCount);
|
|
6860
|
+
}
|
|
6861
|
+
/**
|
|
6862
|
+
* Get compound statistics for a specific attribute combination.
|
|
6863
|
+
*
|
|
6864
|
+
* @param attributes - Array of attribute names
|
|
6865
|
+
* @returns Compound query statistics or undefined
|
|
6866
|
+
*/
|
|
6867
|
+
getCompoundStats(attributes) {
|
|
6868
|
+
const sortedAttrs = [...attributes].sort();
|
|
6869
|
+
const compoundKey = sortedAttrs.join("+");
|
|
6870
|
+
return this.compoundStats.get(compoundKey);
|
|
6871
|
+
}
|
|
6872
|
+
/**
|
|
6873
|
+
* Check if attributes appear in any tracked compound queries.
|
|
6874
|
+
*
|
|
6875
|
+
* @param attribute - The attribute name to check
|
|
6876
|
+
* @returns True if attribute is part of any compound query pattern
|
|
6877
|
+
*/
|
|
6878
|
+
isInCompoundPattern(attribute) {
|
|
6879
|
+
for (const stat of this.compoundStats.values()) {
|
|
6880
|
+
if (stat.attributes.includes(attribute)) {
|
|
6881
|
+
return true;
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6884
|
+
return false;
|
|
6885
|
+
}
|
|
6886
|
+
/**
|
|
6887
|
+
* Update compound index status.
|
|
6888
|
+
*
|
|
6889
|
+
* @param attributes - Array of attribute names
|
|
6890
|
+
* @param hasCompoundIndex - Whether a compound index exists
|
|
6891
|
+
*/
|
|
6892
|
+
updateCompoundIndexStatus(attributes, hasCompoundIndex) {
|
|
6893
|
+
const sortedAttrs = [...attributes].sort();
|
|
6894
|
+
const compoundKey = sortedAttrs.join("+");
|
|
6895
|
+
const stat = this.compoundStats.get(compoundKey);
|
|
6896
|
+
if (stat) {
|
|
6897
|
+
stat.hasCompoundIndex = hasCompoundIndex;
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6059
6900
|
/**
|
|
6060
6901
|
* Get all query statistics.
|
|
6061
6902
|
*
|
|
@@ -6153,6 +6994,7 @@ var QueryPatternTracker = class {
|
|
|
6153
6994
|
*/
|
|
6154
6995
|
clear() {
|
|
6155
6996
|
this.stats.clear();
|
|
6997
|
+
this.compoundStats.clear();
|
|
6156
6998
|
this.queryCounter = 0;
|
|
6157
6999
|
}
|
|
6158
7000
|
/**
|
|
@@ -6161,9 +7003,10 @@ var QueryPatternTracker = class {
|
|
|
6161
7003
|
* @returns Tracking overhead info
|
|
6162
7004
|
*/
|
|
6163
7005
|
getTrackingInfo() {
|
|
6164
|
-
const memoryEstimate = this.stats.size * 200;
|
|
7006
|
+
const memoryEstimate = this.stats.size * 200 + this.compoundStats.size * 300;
|
|
6165
7007
|
return {
|
|
6166
7008
|
patternsTracked: this.stats.size,
|
|
7009
|
+
compoundPatternsTracked: this.compoundStats.size,
|
|
6167
7010
|
totalQueries: this.queryCounter,
|
|
6168
7011
|
samplingRate: this.samplingRate,
|
|
6169
7012
|
memoryEstimate
|
|
@@ -6196,6 +7039,33 @@ var QueryPatternTracker = class {
|
|
|
6196
7039
|
}
|
|
6197
7040
|
}
|
|
6198
7041
|
}
|
|
7042
|
+
/**
|
|
7043
|
+
* Evict the oldest compound query entry (Phase 9.03).
|
|
7044
|
+
*/
|
|
7045
|
+
evictOldestCompound() {
|
|
7046
|
+
let oldestKey = null;
|
|
7047
|
+
let oldestTime = Infinity;
|
|
7048
|
+
for (const [key, stat] of this.compoundStats.entries()) {
|
|
7049
|
+
if (stat.lastQueried < oldestTime) {
|
|
7050
|
+
oldestTime = stat.lastQueried;
|
|
7051
|
+
oldestKey = key;
|
|
7052
|
+
}
|
|
7053
|
+
}
|
|
7054
|
+
if (oldestKey) {
|
|
7055
|
+
this.compoundStats.delete(oldestKey);
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
/**
|
|
7059
|
+
* Prune stale compound statistics (Phase 9.03).
|
|
7060
|
+
*/
|
|
7061
|
+
pruneStaleCompound() {
|
|
7062
|
+
const cutoff = Date.now() - this.statsTtl;
|
|
7063
|
+
for (const [key, stat] of this.compoundStats.entries()) {
|
|
7064
|
+
if (stat.lastQueried < cutoff) {
|
|
7065
|
+
this.compoundStats.delete(key);
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
6199
7069
|
};
|
|
6200
7070
|
|
|
6201
7071
|
// src/query/adaptive/IndexAdvisor.ts
|
|
@@ -6229,6 +7099,12 @@ var IndexAdvisor = class {
|
|
|
6229
7099
|
suggestions.push(suggestion);
|
|
6230
7100
|
}
|
|
6231
7101
|
}
|
|
7102
|
+
const compoundSuggestions = this.getCompoundSuggestions({
|
|
7103
|
+
minQueryCount,
|
|
7104
|
+
minAverageCost,
|
|
7105
|
+
excludeExistingIndexes
|
|
7106
|
+
});
|
|
7107
|
+
suggestions.push(...compoundSuggestions);
|
|
6232
7108
|
suggestions.sort((a, b) => {
|
|
6233
7109
|
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|
6234
7110
|
const priorityDiff = priorityOrder[b.priority] - priorityOrder[a.priority];
|
|
@@ -6428,6 +7304,136 @@ var IndexAdvisor = class {
|
|
|
6428
7304
|
}
|
|
6429
7305
|
return reason;
|
|
6430
7306
|
}
|
|
7307
|
+
// ========================================
|
|
7308
|
+
// Phase 9.03: Compound Index Suggestions
|
|
7309
|
+
// ========================================
|
|
7310
|
+
/**
|
|
7311
|
+
* Get compound index suggestions based on AND query patterns.
|
|
7312
|
+
*
|
|
7313
|
+
* @param options - Suggestion options
|
|
7314
|
+
* @returns Array of compound index suggestions
|
|
7315
|
+
*/
|
|
7316
|
+
getCompoundSuggestions(options = {}) {
|
|
7317
|
+
const {
|
|
7318
|
+
minQueryCount = ADAPTIVE_INDEXING_DEFAULTS.advisor.minQueryCount,
|
|
7319
|
+
minAverageCost = ADAPTIVE_INDEXING_DEFAULTS.advisor.minAverageCost,
|
|
7320
|
+
excludeExistingIndexes = true
|
|
7321
|
+
} = options;
|
|
7322
|
+
const compoundStats = this.tracker.getCompoundStatistics();
|
|
7323
|
+
const suggestions = [];
|
|
7324
|
+
for (const stat of compoundStats) {
|
|
7325
|
+
if (excludeExistingIndexes && stat.hasCompoundIndex) continue;
|
|
7326
|
+
if (stat.queryCount < minQueryCount) continue;
|
|
7327
|
+
if (stat.averageCost < minAverageCost) continue;
|
|
7328
|
+
const suggestion = this.generateCompoundSuggestion(stat);
|
|
7329
|
+
if (suggestion) {
|
|
7330
|
+
suggestions.push(suggestion);
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
return suggestions;
|
|
7334
|
+
}
|
|
7335
|
+
/**
|
|
7336
|
+
* Get a suggestion for a specific compound attribute combination.
|
|
7337
|
+
*
|
|
7338
|
+
* @param attributes - Array of attribute names
|
|
7339
|
+
* @returns Compound index suggestion or null if not recommended
|
|
7340
|
+
*/
|
|
7341
|
+
getCompoundSuggestionFor(attributes) {
|
|
7342
|
+
const stat = this.tracker.getCompoundStats(attributes);
|
|
7343
|
+
if (!stat) return null;
|
|
7344
|
+
return this.generateCompoundSuggestion(stat);
|
|
7345
|
+
}
|
|
7346
|
+
/**
|
|
7347
|
+
* Check if a compound index should be created for the given attributes.
|
|
7348
|
+
*
|
|
7349
|
+
* @param attributes - Array of attribute names
|
|
7350
|
+
* @param threshold - Minimum query count threshold
|
|
7351
|
+
* @returns True if compound index should be created
|
|
7352
|
+
*/
|
|
7353
|
+
shouldCreateCompoundIndex(attributes, threshold = ADAPTIVE_INDEXING_DEFAULTS.autoIndex.threshold) {
|
|
7354
|
+
const stat = this.tracker.getCompoundStats(attributes);
|
|
7355
|
+
if (!stat) return false;
|
|
7356
|
+
return !stat.hasCompoundIndex && stat.queryCount >= threshold;
|
|
7357
|
+
}
|
|
7358
|
+
/**
|
|
7359
|
+
* Generate a suggestion for a compound query pattern.
|
|
7360
|
+
*/
|
|
7361
|
+
generateCompoundSuggestion(stat) {
|
|
7362
|
+
const estimatedBenefit = this.estimateCompoundBenefit(stat);
|
|
7363
|
+
const estimatedCost = this.estimateCompoundMemoryCost(stat);
|
|
7364
|
+
const priority = this.calculateCompoundPriority(stat, estimatedBenefit);
|
|
7365
|
+
return {
|
|
7366
|
+
attribute: stat.compoundKey,
|
|
7367
|
+
indexType: "compound",
|
|
7368
|
+
reason: this.generateCompoundReason(stat, estimatedBenefit),
|
|
7369
|
+
estimatedBenefit,
|
|
7370
|
+
estimatedCost,
|
|
7371
|
+
priority,
|
|
7372
|
+
queryCount: stat.queryCount,
|
|
7373
|
+
averageCost: stat.averageCost,
|
|
7374
|
+
compoundAttributes: stat.attributes
|
|
7375
|
+
};
|
|
7376
|
+
}
|
|
7377
|
+
/**
|
|
7378
|
+
* Estimate performance benefit of adding a compound index.
|
|
7379
|
+
*
|
|
7380
|
+
* Compound indexes provide significant speedup for AND queries:
|
|
7381
|
+
* - Eliminates intersection operations (100-1000× for each attribute)
|
|
7382
|
+
* - Single O(1) lookup instead of multiple index scans
|
|
7383
|
+
*/
|
|
7384
|
+
estimateCompoundBenefit(stat) {
|
|
7385
|
+
const attributeMultiplier = Math.pow(2, stat.attributes.length - 1);
|
|
7386
|
+
let baseBenefit;
|
|
7387
|
+
if (stat.averageCost > 20) {
|
|
7388
|
+
baseBenefit = 1e3;
|
|
7389
|
+
} else if (stat.averageCost > 5) {
|
|
7390
|
+
baseBenefit = 500;
|
|
7391
|
+
} else if (stat.averageCost > 1) {
|
|
7392
|
+
baseBenefit = 100;
|
|
7393
|
+
} else {
|
|
7394
|
+
baseBenefit = 50;
|
|
7395
|
+
}
|
|
7396
|
+
const frequencyMultiplier = Math.min(stat.queryCount / 10, 100);
|
|
7397
|
+
return Math.floor(baseBenefit * attributeMultiplier * frequencyMultiplier);
|
|
7398
|
+
}
|
|
7399
|
+
/**
|
|
7400
|
+
* Estimate memory cost of adding a compound index.
|
|
7401
|
+
*/
|
|
7402
|
+
estimateCompoundMemoryCost(stat) {
|
|
7403
|
+
const bytesPerRecord = MEMORY_OVERHEAD_ESTIMATES.compound;
|
|
7404
|
+
const attributeOverhead = stat.attributes.length * 8;
|
|
7405
|
+
const estimatedRecords = 1e3;
|
|
7406
|
+
return Math.floor(estimatedRecords * (bytesPerRecord + attributeOverhead) * 1.5);
|
|
7407
|
+
}
|
|
7408
|
+
/**
|
|
7409
|
+
* Calculate priority for compound index suggestion.
|
|
7410
|
+
*/
|
|
7411
|
+
calculateCompoundPriority(stat, estimatedBenefit) {
|
|
7412
|
+
if (stat.queryCount > 100 && stat.averageCost > 10) {
|
|
7413
|
+
return "high";
|
|
7414
|
+
}
|
|
7415
|
+
if (stat.queryCount > 500) {
|
|
7416
|
+
return "high";
|
|
7417
|
+
}
|
|
7418
|
+
if (stat.queryCount > 50 || stat.averageCost > 5) {
|
|
7419
|
+
return "medium";
|
|
7420
|
+
}
|
|
7421
|
+
if (estimatedBenefit > 2e3) {
|
|
7422
|
+
return "medium";
|
|
7423
|
+
}
|
|
7424
|
+
return "low";
|
|
7425
|
+
}
|
|
7426
|
+
/**
|
|
7427
|
+
* Generate human-readable reason for compound index suggestion.
|
|
7428
|
+
*/
|
|
7429
|
+
generateCompoundReason(stat, benefit) {
|
|
7430
|
+
const costStr = stat.averageCost.toFixed(2);
|
|
7431
|
+
const attrList = stat.attributes.join(", ");
|
|
7432
|
+
let reason = `Compound AND query on [${attrList}] executed ${stat.queryCount}\xD7 with average cost ${costStr}ms. `;
|
|
7433
|
+
reason += `Expected ~${benefit}\xD7 cumulative speedup with compound index. `;
|
|
7434
|
+
reason += `Eliminates ${stat.attributes.length - 1} ResultSet intersection(s).`;
|
|
7435
|
+
return reason;
|
|
7436
|
+
}
|
|
6431
7437
|
};
|
|
6432
7438
|
|
|
6433
7439
|
// src/query/adaptive/AutoIndexManager.ts
|
|
@@ -6913,11 +7919,20 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
6913
7919
|
// ==================== Index Management ====================
|
|
6914
7920
|
/**
|
|
6915
7921
|
* Add a hash index on an attribute.
|
|
7922
|
+
* If lazyIndexBuilding is enabled, creates a LazyHashIndex instead.
|
|
6916
7923
|
*
|
|
6917
7924
|
* @param attribute - Attribute to index
|
|
6918
|
-
* @returns Created HashIndex
|
|
7925
|
+
* @returns Created HashIndex (or LazyHashIndex)
|
|
6919
7926
|
*/
|
|
6920
7927
|
addHashIndex(attribute) {
|
|
7928
|
+
if (this.options.lazyIndexBuilding) {
|
|
7929
|
+
const index2 = new LazyHashIndex(attribute, {
|
|
7930
|
+
onProgress: this.options.onIndexBuilding
|
|
7931
|
+
});
|
|
7932
|
+
this.indexRegistry.addIndex(index2);
|
|
7933
|
+
this.buildIndex(index2);
|
|
7934
|
+
return index2;
|
|
7935
|
+
}
|
|
6921
7936
|
const index = new HashIndex(attribute);
|
|
6922
7937
|
this.indexRegistry.addIndex(index);
|
|
6923
7938
|
this.buildIndex(index);
|
|
@@ -6926,12 +7941,21 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
6926
7941
|
/**
|
|
6927
7942
|
* Add a navigable index on an attribute.
|
|
6928
7943
|
* Navigable indexes support range queries (gt, gte, lt, lte, between).
|
|
7944
|
+
* If lazyIndexBuilding is enabled, creates a LazyNavigableIndex instead.
|
|
6929
7945
|
*
|
|
6930
7946
|
* @param attribute - Attribute to index
|
|
6931
7947
|
* @param comparator - Optional custom comparator
|
|
6932
|
-
* @returns Created NavigableIndex
|
|
7948
|
+
* @returns Created NavigableIndex (or LazyNavigableIndex)
|
|
6933
7949
|
*/
|
|
6934
7950
|
addNavigableIndex(attribute, comparator) {
|
|
7951
|
+
if (this.options.lazyIndexBuilding) {
|
|
7952
|
+
const index2 = new LazyNavigableIndex(attribute, comparator, {
|
|
7953
|
+
onProgress: this.options.onIndexBuilding
|
|
7954
|
+
});
|
|
7955
|
+
this.indexRegistry.addIndex(index2);
|
|
7956
|
+
this.buildIndex(index2);
|
|
7957
|
+
return index2;
|
|
7958
|
+
}
|
|
6935
7959
|
const index = new NavigableIndex(attribute, comparator);
|
|
6936
7960
|
this.indexRegistry.addIndex(index);
|
|
6937
7961
|
this.buildIndex(index);
|
|
@@ -6940,10 +7964,11 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
6940
7964
|
/**
|
|
6941
7965
|
* Add an inverted index for full-text search on an attribute.
|
|
6942
7966
|
* Inverted indexes support text search queries (contains, containsAll, containsAny).
|
|
7967
|
+
* If lazyIndexBuilding is enabled, creates a LazyInvertedIndex instead.
|
|
6943
7968
|
*
|
|
6944
7969
|
* @param attribute - Text attribute to index
|
|
6945
7970
|
* @param pipeline - Optional custom tokenization pipeline
|
|
6946
|
-
* @returns Created InvertedIndex
|
|
7971
|
+
* @returns Created InvertedIndex (or LazyInvertedIndex)
|
|
6947
7972
|
*
|
|
6948
7973
|
* @example
|
|
6949
7974
|
* ```typescript
|
|
@@ -6955,6 +7980,14 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
6955
7980
|
* ```
|
|
6956
7981
|
*/
|
|
6957
7982
|
addInvertedIndex(attribute, pipeline) {
|
|
7983
|
+
if (this.options.lazyIndexBuilding) {
|
|
7984
|
+
const index2 = new LazyInvertedIndex(attribute, pipeline, {
|
|
7985
|
+
onProgress: this.options.onIndexBuilding
|
|
7986
|
+
});
|
|
7987
|
+
this.indexRegistry.addIndex(index2);
|
|
7988
|
+
this.buildIndex(index2);
|
|
7989
|
+
return index2;
|
|
7990
|
+
}
|
|
6958
7991
|
const index = new InvertedIndex(attribute, pipeline);
|
|
6959
7992
|
this.indexRegistry.addIndex(index);
|
|
6960
7993
|
this.buildIndex(index);
|
|
@@ -7459,6 +8492,58 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
7459
8492
|
isAutoIndexingEnabled() {
|
|
7460
8493
|
return this.autoIndexManager !== null;
|
|
7461
8494
|
}
|
|
8495
|
+
// ==================== Lazy Indexing (Phase 9.01) ====================
|
|
8496
|
+
/**
|
|
8497
|
+
* Check if lazy index building is enabled.
|
|
8498
|
+
*/
|
|
8499
|
+
isLazyIndexingEnabled() {
|
|
8500
|
+
return this.options.lazyIndexBuilding === true;
|
|
8501
|
+
}
|
|
8502
|
+
/**
|
|
8503
|
+
* Force materialization of all lazy indexes.
|
|
8504
|
+
* Useful to pre-warm indexes before critical operations.
|
|
8505
|
+
*
|
|
8506
|
+
* @param progressCallback - Optional progress callback
|
|
8507
|
+
*/
|
|
8508
|
+
materializeAllIndexes(progressCallback) {
|
|
8509
|
+
const callback = progressCallback ?? this.options.onIndexBuilding;
|
|
8510
|
+
for (const index of this.indexRegistry.getAllIndexes()) {
|
|
8511
|
+
if ("isLazy" in index && index.isLazy) {
|
|
8512
|
+
const lazyIndex = index;
|
|
8513
|
+
if (!lazyIndex.isBuilt) {
|
|
8514
|
+
lazyIndex.materialize(callback);
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
/**
|
|
8520
|
+
* Get count of pending records across all lazy indexes.
|
|
8521
|
+
* Returns 0 if no lazy indexes or all are materialized.
|
|
8522
|
+
*/
|
|
8523
|
+
getPendingIndexCount() {
|
|
8524
|
+
let total = 0;
|
|
8525
|
+
for (const index of this.indexRegistry.getAllIndexes()) {
|
|
8526
|
+
if ("isLazy" in index && index.isLazy) {
|
|
8527
|
+
const lazyIndex = index;
|
|
8528
|
+
total += lazyIndex.pendingCount;
|
|
8529
|
+
}
|
|
8530
|
+
}
|
|
8531
|
+
return total;
|
|
8532
|
+
}
|
|
8533
|
+
/**
|
|
8534
|
+
* Check if any lazy indexes are still pending (not built).
|
|
8535
|
+
*/
|
|
8536
|
+
hasUnbuiltIndexes() {
|
|
8537
|
+
for (const index of this.indexRegistry.getAllIndexes()) {
|
|
8538
|
+
if ("isLazy" in index && index.isLazy) {
|
|
8539
|
+
const lazyIndex = index;
|
|
8540
|
+
if (!lazyIndex.isBuilt) {
|
|
8541
|
+
return true;
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
}
|
|
8545
|
+
return false;
|
|
8546
|
+
}
|
|
7462
8547
|
/**
|
|
7463
8548
|
* Track query pattern for adaptive indexing.
|
|
7464
8549
|
*/
|