@atscript/db-mongo 0.1.101 → 0.1.102
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/agg.cjs +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/index.cjs +145 -29
- package/dist/index.d.cts +47 -11
- package/dist/index.d.mts +47 -11
- package/dist/index.mjs +145 -29
- package/dist/plugin.cjs +67 -3
- package/dist/plugin.mjs +67 -3
- package/package.json +2 -2
- /package/dist/{mongo-filter-AihWWQXp.cjs → mongo-filter-1EpqdD-T.cjs} +0 -0
- /package/dist/{mongo-filter-BsocUQG3.mjs → mongo-filter-DBYaF9aH.mjs} +0 -0
package/dist/agg.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_filter = require("./mongo-filter-
|
|
2
|
+
const require_mongo_filter = require("./mongo-filter-1EpqdD-T.cjs");
|
|
3
3
|
let _atscript_db_agg = require("@atscript/db/agg");
|
|
4
4
|
//#region src/agg.ts
|
|
5
5
|
/** Simple accumulators that map directly to `{ $<fn>: '$field' }`. */
|
package/dist/agg.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as buildMongoFilter } from "./mongo-filter-
|
|
1
|
+
import { t as buildMongoFilter } from "./mongo-filter-DBYaF9aH.mjs";
|
|
2
2
|
import { resolveAlias } from "@atscript/db/agg";
|
|
3
3
|
//#region src/agg.ts
|
|
4
4
|
/** Simple accumulators that map directly to `{ $<fn>: '$field' }`. */
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_filter = require("./mongo-filter-
|
|
2
|
+
const require_mongo_filter = require("./mongo-filter-1EpqdD-T.cjs");
|
|
3
3
|
let _atscript_db = require("@atscript/db");
|
|
4
4
|
let mongodb = require("mongodb");
|
|
5
5
|
//#region src/lib/projection-dedupe.ts
|
|
@@ -72,6 +72,9 @@ function containsAggregationExpr(v) {
|
|
|
72
72
|
* `$set` map.
|
|
73
73
|
*/
|
|
74
74
|
var CollectionPatcher = class {
|
|
75
|
+
collection;
|
|
76
|
+
payload;
|
|
77
|
+
ops;
|
|
75
78
|
constructor(collection, payload, ops) {
|
|
76
79
|
this.collection = collection;
|
|
77
80
|
this.payload = payload;
|
|
@@ -630,13 +633,13 @@ function isVectorSearchableImpl(host) {
|
|
|
630
633
|
}
|
|
631
634
|
/** Text search via $search aggregation stage. */
|
|
632
635
|
async function searchImpl(host, text, query, indexName) {
|
|
633
|
-
const plan = buildSearchStage(host, text, indexName);
|
|
636
|
+
const plan = buildSearchStage(host, text, indexName, query.controls);
|
|
634
637
|
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
635
638
|
return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
|
|
636
639
|
}
|
|
637
640
|
/** Text search with faceted count. */
|
|
638
641
|
async function searchWithCountImpl(host, text, query, indexName) {
|
|
639
|
-
const plan = buildSearchStage(host, text, indexName);
|
|
642
|
+
const plan = buildSearchStage(host, text, indexName, query.controls);
|
|
640
643
|
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
641
644
|
return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
|
|
642
645
|
}
|
|
@@ -672,7 +675,7 @@ function resolveThreshold(host, controls, indexName) {
|
|
|
672
675
|
* MongoDB the stage is unsupported entirely — even though the table reports
|
|
673
676
|
* `searchable: true`. The discriminator is the resolved index's `type`.
|
|
674
677
|
*/
|
|
675
|
-
function buildSearchStage(host, text, indexName) {
|
|
678
|
+
function buildSearchStage(host, text, indexName, controls) {
|
|
676
679
|
const index = host.getMongoSearchIndex(indexName);
|
|
677
680
|
if (!index) return;
|
|
678
681
|
if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
|
|
@@ -680,17 +683,58 @@ function buildSearchStage(host, text, indexName) {
|
|
|
680
683
|
stage: { $match: { $text: { $search: text } } },
|
|
681
684
|
classicText: true
|
|
682
685
|
};
|
|
686
|
+
const searchIndex = index;
|
|
687
|
+
const fuzzy = resolveSearchFuzzy(searchIndex, controls);
|
|
688
|
+
const strategy = searchIndex.strategy ?? "compound";
|
|
689
|
+
const autocompletePaths = autocompleteFieldPaths(searchIndex);
|
|
690
|
+
const textClause = () => ({ text: {
|
|
691
|
+
query: text,
|
|
692
|
+
path: { wildcard: "*" },
|
|
693
|
+
...fuzzy ? { fuzzy } : {}
|
|
694
|
+
} });
|
|
695
|
+
const autocompleteClause = (path) => ({ autocomplete: {
|
|
696
|
+
query: text,
|
|
697
|
+
path,
|
|
698
|
+
...fuzzy ? { fuzzy } : {}
|
|
699
|
+
} });
|
|
700
|
+
let body;
|
|
701
|
+
if (strategy === "text" || autocompletePaths.length === 0) body = textClause();
|
|
702
|
+
else if (strategy === "autocomplete") {
|
|
703
|
+
const clauses = autocompletePaths.map(autocompleteClause);
|
|
704
|
+
body = clauses.length === 1 ? clauses[0] : { compound: {
|
|
705
|
+
should: clauses,
|
|
706
|
+
minimumShouldMatch: 1
|
|
707
|
+
} };
|
|
708
|
+
} else body = { compound: {
|
|
709
|
+
should: [textClause(), ...autocompletePaths.map(autocompleteClause)],
|
|
710
|
+
minimumShouldMatch: 1
|
|
711
|
+
} };
|
|
683
712
|
return {
|
|
684
713
|
stage: { $search: {
|
|
685
714
|
index: index.key,
|
|
686
|
-
|
|
687
|
-
query: text,
|
|
688
|
-
path: { wildcard: "*" }
|
|
689
|
-
}
|
|
715
|
+
...body
|
|
690
716
|
} },
|
|
691
717
|
classicText: false
|
|
692
718
|
};
|
|
693
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Resolves query-time fuzzy (typo tolerance): the `$fuzzy` request control
|
|
722
|
+
* overrides the schema-declared `@db.mongo.search.*` fuzzy. Only an edit distance
|
|
723
|
+
* of 1 or 2 is emitted (Atlas rejects 0) — anything else means "no fuzzy".
|
|
724
|
+
*/
|
|
725
|
+
function resolveSearchFuzzy(index, controls) {
|
|
726
|
+
const override = controls?.$fuzzy;
|
|
727
|
+
const maxEdits = override === void 0 ? index.fuzzy?.maxEdits : Number(override);
|
|
728
|
+
return maxEdits === 1 || maxEdits === 2 ? { maxEdits } : void 0;
|
|
729
|
+
}
|
|
730
|
+
/** Field paths in this index that carry an `autocomplete` mapping. */
|
|
731
|
+
function autocompleteFieldPaths(index) {
|
|
732
|
+
const fields = index.definition.mappings?.fields;
|
|
733
|
+
if (!fields) return [];
|
|
734
|
+
const paths = [];
|
|
735
|
+
for (const [path, mapping] of Object.entries(fields)) if ((Array.isArray(mapping) ? mapping : [mapping]).some((m) => m.type === "autocomplete")) paths.push(path);
|
|
736
|
+
return paths;
|
|
737
|
+
}
|
|
694
738
|
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
695
739
|
function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
696
740
|
let index;
|
|
@@ -1289,10 +1333,26 @@ function fieldsMatch(left, right) {
|
|
|
1289
1333
|
if (leftKeys.length !== rightKeys.length) return false;
|
|
1290
1334
|
for (const key of leftKeys) {
|
|
1291
1335
|
if (!(key in right)) return false;
|
|
1292
|
-
if (left[key]
|
|
1336
|
+
if (!fieldMappingEqual(left[key], right[key])) return false;
|
|
1293
1337
|
}
|
|
1294
1338
|
return true;
|
|
1295
1339
|
}
|
|
1340
|
+
/** Order-independent structural compare of a field's Atlas type mapping(s). */
|
|
1341
|
+
function fieldMappingEqual(a, b) {
|
|
1342
|
+
const am = mappingsByType(a);
|
|
1343
|
+
const bm = mappingsByType(b);
|
|
1344
|
+
if (am.size !== bm.size) return false;
|
|
1345
|
+
for (const [type, av] of am) {
|
|
1346
|
+
const bv = bm.get(type);
|
|
1347
|
+
if (!bv || av.analyzer !== bv.analyzer || av.tokenization !== bv.tokenization || av.minGrams !== bv.minGrams || av.maxGrams !== bv.maxGrams || av.foldDiacritics !== bv.foldDiacritics) return false;
|
|
1348
|
+
}
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
function mappingsByType(m) {
|
|
1352
|
+
const map = /* @__PURE__ */ new Map();
|
|
1353
|
+
for (const x of Array.isArray(m) ? m : [m]) map.set(x.type, x);
|
|
1354
|
+
return map;
|
|
1355
|
+
}
|
|
1296
1356
|
function vectorFieldsMatch(left, right) {
|
|
1297
1357
|
if (left.length !== (right || []).length) return false;
|
|
1298
1358
|
const rightMap = /* @__PURE__ */ new Map();
|
|
@@ -1318,6 +1378,8 @@ const validateMongoIdPlugin = (ctx, def, value) => {
|
|
|
1318
1378
|
//#endregion
|
|
1319
1379
|
//#region src/lib/mongo-adapter.ts
|
|
1320
1380
|
var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
1381
|
+
db;
|
|
1382
|
+
client;
|
|
1321
1383
|
_collection;
|
|
1322
1384
|
/** MongoDB-specific indexes (search, vector) — separate from table.indexes. */
|
|
1323
1385
|
_mongoIndexes = /* @__PURE__ */ new Map();
|
|
@@ -1508,13 +1570,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1508
1570
|
const dynamicText = typeMeta.get("db.mongo.search.dynamic");
|
|
1509
1571
|
if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
|
|
1510
1572
|
mappings: { dynamic: true },
|
|
1511
|
-
analyzer: dynamicText.analyzer
|
|
1512
|
-
|
|
1513
|
-
});
|
|
1573
|
+
analyzer: dynamicText.analyzer
|
|
1574
|
+
}, { fuzzy: normalizeSearchFuzzy(dynamicText.fuzzy) });
|
|
1514
1575
|
for (const textSearch of typeMeta.get("db.mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
|
|
1515
1576
|
mappings: { fields: {} },
|
|
1516
|
-
analyzer: textSearch.analyzer
|
|
1517
|
-
|
|
1577
|
+
analyzer: textSearch.analyzer
|
|
1578
|
+
}, {
|
|
1579
|
+
fuzzy: normalizeSearchFuzzy(textSearch.fuzzy),
|
|
1580
|
+
strategy: normalizeSearchStrategy(textSearch.strategy)
|
|
1518
1581
|
});
|
|
1519
1582
|
}
|
|
1520
1583
|
onFieldScanned(field, _type, metadata) {
|
|
@@ -1528,7 +1591,24 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1528
1591
|
const startValue = metadata.get("db.default.increment");
|
|
1529
1592
|
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1530
1593
|
}
|
|
1531
|
-
for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer
|
|
1594
|
+
for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, [index.analyzer ? {
|
|
1595
|
+
type: "string",
|
|
1596
|
+
analyzer: index.analyzer
|
|
1597
|
+
} : { type: "string" }]);
|
|
1598
|
+
for (const ac of metadata.get("db.mongo.search.autocomplete") || []) {
|
|
1599
|
+
const autocomplete = {
|
|
1600
|
+
type: "autocomplete",
|
|
1601
|
+
tokenization: ac.tokenization || "edgeGram",
|
|
1602
|
+
minGrams: ac.minGrams ?? 2,
|
|
1603
|
+
maxGrams: ac.maxGrams ?? 15,
|
|
1604
|
+
foldDiacritics: ac.foldDiacritics ?? true
|
|
1605
|
+
};
|
|
1606
|
+
const companion = ac.analyzer ? {
|
|
1607
|
+
type: "string",
|
|
1608
|
+
analyzer: ac.analyzer
|
|
1609
|
+
} : { type: "string" };
|
|
1610
|
+
this._addFieldToSearchIndex("search_text", ac.indexName, field, [autocomplete, companion]);
|
|
1611
|
+
}
|
|
1532
1612
|
const vectorIndex = metadata.get("db.search.vector");
|
|
1533
1613
|
if (vectorIndex) {
|
|
1534
1614
|
const indexName = vectorIndex.indexName || field;
|
|
@@ -1989,32 +2069,68 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1989
2069
|
}
|
|
1990
2070
|
if (weight) index.weights[field] = weight;
|
|
1991
2071
|
}
|
|
1992
|
-
_setSearchIndex(type, name, definition) {
|
|
2072
|
+
_setSearchIndex(type, name, definition, meta) {
|
|
1993
2073
|
const key = mongoIndexKey(type, name || "DEFAULT");
|
|
1994
|
-
|
|
2074
|
+
const index = {
|
|
1995
2075
|
key,
|
|
1996
2076
|
name: name || "DEFAULT",
|
|
1997
2077
|
type,
|
|
1998
|
-
definition
|
|
1999
|
-
|
|
2078
|
+
definition,
|
|
2079
|
+
fuzzy: meta?.fuzzy,
|
|
2080
|
+
strategy: meta?.strategy
|
|
2081
|
+
};
|
|
2082
|
+
this._mongoIndexes.set(key, index);
|
|
2083
|
+
return index;
|
|
2000
2084
|
}
|
|
2001
|
-
|
|
2085
|
+
/**
|
|
2086
|
+
* Adds (and merges, by mapping `type`) one or more Atlas field-type mappings to
|
|
2087
|
+
* a `search_text` index. Multiple annotations on the same field — e.g.
|
|
2088
|
+
* `@db.mongo.search.text` (string) plus `@db.mongo.search.autocomplete`
|
|
2089
|
+
* (autocomplete + string) — accumulate into a single multi-type mapping
|
|
2090
|
+
* instead of overwriting one another.
|
|
2091
|
+
*/
|
|
2092
|
+
_addFieldToSearchIndex(type, _name, fieldName, mappings) {
|
|
2002
2093
|
const name = _name || "DEFAULT";
|
|
2003
2094
|
let index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2004
|
-
if (!index && type === "search_text") {
|
|
2005
|
-
this._setSearchIndex(type, name, {
|
|
2006
|
-
mappings: { fields: {} },
|
|
2007
|
-
text: { fuzzy: { maxEdits: 0 } }
|
|
2008
|
-
});
|
|
2009
|
-
index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2010
|
-
}
|
|
2095
|
+
if (!index && type === "search_text") index = this._setSearchIndex(type, name, { mappings: { fields: {} } });
|
|
2011
2096
|
if (index) {
|
|
2012
|
-
index.definition.mappings.fields
|
|
2013
|
-
|
|
2097
|
+
const fields = index.definition.mappings.fields;
|
|
2098
|
+
fields[fieldName] = mergeFieldMappings(fields[fieldName], mappings);
|
|
2014
2099
|
}
|
|
2015
2100
|
}
|
|
2016
2101
|
};
|
|
2017
2102
|
/**
|
|
2103
|
+
* Normalizes a declared `fuzzy` arg (`0-2`) into the query-time metadata Atlas
|
|
2104
|
+
* accepts. Atlas only honors an edit distance of `1` or `2`; `0`/undefined means
|
|
2105
|
+
* "no fuzzy", returned as `undefined` so no `fuzzy` clause is ever emitted.
|
|
2106
|
+
*/
|
|
2107
|
+
function normalizeSearchFuzzy(fuzzy) {
|
|
2108
|
+
return fuzzy === 1 || fuzzy === 2 ? { maxEdits: fuzzy } : void 0;
|
|
2109
|
+
}
|
|
2110
|
+
/** Narrows the declared `strategy` arg to a known value (undefined → query layer defaults to "compound"). */
|
|
2111
|
+
function normalizeSearchStrategy(strategy) {
|
|
2112
|
+
return strategy === "compound" || strategy === "autocomplete" || strategy === "text" ? strategy : void 0;
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Merges incoming Atlas field-type mappings into a field's existing mapping,
|
|
2116
|
+
* keyed by `type` (a later mapping of the same type replaces the earlier one).
|
|
2117
|
+
* Collapses to a single object when one type remains, else an array (Atlas's
|
|
2118
|
+
* multi-type field form).
|
|
2119
|
+
*/
|
|
2120
|
+
function mergeFieldMappings(existing, incoming) {
|
|
2121
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2122
|
+
const order = [];
|
|
2123
|
+
const add = (m) => {
|
|
2124
|
+
if (!byType.has(m.type)) order.push(m.type);
|
|
2125
|
+
byType.set(m.type, m);
|
|
2126
|
+
};
|
|
2127
|
+
if (Array.isArray(existing)) existing.forEach(add);
|
|
2128
|
+
else if (existing) add(existing);
|
|
2129
|
+
incoming.forEach(add);
|
|
2130
|
+
const merged = order.map((t) => byType.get(t));
|
|
2131
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2018
2134
|
* Builds a MongoDB update document from a data object that may contain
|
|
2019
2135
|
* field ops (`{ $inc: N }`, `{ $dec: N }`, `{ $mul: N }`).
|
|
2020
2136
|
* Regular fields go into `$set`, ops go into `$inc` / `$mul`. When
|
package/dist/index.d.cts
CHANGED
|
@@ -165,16 +165,45 @@ interface TSearchIndex {
|
|
|
165
165
|
name: string;
|
|
166
166
|
type: "dynamic_text" | "search_text" | "vector";
|
|
167
167
|
definition: TMongoSearchIndexDefinition;
|
|
168
|
+
/**
|
|
169
|
+
* Query-time fuzzy (typo tolerance) declared via `@db.mongo.search.dynamic` /
|
|
170
|
+
* `@db.mongo.search.static`. Carried as index metadata — NOT part of the Atlas
|
|
171
|
+
* index `definition` (Atlas applies fuzzy at query time on the `text`/
|
|
172
|
+
* `autocomplete` operator, not in the index schema). `buildSearchStage` reads
|
|
173
|
+
* this and attaches it to the emitted operator.
|
|
174
|
+
*/
|
|
175
|
+
fuzzy?: {
|
|
176
|
+
maxEdits: number;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The match strategy declared via `@db.mongo.search.static`, locking this
|
|
180
|
+
* index's query shape. `buildSearchStage` reads it; undefined → `"compound"`.
|
|
181
|
+
* - `compound` — wildcard `text` + per-field `autocomplete` (exact ranks above prefix).
|
|
182
|
+
* - `autocomplete` — autocomplete fields only (pure typeahead, no word clause).
|
|
183
|
+
* - `text` — single `text` operator over all string-mapped fields.
|
|
184
|
+
*/
|
|
185
|
+
strategy?: "compound" | "autocomplete" | "text";
|
|
168
186
|
}
|
|
169
187
|
type TMongoIndex = TPlainIndex | TSearchIndex;
|
|
170
188
|
type TVectorSimilarity = "cosine" | "euclidean" | "dotProduct";
|
|
189
|
+
/**
|
|
190
|
+
* One Atlas Search field-type mapping. `type: "string"` is plain word matching;
|
|
191
|
+
* `type: "autocomplete"` enables prefix/typeahead (edgeGram) or substring (nGram).
|
|
192
|
+
* A field may carry several mappings at once (an array of these) — e.g. an
|
|
193
|
+
* autocomplete field double-mapped as `string` so exact-word hits still rank.
|
|
194
|
+
*/
|
|
195
|
+
interface TSearchFieldMapping {
|
|
196
|
+
type: string;
|
|
197
|
+
analyzer?: string;
|
|
198
|
+
tokenization?: "edgeGram" | "rightEdgeGram" | "nGram";
|
|
199
|
+
minGrams?: number;
|
|
200
|
+
maxGrams?: number;
|
|
201
|
+
foldDiacritics?: boolean;
|
|
202
|
+
}
|
|
171
203
|
interface TMongoSearchIndexDefinition {
|
|
172
204
|
mappings?: {
|
|
173
205
|
dynamic?: boolean;
|
|
174
|
-
fields?: Record<string,
|
|
175
|
-
type: string;
|
|
176
|
-
analyzer?: string;
|
|
177
|
-
}>;
|
|
206
|
+
fields?: Record<string, TSearchFieldMapping | TSearchFieldMapping[]>;
|
|
178
207
|
};
|
|
179
208
|
fields?: Array<{
|
|
180
209
|
path: string;
|
|
@@ -183,11 +212,6 @@ interface TMongoSearchIndexDefinition {
|
|
|
183
212
|
numDimensions?: number;
|
|
184
213
|
}>;
|
|
185
214
|
analyzer?: string;
|
|
186
|
-
text?: {
|
|
187
|
-
fuzzy?: {
|
|
188
|
-
maxEdits: number;
|
|
189
|
-
};
|
|
190
|
-
};
|
|
191
215
|
}
|
|
192
216
|
//#endregion
|
|
193
217
|
//#region src/lib/mongo-adapter.d.ts
|
|
@@ -351,8 +375,20 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
351
375
|
*/
|
|
352
376
|
private _getCollationOpts;
|
|
353
377
|
protected _addMongoIndexField(type: TPlainIndex["type"], name: string, field: string, weight?: number): void;
|
|
354
|
-
protected _setSearchIndex(type: TSearchIndex["type"], name: string | undefined, definition: TMongoSearchIndexDefinition
|
|
355
|
-
|
|
378
|
+
protected _setSearchIndex(type: TSearchIndex["type"], name: string | undefined, definition: TMongoSearchIndexDefinition, meta?: {
|
|
379
|
+
fuzzy?: {
|
|
380
|
+
maxEdits: number;
|
|
381
|
+
};
|
|
382
|
+
strategy?: TSearchIndex["strategy"];
|
|
383
|
+
}): TSearchIndex;
|
|
384
|
+
/**
|
|
385
|
+
* Adds (and merges, by mapping `type`) one or more Atlas field-type mappings to
|
|
386
|
+
* a `search_text` index. Multiple annotations on the same field — e.g.
|
|
387
|
+
* `@db.mongo.search.text` (string) plus `@db.mongo.search.autocomplete`
|
|
388
|
+
* (autocomplete + string) — accumulate into a single multi-type mapping
|
|
389
|
+
* instead of overwriting one another.
|
|
390
|
+
*/
|
|
391
|
+
protected _addFieldToSearchIndex(type: TSearchIndex["type"], _name: string | undefined, fieldName: string, mappings: TSearchFieldMapping[]): void;
|
|
356
392
|
}
|
|
357
393
|
//#endregion
|
|
358
394
|
//#region src/lib/mongo-filter.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -165,16 +165,45 @@ interface TSearchIndex {
|
|
|
165
165
|
name: string;
|
|
166
166
|
type: "dynamic_text" | "search_text" | "vector";
|
|
167
167
|
definition: TMongoSearchIndexDefinition;
|
|
168
|
+
/**
|
|
169
|
+
* Query-time fuzzy (typo tolerance) declared via `@db.mongo.search.dynamic` /
|
|
170
|
+
* `@db.mongo.search.static`. Carried as index metadata — NOT part of the Atlas
|
|
171
|
+
* index `definition` (Atlas applies fuzzy at query time on the `text`/
|
|
172
|
+
* `autocomplete` operator, not in the index schema). `buildSearchStage` reads
|
|
173
|
+
* this and attaches it to the emitted operator.
|
|
174
|
+
*/
|
|
175
|
+
fuzzy?: {
|
|
176
|
+
maxEdits: number;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The match strategy declared via `@db.mongo.search.static`, locking this
|
|
180
|
+
* index's query shape. `buildSearchStage` reads it; undefined → `"compound"`.
|
|
181
|
+
* - `compound` — wildcard `text` + per-field `autocomplete` (exact ranks above prefix).
|
|
182
|
+
* - `autocomplete` — autocomplete fields only (pure typeahead, no word clause).
|
|
183
|
+
* - `text` — single `text` operator over all string-mapped fields.
|
|
184
|
+
*/
|
|
185
|
+
strategy?: "compound" | "autocomplete" | "text";
|
|
168
186
|
}
|
|
169
187
|
type TMongoIndex = TPlainIndex | TSearchIndex;
|
|
170
188
|
type TVectorSimilarity = "cosine" | "euclidean" | "dotProduct";
|
|
189
|
+
/**
|
|
190
|
+
* One Atlas Search field-type mapping. `type: "string"` is plain word matching;
|
|
191
|
+
* `type: "autocomplete"` enables prefix/typeahead (edgeGram) or substring (nGram).
|
|
192
|
+
* A field may carry several mappings at once (an array of these) — e.g. an
|
|
193
|
+
* autocomplete field double-mapped as `string` so exact-word hits still rank.
|
|
194
|
+
*/
|
|
195
|
+
interface TSearchFieldMapping {
|
|
196
|
+
type: string;
|
|
197
|
+
analyzer?: string;
|
|
198
|
+
tokenization?: "edgeGram" | "rightEdgeGram" | "nGram";
|
|
199
|
+
minGrams?: number;
|
|
200
|
+
maxGrams?: number;
|
|
201
|
+
foldDiacritics?: boolean;
|
|
202
|
+
}
|
|
171
203
|
interface TMongoSearchIndexDefinition {
|
|
172
204
|
mappings?: {
|
|
173
205
|
dynamic?: boolean;
|
|
174
|
-
fields?: Record<string,
|
|
175
|
-
type: string;
|
|
176
|
-
analyzer?: string;
|
|
177
|
-
}>;
|
|
206
|
+
fields?: Record<string, TSearchFieldMapping | TSearchFieldMapping[]>;
|
|
178
207
|
};
|
|
179
208
|
fields?: Array<{
|
|
180
209
|
path: string;
|
|
@@ -183,11 +212,6 @@ interface TMongoSearchIndexDefinition {
|
|
|
183
212
|
numDimensions?: number;
|
|
184
213
|
}>;
|
|
185
214
|
analyzer?: string;
|
|
186
|
-
text?: {
|
|
187
|
-
fuzzy?: {
|
|
188
|
-
maxEdits: number;
|
|
189
|
-
};
|
|
190
|
-
};
|
|
191
215
|
}
|
|
192
216
|
//#endregion
|
|
193
217
|
//#region src/lib/mongo-adapter.d.ts
|
|
@@ -351,8 +375,20 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
351
375
|
*/
|
|
352
376
|
private _getCollationOpts;
|
|
353
377
|
protected _addMongoIndexField(type: TPlainIndex["type"], name: string, field: string, weight?: number): void;
|
|
354
|
-
protected _setSearchIndex(type: TSearchIndex["type"], name: string | undefined, definition: TMongoSearchIndexDefinition
|
|
355
|
-
|
|
378
|
+
protected _setSearchIndex(type: TSearchIndex["type"], name: string | undefined, definition: TMongoSearchIndexDefinition, meta?: {
|
|
379
|
+
fuzzy?: {
|
|
380
|
+
maxEdits: number;
|
|
381
|
+
};
|
|
382
|
+
strategy?: TSearchIndex["strategy"];
|
|
383
|
+
}): TSearchIndex;
|
|
384
|
+
/**
|
|
385
|
+
* Adds (and merges, by mapping `type`) one or more Atlas field-type mappings to
|
|
386
|
+
* a `search_text` index. Multiple annotations on the same field — e.g.
|
|
387
|
+
* `@db.mongo.search.text` (string) plus `@db.mongo.search.autocomplete`
|
|
388
|
+
* (autocomplete + string) — accumulate into a single multi-type mapping
|
|
389
|
+
* instead of overwriting one another.
|
|
390
|
+
*/
|
|
391
|
+
protected _addFieldToSearchIndex(type: TSearchIndex["type"], _name: string | undefined, fieldName: string, mappings: TSearchFieldMapping[]): void;
|
|
356
392
|
}
|
|
357
393
|
//#endregion
|
|
358
394
|
//#region src/lib/mongo-filter.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as buildMongoFilter } from "./mongo-filter-
|
|
1
|
+
import { t as buildMongoFilter } from "./mongo-filter-DBYaF9aH.mjs";
|
|
2
2
|
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getDbFieldOp, getKeyProps, resolveDesignType } from "@atscript/db";
|
|
3
3
|
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
4
|
//#region src/lib/projection-dedupe.ts
|
|
@@ -71,6 +71,9 @@ function containsAggregationExpr(v) {
|
|
|
71
71
|
* `$set` map.
|
|
72
72
|
*/
|
|
73
73
|
var CollectionPatcher = class {
|
|
74
|
+
collection;
|
|
75
|
+
payload;
|
|
76
|
+
ops;
|
|
74
77
|
constructor(collection, payload, ops) {
|
|
75
78
|
this.collection = collection;
|
|
76
79
|
this.payload = payload;
|
|
@@ -629,13 +632,13 @@ function isVectorSearchableImpl(host) {
|
|
|
629
632
|
}
|
|
630
633
|
/** Text search via $search aggregation stage. */
|
|
631
634
|
async function searchImpl(host, text, query, indexName) {
|
|
632
|
-
const plan = buildSearchStage(host, text, indexName);
|
|
635
|
+
const plan = buildSearchStage(host, text, indexName, query.controls);
|
|
633
636
|
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
634
637
|
return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
|
|
635
638
|
}
|
|
636
639
|
/** Text search with faceted count. */
|
|
637
640
|
async function searchWithCountImpl(host, text, query, indexName) {
|
|
638
|
-
const plan = buildSearchStage(host, text, indexName);
|
|
641
|
+
const plan = buildSearchStage(host, text, indexName, query.controls);
|
|
639
642
|
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
640
643
|
return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
|
|
641
644
|
}
|
|
@@ -671,7 +674,7 @@ function resolveThreshold(host, controls, indexName) {
|
|
|
671
674
|
* MongoDB the stage is unsupported entirely — even though the table reports
|
|
672
675
|
* `searchable: true`. The discriminator is the resolved index's `type`.
|
|
673
676
|
*/
|
|
674
|
-
function buildSearchStage(host, text, indexName) {
|
|
677
|
+
function buildSearchStage(host, text, indexName, controls) {
|
|
675
678
|
const index = host.getMongoSearchIndex(indexName);
|
|
676
679
|
if (!index) return;
|
|
677
680
|
if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
|
|
@@ -679,17 +682,58 @@ function buildSearchStage(host, text, indexName) {
|
|
|
679
682
|
stage: { $match: { $text: { $search: text } } },
|
|
680
683
|
classicText: true
|
|
681
684
|
};
|
|
685
|
+
const searchIndex = index;
|
|
686
|
+
const fuzzy = resolveSearchFuzzy(searchIndex, controls);
|
|
687
|
+
const strategy = searchIndex.strategy ?? "compound";
|
|
688
|
+
const autocompletePaths = autocompleteFieldPaths(searchIndex);
|
|
689
|
+
const textClause = () => ({ text: {
|
|
690
|
+
query: text,
|
|
691
|
+
path: { wildcard: "*" },
|
|
692
|
+
...fuzzy ? { fuzzy } : {}
|
|
693
|
+
} });
|
|
694
|
+
const autocompleteClause = (path) => ({ autocomplete: {
|
|
695
|
+
query: text,
|
|
696
|
+
path,
|
|
697
|
+
...fuzzy ? { fuzzy } : {}
|
|
698
|
+
} });
|
|
699
|
+
let body;
|
|
700
|
+
if (strategy === "text" || autocompletePaths.length === 0) body = textClause();
|
|
701
|
+
else if (strategy === "autocomplete") {
|
|
702
|
+
const clauses = autocompletePaths.map(autocompleteClause);
|
|
703
|
+
body = clauses.length === 1 ? clauses[0] : { compound: {
|
|
704
|
+
should: clauses,
|
|
705
|
+
minimumShouldMatch: 1
|
|
706
|
+
} };
|
|
707
|
+
} else body = { compound: {
|
|
708
|
+
should: [textClause(), ...autocompletePaths.map(autocompleteClause)],
|
|
709
|
+
minimumShouldMatch: 1
|
|
710
|
+
} };
|
|
682
711
|
return {
|
|
683
712
|
stage: { $search: {
|
|
684
713
|
index: index.key,
|
|
685
|
-
|
|
686
|
-
query: text,
|
|
687
|
-
path: { wildcard: "*" }
|
|
688
|
-
}
|
|
714
|
+
...body
|
|
689
715
|
} },
|
|
690
716
|
classicText: false
|
|
691
717
|
};
|
|
692
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* Resolves query-time fuzzy (typo tolerance): the `$fuzzy` request control
|
|
721
|
+
* overrides the schema-declared `@db.mongo.search.*` fuzzy. Only an edit distance
|
|
722
|
+
* of 1 or 2 is emitted (Atlas rejects 0) — anything else means "no fuzzy".
|
|
723
|
+
*/
|
|
724
|
+
function resolveSearchFuzzy(index, controls) {
|
|
725
|
+
const override = controls?.$fuzzy;
|
|
726
|
+
const maxEdits = override === void 0 ? index.fuzzy?.maxEdits : Number(override);
|
|
727
|
+
return maxEdits === 1 || maxEdits === 2 ? { maxEdits } : void 0;
|
|
728
|
+
}
|
|
729
|
+
/** Field paths in this index that carry an `autocomplete` mapping. */
|
|
730
|
+
function autocompleteFieldPaths(index) {
|
|
731
|
+
const fields = index.definition.mappings?.fields;
|
|
732
|
+
if (!fields) return [];
|
|
733
|
+
const paths = [];
|
|
734
|
+
for (const [path, mapping] of Object.entries(fields)) if ((Array.isArray(mapping) ? mapping : [mapping]).some((m) => m.type === "autocomplete")) paths.push(path);
|
|
735
|
+
return paths;
|
|
736
|
+
}
|
|
693
737
|
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
694
738
|
function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
695
739
|
let index;
|
|
@@ -1288,10 +1332,26 @@ function fieldsMatch(left, right) {
|
|
|
1288
1332
|
if (leftKeys.length !== rightKeys.length) return false;
|
|
1289
1333
|
for (const key of leftKeys) {
|
|
1290
1334
|
if (!(key in right)) return false;
|
|
1291
|
-
if (left[key]
|
|
1335
|
+
if (!fieldMappingEqual(left[key], right[key])) return false;
|
|
1292
1336
|
}
|
|
1293
1337
|
return true;
|
|
1294
1338
|
}
|
|
1339
|
+
/** Order-independent structural compare of a field's Atlas type mapping(s). */
|
|
1340
|
+
function fieldMappingEqual(a, b) {
|
|
1341
|
+
const am = mappingsByType(a);
|
|
1342
|
+
const bm = mappingsByType(b);
|
|
1343
|
+
if (am.size !== bm.size) return false;
|
|
1344
|
+
for (const [type, av] of am) {
|
|
1345
|
+
const bv = bm.get(type);
|
|
1346
|
+
if (!bv || av.analyzer !== bv.analyzer || av.tokenization !== bv.tokenization || av.minGrams !== bv.minGrams || av.maxGrams !== bv.maxGrams || av.foldDiacritics !== bv.foldDiacritics) return false;
|
|
1347
|
+
}
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
function mappingsByType(m) {
|
|
1351
|
+
const map = /* @__PURE__ */ new Map();
|
|
1352
|
+
for (const x of Array.isArray(m) ? m : [m]) map.set(x.type, x);
|
|
1353
|
+
return map;
|
|
1354
|
+
}
|
|
1295
1355
|
function vectorFieldsMatch(left, right) {
|
|
1296
1356
|
if (left.length !== (right || []).length) return false;
|
|
1297
1357
|
const rightMap = /* @__PURE__ */ new Map();
|
|
@@ -1317,6 +1377,8 @@ const validateMongoIdPlugin = (ctx, def, value) => {
|
|
|
1317
1377
|
//#endregion
|
|
1318
1378
|
//#region src/lib/mongo-adapter.ts
|
|
1319
1379
|
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
1380
|
+
db;
|
|
1381
|
+
client;
|
|
1320
1382
|
_collection;
|
|
1321
1383
|
/** MongoDB-specific indexes (search, vector) — separate from table.indexes. */
|
|
1322
1384
|
_mongoIndexes = /* @__PURE__ */ new Map();
|
|
@@ -1507,13 +1569,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1507
1569
|
const dynamicText = typeMeta.get("db.mongo.search.dynamic");
|
|
1508
1570
|
if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
|
|
1509
1571
|
mappings: { dynamic: true },
|
|
1510
|
-
analyzer: dynamicText.analyzer
|
|
1511
|
-
|
|
1512
|
-
});
|
|
1572
|
+
analyzer: dynamicText.analyzer
|
|
1573
|
+
}, { fuzzy: normalizeSearchFuzzy(dynamicText.fuzzy) });
|
|
1513
1574
|
for (const textSearch of typeMeta.get("db.mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
|
|
1514
1575
|
mappings: { fields: {} },
|
|
1515
|
-
analyzer: textSearch.analyzer
|
|
1516
|
-
|
|
1576
|
+
analyzer: textSearch.analyzer
|
|
1577
|
+
}, {
|
|
1578
|
+
fuzzy: normalizeSearchFuzzy(textSearch.fuzzy),
|
|
1579
|
+
strategy: normalizeSearchStrategy(textSearch.strategy)
|
|
1517
1580
|
});
|
|
1518
1581
|
}
|
|
1519
1582
|
onFieldScanned(field, _type, metadata) {
|
|
@@ -1527,7 +1590,24 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1527
1590
|
const startValue = metadata.get("db.default.increment");
|
|
1528
1591
|
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1529
1592
|
}
|
|
1530
|
-
for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer
|
|
1593
|
+
for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, [index.analyzer ? {
|
|
1594
|
+
type: "string",
|
|
1595
|
+
analyzer: index.analyzer
|
|
1596
|
+
} : { type: "string" }]);
|
|
1597
|
+
for (const ac of metadata.get("db.mongo.search.autocomplete") || []) {
|
|
1598
|
+
const autocomplete = {
|
|
1599
|
+
type: "autocomplete",
|
|
1600
|
+
tokenization: ac.tokenization || "edgeGram",
|
|
1601
|
+
minGrams: ac.minGrams ?? 2,
|
|
1602
|
+
maxGrams: ac.maxGrams ?? 15,
|
|
1603
|
+
foldDiacritics: ac.foldDiacritics ?? true
|
|
1604
|
+
};
|
|
1605
|
+
const companion = ac.analyzer ? {
|
|
1606
|
+
type: "string",
|
|
1607
|
+
analyzer: ac.analyzer
|
|
1608
|
+
} : { type: "string" };
|
|
1609
|
+
this._addFieldToSearchIndex("search_text", ac.indexName, field, [autocomplete, companion]);
|
|
1610
|
+
}
|
|
1531
1611
|
const vectorIndex = metadata.get("db.search.vector");
|
|
1532
1612
|
if (vectorIndex) {
|
|
1533
1613
|
const indexName = vectorIndex.indexName || field;
|
|
@@ -1988,32 +2068,68 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1988
2068
|
}
|
|
1989
2069
|
if (weight) index.weights[field] = weight;
|
|
1990
2070
|
}
|
|
1991
|
-
_setSearchIndex(type, name, definition) {
|
|
2071
|
+
_setSearchIndex(type, name, definition, meta) {
|
|
1992
2072
|
const key = mongoIndexKey(type, name || "DEFAULT");
|
|
1993
|
-
|
|
2073
|
+
const index = {
|
|
1994
2074
|
key,
|
|
1995
2075
|
name: name || "DEFAULT",
|
|
1996
2076
|
type,
|
|
1997
|
-
definition
|
|
1998
|
-
|
|
2077
|
+
definition,
|
|
2078
|
+
fuzzy: meta?.fuzzy,
|
|
2079
|
+
strategy: meta?.strategy
|
|
2080
|
+
};
|
|
2081
|
+
this._mongoIndexes.set(key, index);
|
|
2082
|
+
return index;
|
|
1999
2083
|
}
|
|
2000
|
-
|
|
2084
|
+
/**
|
|
2085
|
+
* Adds (and merges, by mapping `type`) one or more Atlas field-type mappings to
|
|
2086
|
+
* a `search_text` index. Multiple annotations on the same field — e.g.
|
|
2087
|
+
* `@db.mongo.search.text` (string) plus `@db.mongo.search.autocomplete`
|
|
2088
|
+
* (autocomplete + string) — accumulate into a single multi-type mapping
|
|
2089
|
+
* instead of overwriting one another.
|
|
2090
|
+
*/
|
|
2091
|
+
_addFieldToSearchIndex(type, _name, fieldName, mappings) {
|
|
2001
2092
|
const name = _name || "DEFAULT";
|
|
2002
2093
|
let index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2003
|
-
if (!index && type === "search_text") {
|
|
2004
|
-
this._setSearchIndex(type, name, {
|
|
2005
|
-
mappings: { fields: {} },
|
|
2006
|
-
text: { fuzzy: { maxEdits: 0 } }
|
|
2007
|
-
});
|
|
2008
|
-
index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2009
|
-
}
|
|
2094
|
+
if (!index && type === "search_text") index = this._setSearchIndex(type, name, { mappings: { fields: {} } });
|
|
2010
2095
|
if (index) {
|
|
2011
|
-
index.definition.mappings.fields
|
|
2012
|
-
|
|
2096
|
+
const fields = index.definition.mappings.fields;
|
|
2097
|
+
fields[fieldName] = mergeFieldMappings(fields[fieldName], mappings);
|
|
2013
2098
|
}
|
|
2014
2099
|
}
|
|
2015
2100
|
};
|
|
2016
2101
|
/**
|
|
2102
|
+
* Normalizes a declared `fuzzy` arg (`0-2`) into the query-time metadata Atlas
|
|
2103
|
+
* accepts. Atlas only honors an edit distance of `1` or `2`; `0`/undefined means
|
|
2104
|
+
* "no fuzzy", returned as `undefined` so no `fuzzy` clause is ever emitted.
|
|
2105
|
+
*/
|
|
2106
|
+
function normalizeSearchFuzzy(fuzzy) {
|
|
2107
|
+
return fuzzy === 1 || fuzzy === 2 ? { maxEdits: fuzzy } : void 0;
|
|
2108
|
+
}
|
|
2109
|
+
/** Narrows the declared `strategy` arg to a known value (undefined → query layer defaults to "compound"). */
|
|
2110
|
+
function normalizeSearchStrategy(strategy) {
|
|
2111
|
+
return strategy === "compound" || strategy === "autocomplete" || strategy === "text" ? strategy : void 0;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Merges incoming Atlas field-type mappings into a field's existing mapping,
|
|
2115
|
+
* keyed by `type` (a later mapping of the same type replaces the earlier one).
|
|
2116
|
+
* Collapses to a single object when one type remains, else an array (Atlas's
|
|
2117
|
+
* multi-type field form).
|
|
2118
|
+
*/
|
|
2119
|
+
function mergeFieldMappings(existing, incoming) {
|
|
2120
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2121
|
+
const order = [];
|
|
2122
|
+
const add = (m) => {
|
|
2123
|
+
if (!byType.has(m.type)) order.push(m.type);
|
|
2124
|
+
byType.set(m.type, m);
|
|
2125
|
+
};
|
|
2126
|
+
if (Array.isArray(existing)) existing.forEach(add);
|
|
2127
|
+
else if (existing) add(existing);
|
|
2128
|
+
incoming.forEach(add);
|
|
2129
|
+
const merged = order.map((t) => byType.get(t));
|
|
2130
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2017
2133
|
* Builds a MongoDB update document from a data object that may contain
|
|
2018
2134
|
* field ops (`{ $inc: N }`, `{ $dec: N }`, `{ $mul: N }`).
|
|
2019
2135
|
* Regular fields go into `$set`, ops go into `$inc` / `$mul`. When
|
package/dist/plugin.cjs
CHANGED
|
@@ -20,6 +20,18 @@ const analyzers = [
|
|
|
20
20
|
"lucene.russian",
|
|
21
21
|
"lucene.arabic"
|
|
22
22
|
];
|
|
23
|
+
const tokenizations = [
|
|
24
|
+
"edgeGram",
|
|
25
|
+
"rightEdgeGram",
|
|
26
|
+
"nGram"
|
|
27
|
+
];
|
|
28
|
+
const searchStrategies = [
|
|
29
|
+
"compound",
|
|
30
|
+
"autocomplete",
|
|
31
|
+
"text"
|
|
32
|
+
];
|
|
33
|
+
const strategyDescription = "How `search()` matches a term against this index. Locks the query shape into the index — there is no query-time mode switching.\n\n- `compound` (default) → rank exact-word hits above prefix hits: a wildcard `text` clause **plus** one `autocomplete` clause per autocomplete field. Degrades to plain `text` when the index has no autocomplete field.\n- `autocomplete` → **prefix/typeahead only** — query just the autocomplete fields, no word-match ranking clause.\n- `text` → **word matching only** — a single `text` operator over all string-mapped fields (autocomplete fields are matched via their companion `string` mapping).\n\nTo use the same data with a different strategy, declare a second index and select it per request with `$index`.";
|
|
34
|
+
const fuzzyDescription = "Maximum typo tolerance, applied **at query time** to the search operator.\n\n- `0` (default) → no fuzzy matching (exact tokens).\n- `1` → allows small typos (e.g., `\"mongo\"` ≈ `\"mango\"`).\n- `2` → more typo tolerance (e.g., `\"mongodb\"` ≈ `\"mangodb\"`).\n\nAtlas only accepts an edit distance of `1` or `2`; `0` simply disables fuzzy. Can be overridden per request via the `$fuzzy` query control.";
|
|
23
35
|
/**
|
|
24
36
|
* MongoDB-specific annotations.
|
|
25
37
|
*
|
|
@@ -107,7 +119,7 @@ const annotations = {
|
|
|
107
119
|
optional: true,
|
|
108
120
|
name: "fuzzy",
|
|
109
121
|
type: "number",
|
|
110
|
-
description:
|
|
122
|
+
description: fuzzyDescription
|
|
111
123
|
}]
|
|
112
124
|
}),
|
|
113
125
|
static: new _atscript_core.AnnotationSpec({
|
|
@@ -126,13 +138,20 @@ const annotations = {
|
|
|
126
138
|
optional: true,
|
|
127
139
|
name: "fuzzy",
|
|
128
140
|
type: "number",
|
|
129
|
-
description:
|
|
141
|
+
description: fuzzyDescription
|
|
130
142
|
},
|
|
131
143
|
{
|
|
132
144
|
optional: true,
|
|
133
145
|
name: "indexName",
|
|
134
146
|
type: "string",
|
|
135
|
-
description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
|
|
147
|
+
description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text` or `@db.mongo.search.autocomplete`. If not set, defaults to `\"DEFAULT\"`."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
optional: true,
|
|
151
|
+
name: "strategy",
|
|
152
|
+
type: "string",
|
|
153
|
+
description: strategyDescription,
|
|
154
|
+
values: searchStrategies
|
|
136
155
|
}
|
|
137
156
|
]
|
|
138
157
|
}),
|
|
@@ -152,6 +171,51 @@ const annotations = {
|
|
|
152
171
|
type: "string",
|
|
153
172
|
description: "The **name of the search index** defined in `@db.mongo.search.static`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
|
|
154
173
|
}]
|
|
174
|
+
}),
|
|
175
|
+
autocomplete: new _atscript_core.AnnotationSpec({
|
|
176
|
+
description: "Marks a field for **prefix / typeahead (as-you-type)** matching in a MongoDB Atlas Search Index.\n\n- Indexes the field as the Atlas **`autocomplete`** type, **and** double-maps it as `string` so exact-word hits still rank.\n- Lets `search()` match partial words: with the default `edgeGram` tokenization, `\"art\"` matches `\"Artem\"` **as you type** (no whole word required).\n- Use `nGram` tokenization for true mid-word (infix/substring) matching at higher index cost.\n- Like `@db.mongo.search.text`, the field joins the index named by `indexName` (or the default index).\n\n**Example:**\n```atscript\n@db.mongo.search.autocomplete \"users\"\nusername: string\n```\n",
|
|
177
|
+
nodeType: ["prop"],
|
|
178
|
+
multiple: true,
|
|
179
|
+
argument: [
|
|
180
|
+
{
|
|
181
|
+
optional: true,
|
|
182
|
+
name: "indexName",
|
|
183
|
+
type: "string",
|
|
184
|
+
description: "The **name of the search index** (defined by `@db.mongo.search.static`) this field joins. If not set, defaults to `\"DEFAULT\"`."
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
optional: true,
|
|
188
|
+
name: "tokenization",
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "How the field is tokenized for partial matching:\n\n- `\"edgeGram\"` (default) → **prefix** matching from the start of each word (`\"art\"` → `\"Artem\"`).\n- `\"nGram\"` → **substring/infix** matching anywhere inside a word (`\"tem\"` → `\"Artem\"`); larger index, slower builds.\n- `\"rightEdgeGram\"` → **suffix** matching from the end of each word.",
|
|
191
|
+
values: tokenizations
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
optional: true,
|
|
195
|
+
name: "minGrams",
|
|
196
|
+
type: "number",
|
|
197
|
+
description: "Minimum number of characters per indexed sequence. Defaults to `2`."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
optional: true,
|
|
201
|
+
name: "maxGrams",
|
|
202
|
+
type: "number",
|
|
203
|
+
description: "Maximum number of characters per indexed sequence. Defaults to `15`."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
optional: true,
|
|
207
|
+
name: "foldDiacritics",
|
|
208
|
+
type: "boolean",
|
|
209
|
+
description: "Whether to fold (ignore) diacritics so `\"café\"` matches `\"cafe\"`. Defaults to `true`."
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
optional: true,
|
|
213
|
+
name: "analyzer",
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "The text analyzer for the companion `string` mapping. Defaults to `\"lucene.standard\"`.",
|
|
216
|
+
values: analyzers
|
|
217
|
+
}
|
|
218
|
+
]
|
|
155
219
|
})
|
|
156
220
|
}
|
|
157
221
|
};
|
package/dist/plugin.mjs
CHANGED
|
@@ -16,6 +16,18 @@ const analyzers = [
|
|
|
16
16
|
"lucene.russian",
|
|
17
17
|
"lucene.arabic"
|
|
18
18
|
];
|
|
19
|
+
const tokenizations = [
|
|
20
|
+
"edgeGram",
|
|
21
|
+
"rightEdgeGram",
|
|
22
|
+
"nGram"
|
|
23
|
+
];
|
|
24
|
+
const searchStrategies = [
|
|
25
|
+
"compound",
|
|
26
|
+
"autocomplete",
|
|
27
|
+
"text"
|
|
28
|
+
];
|
|
29
|
+
const strategyDescription = "How `search()` matches a term against this index. Locks the query shape into the index — there is no query-time mode switching.\n\n- `compound` (default) → rank exact-word hits above prefix hits: a wildcard `text` clause **plus** one `autocomplete` clause per autocomplete field. Degrades to plain `text` when the index has no autocomplete field.\n- `autocomplete` → **prefix/typeahead only** — query just the autocomplete fields, no word-match ranking clause.\n- `text` → **word matching only** — a single `text` operator over all string-mapped fields (autocomplete fields are matched via their companion `string` mapping).\n\nTo use the same data with a different strategy, declare a second index and select it per request with `$index`.";
|
|
30
|
+
const fuzzyDescription = "Maximum typo tolerance, applied **at query time** to the search operator.\n\n- `0` (default) → no fuzzy matching (exact tokens).\n- `1` → allows small typos (e.g., `\"mongo\"` ≈ `\"mango\"`).\n- `2` → more typo tolerance (e.g., `\"mongodb\"` ≈ `\"mangodb\"`).\n\nAtlas only accepts an edit distance of `1` or `2`; `0` simply disables fuzzy. Can be overridden per request via the `$fuzzy` query control.";
|
|
19
31
|
/**
|
|
20
32
|
* MongoDB-specific annotations.
|
|
21
33
|
*
|
|
@@ -103,7 +115,7 @@ const annotations = {
|
|
|
103
115
|
optional: true,
|
|
104
116
|
name: "fuzzy",
|
|
105
117
|
type: "number",
|
|
106
|
-
description:
|
|
118
|
+
description: fuzzyDescription
|
|
107
119
|
}]
|
|
108
120
|
}),
|
|
109
121
|
static: new AnnotationSpec({
|
|
@@ -122,13 +134,20 @@ const annotations = {
|
|
|
122
134
|
optional: true,
|
|
123
135
|
name: "fuzzy",
|
|
124
136
|
type: "number",
|
|
125
|
-
description:
|
|
137
|
+
description: fuzzyDescription
|
|
126
138
|
},
|
|
127
139
|
{
|
|
128
140
|
optional: true,
|
|
129
141
|
name: "indexName",
|
|
130
142
|
type: "string",
|
|
131
|
-
description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
|
|
143
|
+
description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text` or `@db.mongo.search.autocomplete`. If not set, defaults to `\"DEFAULT\"`."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
optional: true,
|
|
147
|
+
name: "strategy",
|
|
148
|
+
type: "string",
|
|
149
|
+
description: strategyDescription,
|
|
150
|
+
values: searchStrategies
|
|
132
151
|
}
|
|
133
152
|
]
|
|
134
153
|
}),
|
|
@@ -148,6 +167,51 @@ const annotations = {
|
|
|
148
167
|
type: "string",
|
|
149
168
|
description: "The **name of the search index** defined in `@db.mongo.search.static`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
|
|
150
169
|
}]
|
|
170
|
+
}),
|
|
171
|
+
autocomplete: new AnnotationSpec({
|
|
172
|
+
description: "Marks a field for **prefix / typeahead (as-you-type)** matching in a MongoDB Atlas Search Index.\n\n- Indexes the field as the Atlas **`autocomplete`** type, **and** double-maps it as `string` so exact-word hits still rank.\n- Lets `search()` match partial words: with the default `edgeGram` tokenization, `\"art\"` matches `\"Artem\"` **as you type** (no whole word required).\n- Use `nGram` tokenization for true mid-word (infix/substring) matching at higher index cost.\n- Like `@db.mongo.search.text`, the field joins the index named by `indexName` (or the default index).\n\n**Example:**\n```atscript\n@db.mongo.search.autocomplete \"users\"\nusername: string\n```\n",
|
|
173
|
+
nodeType: ["prop"],
|
|
174
|
+
multiple: true,
|
|
175
|
+
argument: [
|
|
176
|
+
{
|
|
177
|
+
optional: true,
|
|
178
|
+
name: "indexName",
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "The **name of the search index** (defined by `@db.mongo.search.static`) this field joins. If not set, defaults to `\"DEFAULT\"`."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
optional: true,
|
|
184
|
+
name: "tokenization",
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "How the field is tokenized for partial matching:\n\n- `\"edgeGram\"` (default) → **prefix** matching from the start of each word (`\"art\"` → `\"Artem\"`).\n- `\"nGram\"` → **substring/infix** matching anywhere inside a word (`\"tem\"` → `\"Artem\"`); larger index, slower builds.\n- `\"rightEdgeGram\"` → **suffix** matching from the end of each word.",
|
|
187
|
+
values: tokenizations
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
optional: true,
|
|
191
|
+
name: "minGrams",
|
|
192
|
+
type: "number",
|
|
193
|
+
description: "Minimum number of characters per indexed sequence. Defaults to `2`."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
optional: true,
|
|
197
|
+
name: "maxGrams",
|
|
198
|
+
type: "number",
|
|
199
|
+
description: "Maximum number of characters per indexed sequence. Defaults to `15`."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
optional: true,
|
|
203
|
+
name: "foldDiacritics",
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "Whether to fold (ignore) diacritics so `\"café\"` matches `\"cafe\"`. Defaults to `true`."
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
optional: true,
|
|
209
|
+
name: "analyzer",
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "The text analyzer for the companion `string` mapping. Defaults to `\"lucene.standard\"`.",
|
|
212
|
+
values: analyzers
|
|
213
|
+
}
|
|
214
|
+
]
|
|
151
215
|
})
|
|
152
216
|
}
|
|
153
217
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.102",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@atscript/core": "^0.1.74",
|
|
57
57
|
"@atscript/typescript": "^0.1.74",
|
|
58
58
|
"mongodb": "^6.17.0",
|
|
59
|
-
"@atscript/db": "^0.1.
|
|
59
|
+
"@atscript/db": "^0.1.102"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"postinstall": "asc -f dts",
|
|
File without changes
|
|
File without changes
|