@atscript/db-mongo 0.1.98 → 0.1.100

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.cjs CHANGED
@@ -630,15 +630,15 @@ function isVectorSearchableImpl(host) {
630
630
  }
631
631
  /** Text search via $search aggregation stage. */
632
632
  async function searchImpl(host, text, query, indexName) {
633
- const stage = buildSearchStage(host, text, indexName);
634
- if (!stage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
635
- return runSearchPipeline(host, stage, query, "search");
633
+ const plan = buildSearchStage(host, text, indexName);
634
+ if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
635
+ return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
636
636
  }
637
637
  /** Text search with faceted count. */
638
638
  async function searchWithCountImpl(host, text, query, indexName) {
639
- const stage = buildSearchStage(host, text, indexName);
640
- if (!stage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
641
- return runSearchWithCountPipeline(host, stage, query, "searchWithCount");
639
+ const plan = buildSearchStage(host, text, indexName);
640
+ if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
641
+ return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
642
642
  }
643
643
  /** Vector search via $vectorSearch aggregation stage. */
644
644
  async function vectorSearchImpl(host, vector, query, indexName) {
@@ -656,18 +656,40 @@ function resolveThreshold(host, controls, indexName) {
656
656
  if (queryThreshold !== void 0) return queryThreshold;
657
657
  return host.getVectorThreshold(indexName);
658
658
  }
659
- /** Builds a MongoDB $search pipeline stage for text search. */
659
+ /**
660
+ * Builds the first pipeline stage for a text search and reports whether it is a
661
+ * classic `$text` query (vs. an Atlas `$search`).
662
+ *
663
+ * Two distinct execution paths share the `search()` API:
664
+ * - **Classic text** (`@db.index.fulltext`, or an adapter-scanned `text`
665
+ * index): served by the collection's MongoDB text index via `$match $text`.
666
+ * Works on community MongoDB and Atlas alike.
667
+ * - **Atlas Search** (`search_text` / `dynamic_text`): served by `mongot` via
668
+ * `$search`, which can ONLY resolve Atlas Search indexes.
669
+ *
670
+ * Routing a classic index through `$search` (as this used to) is guaranteed to
671
+ * fail at runtime — `$search` can't see a classic text index, and on community
672
+ * MongoDB the stage is unsupported entirely — even though the table reports
673
+ * `searchable: true`. The discriminator is the resolved index's `type`.
674
+ */
660
675
  function buildSearchStage(host, text, indexName) {
661
676
  const index = host.getMongoSearchIndex(indexName);
662
677
  if (!index) return;
663
678
  if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
664
- return { $search: {
665
- index: index.key,
666
- text: {
667
- query: text,
668
- path: { wildcard: "*" }
669
- }
670
- } };
679
+ if (index.type === "text") return {
680
+ stage: { $match: { $text: { $search: text } } },
681
+ classicText: true
682
+ };
683
+ return {
684
+ stage: { $search: {
685
+ index: index.key,
686
+ text: {
687
+ query: text,
688
+ path: { wildcard: "*" }
689
+ }
690
+ } },
691
+ classicText: false
692
+ };
671
693
  }
672
694
  /** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
673
695
  function buildVectorSearchStage(host, vector, indexName, limit) {
@@ -698,16 +720,17 @@ function buildVectorSearchStage(host, vector, indexName, limit) {
698
720
  } };
699
721
  }
700
722
  /** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
701
- async function runSearchPipeline(host, stage, query, label, threshold) {
723
+ async function runSearchPipeline(host, stage, query, label, threshold, classicText = false) {
702
724
  const filter = require_mongo_filter.buildMongoFilter(query.filter);
703
725
  const controls = query.controls || {};
704
726
  const pipeline = [stage];
705
727
  if (threshold !== void 0) {
706
728
  pipeline.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
707
729
  pipeline.push({ $match: { _score: { $gte: threshold } } });
708
- }
730
+ } else if (classicText) pipeline.push({ $addFields: { _score: { $meta: "textScore" } } });
709
731
  pipeline.push({ $match: filter });
710
732
  if (controls.$sort) pipeline.push({ $sort: controls.$sort });
733
+ else if (classicText) pipeline.push({ $sort: { _score: -1 } });
711
734
  if (controls.$skip) pipeline.push({ $skip: controls.$skip });
712
735
  if (controls.$limit) pipeline.push({ $limit: controls.$limit });
713
736
  else pipeline.push({ $limit: 1e3 });
@@ -719,16 +742,17 @@ async function runSearchPipeline(host, stage, query, label, threshold) {
719
742
  return wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
720
743
  }
721
744
  /** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
722
- async function runSearchWithCountPipeline(host, stage, query, label, threshold) {
745
+ async function runSearchWithCountPipeline(host, stage, query, label, threshold, classicText = false) {
723
746
  const filter = require_mongo_filter.buildMongoFilter(query.filter);
724
747
  const controls = query.controls || {};
725
748
  const preStages = [];
726
749
  if (threshold !== void 0) {
727
750
  preStages.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
728
751
  preStages.push({ $match: { _score: { $gte: threshold } } });
729
- }
752
+ } else if (classicText) preStages.push({ $addFields: { _score: { $meta: "textScore" } } });
730
753
  const dataStages = [];
731
754
  if (controls.$sort) dataStages.push({ $sort: controls.$sort });
755
+ else if (classicText) dataStages.push({ $sort: { _score: -1 } });
732
756
  if (controls.$skip) dataStages.push({ $skip: controls.$skip });
733
757
  if (controls.$limit) dataStages.push({ $limit: controls.$limit });
734
758
  if (controls.$select) {
@@ -1058,7 +1082,7 @@ async function syncIndexesImpl(host) {
1058
1082
  mongoType = "text";
1059
1083
  for (const f of index.fields) {
1060
1084
  fields[f.name] = "text";
1061
- if (f.weight) weights[f.name] = f.weight;
1085
+ weights[f.name] = f.weight ?? 1;
1062
1086
  }
1063
1087
  } else {
1064
1088
  mongoType = index.type;
@@ -1504,11 +1528,6 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
1504
1528
  const startValue = metadata.get("db.default.increment");
1505
1529
  this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
1506
1530
  }
1507
- for (const index of metadata.get("db.index.fulltext") || []) {
1508
- const name = typeof index === "object" ? index.name || "" : "";
1509
- const weight = typeof index === "object" ? index.weight || 1 : 1;
1510
- this._addMongoIndexField("text", name, field, weight);
1511
- }
1512
1531
  for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer);
1513
1532
  const vectorIndex = metadata.get("db.search.vector");
1514
1533
  if (vectorIndex) {
package/dist/index.mjs CHANGED
@@ -629,15 +629,15 @@ function isVectorSearchableImpl(host) {
629
629
  }
630
630
  /** Text search via $search aggregation stage. */
631
631
  async function searchImpl(host, text, query, indexName) {
632
- const stage = buildSearchStage(host, text, indexName);
633
- if (!stage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
634
- return runSearchPipeline(host, stage, query, "search");
632
+ const plan = buildSearchStage(host, text, indexName);
633
+ if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
634
+ return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
635
635
  }
636
636
  /** Text search with faceted count. */
637
637
  async function searchWithCountImpl(host, text, query, indexName) {
638
- const stage = buildSearchStage(host, text, indexName);
639
- if (!stage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
640
- return runSearchWithCountPipeline(host, stage, query, "searchWithCount");
638
+ const plan = buildSearchStage(host, text, indexName);
639
+ if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
640
+ return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
641
641
  }
642
642
  /** Vector search via $vectorSearch aggregation stage. */
643
643
  async function vectorSearchImpl(host, vector, query, indexName) {
@@ -655,18 +655,40 @@ function resolveThreshold(host, controls, indexName) {
655
655
  if (queryThreshold !== void 0) return queryThreshold;
656
656
  return host.getVectorThreshold(indexName);
657
657
  }
658
- /** Builds a MongoDB $search pipeline stage for text search. */
658
+ /**
659
+ * Builds the first pipeline stage for a text search and reports whether it is a
660
+ * classic `$text` query (vs. an Atlas `$search`).
661
+ *
662
+ * Two distinct execution paths share the `search()` API:
663
+ * - **Classic text** (`@db.index.fulltext`, or an adapter-scanned `text`
664
+ * index): served by the collection's MongoDB text index via `$match $text`.
665
+ * Works on community MongoDB and Atlas alike.
666
+ * - **Atlas Search** (`search_text` / `dynamic_text`): served by `mongot` via
667
+ * `$search`, which can ONLY resolve Atlas Search indexes.
668
+ *
669
+ * Routing a classic index through `$search` (as this used to) is guaranteed to
670
+ * fail at runtime — `$search` can't see a classic text index, and on community
671
+ * MongoDB the stage is unsupported entirely — even though the table reports
672
+ * `searchable: true`. The discriminator is the resolved index's `type`.
673
+ */
659
674
  function buildSearchStage(host, text, indexName) {
660
675
  const index = host.getMongoSearchIndex(indexName);
661
676
  if (!index) return;
662
677
  if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
663
- return { $search: {
664
- index: index.key,
665
- text: {
666
- query: text,
667
- path: { wildcard: "*" }
668
- }
669
- } };
678
+ if (index.type === "text") return {
679
+ stage: { $match: { $text: { $search: text } } },
680
+ classicText: true
681
+ };
682
+ return {
683
+ stage: { $search: {
684
+ index: index.key,
685
+ text: {
686
+ query: text,
687
+ path: { wildcard: "*" }
688
+ }
689
+ } },
690
+ classicText: false
691
+ };
670
692
  }
671
693
  /** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
672
694
  function buildVectorSearchStage(host, vector, indexName, limit) {
@@ -697,16 +719,17 @@ function buildVectorSearchStage(host, vector, indexName, limit) {
697
719
  } };
698
720
  }
699
721
  /** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
700
- async function runSearchPipeline(host, stage, query, label, threshold) {
722
+ async function runSearchPipeline(host, stage, query, label, threshold, classicText = false) {
701
723
  const filter = buildMongoFilter(query.filter);
702
724
  const controls = query.controls || {};
703
725
  const pipeline = [stage];
704
726
  if (threshold !== void 0) {
705
727
  pipeline.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
706
728
  pipeline.push({ $match: { _score: { $gte: threshold } } });
707
- }
729
+ } else if (classicText) pipeline.push({ $addFields: { _score: { $meta: "textScore" } } });
708
730
  pipeline.push({ $match: filter });
709
731
  if (controls.$sort) pipeline.push({ $sort: controls.$sort });
732
+ else if (classicText) pipeline.push({ $sort: { _score: -1 } });
710
733
  if (controls.$skip) pipeline.push({ $skip: controls.$skip });
711
734
  if (controls.$limit) pipeline.push({ $limit: controls.$limit });
712
735
  else pipeline.push({ $limit: 1e3 });
@@ -718,16 +741,17 @@ async function runSearchPipeline(host, stage, query, label, threshold) {
718
741
  return wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
719
742
  }
720
743
  /** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
721
- async function runSearchWithCountPipeline(host, stage, query, label, threshold) {
744
+ async function runSearchWithCountPipeline(host, stage, query, label, threshold, classicText = false) {
722
745
  const filter = buildMongoFilter(query.filter);
723
746
  const controls = query.controls || {};
724
747
  const preStages = [];
725
748
  if (threshold !== void 0) {
726
749
  preStages.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
727
750
  preStages.push({ $match: { _score: { $gte: threshold } } });
728
- }
751
+ } else if (classicText) preStages.push({ $addFields: { _score: { $meta: "textScore" } } });
729
752
  const dataStages = [];
730
753
  if (controls.$sort) dataStages.push({ $sort: controls.$sort });
754
+ else if (classicText) dataStages.push({ $sort: { _score: -1 } });
731
755
  if (controls.$skip) dataStages.push({ $skip: controls.$skip });
732
756
  if (controls.$limit) dataStages.push({ $limit: controls.$limit });
733
757
  if (controls.$select) {
@@ -1057,7 +1081,7 @@ async function syncIndexesImpl(host) {
1057
1081
  mongoType = "text";
1058
1082
  for (const f of index.fields) {
1059
1083
  fields[f.name] = "text";
1060
- if (f.weight) weights[f.name] = f.weight;
1084
+ weights[f.name] = f.weight ?? 1;
1061
1085
  }
1062
1086
  } else {
1063
1087
  mongoType = index.type;
@@ -1503,11 +1527,6 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
1503
1527
  const startValue = metadata.get("db.default.increment");
1504
1528
  this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
1505
1529
  }
1506
- for (const index of metadata.get("db.index.fulltext") || []) {
1507
- const name = typeof index === "object" ? index.name || "" : "";
1508
- const weight = typeof index === "object" ? index.weight || 1 : 1;
1509
- this._addMongoIndexField("text", name, field, weight);
1510
- }
1511
1530
  for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer);
1512
1531
  const vectorIndex = metadata.get("db.search.vector");
1513
1532
  if (vectorIndex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.98",
3
+ "version": "0.1.100",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -46,17 +46,17 @@
46
46
  "access": "public"
47
47
  },
48
48
  "devDependencies": {
49
- "@atscript/core": "^0.1.71",
50
- "@atscript/typescript": "^0.1.71",
49
+ "@atscript/core": "^0.1.73",
50
+ "@atscript/typescript": "^0.1.73",
51
51
  "mongodb": "^6.17.0",
52
52
  "mongodb-memory-server-core": "^10.0.0",
53
- "unplugin-atscript": "^0.1.71"
53
+ "unplugin-atscript": "^0.1.73"
54
54
  },
55
55
  "peerDependencies": {
56
- "@atscript/core": "^0.1.71",
57
- "@atscript/typescript": "^0.1.71",
56
+ "@atscript/core": "^0.1.73",
57
+ "@atscript/typescript": "^0.1.73",
58
58
  "mongodb": "^6.17.0",
59
- "@atscript/db": "^0.1.98"
59
+ "@atscript/db": "^0.1.100"
60
60
  },
61
61
  "scripts": {
62
62
  "postinstall": "asc -f dts",