taladb 0.9.4 → 0.10.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/README.md +2 -2
- package/dist/index.browser.mjs +218 -24
- package/dist/index.d.mts +163 -5
- package/dist/index.d.ts +163 -5
- package/dist/index.js +218 -24
- package/dist/index.mjs +218 -24
- package/dist/index.react-native.mjs +218 -24
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Local-first document database for React, React Native, and Node.js — powered by a Rust/WASM core with zero GC pauses.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/taladb)
|
|
6
|
-
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
## What is TalaDB?
|
|
9
9
|
|
|
@@ -147,4 +147,4 @@ await runMigrations(db, [
|
|
|
147
147
|
|
|
148
148
|
## License
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
Apache 2.0 © [ThinkGrid Labs](https://github.com/thinkgrid-labs)
|
package/dist/index.browser.mjs
CHANGED
|
@@ -659,18 +659,52 @@ function deepEqual(a, b) {
|
|
|
659
659
|
);
|
|
660
660
|
}
|
|
661
661
|
function applySchema(col, options) {
|
|
662
|
-
const {
|
|
662
|
+
const {
|
|
663
|
+
schema,
|
|
664
|
+
validateOnRead = false,
|
|
665
|
+
migrateDocument,
|
|
666
|
+
downgradeDocument,
|
|
667
|
+
syncSchema,
|
|
668
|
+
persistMigrations = false,
|
|
669
|
+
allowFieldRemoval = false,
|
|
670
|
+
retiredFields = []
|
|
671
|
+
} = options;
|
|
663
672
|
const targetVersion = syncSchema?.version ?? 0;
|
|
664
673
|
if (migrateDocument && targetVersion < 1) {
|
|
665
674
|
throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
|
|
666
675
|
}
|
|
676
|
+
if (downgradeDocument && targetVersion < 1) {
|
|
677
|
+
throw new Error("CollectionOptions.downgradeDocument requires syncSchema.version (the shape this build reads)");
|
|
678
|
+
}
|
|
667
679
|
if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
|
|
668
680
|
throw new Error(
|
|
669
681
|
"CollectionOptions.syncSchema.renames/defaults require syncSchema.version >= 1 \u2014 without a version the import migration step never runs and documents missing the renamed/defaulted fields are quarantined instead of upgraded"
|
|
670
682
|
);
|
|
671
683
|
}
|
|
672
684
|
const stampVersion = targetVersion > 0;
|
|
673
|
-
if (!schema && !migrateDocument && !stampVersion) return col;
|
|
685
|
+
if (!schema && !migrateDocument && !downgradeDocument && !stampVersion) return col;
|
|
686
|
+
const retired = new Set(retiredFields);
|
|
687
|
+
const engineOwned = /* @__PURE__ */ new Set([
|
|
688
|
+
"_id",
|
|
689
|
+
"_v",
|
|
690
|
+
"_changed_at",
|
|
691
|
+
"_remote",
|
|
692
|
+
"_remote_rev",
|
|
693
|
+
"_replica_scope"
|
|
694
|
+
]);
|
|
695
|
+
const downcastViews = /* @__PURE__ */ new WeakSet();
|
|
696
|
+
function preserveFields(original, next, preserveUnknown, preserveVersion = true) {
|
|
697
|
+
let out = null;
|
|
698
|
+
for (const k of Object.keys(original)) {
|
|
699
|
+
const ownedField = engineOwned.has(k) && (k !== "_v" || preserveVersion);
|
|
700
|
+
const mustRestore = ownedField || preserveUnknown && !retired.has(k) && !(k in next);
|
|
701
|
+
if (!mustRestore) continue;
|
|
702
|
+
if (!ownedField && k in next) continue;
|
|
703
|
+
out ?? (out = { ...next });
|
|
704
|
+
out[k] = original[k];
|
|
705
|
+
}
|
|
706
|
+
return out ?? next;
|
|
707
|
+
}
|
|
674
708
|
function parseWrite(doc, label) {
|
|
675
709
|
try {
|
|
676
710
|
return schema.parse(doc);
|
|
@@ -678,6 +712,43 @@ function applySchema(col, options) {
|
|
|
678
712
|
throw new TalaDbValidationError(err, label);
|
|
679
713
|
}
|
|
680
714
|
}
|
|
715
|
+
function assertWritableDocument(doc, label) {
|
|
716
|
+
if (doc && typeof doc === "object" && downcastViews.has(doc)) {
|
|
717
|
+
throw new Error(`${label}: a downgradeDocument result is a read-only compatibility view`);
|
|
718
|
+
}
|
|
719
|
+
const version = doc?._v;
|
|
720
|
+
if (targetVersion > 0 && typeof version === "number" && version > targetVersion) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`${label}: this client supports schema v${targetVersion}, but the document is v${version}`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function writableFilter(filter) {
|
|
727
|
+
if (targetVersion < 1) return filter;
|
|
728
|
+
return {
|
|
729
|
+
$and: [
|
|
730
|
+
filter,
|
|
731
|
+
{
|
|
732
|
+
$or: [
|
|
733
|
+
{ _v: { $exists: false } },
|
|
734
|
+
{ _v: { $lte: targetVersion } }
|
|
735
|
+
]
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function assertSafeUpdate(update) {
|
|
741
|
+
const record = update;
|
|
742
|
+
for (const op of ["$set", "$unset", "$inc", "$push", "$pull"]) {
|
|
743
|
+
const fields = record[op];
|
|
744
|
+
if (!fields) continue;
|
|
745
|
+
for (const field of Object.keys(fields)) {
|
|
746
|
+
if (engineOwned.has(field)) {
|
|
747
|
+
throw new Error(`update cannot modify engine-owned field '${field}'`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
681
752
|
function stamp(doc) {
|
|
682
753
|
if (!stampVersion || doc._v !== void 0) return doc;
|
|
683
754
|
return { ...doc, _v: targetVersion };
|
|
@@ -693,50 +764,86 @@ function applySchema(col, options) {
|
|
|
693
764
|
if (k === "_id") continue;
|
|
694
765
|
if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
|
|
695
766
|
}
|
|
696
|
-
|
|
697
|
-
|
|
767
|
+
if (allowFieldRemoval || retired.size > 0) {
|
|
768
|
+
for (const k of Object.keys(original)) {
|
|
769
|
+
if (k !== "_id" && !(k in migrated) && (allowFieldRemoval || retired.has(k))) $unset[k] = true;
|
|
770
|
+
}
|
|
698
771
|
}
|
|
699
772
|
const update = {};
|
|
700
773
|
if (Object.keys($set).length) update.$set = $set;
|
|
701
774
|
if (Object.keys($unset).length) update.$unset = $unset;
|
|
702
775
|
return Object.keys(update).length ? update : null;
|
|
703
776
|
}
|
|
704
|
-
function
|
|
705
|
-
if (!migrateDocument) return doc;
|
|
777
|
+
function normalizeRead(doc) {
|
|
706
778
|
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
707
|
-
if (fromVersion
|
|
708
|
-
|
|
779
|
+
if (migrateDocument && fromVersion < targetVersion) {
|
|
780
|
+
const up = { ...migrateDocument(doc, fromVersion), _v: targetVersion };
|
|
781
|
+
return {
|
|
782
|
+
// The migration owns the version transition, while every other
|
|
783
|
+
// engine-owned field continues to come from the stored document.
|
|
784
|
+
value: allowFieldRemoval ? preserveFields(doc, up, false, false) : preserveFields(doc, up, true, false),
|
|
785
|
+
persistable: true
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (downgradeDocument && fromVersion > targetVersion) {
|
|
789
|
+
const projected = {
|
|
790
|
+
...downgradeDocument(doc, fromVersion),
|
|
791
|
+
...doc._id !== void 0 ? { _id: doc._id } : {},
|
|
792
|
+
_v: fromVersion
|
|
793
|
+
};
|
|
794
|
+
downcastViews.add(projected);
|
|
795
|
+
return { value: projected, persistable: false };
|
|
796
|
+
}
|
|
797
|
+
return { value: doc, persistable: true };
|
|
709
798
|
}
|
|
710
799
|
function validateRead(doc) {
|
|
711
800
|
if (!validateOnRead || !schema) return doc;
|
|
712
801
|
try {
|
|
713
|
-
|
|
802
|
+
const parsed = schema.parse(doc);
|
|
803
|
+
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
804
|
+
return preserveFields(doc, parsed, targetVersion > 0 && fromVersion > targetVersion);
|
|
714
805
|
} catch (err) {
|
|
715
806
|
throw new TalaDbValidationError(err, "read");
|
|
716
807
|
}
|
|
717
808
|
}
|
|
718
|
-
async function persistAll(originals,
|
|
809
|
+
async function persistAll(originals, normalized) {
|
|
719
810
|
if (!persistMigrations) return;
|
|
720
811
|
for (let i = 0; i < originals.length; i++) {
|
|
721
812
|
const original = originals[i];
|
|
722
|
-
if (
|
|
723
|
-
const
|
|
813
|
+
if (!normalized[i].persistable) continue;
|
|
814
|
+
const migrated = normalized[i].value;
|
|
815
|
+
if (migrated === original || typeof original._id !== "string") continue;
|
|
816
|
+
const update = diffUpdate(original, migrated);
|
|
724
817
|
if (!update) continue;
|
|
725
818
|
try {
|
|
726
|
-
|
|
819
|
+
const guards = [{ _id: original._id }];
|
|
820
|
+
if (original._v === void 0) guards.push({ _v: { $exists: false } });
|
|
821
|
+
else guards.push({ _v: original._v });
|
|
822
|
+
if (original._changed_at !== void 0) {
|
|
823
|
+
guards.push({ _changed_at: original._changed_at });
|
|
824
|
+
}
|
|
825
|
+
await col.updateOne({ $and: guards }, update);
|
|
727
826
|
} catch {
|
|
728
827
|
}
|
|
729
828
|
}
|
|
730
829
|
}
|
|
731
|
-
const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
|
|
830
|
+
const wrapReads = Boolean(migrateDocument) || Boolean(downgradeDocument) || validateOnRead && Boolean(schema);
|
|
732
831
|
const wrapWrites = Boolean(schema) || stampVersion;
|
|
832
|
+
function pipelinePreservesDocuments(pipeline) {
|
|
833
|
+
return pipeline.every((stage) => !("$group" in stage) && !("$project" in stage));
|
|
834
|
+
}
|
|
835
|
+
function normalizeViewRows(docs) {
|
|
836
|
+
return docs.map((doc) => validateRead(normalizeRead(doc).value));
|
|
837
|
+
}
|
|
733
838
|
return {
|
|
734
839
|
...col,
|
|
735
840
|
insert: wrapWrites ? async (doc) => {
|
|
841
|
+
assertWritableDocument(doc, "insert");
|
|
736
842
|
if (schema) parseWrite(doc, "insert");
|
|
737
843
|
return col.insert(stamp(doc));
|
|
738
844
|
} : col.insert.bind(col),
|
|
739
845
|
insertMany: wrapWrites ? async (docs) => {
|
|
846
|
+
docs.forEach((doc, i) => assertWritableDocument(doc, `insertMany[${i}]`));
|
|
740
847
|
if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
|
|
741
848
|
return col.insertMany(docs.map(stamp));
|
|
742
849
|
} : col.insertMany.bind(col),
|
|
@@ -745,6 +852,14 @@ function applySchema(col, options) {
|
|
|
745
852
|
// runtime schema check have to be the same seam, or a malformed server
|
|
746
853
|
// response walks straight into a typed collection.
|
|
747
854
|
replaceManyWithIds: wrapWrites ? async (docs, origin) => {
|
|
855
|
+
if (origin !== "remote") {
|
|
856
|
+
docs.forEach((doc, i) => assertWritableDocument(doc, `replaceManyWithIds[${i}]`));
|
|
857
|
+
if (targetVersion > 0) {
|
|
858
|
+
throw new Error(
|
|
859
|
+
"local replaceManyWithIds is disabled on versioned collections; use updateOne/updateMany so schema-version guards are atomic"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
748
863
|
if (schema) docs.forEach((doc, i) => {
|
|
749
864
|
const { _replica_scope, _remote_rev, ...schemaDoc } = doc;
|
|
750
865
|
void _replica_scope;
|
|
@@ -753,38 +868,75 @@ function applySchema(col, options) {
|
|
|
753
868
|
});
|
|
754
869
|
return col.replaceManyWithIds(docs.map((d) => stampDoc(d)), origin);
|
|
755
870
|
} : col.replaceManyWithIds.bind(col),
|
|
871
|
+
deleteManyWithIds: stampVersion ? async (ids, origin) => {
|
|
872
|
+
if (origin !== "remote") {
|
|
873
|
+
throw new Error(
|
|
874
|
+
"local deleteManyWithIds is disabled on versioned collections; use deleteOne/deleteMany so schema-version guards are atomic"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return col.deleteManyWithIds(ids, origin);
|
|
878
|
+
} : col.deleteManyWithIds.bind(col),
|
|
879
|
+
updateOne: wrapWrites ? async (filter, update) => {
|
|
880
|
+
assertSafeUpdate(update);
|
|
881
|
+
return col.updateOne(writableFilter(filter), update);
|
|
882
|
+
} : col.updateOne.bind(col),
|
|
883
|
+
updateMany: wrapWrites ? async (filter, update) => {
|
|
884
|
+
assertSafeUpdate(update);
|
|
885
|
+
return col.updateMany(writableFilter(filter), update);
|
|
886
|
+
} : col.updateMany.bind(col),
|
|
887
|
+
deleteOne: stampVersion ? (filter) => col.deleteOne(writableFilter(filter)) : col.deleteOne.bind(col),
|
|
888
|
+
deleteMany: stampVersion ? (filter) => col.deleteMany(writableFilter(filter)) : col.deleteMany.bind(col),
|
|
756
889
|
find: wrapReads ? async (filter) => {
|
|
757
890
|
const docs = await col.find(filter);
|
|
758
|
-
const
|
|
759
|
-
await persistAll(docs,
|
|
760
|
-
return
|
|
891
|
+
const normalized = docs.map(normalizeRead);
|
|
892
|
+
await persistAll(docs, normalized);
|
|
893
|
+
return normalized.map((n) => validateRead(n.value));
|
|
761
894
|
} : col.find.bind(col),
|
|
762
895
|
findOne: wrapReads ? async (filter) => {
|
|
763
896
|
const doc = await col.findOne(filter);
|
|
764
897
|
if (doc === null) return null;
|
|
765
|
-
const
|
|
766
|
-
await persistAll([doc], [
|
|
767
|
-
return validateRead(
|
|
898
|
+
const normalized = normalizeRead(doc);
|
|
899
|
+
await persistAll([doc], [normalized]);
|
|
900
|
+
return validateRead(normalized.value);
|
|
768
901
|
} : col.findOne.bind(col),
|
|
902
|
+
aggregate: wrapReads ? async (pipeline) => {
|
|
903
|
+
const docs = await col.aggregate(pipeline);
|
|
904
|
+
return pipelinePreservesDocuments(pipeline) ? normalizeViewRows(docs) : docs;
|
|
905
|
+
} : col.aggregate.bind(col),
|
|
769
906
|
// Live queries feed every @taladb/react hook (useFind, useFindOne,
|
|
770
907
|
// useQueries). Leaving them unwrapped meant React components received the
|
|
771
908
|
// un-migrated shape while a direct find() returned the migrated one.
|
|
772
909
|
subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
|
|
773
910
|
filter,
|
|
774
911
|
(docs) => {
|
|
775
|
-
const
|
|
912
|
+
const normalized = docs.map(normalizeRead);
|
|
776
913
|
let out;
|
|
777
914
|
try {
|
|
778
|
-
out =
|
|
915
|
+
out = normalized.map((n) => validateRead(n.value));
|
|
779
916
|
} catch (err) {
|
|
780
917
|
onError?.(err);
|
|
781
918
|
return;
|
|
782
919
|
}
|
|
783
920
|
callback(out);
|
|
784
|
-
void persistAll(docs,
|
|
921
|
+
void persistAll(docs, normalized);
|
|
785
922
|
},
|
|
786
923
|
onError
|
|
787
|
-
) : col.subscribe.bind(col)
|
|
924
|
+
) : col.subscribe.bind(col),
|
|
925
|
+
subscribeAggregate: wrapReads ? (pipeline, callback, onError) => col.subscribeAggregate(
|
|
926
|
+
pipeline,
|
|
927
|
+
(docs) => {
|
|
928
|
+
if (!pipelinePreservesDocuments(pipeline)) {
|
|
929
|
+
callback(docs);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
callback(normalizeViewRows(docs));
|
|
934
|
+
} catch (error) {
|
|
935
|
+
onError?.(error);
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
onError
|
|
939
|
+
) : col.subscribeAggregate.bind(col)
|
|
788
940
|
};
|
|
789
941
|
}
|
|
790
942
|
function detectPlatform() {
|
|
@@ -994,6 +1146,30 @@ async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
|
994
1146
|
});
|
|
995
1147
|
return JSON.parse(json);
|
|
996
1148
|
},
|
|
1149
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1150
|
+
const json = await proxy.send("searchText", {
|
|
1151
|
+
collection: name,
|
|
1152
|
+
field,
|
|
1153
|
+
query,
|
|
1154
|
+
topK,
|
|
1155
|
+
filterJson: filter ? JSON.stringify(filter) : "null",
|
|
1156
|
+
optionsJson: options ? JSON.stringify(options) : "null"
|
|
1157
|
+
});
|
|
1158
|
+
return JSON.parse(json);
|
|
1159
|
+
},
|
|
1160
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1161
|
+
const json = await proxy.send("hybridSearch", {
|
|
1162
|
+
collection: name,
|
|
1163
|
+
textField: text.textField,
|
|
1164
|
+
text: text.text,
|
|
1165
|
+
vectorField: vector.vectorField,
|
|
1166
|
+
vectorJson: JSON.stringify(vector.vector),
|
|
1167
|
+
topK,
|
|
1168
|
+
filterJson: filter ? JSON.stringify(filter) : "null",
|
|
1169
|
+
optionsJson: options ? JSON.stringify(options) : "null"
|
|
1170
|
+
});
|
|
1171
|
+
return JSON.parse(json);
|
|
1172
|
+
},
|
|
997
1173
|
subscribe: (filter, callback, onError) => nudgedPoller(
|
|
998
1174
|
() => proxy.send("find", {
|
|
999
1175
|
collection: name,
|
|
@@ -1138,6 +1314,12 @@ async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
|
1138
1314
|
const raw = await col.findNearest(field, vector, topK, filter ?? null);
|
|
1139
1315
|
return raw;
|
|
1140
1316
|
},
|
|
1317
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1318
|
+
return col.searchText(field, query, topK, filter ?? null, options ?? null);
|
|
1319
|
+
},
|
|
1320
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1321
|
+
return col.hybridSearch(text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
|
|
1322
|
+
},
|
|
1141
1323
|
subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError),
|
|
1142
1324
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => wrapped.aggregate(pipeline), callback, onError)
|
|
1143
1325
|
};
|
|
@@ -1220,6 +1402,18 @@ async function createNativeDB(_dbName, migrations) {
|
|
|
1220
1402
|
const raw = native.findNearest(name, field, vector, topK, filter ?? null);
|
|
1221
1403
|
return raw;
|
|
1222
1404
|
},
|
|
1405
|
+
searchText: async (field, query, topK, filter, options) => {
|
|
1406
|
+
if (!native.searchText) {
|
|
1407
|
+
throw new Error("searchText requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
|
|
1408
|
+
}
|
|
1409
|
+
return native.searchText(name, field, query, topK, filter ?? null, options ?? null);
|
|
1410
|
+
},
|
|
1411
|
+
hybridSearch: async (text, vector, topK, filter, options) => {
|
|
1412
|
+
if (!native.hybridSearch) {
|
|
1413
|
+
throw new Error("hybridSearch requires @taladb/react-native \u2265 0.10 \u2014 rebuild the native module");
|
|
1414
|
+
}
|
|
1415
|
+
return native.hybridSearch(name, text.textField, text.text, vector.vectorField, vector.vector, topK, filter ?? null, options ?? null);
|
|
1416
|
+
},
|
|
1223
1417
|
subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError),
|
|
1224
1418
|
subscribeAggregate: (pipeline, callback, onError) => makePoller(async () => native.aggregate(name, pipeline), callback, onError)
|
|
1225
1419
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -36,6 +36,67 @@ interface VectorSearchResult<T extends Document = Document> {
|
|
|
36
36
|
*/
|
|
37
37
|
score: number;
|
|
38
38
|
}
|
|
39
|
+
interface TextSearchResult<T extends Document = Document> {
|
|
40
|
+
/** The matched document. */
|
|
41
|
+
document: T;
|
|
42
|
+
/**
|
|
43
|
+
* BM25 relevance score — higher means more relevant. Unbounded above, and
|
|
44
|
+
* only meaningful for ordering within a single query's result set.
|
|
45
|
+
*/
|
|
46
|
+
score: number;
|
|
47
|
+
}
|
|
48
|
+
/** Tuning for `searchText`'s BM25 ranking. */
|
|
49
|
+
interface TextSearchOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Term-frequency saturation (BM25 `k1`, default `1.2`). Higher values let a
|
|
52
|
+
* repeated term keep adding relevance for longer.
|
|
53
|
+
*/
|
|
54
|
+
k1?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Length normalisation (BM25 `b`, default `0.75`). `0` ignores document
|
|
57
|
+
* length; `1` normalises fully by length relative to the corpus average.
|
|
58
|
+
*/
|
|
59
|
+
b?: number;
|
|
60
|
+
}
|
|
61
|
+
interface HybridSearchResult<T extends Document = Document> {
|
|
62
|
+
/** The matched document. */
|
|
63
|
+
document: T;
|
|
64
|
+
/**
|
|
65
|
+
* Fused reciprocal-rank-fusion score. Small by construction and meaningful
|
|
66
|
+
* only as an ordering within one result set — never a similarity or a
|
|
67
|
+
* confidence.
|
|
68
|
+
*/
|
|
69
|
+
score: number;
|
|
70
|
+
/**
|
|
71
|
+
* Zero-based position in the text ranking, or `null` if the text retriever
|
|
72
|
+
* did not return this document.
|
|
73
|
+
*/
|
|
74
|
+
textRank: number | null;
|
|
75
|
+
/**
|
|
76
|
+
* Zero-based position in the vector ranking, or `null` if the vector
|
|
77
|
+
* retriever did not return this document.
|
|
78
|
+
*/
|
|
79
|
+
vectorRank: number | null;
|
|
80
|
+
}
|
|
81
|
+
/** Tuning for `hybridSearch`'s fusion and per-retriever scoring. */
|
|
82
|
+
interface HybridSearchOptions extends TextSearchOptions {
|
|
83
|
+
/**
|
|
84
|
+
* Reciprocal rank fusion smoothing constant (default `60`). Larger values
|
|
85
|
+
* flatten the advantage of the very top ranks.
|
|
86
|
+
*/
|
|
87
|
+
rrfK?: number;
|
|
88
|
+
/** Relative weight of the text ranking (default `1`). Set `0` to disable it. */
|
|
89
|
+
textWeight?: number;
|
|
90
|
+
/** Relative weight of the vector ranking (default `1`). Set `0` to disable it. */
|
|
91
|
+
vectorWeight?: number;
|
|
92
|
+
/**
|
|
93
|
+
* How many candidates to pull from each retriever before fusing
|
|
94
|
+
* (default `max(topK * 4, 20)`). Raise it for better recall at more cost;
|
|
95
|
+
* fusing only `topK` from each side drops documents that rank just outside
|
|
96
|
+
* one retriever but high in the other.
|
|
97
|
+
*/
|
|
98
|
+
candidates?: number;
|
|
99
|
+
}
|
|
39
100
|
type Value = null | boolean | number | string | Uint8Array | Value[] | {
|
|
40
101
|
[key: string]: Value;
|
|
41
102
|
};
|
|
@@ -221,12 +282,42 @@ interface CollectionOptions<T extends Document = Document> {
|
|
|
221
282
|
* });
|
|
222
283
|
*/
|
|
223
284
|
migrateDocument?: (doc: T, fromVersion: number) => T;
|
|
285
|
+
/**
|
|
286
|
+
* Lazy, read-time **downcast** — the mirror of {@link migrateDocument}, for a
|
|
287
|
+
* document written by a *newer* peer. When set, every document returned by
|
|
288
|
+
* `find` / `findOne` whose `_v` is **above** `syncSchema.version` is passed
|
|
289
|
+
* through `downgradeDocument(doc, fromVersion)` and projected into the shape
|
|
290
|
+
* this build understands, so application code on an old client sees a shape
|
|
291
|
+
* it can actually read instead of an unexpected future one.
|
|
292
|
+
*
|
|
293
|
+
* Requires `syncSchema.version`. Must be pure and deterministic.
|
|
294
|
+
*
|
|
295
|
+
* **The projection is view-only and is never persisted**, regardless of
|
|
296
|
+
* {@link persistMigrations}. The stored document keeps its original `_v` and
|
|
297
|
+
* its newer fields intact, because this replica must continue to replicate
|
|
298
|
+
* that document faithfully to other peers — an old client is a *reader* of a
|
|
299
|
+
* newer shape, never its editor. For the same reason the returned document
|
|
300
|
+
* keeps its original (higher) `_v`: it is a projection of a v-N document, not
|
|
301
|
+
* a v-M one, and writing it back wholesale would tell the fleet otherwise.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* // This build understands v1. A v2 peer split `name` into first/last.
|
|
305
|
+
* const users = db.collection<User>('users', {
|
|
306
|
+
* syncSchema: { version: 1 },
|
|
307
|
+
* downgradeDocument: (doc) => ({ ...doc, name: `${doc.first} ${doc.last}` }),
|
|
308
|
+
* });
|
|
309
|
+
*/
|
|
310
|
+
downgradeDocument?: (doc: Readonly<T>, fromVersion: number) => T;
|
|
224
311
|
/**
|
|
225
312
|
* When `true`, a document upgraded by {@link migrateDocument} on read is
|
|
226
|
-
* **written back** to storage (a best-effort `updateOne` computing
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
313
|
+
* **written back** to storage (a best-effort `updateOne` computing a `$set`
|
|
314
|
+
* diff) so the migration becomes permanent — after which filters and indexes
|
|
315
|
+
* on the new shape match it. Default `false` (the migrated shape is returned
|
|
316
|
+
* but not persisted).
|
|
317
|
+
*
|
|
318
|
+
* The write-back is **additive-only** except for fields explicitly listed in
|
|
319
|
+
* {@link retiredFields}. A field present in storage but absent from the
|
|
320
|
+
* migrated document is otherwise left alone rather than `$unset`.
|
|
230
321
|
*
|
|
231
322
|
* Trade-offs: reads that encounter un-migrated documents now issue writes
|
|
232
323
|
* (which fire live-query and sync-hook notifications like any other write);
|
|
@@ -234,6 +325,35 @@ interface CollectionOptions<T extends Document = Document> {
|
|
|
234
325
|
* one-shot eager rewrite instead, prefer `openDB({ migrations })`.
|
|
235
326
|
*/
|
|
236
327
|
persistMigrations?: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Fields an upcast is explicitly allowed to remove during persist-on-read.
|
|
330
|
+
* Prefer this precise list to {@link allowFieldRemoval}: omissions of any
|
|
331
|
+
* other field remain additive and are preserved.
|
|
332
|
+
*/
|
|
333
|
+
retiredFields?: (keyof T & string)[];
|
|
334
|
+
/**
|
|
335
|
+
* How to read a field that {@link migrateDocument} left out of its output:
|
|
336
|
+
* as an intentional removal (`true`), or as a field the migration simply
|
|
337
|
+
* never heard of (`false`, the default).
|
|
338
|
+
*
|
|
339
|
+
* Default `false` — omitted fields are **preserved**: kept on the document
|
|
340
|
+
* returned to your code, and left in storage by the {@link persistMigrations}
|
|
341
|
+
* write-back rather than `$unset`.
|
|
342
|
+
*
|
|
343
|
+
* This default exists because on a synced collection the two cases are
|
|
344
|
+
* indistinguishable from the migration's output, and guessing "removal" is
|
|
345
|
+
* the destructive guess. A migration written today cannot mention a field a
|
|
346
|
+
* *newer* peer will add tomorrow, so an innocent `(doc) => ({ id, name })`
|
|
347
|
+
* becomes a deletion of a field its author never heard of — and under
|
|
348
|
+
* whole-document LWW that deletion replicates to the whole fleet. An old
|
|
349
|
+
* replica has to stay a faithful carrier of shapes it does not understand.
|
|
350
|
+
*
|
|
351
|
+
* Set `true` only on a collection that never syncs, or during a deliberate
|
|
352
|
+
* add → backfill → dual-read → **retire** rollout, where you already know the
|
|
353
|
+
* whole fleet has stopped writing the field.
|
|
354
|
+
*/
|
|
355
|
+
/** @deprecated Prefer {@link retiredFields}; this treats every omission as removal. */
|
|
356
|
+
allowFieldRemoval?: boolean;
|
|
237
357
|
}
|
|
238
358
|
/** A single MongoDB-style aggregation stage. */
|
|
239
359
|
type AggregateStage<T extends Document = Document> = {
|
|
@@ -351,6 +471,44 @@ interface Collection<T extends Document = Document> {
|
|
|
351
471
|
createFtsIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
|
|
352
472
|
/** Drop a full-text search index. */
|
|
353
473
|
dropFtsIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
|
|
474
|
+
/**
|
|
475
|
+
* Rank documents against a free-text `query` using BM25, most relevant first.
|
|
476
|
+
*
|
|
477
|
+
* Unlike the `$contains` filter, which requires **every** token to be
|
|
478
|
+
* present, this uses OR semantics — a document that matches more of the
|
|
479
|
+
* query simply scores higher. Requires an FTS index on `field`.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* const hits = await articles.searchText('body', 'reset my password', 5);
|
|
483
|
+
* // hits: Array<{ document: Article, score: number }>
|
|
484
|
+
*/
|
|
485
|
+
searchText(field: keyof Omit<T, '_id'> & string, query: string, topK: number, filter?: Filter<T>, options?: TextSearchOptions): Promise<TextSearchResult<T>[]>;
|
|
486
|
+
/**
|
|
487
|
+
* Hybrid retrieval: rank by keyword relevance (BM25) **and** vector
|
|
488
|
+
* similarity, then fuse the two rankings with reciprocal rank fusion.
|
|
489
|
+
*
|
|
490
|
+
* The two retrievers fail differently — keyword search misses paraphrases,
|
|
491
|
+
* vector search misses exact identifiers and rare proper nouns — so fusing
|
|
492
|
+
* them recovers both. A document both retrievers rank well outranks one that
|
|
493
|
+
* only a single retriever found. Requires an FTS index on `textField` and a
|
|
494
|
+
* vector index on `vectorField`.
|
|
495
|
+
*
|
|
496
|
+
* The optional `filter` is applied to both retrievers before ranking.
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* const hits = await articles.hybridSearch(
|
|
500
|
+
* { textField: 'body', text: 'reset my password' },
|
|
501
|
+
* { vectorField: 'embedding', vector: queryVec },
|
|
502
|
+
* 5,
|
|
503
|
+
* );
|
|
504
|
+
*/
|
|
505
|
+
hybridSearch(text: {
|
|
506
|
+
textField: keyof Omit<T, '_id'> & string;
|
|
507
|
+
text: string;
|
|
508
|
+
}, vector: {
|
|
509
|
+
vectorField: keyof Omit<T, '_id'> & string;
|
|
510
|
+
vector: number[];
|
|
511
|
+
}, topK: number, filter?: Filter<T>, options?: HybridSearchOptions): Promise<HybridSearchResult<T>[]>;
|
|
354
512
|
/**
|
|
355
513
|
* Return the indexes that currently exist on this collection.
|
|
356
514
|
*
|
|
@@ -1321,4 +1479,4 @@ interface OpenDBOptions {
|
|
|
1321
1479
|
*/
|
|
1322
1480
|
declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
|
|
1323
1481
|
|
|
1324
|
-
export { type AggregatePipeline, type AggregateStage, type BootstrapPage, type BootstrapRequest, type BridgeQuery, type BridgeResult, COVERAGE_COLLECTION, type Collection, type CollectionIndexInfo, type CollectionOptions, type CoordinatorOptions, type CoverageKey, type CoverageState, CoverageStore, type CursorSyncAdapter, type DeltaPage, type Document, type DurabilityConfig, type Filter, HttpSyncAdapter, type Migration, type OpenDBOptions, type PullResult, REPLICA_REVISION_FIELD, REPLICA_SCOPE_FIELD, type RemoteKey, ReplicationCoordinator, type ReplicationSource, type RestSourceOptions, type Schema, type SerializedChangeset, type SyncAdapter, type SyncConfig, type SyncDirection, type SyncOptions, type SyncResult, type TalaDB, type TalaDbConfig, TalaDbValidationError, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, type WriteOrigin, applySchema, coverageKey, createRestSource, deriveDocId, isAuthoritative, openDB, progress, rowsApplied, runMigrations };
|
|
1482
|
+
export { type AggregatePipeline, type AggregateStage, type BootstrapPage, type BootstrapRequest, type BridgeQuery, type BridgeResult, COVERAGE_COLLECTION, type Collection, type CollectionIndexInfo, type CollectionOptions, type CoordinatorOptions, type CoverageKey, type CoverageState, CoverageStore, type CursorSyncAdapter, type DeltaPage, type Document, type DurabilityConfig, type Filter, HttpSyncAdapter, type HybridSearchOptions, type HybridSearchResult, type Migration, type OpenDBOptions, type PullResult, REPLICA_REVISION_FIELD, REPLICA_SCOPE_FIELD, type RemoteKey, ReplicationCoordinator, type ReplicationSource, type RestSourceOptions, type Schema, type SerializedChangeset, type SyncAdapter, type SyncConfig, type SyncDirection, type SyncOptions, type SyncResult, type TalaDB, type TalaDbConfig, TalaDbValidationError, type TextSearchOptions, type TextSearchResult, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, type WriteOrigin, applySchema, coverageKey, createRestSource, deriveDocId, isAuthoritative, openDB, progress, rowsApplied, runMigrations };
|