document-dataply 0.0.10-alpha.5 → 0.0.10-alpha.7

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/cjs/index.js CHANGED
@@ -10466,6 +10466,26 @@ function tokenize(text, options) {
10466
10466
  }
10467
10467
 
10468
10468
  // src/core/Optimizer.ts
10469
+ var SELECTIVITY = {
10470
+ /** O(log N) 포인트 룩업 */
10471
+ EQUAL: 0.01,
10472
+ /** 양쪽 바운드(gte+lte) 범위 스캔 */
10473
+ BOUNDED_RANGE: 0.33,
10474
+ /** 한쪽 바운드(gte 또는 lte)만 있을 때, 중간부터 풀스캔 */
10475
+ HALF_RANGE: 0.5,
10476
+ /** Or 조건: B+Tree 내부 풀스캔 */
10477
+ OR: 0.9,
10478
+ /** Like 조건: B+Tree 내부 풀스캔 */
10479
+ LIKE: 0.9,
10480
+ /** 알 수 없는 조건 */
10481
+ UNKNOWN: 0.9,
10482
+ /** FTS 통계 없을 때 보수적 추정 */
10483
+ FTS_DEFAULT: 0.5,
10484
+ /** 정렬 비용 가중치 (orderBy 미지원 시) */
10485
+ SORT_PENALTY: 0.5,
10486
+ /** 인메모리 정렬이 유의미해지는 임계 문서 수 */
10487
+ SORT_THRESHOLD: 1e4
10488
+ };
10469
10489
  var Optimizer = class {
10470
10490
  constructor(api) {
10471
10491
  this.api = api;
@@ -10477,7 +10497,7 @@ var Optimizer = class {
10477
10497
  const primaryField = config.fields[0];
10478
10498
  if (!queryFields.has(primaryField)) return null;
10479
10499
  const builtCondition = {};
10480
- let score = 0;
10500
+ let selectivity = 1;
10481
10501
  let isConsecutive = true;
10482
10502
  const coveredFields = [];
10483
10503
  const compositeVerifyFields = [];
@@ -10492,13 +10512,12 @@ var Optimizer = class {
10492
10512
  continue;
10493
10513
  }
10494
10514
  coveredFields.push(field);
10495
- score += 1;
10496
10515
  if (isConsecutive) {
10497
10516
  const cond = query[field];
10498
10517
  if (cond !== void 0) {
10499
10518
  let isBounded = false;
10500
10519
  if (typeof cond !== "object" || cond === null) {
10501
- score += 100;
10520
+ selectivity *= SELECTIVITY.EQUAL;
10502
10521
  startValues.push(cond);
10503
10522
  endValues.push(cond);
10504
10523
  startOperator = "primaryGte";
@@ -10506,7 +10525,7 @@ var Optimizer = class {
10506
10525
  isBounded = true;
10507
10526
  } else if ("primaryEqual" in cond || "equal" in cond) {
10508
10527
  const val = cond.primaryEqual?.v ?? cond.equal?.v ?? cond.primaryEqual ?? cond.equal;
10509
- score += 100;
10528
+ selectivity *= SELECTIVITY.EQUAL;
10510
10529
  startValues.push(val);
10511
10530
  endValues.push(val);
10512
10531
  startOperator = "primaryGte";
@@ -10514,7 +10533,7 @@ var Optimizer = class {
10514
10533
  isBounded = true;
10515
10534
  } else if ("primaryGte" in cond || "gte" in cond) {
10516
10535
  const val = cond.primaryGte?.v ?? cond.gte?.v ?? cond.primaryGte ?? cond.gte;
10517
- score += 50;
10536
+ selectivity *= SELECTIVITY.HALF_RANGE;
10518
10537
  isConsecutive = false;
10519
10538
  startValues.push(val);
10520
10539
  startOperator = "primaryGte";
@@ -10522,7 +10541,7 @@ var Optimizer = class {
10522
10541
  isBounded = true;
10523
10542
  } else if ("primaryGt" in cond || "gt" in cond) {
10524
10543
  const val = cond.primaryGt?.v ?? cond.gt?.v ?? cond.primaryGt ?? cond.gt;
10525
- score += 50;
10544
+ selectivity *= SELECTIVITY.HALF_RANGE;
10526
10545
  isConsecutive = false;
10527
10546
  startValues.push(val);
10528
10547
  startOperator = "primaryGt";
@@ -10530,7 +10549,7 @@ var Optimizer = class {
10530
10549
  isBounded = true;
10531
10550
  } else if ("primaryLte" in cond || "lte" in cond) {
10532
10551
  const val = cond.primaryLte?.v ?? cond.lte?.v ?? cond.primaryLte ?? cond.lte;
10533
- score += 50;
10552
+ selectivity *= SELECTIVITY.HALF_RANGE;
10534
10553
  isConsecutive = false;
10535
10554
  endValues.push(val);
10536
10555
  endOperator = "primaryLte";
@@ -10538,20 +10557,20 @@ var Optimizer = class {
10538
10557
  isBounded = true;
10539
10558
  } else if ("primaryLt" in cond || "lt" in cond) {
10540
10559
  const val = cond.primaryLt?.v ?? cond.lt?.v ?? cond.primaryLt ?? cond.lt;
10541
- score += 50;
10560
+ selectivity *= SELECTIVITY.HALF_RANGE;
10542
10561
  isConsecutive = false;
10543
10562
  endValues.push(val);
10544
10563
  endOperator = "primaryLt";
10545
10564
  if (startValues.length > 0) startOperator = "primaryGte";
10546
10565
  isBounded = true;
10547
10566
  } else if ("primaryOr" in cond || "or" in cond) {
10548
- score += 20;
10567
+ selectivity *= SELECTIVITY.OR;
10549
10568
  isConsecutive = false;
10550
10569
  } else if ("like" in cond) {
10551
- score += 15;
10570
+ selectivity *= SELECTIVITY.LIKE;
10552
10571
  isConsecutive = false;
10553
10572
  } else {
10554
- score += 10;
10573
+ selectivity *= SELECTIVITY.UNKNOWN;
10555
10574
  isConsecutive = false;
10556
10575
  }
10557
10576
  if (!isBounded && field !== primaryField) {
@@ -10598,9 +10617,6 @@ var Optimizer = class {
10598
10617
  }
10599
10618
  if (!isExactMatch) break;
10600
10619
  }
10601
- if (isIndexOrderSupported) {
10602
- score += 200;
10603
- }
10604
10620
  }
10605
10621
  return {
10606
10622
  tree: treeTx,
@@ -10608,7 +10624,7 @@ var Optimizer = class {
10608
10624
  field: primaryField,
10609
10625
  indexName,
10610
10626
  isFtsMatch: false,
10611
- score,
10627
+ selectivity,
10612
10628
  compositeVerifyFields,
10613
10629
  coveredFields,
10614
10630
  isIndexOrderSupported
@@ -10616,7 +10632,7 @@ var Optimizer = class {
10616
10632
  }
10617
10633
  /**
10618
10634
  * FTS 타입 인덱스의 선택도를 평가합니다.
10619
- * FTSTermCount 통계가 있으면 토큰 빈도 기반 동적 score를 산출합니다.
10635
+ * FTSTermCount 통계가 있으면 실측 데이터 기반으로 선택도를 산출합니다.
10620
10636
  */
10621
10637
  evaluateFTSCandidate(indexName, config, query, queryFields, treeTx) {
10622
10638
  const field = config.fields;
@@ -10625,18 +10641,14 @@ var Optimizer = class {
10625
10641
  if (!condition || typeof condition !== "object" || !("match" in condition)) return null;
10626
10642
  const ftsConfig = this.api.indexManager.getFtsConfig(config);
10627
10643
  const matchTokens = ftsConfig ? tokenize(condition.match, ftsConfig) : [];
10628
- const MAX_FTS_SCORE = 400;
10629
- const MIN_FTS_SCORE = 10;
10630
- const DEFAULT_FTS_SCORE = 90;
10631
- let score = DEFAULT_FTS_SCORE;
10644
+ let selectivity = SELECTIVITY.FTS_DEFAULT;
10632
10645
  const termCountProvider = this.api.analysisManager.getProvider("fts_term_count");
10633
10646
  if (termCountProvider && termCountProvider.hasSampleData && ftsConfig && matchTokens.length > 0) {
10634
10647
  const strategy = ftsConfig.tokenizer === "ngram" ? `${ftsConfig.gramSize}gram` : ftsConfig.tokenizer;
10635
10648
  const minCount = termCountProvider.getMinTokenCount(field, strategy, matchTokens);
10636
10649
  if (minCount >= 0) {
10637
10650
  const sampleSize = termCountProvider.getSampleSize();
10638
- const selectivityRatio = Math.min(minCount / sampleSize, 1);
10639
- score = Math.round(MAX_FTS_SCORE * (1 - selectivityRatio) + MIN_FTS_SCORE);
10651
+ selectivity = Math.min(minCount / sampleSize, 1);
10640
10652
  }
10641
10653
  }
10642
10654
  return {
@@ -10646,18 +10658,36 @@ var Optimizer = class {
10646
10658
  indexName,
10647
10659
  isFtsMatch: true,
10648
10660
  matchTokens,
10649
- score,
10661
+ selectivity,
10650
10662
  compositeVerifyFields: [],
10651
10663
  coveredFields: [field],
10652
10664
  isIndexOrderSupported: false
10653
10665
  };
10654
10666
  }
10655
10667
  /**
10656
- * 실행할 최적의 인덱스를 선택합니다. (최적 드라이버 선택)
10668
+ * 비용 계산: effectiveScanCost + sortPenalty
10669
+ * - effectiveScanCost: 인덱스 순서 지원 + limit 존재 시 조기 종료 이점 반영
10670
+ * - sortPenalty: 인메모리 정렬의 절대 문서 수 기반 비용
10671
+ * - hasUncoveredFilters: 드라이버가 커버하지 못하는 비-FTS 필터 존재 여부
10672
+ * true일 경우 topK/N 조기 종료를 적용하지 않음
10673
+ * (uncovered 필터가 행을 탈락시킬 수 있어 topK개 이상 스캔 필요)
10657
10674
  */
10658
- async getSelectivityCandidate(query, orderByField) {
10675
+ calculateCost(selectivity, isIndexOrderSupported, orderByField, N, topK, hasUncoveredFilters = false) {
10676
+ const effectiveScanCost = isIndexOrderSupported && isFinite(topK) && N > 0 && !hasUncoveredFilters ? Math.min(topK / N, selectivity) : selectivity;
10677
+ const estimatedSortDocs = selectivity * N;
10678
+ const sortPenalty = orderByField && !isIndexOrderSupported ? Math.min(estimatedSortDocs / SELECTIVITY.SORT_THRESHOLD, 1) * SELECTIVITY.SORT_PENALTY : 0;
10679
+ return effectiveScanCost + sortPenalty;
10680
+ }
10681
+ /**
10682
+ * 실행할 최적의 인덱스를 선택합니다. (비용 기반 최적 드라이버 선택)
10683
+ * cost = selectivity + sortPenalty (낮을수록 좋음)
10684
+ */
10685
+ async getSelectivityCandidate(query, orderByField, limit = Infinity, offset = 0) {
10659
10686
  const queryFields = new Set(Object.keys(query));
10660
10687
  const candidates = [];
10688
+ const metadata = await this.api.getMetadata();
10689
+ const N = metadata.rowCount;
10690
+ const topK = isFinite(limit) ? offset + limit : Infinity;
10661
10691
  for (const [indexName, config] of this.api.indexManager.registeredIndices) {
10662
10692
  const tree = this.api.trees.get(indexName);
10663
10693
  if (!tree) continue;
@@ -10671,7 +10701,22 @@ var Optimizer = class {
10671
10701
  treeTx,
10672
10702
  orderByField
10673
10703
  );
10674
- if (candidate) candidates.push(candidate);
10704
+ if (candidate) {
10705
+ const hasUncoveredFilters = ![...queryFields].every(
10706
+ (f) => candidate.coveredFields.includes(f)
10707
+ );
10708
+ candidates.push({
10709
+ ...candidate,
10710
+ cost: this.calculateCost(
10711
+ candidate.selectivity,
10712
+ candidate.isIndexOrderSupported,
10713
+ orderByField,
10714
+ N,
10715
+ topK,
10716
+ hasUncoveredFilters
10717
+ )
10718
+ });
10719
+ }
10675
10720
  } else if (config.type === "fts") {
10676
10721
  const treeTx = await tree.createTransaction();
10677
10722
  const candidate = this.evaluateFTSCandidate(
@@ -10681,7 +10726,19 @@ var Optimizer = class {
10681
10726
  queryFields,
10682
10727
  treeTx
10683
10728
  );
10684
- if (candidate) candidates.push(candidate);
10729
+ if (candidate) {
10730
+ candidates.push({
10731
+ ...candidate,
10732
+ cost: this.calculateCost(
10733
+ candidate.selectivity,
10734
+ candidate.isIndexOrderSupported,
10735
+ orderByField,
10736
+ N,
10737
+ topK,
10738
+ true
10739
+ )
10740
+ });
10741
+ }
10685
10742
  }
10686
10743
  }
10687
10744
  const rollback = () => {
@@ -10694,7 +10751,7 @@ var Optimizer = class {
10694
10751
  return null;
10695
10752
  }
10696
10753
  candidates.sort((a, b) => {
10697
- if (b.score !== a.score) return b.score - a.score;
10754
+ if (a.cost !== b.cost) return a.cost - b.cost;
10698
10755
  const aConfig = this.api.indexManager.registeredIndices.get(a.indexName);
10699
10756
  const bConfig = this.api.indexManager.registeredIndices.get(b.indexName);
10700
10757
  const aFieldCount = aConfig ? Array.isArray(aConfig.fields) ? aConfig.fields.length : 1 : 0;
@@ -10709,8 +10766,8 @@ var Optimizer = class {
10709
10766
  const candidate = nonDriverCandidates[i];
10710
10767
  let isSubset = false;
10711
10768
  for (let j = 0, oLen = others.length; j < oLen; j++) {
10712
- const higher = others[j];
10713
- if (candidate.coveredFields.every((f) => higher.coveredFields.includes(f))) {
10769
+ const better = others[j];
10770
+ if (candidate.coveredFields.every((f) => better.coveredFields.includes(f))) {
10714
10771
  isSubset = true;
10715
10772
  break;
10716
10773
  }
@@ -10915,12 +10972,14 @@ var QueryManager = class {
10915
10972
  rollback();
10916
10973
  return new Float64Array(Array.from(keys || []));
10917
10974
  }
10918
- async getDriverKeys(query, orderBy, sortOrder = "asc") {
10975
+ async getDriverKeys(query, orderBy, sortOrder = "asc", limit = Infinity, offset = 0) {
10919
10976
  const isQueryEmpty = Object.keys(query).length === 0;
10920
10977
  const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
10921
10978
  const selectivity = await this.optimizer.getSelectivityCandidate(
10922
10979
  this.verboseQuery(normalizedQuery),
10923
- orderBy
10980
+ orderBy,
10981
+ limit,
10982
+ offset
10924
10983
  );
10925
10984
  if (!selectivity) return null;
10926
10985
  const { driver, others, compositeVerifyConditions, rollback } = selectivity;
@@ -10996,12 +11055,24 @@ var QueryManager = class {
10996
11055
  }
10997
11056
  return true;
10998
11057
  }
10999
- adjustChunkSize(currentChunkSize, chunkTotalSize) {
11000
- if (chunkTotalSize <= 0) return currentChunkSize;
11001
- const { verySmallChunkSize, smallChunkSize } = this.getFreeMemoryChunkSize();
11002
- if (chunkTotalSize < verySmallChunkSize) return currentChunkSize * 2;
11003
- if (chunkTotalSize > smallChunkSize) return Math.max(Math.floor(currentChunkSize / 2), 20);
11004
- return currentChunkSize;
11058
+ /**
11059
+ * 최적화 공식: x = x * Math.sqrt(z / n)
11060
+ * @param currentChunkSize 현재 청크 크기
11061
+ * @param matchedCount 매칭된 문서 개수
11062
+ * @param limit 최대 문서 개수
11063
+ * @param chunkTotalSize 청크 내 문서 총 크기
11064
+ * @returns
11065
+ */
11066
+ adjustChunkSize(currentChunkSize, matchedCount, limit, chunkTotalSize) {
11067
+ if (matchedCount <= 0 || chunkTotalSize <= 0) return currentChunkSize;
11068
+ const n = Math.max(matchedCount, 1);
11069
+ const z = isFinite(limit) ? limit : currentChunkSize * 10;
11070
+ const nextChunkSize = Math.ceil(currentChunkSize * Math.sqrt(z / n));
11071
+ const { smallChunkSize } = this.getFreeMemoryChunkSize();
11072
+ const avgDocSize = chunkTotalSize / currentChunkSize;
11073
+ const maxSafeChunkSize = Math.max(Math.floor(smallChunkSize / avgDocSize), 20);
11074
+ const finalChunkSize = Math.max(Math.min(nextChunkSize, maxSafeChunkSize), 20);
11075
+ return finalChunkSize;
11005
11076
  }
11006
11077
  async *processChunkedKeysWithVerify(keysStream, startIdx, initialChunkSize, limit, ftsConditions, compositeVerifyConditions, others, tx) {
11007
11078
  const verifyOthers = others.filter((o) => !o.isFtsMatch);
@@ -11017,6 +11088,7 @@ var QueryManager = class {
11017
11088
  let chunk = [];
11018
11089
  let chunkSize = 0;
11019
11090
  let dropped = 0;
11091
+ let nAccumulated = 0;
11020
11092
  const processChunk = async (pks) => {
11021
11093
  const docs = [];
11022
11094
  const rawResults = await this.api.selectMany(new Float64Array(pks), false, tx);
@@ -11068,8 +11140,9 @@ var QueryManager = class {
11068
11140
  }
11069
11141
  docs.push(doc);
11070
11142
  }
11143
+ nAccumulated += docs.length;
11071
11144
  if (!isReadQuotaLimited) {
11072
- currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
11145
+ currentChunkSize = this.adjustChunkSize(currentChunkSize, nAccumulated, limit, chunkTotalSize);
11073
11146
  }
11074
11147
  return docs;
11075
11148
  };
@@ -11139,12 +11212,13 @@ var QueryManager = class {
11139
11212
  }
11140
11213
  }
11141
11214
  }
11142
- const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11215
+ const driverResult = await self.getDriverKeys(query, orderByField, sortOrder, limit, offset);
11143
11216
  if (!driverResult) return;
11144
11217
  const { keysStream, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11145
11218
  const initialChunkSize = self.api.options.pageSize;
11219
+ const isInMemorySort = !isDriverOrderByField && orderByField;
11146
11220
  try {
11147
- if (!isDriverOrderByField && orderByField) {
11221
+ if (isInMemorySort) {
11148
11222
  const topK = limit === Infinity ? Infinity : offset + limit;
11149
11223
  let heap = null;
11150
11224
  if (topK !== Infinity) {
@@ -13,31 +13,41 @@ export declare class Optimizer<T extends Record<string, any>> {
13
13
  readonly field: any;
14
14
  readonly indexName: string;
15
15
  readonly isFtsMatch: false;
16
- readonly score: number;
16
+ readonly selectivity: number;
17
17
  readonly compositeVerifyFields: string[];
18
18
  readonly coveredFields: string[];
19
19
  readonly isIndexOrderSupported: boolean;
20
20
  } | null;
21
21
  /**
22
22
  * FTS 타입 인덱스의 선택도를 평가합니다.
23
- * FTSTermCount 통계가 있으면 토큰 빈도 기반 동적 score를 산출합니다.
23
+ * FTSTermCount 통계가 있으면 실측 데이터 기반으로 선택도를 산출합니다.
24
24
  */
25
25
  evaluateFTSCandidate<U extends Partial<DocumentDataplyQuery<T>>, V extends DataplyTreeValue<U>>(indexName: string, config: any, query: Partial<DocumentDataplyQuery<V>>, queryFields: Set<string>, treeTx: BPTreeAsync<string | number, V>): {
26
- readonly tree: BPTreeAsync<string | number, V>;
27
- readonly condition: any;
28
- readonly field: any;
29
- readonly indexName: string;
30
- readonly isFtsMatch: true;
31
- readonly matchTokens: string[];
32
- readonly score: number;
33
- readonly compositeVerifyFields: readonly [];
34
- readonly coveredFields: readonly [any];
35
- readonly isIndexOrderSupported: false;
26
+ tree: BPTreeAsync<string | number, V>;
27
+ condition: any;
28
+ field: any;
29
+ indexName: string;
30
+ isFtsMatch: boolean;
31
+ matchTokens: string[];
32
+ selectivity: number;
33
+ compositeVerifyFields: never[];
34
+ coveredFields: any[];
35
+ isIndexOrderSupported: boolean;
36
36
  } | null;
37
37
  /**
38
- * 실행할 최적의 인덱스를 선택합니다. (최적 드라이버 선택)
38
+ * 비용 계산: effectiveScanCost + sortPenalty
39
+ * - effectiveScanCost: 인덱스 순서 지원 + limit 존재 시 조기 종료 이점 반영
40
+ * - sortPenalty: 인메모리 정렬의 절대 문서 수 기반 비용
41
+ * - hasUncoveredFilters: 드라이버가 커버하지 못하는 비-FTS 필터 존재 여부
42
+ * true일 경우 topK/N 조기 종료를 적용하지 않음
43
+ * (uncovered 필터가 행을 탈락시킬 수 있어 topK개 이상 스캔 필요)
44
+ */
45
+ private calculateCost;
46
+ /**
47
+ * 실행할 최적의 인덱스를 선택합니다. (비용 기반 최적 드라이버 선택)
48
+ * cost = selectivity + sortPenalty (낮을수록 좋음)
39
49
  */
40
- getSelectivityCandidate<U extends Partial<DocumentDataplyQuery<T>>, V extends DataplyTreeValue<U>>(query: Partial<DocumentDataplyQuery<V>>, orderByField?: string): Promise<{
50
+ getSelectivityCandidate<U extends Partial<DocumentDataplyQuery<T>>, V extends DataplyTreeValue<U>>(query: Partial<DocumentDataplyQuery<V>>, orderByField?: string, limit?: number, offset?: number): Promise<{
41
51
  driver: ({
42
52
  tree: BPTreeAsync<number, V>;
43
53
  condition: Partial<DocumentDataplyCondition<U>>;
@@ -18,7 +18,7 @@ export declare class QueryManager<T extends DocumentJSON> {
18
18
  private applyCandidateByFTSStream;
19
19
  private applyCandidateStream;
20
20
  getKeys(query: Partial<DocumentDataplyQuery<T>>, orderBy?: string, sortOrder?: 'asc' | 'desc'): Promise<Float64Array>;
21
- getDriverKeys(query: Partial<DocumentDataplyQuery<T>>, orderBy?: string, sortOrder?: 'asc' | 'desc'): Promise<{
21
+ getDriverKeys(query: Partial<DocumentDataplyQuery<T>>, orderBy?: string, sortOrder?: 'asc' | 'desc', limit?: number, offset?: number): Promise<{
22
22
  keysStream: AsyncIterableIterator<number>;
23
23
  others: {
24
24
  tree: BPTreeAsync<string | number, DataplyTreeValue<Primitive>>;
@@ -45,7 +45,15 @@ export declare class QueryManager<T extends DocumentJSON> {
45
45
  condition: any;
46
46
  }[]): boolean;
47
47
  verifyValue(value: Primitive, condition: any): boolean;
48
- adjustChunkSize(currentChunkSize: number, chunkTotalSize: number): number;
48
+ /**
49
+ * 최적화 공식: x = x * Math.sqrt(z / n)
50
+ * @param currentChunkSize 현재 청크 크기
51
+ * @param matchedCount 매칭된 문서 개수
52
+ * @param limit 최대 문서 개수
53
+ * @param chunkTotalSize 청크 내 문서 총 크기
54
+ * @returns
55
+ */
56
+ adjustChunkSize(currentChunkSize: number, matchedCount: number, limit: number, chunkTotalSize: number): number;
49
57
  processChunkedKeysWithVerify(keysStream: AsyncIterableIterator<number>, startIdx: number, initialChunkSize: number, limit: number, ftsConditions: {
50
58
  field: string;
51
59
  matchTokens: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-dataply",
3
- "version": "0.0.10-alpha.5",
3
+ "version": "0.0.10-alpha.7",
4
4
  "description": "Simple and powerful JSON document database supporting complex queries and flexible indexing policies.",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",