libsql-search 0.7.0 → 0.8.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 +28 -48
- package/dist/index.cjs +184 -31
- package/dist/index.d.ts +56 -8
- package/dist/index.esm.js +184 -32
- package/docs/API.md +182 -112
- package/docs/INDEXING.md +88 -26
- package/docs/INTEGRATIONS.md +220 -40
- package/docs/MIGRATIONS.md +176 -0
- package/docs/PROVIDERS.md +86 -245
- package/docs/README.md +7 -9
- package/docs/RELEASING.md +11 -3
- package/docs/TESTING.md +138 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -811,6 +811,16 @@ function normalizeVectorDimensions(dimensions) {
|
|
|
811
811
|
return dimensions;
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
+
class IndexingError extends Error {
|
|
815
|
+
phase;
|
|
816
|
+
failures;
|
|
817
|
+
constructor(message, phase, failures = [], options) {
|
|
818
|
+
super(message, options);
|
|
819
|
+
this.name = "IndexingError";
|
|
820
|
+
this.phase = phase;
|
|
821
|
+
this.failures = failures;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
814
824
|
async function indexContent(options) {
|
|
815
825
|
const {
|
|
816
826
|
client,
|
|
@@ -819,32 +829,92 @@ async function indexContent(options) {
|
|
|
819
829
|
fileExtensions = [".md", ".markdown"],
|
|
820
830
|
exclude = ["node_modules", ".git", "dist", "build"],
|
|
821
831
|
tableName = "articles",
|
|
822
|
-
onProgress
|
|
832
|
+
onProgress,
|
|
833
|
+
failurePolicy = "abort",
|
|
834
|
+
allowEmptyIndex = false
|
|
823
835
|
} = options;
|
|
824
836
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
837
|
+
let files;
|
|
838
|
+
try {
|
|
839
|
+
files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
throw new IndexingError(
|
|
842
|
+
`Failed to scan ${contentPath} for source files. The existing index was left unchanged.`,
|
|
843
|
+
"build",
|
|
844
|
+
[],
|
|
845
|
+
{ cause: toError(error) }
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
files.sort((a, b) => a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0);
|
|
849
|
+
if (files.length === 0 && !allowEmptyIndex) {
|
|
850
|
+
throw new IndexingError(
|
|
851
|
+
`No source files found in ${contentPath}. The existing index was left unchanged. Pass allowEmptyIndex: true to intentionally empty the index.`,
|
|
852
|
+
"build"
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const documents = [];
|
|
856
|
+
const failures = [];
|
|
857
|
+
const slugOwners = /* @__PURE__ */ new Map();
|
|
833
858
|
for (let i = 0; i < files.length; i++) {
|
|
834
859
|
const file = files[i];
|
|
835
860
|
if (onProgress) {
|
|
836
861
|
onProgress(i + 1, files.length, file.relativePath);
|
|
837
862
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
863
|
+
const outcome = await buildDocument(file, embeddingOptions);
|
|
864
|
+
let failure;
|
|
865
|
+
if (outcome.ok) {
|
|
866
|
+
const owner = slugOwners.get(outcome.document.slug);
|
|
867
|
+
if (owner === void 0) {
|
|
868
|
+
slugOwners.set(outcome.document.slug, file.relativePath);
|
|
869
|
+
documents.push(outcome.document);
|
|
870
|
+
} else {
|
|
871
|
+
failure = {
|
|
872
|
+
file: file.relativePath,
|
|
873
|
+
stage: "parse",
|
|
874
|
+
error: new Error(
|
|
875
|
+
`Duplicate slug "${outcome.document.slug}": ${file.relativePath} collides with ${owner}`
|
|
876
|
+
)
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
failure = {
|
|
881
|
+
file: file.relativePath,
|
|
882
|
+
stage: outcome.stage,
|
|
883
|
+
error: outcome.error
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (failure === void 0) {
|
|
887
|
+
continue;
|
|
845
888
|
}
|
|
889
|
+
if (failurePolicy === "abort") {
|
|
890
|
+
throw new IndexingError(
|
|
891
|
+
`Failed to ${failure.stage} ${file.relativePath}: ${failure.error.message}. The existing index was left unchanged. Pass failurePolicy: 'skip' to rebuild from the remaining files.`,
|
|
892
|
+
"build",
|
|
893
|
+
[failure],
|
|
894
|
+
{ cause: failure.error }
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
console.error(`Skipping ${file.relativePath} (${failure.stage} failed):`, failure.error);
|
|
898
|
+
failures.push(failure);
|
|
846
899
|
}
|
|
847
|
-
|
|
900
|
+
if (files.length > 0 && documents.length === 0) {
|
|
901
|
+
throw new IndexingError(
|
|
902
|
+
`All ${files.length} source file(s) in ${contentPath} failed to index. The existing index was left unchanged.`,
|
|
903
|
+
"build",
|
|
904
|
+
failures,
|
|
905
|
+
{ cause: failures[0].error }
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
const success = documents.length;
|
|
909
|
+
await replaceIndex(client, quotedTableName, documents, failures);
|
|
910
|
+
return {
|
|
911
|
+
success,
|
|
912
|
+
failed: failures.length,
|
|
913
|
+
total: files.length,
|
|
914
|
+
replaced: true,
|
|
915
|
+
partial: failures.length > 0,
|
|
916
|
+
failures
|
|
917
|
+
};
|
|
848
918
|
}
|
|
849
919
|
async function findFiles(dir, baseDir, extensions, exclude) {
|
|
850
920
|
const files = [];
|
|
@@ -868,34 +938,113 @@ async function findFiles(dir, baseDir, extensions, exclude) {
|
|
|
868
938
|
}
|
|
869
939
|
return files;
|
|
870
940
|
}
|
|
871
|
-
async function
|
|
872
|
-
|
|
873
|
-
|
|
941
|
+
async function buildDocument(file, embeddingOptions) {
|
|
942
|
+
let raw;
|
|
943
|
+
try {
|
|
944
|
+
raw = await readFile(file.fullPath, "utf-8");
|
|
945
|
+
} catch (error) {
|
|
946
|
+
return { ok: false, stage: "read", error: toError(error) };
|
|
947
|
+
}
|
|
948
|
+
let parsed;
|
|
949
|
+
try {
|
|
950
|
+
parsed = parseFile(file, raw);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
return { ok: false, stage: "parse", error: toError(error) };
|
|
953
|
+
}
|
|
954
|
+
let embedding;
|
|
955
|
+
try {
|
|
956
|
+
embedding = await generateEmbedding(parsed.embeddingText, {
|
|
957
|
+
...embeddingOptions,
|
|
958
|
+
intent: embeddingOptions.intent ?? "document"
|
|
959
|
+
});
|
|
960
|
+
} catch (error) {
|
|
961
|
+
return { ok: false, stage: "embed", error: toError(error) };
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
ok: true,
|
|
965
|
+
document: {
|
|
966
|
+
slug: parsed.slug,
|
|
967
|
+
title: parsed.title,
|
|
968
|
+
content: parsed.content,
|
|
969
|
+
folder: file.folder,
|
|
970
|
+
tags: parsed.tags,
|
|
971
|
+
serializedTags: parsed.serializedTags,
|
|
972
|
+
embedding,
|
|
973
|
+
metadata: parsed.metadata
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function parseFile(file, raw) {
|
|
978
|
+
const { data: frontMatter, content: markdown } = matter(raw);
|
|
874
979
|
const slug = file.relativePath.replace(/\.(md|markdown)$/, "").replace(/\\/g, "/");
|
|
875
|
-
const title = frontMatter.title
|
|
980
|
+
const title = resolveTitle(file, frontMatter.title);
|
|
876
981
|
const tags = Array.isArray(frontMatter.tags) ? frontMatter.tags : [];
|
|
982
|
+
const serializedTags = JSON.stringify(tags);
|
|
877
983
|
const embeddingText = prepareTextForEmbedding({
|
|
878
984
|
title,
|
|
879
985
|
description: frontMatter.description,
|
|
880
986
|
content: markdown,
|
|
881
987
|
tags
|
|
882
988
|
});
|
|
883
|
-
const embedding = await generateEmbedding(embeddingText, {
|
|
884
|
-
...embeddingOptions,
|
|
885
|
-
intent: embeddingOptions.intent ?? "document"
|
|
886
|
-
});
|
|
887
989
|
return {
|
|
888
990
|
slug,
|
|
889
991
|
title,
|
|
890
992
|
content: markdown,
|
|
891
|
-
folder: file.folder,
|
|
892
993
|
tags,
|
|
893
|
-
|
|
994
|
+
serializedTags,
|
|
995
|
+
embeddingText,
|
|
894
996
|
metadata: frontMatter
|
|
895
997
|
};
|
|
896
998
|
}
|
|
897
|
-
|
|
898
|
-
|
|
999
|
+
function resolveTitle(file, rawTitle) {
|
|
1000
|
+
if (!rawTitle) {
|
|
1001
|
+
return fallbackTitle(file);
|
|
1002
|
+
}
|
|
1003
|
+
if (typeof rawTitle === "string") {
|
|
1004
|
+
return rawTitle;
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof rawTitle === "number" || typeof rawTitle === "bigint" || typeof rawTitle === "boolean") {
|
|
1007
|
+
return String(rawTitle);
|
|
1008
|
+
}
|
|
1009
|
+
if (rawTitle instanceof Date) {
|
|
1010
|
+
return rawTitle.toISOString();
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Unsupported frontmatter title of type ${describeType(rawTitle)}: expected a string`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
function fallbackTitle(file) {
|
|
1017
|
+
return file.relativePath.split("/").pop()?.replace(/\.(md|markdown)$/, "").replace(/-/g, " ") || "Untitled";
|
|
1018
|
+
}
|
|
1019
|
+
function describeType(value) {
|
|
1020
|
+
return Array.isArray(value) ? "array" : typeof value;
|
|
1021
|
+
}
|
|
1022
|
+
async function replaceIndex(client, quotedTableName, documents, failures) {
|
|
1023
|
+
const statements = [`DELETE FROM ${quotedTableName}`];
|
|
1024
|
+
try {
|
|
1025
|
+
for (let i = 0; i < documents.length; i++) {
|
|
1026
|
+
const document = documents[i];
|
|
1027
|
+
if (document !== void 0) {
|
|
1028
|
+
statements.push(createInsertStatement(document, quotedTableName));
|
|
1029
|
+
documents[i] = void 0;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} finally {
|
|
1033
|
+
documents.length = 0;
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
await client.batch(statements, "write");
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
throw new IndexingError(
|
|
1039
|
+
`Failed to replace the contents of ${quotedTableName}. The transaction was rolled back and the existing index was left unchanged.`,
|
|
1040
|
+
"replace",
|
|
1041
|
+
failures,
|
|
1042
|
+
{ cause: toError(error) }
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function createInsertStatement(document, quotedTableName) {
|
|
1047
|
+
return {
|
|
899
1048
|
sql: `INSERT INTO ${quotedTableName}
|
|
900
1049
|
(slug, title, content, folder, tags, embedding, created_at, updated_at)
|
|
901
1050
|
VALUES (?, ?, ?, ?, ?, vector(?), datetime('now'), datetime('now'))`,
|
|
@@ -904,10 +1053,13 @@ async function insertDocument(client, document, quotedTableName) {
|
|
|
904
1053
|
document.title,
|
|
905
1054
|
document.content,
|
|
906
1055
|
document.folder,
|
|
907
|
-
|
|
1056
|
+
document.serializedTags,
|
|
908
1057
|
JSON.stringify(document.embedding)
|
|
909
1058
|
]
|
|
910
|
-
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function toError(error) {
|
|
1062
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
911
1063
|
}
|
|
912
1064
|
async function createTable(client, tableName = "articles", dimensions = 384) {
|
|
913
1065
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
@@ -1057,4 +1209,4 @@ async function getFolders(client, tableName = "articles") {
|
|
|
1057
1209
|
return results.rows.map((row) => row.folder);
|
|
1058
1210
|
}
|
|
1059
1211
|
|
|
1060
|
-
export { createEmbeddingProvider, createTable, generateEmbedding, generateEmbeddings, getAllArticles, getArticleBySlug, getArticlesByFolder, getEmbeddingProviderMetadata, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search, validateEmbeddingBatch };
|
|
1212
|
+
export { IndexingError, createEmbeddingProvider, createTable, generateEmbedding, generateEmbeddings, getAllArticles, getArticleBySlug, getArticlesByFolder, getEmbeddingProviderMetadata, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search, validateEmbeddingBatch };
|