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/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
- const files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
826
- if (files.length === 0) {
827
- console.warn(`No files found in ${contentPath}`);
828
- return { success: 0, failed: 0, total: 0 };
829
- }
830
- await client.execute(`DELETE FROM ${quotedTableName}`);
831
- let success = 0;
832
- let failed = 0;
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
- try {
839
- const document = await processFile(file, embeddingOptions);
840
- await insertDocument(client, document, quotedTableName);
841
- success++;
842
- } catch (error) {
843
- console.error(`Failed to index ${file.relativePath}:`, error);
844
- failed++;
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
- return { success, failed, total: files.length };
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 processFile(file, embeddingOptions) {
872
- const content = await readFile(file.fullPath, "utf-8");
873
- const { data: frontMatter, content: markdown } = matter(content);
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 || file.relativePath.split("/").pop()?.replace(/\.(md|markdown)$/, "").replace(/-/g, " ") || "Untitled";
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
- embedding,
994
+ serializedTags,
995
+ embeddingText,
894
996
  metadata: frontMatter
895
997
  };
896
998
  }
897
- async function insertDocument(client, document, quotedTableName) {
898
- await client.execute({
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
- JSON.stringify(document.tags),
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 };