doc2vec 1.2.0 → 2.0.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.
@@ -139,6 +139,29 @@ class ContentProcessor {
139
139
  });
140
140
  logger.debug('Turndown rules setup complete');
141
141
  }
142
+ convertHtmlToMarkdown(html) {
143
+ if (!html || !html.trim()) {
144
+ return '';
145
+ }
146
+ // Sanitize the HTML first
147
+ const cleanHtml = (0, sanitize_html_1.default)(html, {
148
+ allowedTags: [
149
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
150
+ 'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
151
+ 'div', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
152
+ 'blockquote', 'br'
153
+ ],
154
+ allowedAttributes: {
155
+ 'a': ['href'],
156
+ 'pre': ['class', 'data-language'],
157
+ 'code': ['class', 'data-language'],
158
+ 'div': ['class'],
159
+ 'span': ['class']
160
+ }
161
+ });
162
+ // Convert to markdown using TurndownService
163
+ return this.turndownService.turndown(cleanHtml).trim();
164
+ }
142
165
  async parseSitemap(sitemapUrl, logger) {
143
166
  logger.info(`Parsing sitemap from ${sitemapUrl}`);
144
167
  try {
@@ -324,14 +347,32 @@ class ContentProcessor {
324
347
  // Original HTML page processing logic
325
348
  let browser = null;
326
349
  try {
350
+ // Use system Chromium if available (for Docker environments)
351
+ let executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
352
+ if (!executablePath) {
353
+ if (fs.existsSync('/usr/bin/chromium')) {
354
+ executablePath = '/usr/bin/chromium';
355
+ }
356
+ else if (fs.existsSync('/usr/bin/chromium-browser')) {
357
+ executablePath = '/usr/bin/chromium-browser';
358
+ }
359
+ }
327
360
  browser = await puppeteer_1.default.launch({
361
+ executablePath,
328
362
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
329
363
  });
330
364
  const page = await browser.newPage();
331
365
  logger.debug(`Navigating to ${url}`);
332
366
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
333
367
  const htmlContent = await page.evaluate(() => {
334
- const mainContentElement = document.querySelector('div[role="main"].document') || document.querySelector('main') || document.body;
368
+ // 💡 Try specific content selectors first, then fall back to broader ones
369
+ const mainContentElement = document.querySelector('.docs-content') || // Common docs pattern
370
+ document.querySelector('.doc-content') || // Alternative docs pattern
371
+ document.querySelector('.markdown-body') || // GitHub-style
372
+ document.querySelector('article') || // Semantic article
373
+ document.querySelector('div[role="main"].document') ||
374
+ document.querySelector('main') ||
375
+ document.body;
335
376
  return mainContentElement.innerHTML;
336
377
  });
337
378
  if (htmlContent.length > sourceConfig.max_size) {
@@ -347,10 +388,24 @@ class ContentProcessor {
347
388
  pre.setAttribute('data-readable-content-score', '100');
348
389
  this.markCodeParents(pre.parentElement);
349
390
  });
391
+ // 💡 Extract H1s BEFORE Readability - it often strips them as "chrome"
392
+ // We'll inject them back after Readability processing
393
+ const h1Elements = document.querySelectorAll('h1');
394
+ const extractedH1s = [];
395
+ logger.debug(`[Readability Debug] Found ${h1Elements.length} H1 elements before Readability`);
396
+ h1Elements.forEach((h1, index) => {
397
+ const h1Text = h1.textContent?.trim() || '';
398
+ // Skip empty H1s or icon-only H1s (like "link" anchors)
399
+ if (h1Text && h1Text.length > 3 && !h1Text.match(/^(link|#|menu|close)$/i)) {
400
+ extractedH1s.push(h1Text);
401
+ logger.debug(`[Readability Debug] Extracted H1[${index}]: "${h1Text.substring(0, 50)}..."`);
402
+ }
403
+ h1.classList.add('original-h1');
404
+ });
350
405
  logger.debug(`Applying Readability to extract main content`);
351
406
  const reader = new readability_1.Readability(document, {
352
407
  charThreshold: 20,
353
- classesToPreserve: ['article-content'],
408
+ classesToPreserve: ['article-content', 'original-h1'],
354
409
  });
355
410
  const article = reader.parse();
356
411
  if (!article) {
@@ -358,8 +413,35 @@ class ContentProcessor {
358
413
  await browser.close();
359
414
  return null;
360
415
  }
361
- logger.debug(`Sanitizing HTML (${article.content.length} chars)`);
362
- const cleanHtml = (0, sanitize_html_1.default)(article.content, {
416
+ // Debug: Log what Readability extracted
417
+ logger.debug(`[Readability Debug] article.title: "${article.title}"`);
418
+ logger.debug(`[Readability Debug] article.content length: ${article.content?.length}`);
419
+ logger.debug(`[Readability Debug] article.content starts with: "${article.content?.substring(0, 200)}..."`);
420
+ logger.debug(`[Readability Debug] Contains H1 tag: ${article.content?.includes('<h1')}`);
421
+ logger.debug(`[Readability Debug] Contains H2 tag: ${article.content?.includes('<h2')}`);
422
+ logger.debug(`[Readability Debug] Contains original-h1 class: ${article.content?.includes('original-h1')}`);
423
+ // 💡 Restore H1s: find elements with our marker class and convert back from H2
424
+ const articleDom = new jsdom_1.JSDOM(article.content);
425
+ const articleDoc = articleDom.window.document;
426
+ const originalH1Elements = articleDoc.querySelectorAll('.original-h1');
427
+ logger.debug(`[Readability Debug] Found ${originalH1Elements.length} elements with .original-h1 class to restore`);
428
+ originalH1Elements.forEach((heading, index) => {
429
+ logger.debug(`[Readability Debug] Restoring[${index}]: tagName=${heading.tagName}, text="${heading.textContent?.trim().substring(0, 50)}..."`);
430
+ // Create a new H1 element with the same content
431
+ const h1 = articleDoc.createElement('h1');
432
+ h1.innerHTML = heading.innerHTML;
433
+ // Copy other attributes except class
434
+ Array.from(heading.attributes).forEach(attr => {
435
+ if (attr.name !== 'class') {
436
+ h1.setAttribute(attr.name, attr.value);
437
+ }
438
+ });
439
+ heading.replaceWith(h1);
440
+ });
441
+ const restoredContent = articleDoc.body.innerHTML;
442
+ logger.debug(`[Readability Debug] Restored content contains H1: ${restoredContent.includes('<h1')}`);
443
+ logger.debug(`Sanitizing HTML (${restoredContent.length} chars)`);
444
+ const cleanHtml = (0, sanitize_html_1.default)(restoredContent, {
363
445
  allowedTags: [
364
446
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
365
447
  'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
@@ -374,7 +456,26 @@ class ContentProcessor {
374
456
  }
375
457
  });
376
458
  logger.debug(`Converting HTML to Markdown`);
377
- const markdown = this.turndownService.turndown(cleanHtml);
459
+ let markdown = this.turndownService.turndown(cleanHtml);
460
+ // 💡 Inject extracted H1s back if they're not in the markdown
461
+ // Readability often strips them as "page chrome"
462
+ // Use article.title as fallback if no H1 was extracted
463
+ const pageTitle = extractedH1s.length > 0 ? extractedH1s[0] : (article.title?.trim() || '');
464
+ if (pageTitle) {
465
+ // Check if markdown already starts with this exact H1 (allowing for leading whitespace)
466
+ const normalizedTitle = pageTitle.replace(/\s+/g, ' ');
467
+ const markdownFirstLine = markdown.trimStart().split('\n')[0] || '';
468
+ const existingH1Match = markdownFirstLine.match(/^#\s+(.+)$/);
469
+ const existingH1Text = existingH1Match ? existingH1Match[1].replace(/\s+/g, ' ').trim() : '';
470
+ // Only inject if markdown doesn't already start with this H1
471
+ if (!existingH1Match || existingH1Text !== normalizedTitle) {
472
+ markdown = `# ${pageTitle}\n\n${markdown}`;
473
+ logger.debug(`[Readability Debug] Injected page title as H1: "${pageTitle}"`);
474
+ }
475
+ else {
476
+ logger.debug(`[Readability Debug] H1 "${pageTitle}" already present in markdown`);
477
+ }
478
+ }
378
479
  logger.debug(`Markdown conversion complete (${markdown.length} chars)`);
379
480
  return markdown;
380
481
  }
@@ -398,6 +499,68 @@ class ContentProcessor {
398
499
  }
399
500
  this.markCodeParents(node.parentElement);
400
501
  }
502
+ async convertDocToMarkdown(filePath, logger) {
503
+ logger.debug(`Converting DOC to markdown: ${filePath}`);
504
+ try {
505
+ // Dynamic import for word-extractor
506
+ const WordExtractor = (await Promise.resolve().then(() => __importStar(require('word-extractor')))).default;
507
+ const extractor = new WordExtractor();
508
+ const extracted = await extractor.extract(filePath);
509
+ const text = extracted.getBody();
510
+ // Create markdown with filename as title
511
+ let markdown = `# ${path.basename(filePath, '.doc')}\n\n`;
512
+ // Clean up the text and add to markdown
513
+ const cleanedText = text
514
+ .replace(/\r\n/g, '\n') // Normalize line endings
515
+ .replace(/\n{3,}/g, '\n\n') // Remove excessive line breaks
516
+ .trim();
517
+ markdown += cleanedText;
518
+ logger.debug(`Converted DOC to ${markdown.length} characters of markdown`);
519
+ return markdown;
520
+ }
521
+ catch (error) {
522
+ logger.error(`Failed to convert DOC ${filePath}:`, error);
523
+ throw error;
524
+ }
525
+ }
526
+ async convertDocxToMarkdown(filePath, logger) {
527
+ logger.debug(`Converting DOCX to markdown: ${filePath}`);
528
+ try {
529
+ // Dynamic import for mammoth
530
+ const mammoth = await Promise.resolve().then(() => __importStar(require('mammoth')));
531
+ const result = await mammoth.convertToHtml({ path: filePath });
532
+ const html = result.value;
533
+ // Log any warnings from mammoth
534
+ if (result.messages.length > 0) {
535
+ logger.debug(`Mammoth warnings: ${result.messages.map(m => m.message).join(', ')}`);
536
+ }
537
+ // Create markdown with filename as title
538
+ let markdown = `# ${path.basename(filePath, '.docx')}\n\n`;
539
+ // Convert HTML to Markdown using turndown
540
+ const cleanHtml = (0, sanitize_html_1.default)(html, {
541
+ allowedTags: [
542
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
543
+ 'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
544
+ 'div', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'br'
545
+ ],
546
+ allowedAttributes: {
547
+ 'a': ['href'],
548
+ 'pre': ['class'],
549
+ 'code': ['class']
550
+ }
551
+ });
552
+ const convertedContent = this.turndownService.turndown(cleanHtml);
553
+ markdown += convertedContent;
554
+ // Clean up excessive line breaks
555
+ markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
556
+ logger.debug(`Converted DOCX to ${markdown.length} characters of markdown`);
557
+ return markdown;
558
+ }
559
+ catch (error) {
560
+ logger.error(`Failed to convert DOCX ${filePath}:`, error);
561
+ throw error;
562
+ }
563
+ }
401
564
  async convertPdfToMarkdown(filePath, logger) {
402
565
  logger.debug(`Converting PDF to markdown: ${filePath}`);
403
566
  try {
@@ -588,6 +751,16 @@ class ContentProcessor {
588
751
  logger.debug(`Processing PDF file: ${filePath}`);
589
752
  processedContent = await this.convertPdfToMarkdown(filePath, logger);
590
753
  }
754
+ else if (extension === '.doc') {
755
+ // Handle legacy Word DOC files
756
+ logger.debug(`Processing DOC file: ${filePath}`);
757
+ processedContent = await this.convertDocToMarkdown(filePath, logger);
758
+ }
759
+ else if (extension === '.docx') {
760
+ // Handle modern Word DOCX files
761
+ logger.debug(`Processing DOCX file: ${filePath}`);
762
+ processedContent = await this.convertDocxToMarkdown(filePath, logger);
763
+ }
591
764
  else {
592
765
  // Handle text-based files
593
766
  content = fs.readFileSync(filePath, { encoding: encoding });
@@ -641,84 +814,157 @@ class ContentProcessor {
641
814
  }
642
815
  async chunkMarkdown(markdown, sourceConfig, url) {
643
816
  const logger = this.logger.child('chunker');
644
- logger.debug(`Chunking markdown from ${url} (${markdown.length} chars)`);
817
+ // --- Configuration ---
645
818
  const MAX_TOKENS = 1000;
819
+ const MIN_TOKENS = 150; // 💡 Merges "OpenAI-compatible" sentence into the next block
820
+ const OVERLAP_PERCENT = 0.1; // 10% overlap for large splits
646
821
  const chunks = [];
647
822
  const lines = markdown.split("\n");
648
- let currentChunk = "";
823
+ let buffer = "";
649
824
  let headingHierarchy = [];
650
- const processChunk = () => {
651
- if (currentChunk.trim()) {
652
- const tokens = utils_1.Utils.tokenize(currentChunk);
653
- if (tokens.length > MAX_TOKENS) {
654
- logger.debug(`Chunk exceeds max token count (${tokens.length}), splitting into smaller chunks`);
655
- let subChunk = "";
656
- let tokenCount = 0;
657
- const overlapSize = Math.floor(MAX_TOKENS * 0.05);
658
- let lastTokens = [];
659
- for (const token of tokens) {
660
- if (tokenCount + 1 > MAX_TOKENS) {
661
- chunks.push(createDocumentChunk(subChunk, headingHierarchy));
662
- subChunk = lastTokens.join("") + token;
663
- tokenCount = lastTokens.length + 1;
664
- lastTokens = [];
665
- }
666
- else {
667
- subChunk += token;
668
- tokenCount++;
669
- lastTokens.push(token);
670
- if (lastTokens.length > overlapSize) {
671
- lastTokens.shift();
672
- }
673
- }
674
- }
675
- if (subChunk) {
676
- chunks.push(createDocumentChunk(subChunk, headingHierarchy));
677
- }
678
- }
679
- else {
680
- chunks.push(createDocumentChunk(currentChunk, headingHierarchy));
681
- }
825
+ let bufferHeadings = []; // Track headings in current buffer
826
+ let chunkCounter = 0; // Tracks chunk position within this page for ordering
827
+ /**
828
+ * Computes the topic hierarchy for merged content.
829
+ * When merging sibling sections (same level), uses their parent heading.
830
+ * Otherwise uses the current hierarchy.
831
+ */
832
+ const computeTopicHierarchy = () => {
833
+ if (bufferHeadings.length === 0) {
834
+ return headingHierarchy;
682
835
  }
683
- currentChunk = "";
836
+ // Find the deepest level (most recent headings)
837
+ const deepestLevel = Math.max(...bufferHeadings.map(h => h.level));
838
+ // Get all headings at the deepest level
839
+ const deepestHeadings = bufferHeadings.filter(h => h.level === deepestLevel);
840
+ // If we have multiple sibling headings at the deepest level, use their parent
841
+ if (deepestHeadings.length > 1 && deepestLevel > 1) {
842
+ // Use parent heading (one level up from the sibling headings)
843
+ // headingHierarchy still contains the parent at index (deepestLevel - 2)
844
+ // We want everything up to (but not including) the deepest level
845
+ return headingHierarchy.slice(0, deepestLevel - 1);
846
+ }
847
+ // Single heading or different levels: use the current hierarchy
848
+ // This reflects the most recent heading which is appropriate
849
+ return headingHierarchy;
684
850
  };
851
+ /**
852
+ * Internal helper to create the final chunk object with injected context.
853
+ */
685
854
  const createDocumentChunk = (content, hierarchy) => {
686
- const chunkId = utils_1.Utils.generateHash(content);
687
- logger.debug(`Created chunk ${chunkId.substring(0, 8)}... with ${content.length} chars`);
688
- return {
689
- content,
855
+ // 💡 BREADCRUMB INJECTION
856
+ // We prepend the hierarchy to the text. This makes the vector highly relevant
857
+ // to searches for parent topics even if the body doesn't mention them.
858
+ const breadcrumbs = hierarchy.filter(h => h).join(" > ");
859
+ const contextPrefix = breadcrumbs ? `[Topic: ${breadcrumbs}]\n` : "";
860
+ const searchableText = contextPrefix + content.trim();
861
+ const chunkId = utils_1.Utils.generateHash(searchableText);
862
+ const chunk = {
863
+ content: searchableText,
690
864
  metadata: {
691
865
  product_name: sourceConfig.product_name,
692
866
  version: sourceConfig.version,
693
- heading_hierarchy: [...hierarchy],
867
+ heading_hierarchy: hierarchy.filter(h => h),
694
868
  section: hierarchy[hierarchy.length - 1] || "Introduction",
695
869
  chunk_id: chunkId,
696
870
  url: url,
697
- hash: utils_1.Utils.generateHash(content)
871
+ hash: chunkId,
872
+ chunk_index: Math.floor(chunkCounter),
873
+ total_chunks: 0 // Placeholder, will be updated after all chunks are created
698
874
  }
699
875
  };
876
+ chunkCounter++; // Increment for next chunk
877
+ return chunk;
700
878
  };
879
+ /**
880
+ * Flushes the current buffer into the chunks array.
881
+ * Uses sub-splitting logic if the buffer exceeds MAX_TOKENS.
882
+ */
883
+ const flushBuffer = (force = false) => {
884
+ const trimmedBuffer = buffer.trim();
885
+ if (!trimmedBuffer)
886
+ return;
887
+ const tokenCount = utils_1.Utils.tokenize(trimmedBuffer).length;
888
+ // 💡 SEMANTIC MERGING
889
+ // If the current section is too short (like just a title or a one-liner),
890
+ // we don't flush yet unless it's the end of the file (force=true).
891
+ if (tokenCount < MIN_TOKENS && !force) {
892
+ return;
893
+ }
894
+ // Compute the appropriate topic hierarchy for merged content
895
+ const topicHierarchy = computeTopicHierarchy();
896
+ if (tokenCount > MAX_TOKENS) {
897
+ // 💡 RECURSIVE OVERLAP SPLITTING
898
+ // If the section is a massive guide, split it but keep headers on every sub-piece.
899
+ const tokens = utils_1.Utils.tokenize(trimmedBuffer);
900
+ const overlapSize = Math.floor(MAX_TOKENS * OVERLAP_PERCENT);
901
+ for (let i = 0; i < tokens.length; i += (MAX_TOKENS - overlapSize)) {
902
+ const subTokens = tokens.slice(i, i + MAX_TOKENS);
903
+ const subContent = subTokens.join("");
904
+ chunks.push(createDocumentChunk(subContent, topicHierarchy));
905
+ }
906
+ }
907
+ else {
908
+ chunks.push(createDocumentChunk(trimmedBuffer, topicHierarchy));
909
+ }
910
+ buffer = ""; // Reset buffer after successful flush
911
+ bufferHeadings = []; // Reset tracked headings
912
+ };
913
+ // --- Main Processing Loop ---
701
914
  for (const line of lines) {
702
- if (line.startsWith("#")) {
703
- processChunk();
915
+ const isHeading = line.startsWith("#");
916
+ if (isHeading) {
917
+ // Update Hierarchy Stack for the new heading
704
918
  const levelMatch = line.match(/^(#+)/);
705
- let level = levelMatch ? levelMatch[1].length : 1;
706
- const heading = line.replace(/^#+\s*/, "").trim();
707
- logger.debug(`Found heading (level ${level}): ${heading}`);
708
- while (headingHierarchy.length < level - 1) {
709
- headingHierarchy.push("");
710
- }
711
- if (level <= headingHierarchy.length) {
712
- headingHierarchy = headingHierarchy.slice(0, level - 1);
919
+ const level = levelMatch ? levelMatch[1].length : 1;
920
+ // Clean heading: remove markdown prefix and anchor links like [](#anchor-id)
921
+ const headingText = line
922
+ .replace(/^#+\s*/, "") // Remove ## prefix
923
+ .replace(/\[.*?\]\(#[^)]*\)/g, "") // Remove [text](#anchor) patterns
924
+ .replace(/\[\]\(#[^)]*\)/g, "") // Remove [](#anchor) patterns
925
+ .trim();
926
+ // Check if we should merge with previous content
927
+ const currentTokenCount = utils_1.Utils.tokenize(buffer.trim()).length;
928
+ const hasBufferContent = currentTokenCount > 0;
929
+ const bufferIsSmall = currentTokenCount < MIN_TOKENS;
930
+ // Only merge if:
931
+ // 1. Buffer has content and is small
932
+ // 2. Buffer has tracked headings (we're merging sections, not just content)
933
+ // 3. New heading is at same or deeper level than the deepest heading in buffer (siblings or children)
934
+ // If new heading is shallower (e.g., H2 after H3), it's a new section - flush first
935
+ const deepestBufferLevel = bufferHeadings.length > 0
936
+ ? Math.max(...bufferHeadings.map(h => h.level))
937
+ : 0;
938
+ const shouldMerge = hasBufferContent && bufferIsSmall && bufferHeadings.length > 0 &&
939
+ level >= deepestBufferLevel;
940
+ if (!shouldMerge && hasBufferContent) {
941
+ // Buffer is large enough OR new heading starts a new section - flush first
942
+ flushBuffer();
713
943
  }
714
- headingHierarchy[level - 1] = heading;
944
+ // If shouldMerge is true, we keep the buffer and merge the sections
945
+ // Reset hierarchy below this level (e.g., H2 reset should clear previous H3s)
946
+ headingHierarchy = headingHierarchy.slice(0, level - 1);
947
+ headingHierarchy[level - 1] = headingText;
948
+ // Track this heading in the buffer
949
+ bufferHeadings.push({ level, text: headingText });
950
+ buffer += `${line}\n`;
715
951
  }
716
952
  else {
717
- currentChunk += `${line}\n`;
953
+ buffer += `${line}\n`;
954
+ // Safety valve: if a single section is huge, flush it periodically
955
+ if (utils_1.Utils.tokenize(buffer).length >= MAX_TOKENS) {
956
+ flushBuffer();
957
+ }
718
958
  }
719
959
  }
720
- processChunk();
721
- logger.debug(`Chunking complete, created ${chunks.length} chunks`);
960
+ // Final sweep
961
+ flushBuffer(true);
962
+ // Update all chunks with the final total count
963
+ const totalChunks = Math.floor(chunks.length);
964
+ for (const chunk of chunks) {
965
+ chunk.metadata.total_chunks = totalChunks;
966
+ }
967
+ logger.debug(`Chunking complete: ${chunks.length} rich context chunks created.`);
722
968
  return chunks;
723
969
  }
724
970
  }
package/dist/database.js CHANGED
@@ -64,7 +64,9 @@ class DatabaseManager {
64
64
  chunk_id TEXT UNIQUE,
65
65
  content TEXT,
66
66
  url TEXT,
67
- hash TEXT
67
+ hash TEXT,
68
+ chunk_index INTEGER,
69
+ total_chunks INTEGER
68
70
  );
69
71
  `);
70
72
  logger.info(`SQLite database initialized successfully`);
@@ -226,12 +228,12 @@ class DatabaseManager {
226
228
  static prepareSQLiteStatements(db) {
227
229
  return {
228
230
  insertStmt: db.prepare(`
229
- INSERT INTO vec_items (embedding, product_name, version, heading_hierarchy, section, chunk_id, content, url, hash)
230
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
231
+ INSERT INTO vec_items (embedding, product_name, version, heading_hierarchy, section, chunk_id, content, url, hash, chunk_index, total_chunks)
232
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
231
233
  `),
232
234
  checkHashStmt: db.prepare(`SELECT hash FROM vec_items WHERE chunk_id = ?`),
233
235
  updateStmt: db.prepare(`
234
- UPDATE vec_items SET embedding = ?, product_name = ?, version = ?, heading_hierarchy = ?, section = ?, content = ?, url = ?, hash = ?
236
+ UPDATE vec_items SET embedding = ?, product_name = ?, version = ?, heading_hierarchy = ?, section = ?, content = ?, url = ?, hash = ?, chunk_index = ?, total_chunks = ?
235
237
  WHERE chunk_id = ?
236
238
  `),
237
239
  getAllChunkIdsStmt: db.prepare(`SELECT chunk_id FROM vec_items`),
@@ -242,22 +244,14 @@ class DatabaseManager {
242
244
  const { insertStmt, updateStmt } = this.prepareSQLiteStatements(db);
243
245
  const hash = chunkHash || utils_1.Utils.generateHash(chunk.content);
244
246
  const transaction = db.transaction(() => {
245
- const params = [
246
- new Float32Array(embedding),
247
- chunk.metadata.product_name,
248
- chunk.metadata.version,
249
- JSON.stringify(chunk.metadata.heading_hierarchy),
250
- chunk.metadata.section,
251
- chunk.metadata.chunk_id,
252
- chunk.content,
253
- chunk.metadata.url,
254
- hash
255
- ];
247
+ // Use BigInt for true integer representation in SQLite vec0
248
+ const chunkIndex = BigInt(chunk.metadata.chunk_index | 0);
249
+ const totalChunks = BigInt(chunk.metadata.total_chunks | 0);
256
250
  try {
257
- insertStmt.run(params);
251
+ insertStmt.run(new Float32Array(embedding), chunk.metadata.product_name, chunk.metadata.version, JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.metadata.chunk_id, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks);
258
252
  }
259
253
  catch (error) {
260
- updateStmt.run([...params.slice(0, 8), chunk.metadata.chunk_id]);
254
+ updateStmt.run(new Float32Array(embedding), chunk.metadata.product_name, chunk.metadata.version, JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks, chunk.metadata.chunk_id);
261
255
  }
262
256
  });
263
257
  transaction();
@@ -288,6 +282,8 @@ class DatabaseManager {
288
282
  url: chunk.metadata.url,
289
283
  hash: hash,
290
284
  original_chunk_id: chunk.metadata.chunk_id,
285
+ chunk_index: chunk.metadata.chunk_index,
286
+ total_chunks: chunk.metadata.total_chunks,
291
287
  },
292
288
  };
293
289
  await client.upsert(collectionName, {