@veewo/claw-core 0.1.86 → 0.1.87
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/src/context.js +5 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/embedding-token-chunker.d.ts +1 -0
- package/dist/src/embedding-token-chunker.js +11 -2
- package/dist/src/embedding-token-chunker.js.map +1 -1
- package/dist/src/embedding-worker.js +1 -1
- package/dist/src/embedding-worker.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init.js +2 -1
- package/dist/src/init.js.map +1 -1
- package/dist/src/knowledge-document.d.ts +27 -0
- package/dist/src/knowledge-document.js +189 -0
- package/dist/src/knowledge-document.js.map +1 -0
- package/dist/src/knowledge-governance.d.ts +22 -0
- package/dist/src/knowledge-governance.js +75 -0
- package/dist/src/knowledge-governance.js.map +1 -0
- package/dist/src/knowledge-sidecar.d.ts +3 -0
- package/dist/src/knowledge-sidecar.js +1 -0
- package/dist/src/knowledge-sidecar.js.map +1 -1
- package/dist/src/memory-query.d.ts +2 -0
- package/dist/src/memory-query.js +25 -4
- package/dist/src/memory-query.js.map +1 -1
- package/dist/src/memory.js +413 -61
- package/dist/src/memory.js.map +1 -1
- package/dist/src/plan-templates.d.ts +1 -0
- package/dist/src/plan-templates.js +61 -10
- package/dist/src/plan-templates.js.map +1 -1
- package/dist/src/project-check.js +6 -1
- package/dist/src/project-check.js.map +1 -1
- package/dist/src/project-defaults.d.ts +1 -0
- package/dist/src/project-defaults.js +1 -0
- package/dist/src/project-defaults.js.map +1 -1
- package/dist/src/templates/plans/default.d.ts +1 -0
- package/dist/src/templates/plans/default.js +1 -0
- package/dist/src/templates/plans/default.js.map +1 -1
- package/dist/src/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/src/memory.js
CHANGED
|
@@ -10,6 +10,7 @@ import { resolveDefaultLocalEmbeddingDimensions } from "./embedding-defaults.js"
|
|
|
10
10
|
import { requestPersistentEmbedding } from "./embedding-daemon-protocol.js";
|
|
11
11
|
import { ClawError } from "./errors.js";
|
|
12
12
|
import { readJsonFile, readTextFile } from "./io.js";
|
|
13
|
+
import { analyzeKnowledgeDocument } from "./knowledge-document.js";
|
|
13
14
|
import { buildProjectKeywordSearchPlan, buildProjectQueryIntent } from "./memory-query.js";
|
|
14
15
|
const DEFAULT_PROJECT_REFRESH_FILE_LIMIT = 100;
|
|
15
16
|
const PROJECT_SEARCH_CANDIDATE_MULTIPLIER = 8;
|
|
@@ -21,7 +22,7 @@ const DEFAULT_EMBEDDING_MAX_CHARS = DEFAULT_EMBEDDING_MAX_TOKENS * DEFAULT_EMBED
|
|
|
21
22
|
const DEFAULT_MEMORY_SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
22
23
|
const PROJECT_QUERY_EMBEDDING_CACHE_LIMIT = 128;
|
|
23
24
|
const PROJECT_QUERY_EMBEDDING_CACHE_VERSION = "v1";
|
|
24
|
-
const PROJECT_EMBEDDING_CHUNKING_VERSION = "
|
|
25
|
+
const PROJECT_EMBEDDING_CHUNKING_VERSION = "generic-knowledge-markers-v3";
|
|
25
26
|
export function buildMemoryIndex(input) {
|
|
26
27
|
const { scope, project, task } = resolveMemoryScope(input);
|
|
27
28
|
if (!isProjectMemoryEnabled(project)) {
|
|
@@ -452,6 +453,11 @@ function prepareSchema(db) {
|
|
|
452
453
|
" source_path TEXT NOT NULL,",
|
|
453
454
|
" kind TEXT NOT NULL,",
|
|
454
455
|
" chunk_text TEXT NOT NULL,",
|
|
456
|
+
" heading_path TEXT NOT NULL DEFAULT '',",
|
|
457
|
+
" document_kind TEXT NOT NULL DEFAULT 'other',",
|
|
458
|
+
" document_state TEXT,",
|
|
459
|
+
" chunk_state TEXT,",
|
|
460
|
+
" dated TEXT,",
|
|
455
461
|
" embedding_json TEXT NOT NULL,",
|
|
456
462
|
" PRIMARY KEY (doc_id, chunk_index)",
|
|
457
463
|
");",
|
|
@@ -468,6 +474,19 @@ function prepareSchema(db) {
|
|
|
468
474
|
if (!docsColumns.some((column) => column.name === "content_hash")) {
|
|
469
475
|
db.exec("ALTER TABLE docs ADD COLUMN content_hash TEXT;");
|
|
470
476
|
}
|
|
477
|
+
const embeddingColumns = db.prepare("PRAGMA table_info(doc_embeddings)").all();
|
|
478
|
+
const embeddingMigrations = [
|
|
479
|
+
["heading_path", "TEXT NOT NULL DEFAULT ''"],
|
|
480
|
+
["document_kind", "TEXT NOT NULL DEFAULT 'other'"],
|
|
481
|
+
["document_state", "TEXT"],
|
|
482
|
+
["chunk_state", "TEXT"],
|
|
483
|
+
["dated", "TEXT"],
|
|
484
|
+
];
|
|
485
|
+
for (const [name, declaration] of embeddingMigrations) {
|
|
486
|
+
if (!embeddingColumns.some((column) => column.name === name)) {
|
|
487
|
+
db.exec(`ALTER TABLE doc_embeddings ADD COLUMN ${name} ${declaration};`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
471
490
|
}
|
|
472
491
|
function resolveProjectMemoryEmbeddingConfig(project) {
|
|
473
492
|
if (!isProjectMemoryEnabled(project)) {
|
|
@@ -575,24 +594,29 @@ function generateDocEmbeddings(docs, embedding) {
|
|
|
575
594
|
if (!canBuildProjectVectors(embedding)) {
|
|
576
595
|
return [];
|
|
577
596
|
}
|
|
578
|
-
const chunks = docs.flatMap((doc) => chunkMarkdownContent(doc.content).map((
|
|
597
|
+
const chunks = docs.flatMap((doc) => chunkMarkdownContent(doc.content, doc.sourcePath, doc.kind).map((chunk, chunkIndex) => ({
|
|
579
598
|
docId: doc.docId,
|
|
580
599
|
chunkIndex,
|
|
581
600
|
sourcePath: doc.sourcePath,
|
|
582
601
|
kind: doc.kind,
|
|
583
|
-
|
|
602
|
+
...chunk,
|
|
584
603
|
})));
|
|
585
604
|
if (chunks.length === 0) {
|
|
586
605
|
return [];
|
|
587
606
|
}
|
|
588
607
|
const output = runEmbeddingWorker({
|
|
589
608
|
embedding,
|
|
590
|
-
texts: chunks.map((chunk) =>
|
|
609
|
+
texts: chunks.map((chunk) => embedding.provider === "local"
|
|
610
|
+
? chunk.bodyText
|
|
611
|
+
: joinChunkContext(chunk.contextPrefix, chunk.bodyText)),
|
|
612
|
+
...(embedding.provider === "local"
|
|
613
|
+
? { textPrefixes: chunks.map((chunk) => chunk.contextPrefix) }
|
|
614
|
+
: {}),
|
|
591
615
|
splitIntoTokenWindows: embedding.provider === "local",
|
|
592
616
|
});
|
|
593
617
|
const segments = output.segments ?? chunks.map((chunk, sourceTextIndex) => ({
|
|
594
618
|
sourceTextIndex,
|
|
595
|
-
text: chunk.
|
|
619
|
+
text: joinChunkContext(chunk.contextPrefix, chunk.bodyText),
|
|
596
620
|
}));
|
|
597
621
|
if (output.vectors.length !== segments.length) {
|
|
598
622
|
throw new Error(`Embedding worker returned ${output.vectors.length} vectors for ${segments.length} text segments.`);
|
|
@@ -615,11 +639,11 @@ function generateDocEmbeddings(docs, embedding) {
|
|
|
615
639
|
}
|
|
616
640
|
function insertDocEmbeddings(db, embeddings) {
|
|
617
641
|
const insertEmbedding = db.prepare([
|
|
618
|
-
"INSERT OR REPLACE INTO doc_embeddings (doc_id, chunk_index, source_path, kind, chunk_text, embedding_json)",
|
|
619
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
642
|
+
"INSERT OR REPLACE INTO doc_embeddings (doc_id, chunk_index, source_path, kind, chunk_text, heading_path, document_kind, document_state, chunk_state, dated, embedding_json)",
|
|
643
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
620
644
|
].join(" "));
|
|
621
645
|
embeddings.forEach((embedding) => {
|
|
622
|
-
insertEmbedding.run(embedding.docId, embedding.chunkIndex, embedding.sourcePath, embedding.kind, embedding.chunkText, JSON.stringify(embedding.vector));
|
|
646
|
+
insertEmbedding.run(embedding.docId, embedding.chunkIndex, embedding.sourcePath, embedding.kind, embedding.chunkText, embedding.headingPath, embedding.documentKind, embedding.documentState, embedding.state, embedding.dated, JSON.stringify(embedding.vector));
|
|
623
647
|
});
|
|
624
648
|
}
|
|
625
649
|
function listDocsMissingEmbeddings(db) {
|
|
@@ -672,44 +696,158 @@ function canBuildProjectVectors(embedding) {
|
|
|
672
696
|
function hashMemoryContent(content) {
|
|
673
697
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
674
698
|
}
|
|
675
|
-
function chunkMarkdownContent(content) {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
let
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
699
|
+
function chunkMarkdownContent(content, sourcePath, sourceKind) {
|
|
700
|
+
const isCanonicalKnowledge = sourceKind === "truth_doc";
|
|
701
|
+
const analysis = isCanonicalKnowledge ? analyzeKnowledgeDocument(content, sourcePath) : null;
|
|
702
|
+
const documentKind = analysis?.kind ?? "other";
|
|
703
|
+
const documentState = analysis?.state ?? null;
|
|
704
|
+
const headingStack = [];
|
|
705
|
+
let inFence = false;
|
|
706
|
+
let pendingSectionState = null;
|
|
707
|
+
let pendingDate = null;
|
|
708
|
+
const paragraphs = content
|
|
709
|
+
.split(/\r?\n\s*\r?\n/gu)
|
|
710
|
+
.map((paragraph) => paragraph.trim())
|
|
711
|
+
.filter(Boolean);
|
|
712
|
+
const pieces = [];
|
|
713
|
+
for (const paragraph of paragraphs) {
|
|
714
|
+
const bodyLines = [];
|
|
715
|
+
for (const line of paragraph.split(/\r?\n/gu)) {
|
|
716
|
+
if (/^\s*(```|~~~)/u.test(line)) {
|
|
717
|
+
inFence = !inFence;
|
|
718
|
+
bodyLines.push(line);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (inFence) {
|
|
722
|
+
bodyLines.push(line);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const sectionState = parseKnowledgeStateComment(line);
|
|
726
|
+
if (sectionState) {
|
|
727
|
+
pendingSectionState = sectionState;
|
|
728
|
+
pendingDate = null;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const dated = parseKnowledgeDatedComment(line);
|
|
732
|
+
if (dated) {
|
|
733
|
+
pendingDate = dated;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (isKnowledgeDocumentStateComment(line)) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
bodyLines.push(line);
|
|
740
|
+
const heading = /^(#{1,6})\s+(.+?)\s*$/u.exec(line.trim());
|
|
741
|
+
if (!heading) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const level = heading[1].length;
|
|
745
|
+
const replaceFrom = headingStack.findIndex((entry) => entry.level >= level);
|
|
746
|
+
if (replaceFrom >= 0) {
|
|
747
|
+
headingStack.splice(replaceFrom);
|
|
748
|
+
}
|
|
749
|
+
const headingText = heading[2].trim();
|
|
750
|
+
headingStack.push({
|
|
751
|
+
level,
|
|
752
|
+
text: headingText,
|
|
753
|
+
state: pendingSectionState,
|
|
754
|
+
dated: pendingDate,
|
|
755
|
+
});
|
|
756
|
+
pendingSectionState = null;
|
|
757
|
+
pendingDate = null;
|
|
687
758
|
}
|
|
688
|
-
|
|
689
|
-
|
|
759
|
+
const bodyParagraph = bodyLines.join("\n").trim();
|
|
760
|
+
if (!bodyParagraph) {
|
|
690
761
|
continue;
|
|
691
762
|
}
|
|
692
|
-
|
|
693
|
-
|
|
763
|
+
const headingPath = headingStack.map((entry) => entry.text).join(" > ");
|
|
764
|
+
const state = [...headingStack].reverse().find((entry) => entry.state)?.state
|
|
765
|
+
?? documentState;
|
|
766
|
+
const dated = [...headingStack].reverse().find((entry) => entry.dated)?.dated ?? null;
|
|
767
|
+
const contextPrefix = buildChunkContextPrefix({
|
|
768
|
+
headingPath,
|
|
769
|
+
documentKind,
|
|
770
|
+
documentState,
|
|
771
|
+
state,
|
|
772
|
+
dated,
|
|
773
|
+
});
|
|
774
|
+
const contextSeparatorLength = contextPrefix ? 2 : 0;
|
|
775
|
+
const maxBodyChars = Math.max(256, DEFAULT_EMBEDDING_MAX_CHARS - contextPrefix.length - contextSeparatorLength);
|
|
776
|
+
const targetBodyChars = Math.max(128, DEFAULT_EMBEDDING_TARGET_CHARS - contextPrefix.length - contextSeparatorLength);
|
|
777
|
+
for (const bodyText of splitOversizedMarkdownChunk(bodyParagraph, maxBodyChars, targetBodyChars)) {
|
|
778
|
+
pieces.push({
|
|
779
|
+
bodyText,
|
|
780
|
+
contextPrefix,
|
|
781
|
+
headingPath,
|
|
782
|
+
documentKind,
|
|
783
|
+
documentState,
|
|
784
|
+
state,
|
|
785
|
+
dated,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
694
788
|
}
|
|
695
|
-
|
|
696
|
-
|
|
789
|
+
const merged = [];
|
|
790
|
+
for (const piece of pieces) {
|
|
791
|
+
const previous = merged[merged.length - 1];
|
|
792
|
+
if (previous
|
|
793
|
+
&& sameChunkContext(previous, piece)
|
|
794
|
+
&& joinChunkContext(previous.contextPrefix, `${previous.bodyText}\n\n${piece.bodyText}`).length
|
|
795
|
+
<= DEFAULT_EMBEDDING_TARGET_CHARS) {
|
|
796
|
+
previous.bodyText = `${previous.bodyText}\n\n${piece.bodyText}`;
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
merged.push({ ...piece });
|
|
800
|
+
}
|
|
697
801
|
}
|
|
698
|
-
return
|
|
802
|
+
return merged;
|
|
803
|
+
}
|
|
804
|
+
function parseKnowledgeStateComment(line) {
|
|
805
|
+
const match = /^\s*<!--\s*state:\s*(current|accepted|history|historical|superseded)\s*-->\s*$/iu.exec(line);
|
|
806
|
+
const value = match?.[1].toLowerCase();
|
|
807
|
+
if (value === "history" || value === "historical") {
|
|
808
|
+
return "historical";
|
|
809
|
+
}
|
|
810
|
+
return value === "current" || value === "accepted" || value === "superseded" ? value : null;
|
|
811
|
+
}
|
|
812
|
+
function parseKnowledgeDatedComment(line) {
|
|
813
|
+
return /^\s*<!--\s*dated:\s*(\d{4}-\d{2}-\d{2})\s*-->\s*$/iu.exec(line)?.[1] ?? null;
|
|
814
|
+
}
|
|
815
|
+
function isKnowledgeDocumentStateComment(line) {
|
|
816
|
+
return /^\s*<!--\s*document-state:\s*(?:current|accepted|history|historical|superseded)\s*-->\s*$/iu.test(line);
|
|
817
|
+
}
|
|
818
|
+
function buildChunkContextPrefix(input) {
|
|
819
|
+
const parts = [];
|
|
820
|
+
if (input.documentKind !== "other") {
|
|
821
|
+
parts.push(`[knowledge:doc=${input.documentKind} doc_state=${input.documentState ?? "unknown"} state=${input.state ?? "unknown"}${input.dated ? ` dated=${input.dated}` : ""}]`);
|
|
822
|
+
}
|
|
823
|
+
if (input.headingPath) {
|
|
824
|
+
parts.push(`Heading: ${input.headingPath}`);
|
|
825
|
+
}
|
|
826
|
+
return parts.join("\n");
|
|
827
|
+
}
|
|
828
|
+
function joinChunkContext(prefix, body) {
|
|
829
|
+
return prefix ? `${prefix}\n\n${body}` : body;
|
|
830
|
+
}
|
|
831
|
+
function sameChunkContext(left, right) {
|
|
832
|
+
return left.contextPrefix === right.contextPrefix
|
|
833
|
+
&& left.documentKind === right.documentKind
|
|
834
|
+
&& left.documentState === right.documentState
|
|
835
|
+
&& left.state === right.state
|
|
836
|
+
&& left.dated === right.dated;
|
|
699
837
|
}
|
|
700
|
-
function splitOversizedMarkdownChunk(chunk) {
|
|
701
|
-
if (chunk.length <=
|
|
838
|
+
function splitOversizedMarkdownChunk(chunk, maxChars = DEFAULT_EMBEDDING_MAX_CHARS, targetChars = DEFAULT_EMBEDDING_TARGET_CHARS) {
|
|
839
|
+
if (chunk.length <= maxChars) {
|
|
702
840
|
return [chunk];
|
|
703
841
|
}
|
|
704
842
|
const pieces = [];
|
|
705
843
|
let start = 0;
|
|
706
844
|
while (start < chunk.length) {
|
|
707
845
|
const remaining = chunk.length - start;
|
|
708
|
-
if (remaining <=
|
|
846
|
+
if (remaining <= maxChars) {
|
|
709
847
|
pieces.push(chunk.slice(start).trim());
|
|
710
848
|
break;
|
|
711
849
|
}
|
|
712
|
-
const preferredSplit = findPreferredChunkBoundary(chunk, start, Math.min(start +
|
|
850
|
+
const preferredSplit = findPreferredChunkBoundary(chunk, start, Math.min(start + targetChars, chunk.length), Math.min(start + maxChars, chunk.length), targetChars);
|
|
713
851
|
pieces.push(chunk.slice(start, preferredSplit).trim());
|
|
714
852
|
start = preferredSplit;
|
|
715
853
|
while (start < chunk.length && /\s/.test(chunk[start] ?? "")) {
|
|
@@ -718,8 +856,8 @@ function splitOversizedMarkdownChunk(chunk) {
|
|
|
718
856
|
}
|
|
719
857
|
return pieces.filter((piece) => piece.length > 0);
|
|
720
858
|
}
|
|
721
|
-
function findPreferredChunkBoundary(text, start, preferredEnd, hardEnd) {
|
|
722
|
-
const lowerBound = Math.max(start + Math.floor(
|
|
859
|
+
function findPreferredChunkBoundary(text, start, preferredEnd, hardEnd, targetChars = DEFAULT_EMBEDDING_TARGET_CHARS) {
|
|
860
|
+
const lowerBound = Math.max(start + Math.floor(targetChars / 2), start + 1);
|
|
723
861
|
for (let index = preferredEnd; index >= lowerBound; index -= 1) {
|
|
724
862
|
const current = text[index];
|
|
725
863
|
const previous = text[index - 1];
|
|
@@ -829,6 +967,7 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
829
967
|
throw new ClawError("MEMORY_VECTOR_INDEX_REQUIRED", "Project search requires a refreshed vector index. Run `claw search index --refresh` first.");
|
|
830
968
|
}
|
|
831
969
|
const queryIntent = buildProjectQueryIntent(query);
|
|
970
|
+
const temporalIntent = detectTemporalQueryIntent(query);
|
|
832
971
|
const candidateLimit = Math.max(limit * PROJECT_SEARCH_CANDIDATE_MULTIPLIER, 40);
|
|
833
972
|
const projectDocs = db
|
|
834
973
|
.prepare("SELECT source_path, kind, content FROM docs")
|
|
@@ -864,7 +1003,7 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
864
1003
|
const queryEmbedding = resolveProjectQueryEmbedding(db, embedding, queryIntent.embeddingText || query);
|
|
865
1004
|
const vectorRows = db
|
|
866
1005
|
.prepare([
|
|
867
|
-
"SELECT source_path, kind, chunk_text, embedding_json",
|
|
1006
|
+
"SELECT source_path, kind, chunk_text, heading_path, document_kind, document_state, chunk_state, dated, embedding_json",
|
|
868
1007
|
"FROM doc_embeddings",
|
|
869
1008
|
].join(" "))
|
|
870
1009
|
.all();
|
|
@@ -880,30 +1019,48 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
880
1019
|
snippet: buildSnippet(row.chunk_text),
|
|
881
1020
|
similarity: cosineSimilarity(queryEmbedding.vector, parseEmbeddingJson(row.embedding_json)),
|
|
882
1021
|
exactBoost: signals?.exactBoost ?? 0,
|
|
1022
|
+
temporalBoost: calculateTemporalChunkBoost({
|
|
1023
|
+
intent: temporalIntent,
|
|
1024
|
+
documentState: row.document_state,
|
|
1025
|
+
state: row.chunk_state,
|
|
1026
|
+
dated: row.dated,
|
|
1027
|
+
}),
|
|
1028
|
+
documentKind: row.document_kind,
|
|
1029
|
+
documentState: row.document_state,
|
|
1030
|
+
state: row.chunk_state,
|
|
1031
|
+
dated: row.dated,
|
|
1032
|
+
headingPath: row.heading_path,
|
|
883
1033
|
};
|
|
884
1034
|
})
|
|
885
1035
|
.filter((row) => Number.isFinite(row.similarity))
|
|
886
1036
|
.sort((left, right) => {
|
|
887
|
-
const leftScore = left.similarity + left.exactBoost;
|
|
888
|
-
const rightScore = right.similarity + right.exactBoost;
|
|
1037
|
+
const leftScore = left.similarity + left.exactBoost + left.temporalBoost;
|
|
1038
|
+
const rightScore = right.similarity + right.exactBoost + right.temporalBoost;
|
|
889
1039
|
return rightScore - leftScore;
|
|
890
1040
|
});
|
|
891
1041
|
const bestVectorBySource = new Map();
|
|
892
1042
|
for (const row of rankedVectors) {
|
|
893
1043
|
const existing = bestVectorBySource.get(row.sourcePath);
|
|
894
|
-
|
|
1044
|
+
const vectorScore = row.similarity + row.temporalBoost;
|
|
1045
|
+
if (!existing || vectorScore > existing.vectorScore) {
|
|
895
1046
|
bestVectorBySource.set(row.sourcePath, {
|
|
896
1047
|
kind: row.kind,
|
|
897
1048
|
snippet: row.snippet,
|
|
898
1049
|
similarity: row.similarity,
|
|
1050
|
+
vectorScore,
|
|
1051
|
+
documentKind: row.documentKind,
|
|
1052
|
+
documentState: row.documentState,
|
|
1053
|
+
state: row.state,
|
|
1054
|
+
dated: row.dated,
|
|
1055
|
+
headingPath: row.headingPath,
|
|
899
1056
|
});
|
|
900
1057
|
}
|
|
901
1058
|
}
|
|
902
1059
|
const fused = new Map();
|
|
903
1060
|
Array.from(bestVectorBySource.entries())
|
|
904
1061
|
.sort((left, right) => {
|
|
905
|
-
const leftScore = left[1].
|
|
906
|
-
const rightScore = right[1].
|
|
1062
|
+
const leftScore = left[1].vectorScore + (docSignals.get(left[0])?.exactBoost ?? 0);
|
|
1063
|
+
const rightScore = right[1].vectorScore + (docSignals.get(right[0])?.exactBoost ?? 0);
|
|
907
1064
|
return rightScore - leftScore;
|
|
908
1065
|
})
|
|
909
1066
|
.slice(0, candidateLimit)
|
|
@@ -913,14 +1070,19 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
913
1070
|
sourcePath,
|
|
914
1071
|
kind: row.kind,
|
|
915
1072
|
snippet: row.snippet,
|
|
916
|
-
score: reciprocalRankScore(index + 1, 0.
|
|
1073
|
+
score: reciprocalRankScore(index + 1, 0.5) + (signals?.exactBoost ?? 0),
|
|
917
1074
|
vectorRank: index + 1,
|
|
1075
|
+
documentKind: row.documentKind,
|
|
1076
|
+
documentState: row.documentState,
|
|
1077
|
+
state: row.state,
|
|
1078
|
+
dated: row.dated,
|
|
1079
|
+
headingPath: row.headingPath,
|
|
918
1080
|
});
|
|
919
1081
|
});
|
|
920
1082
|
ftsRows.forEach((row, index) => {
|
|
921
1083
|
const existing = fused.get(row.source_path);
|
|
922
1084
|
const signals = docSignals.get(row.source_path);
|
|
923
|
-
const nextScore = (existing?.score ?? 0) + reciprocalRankScore(index + 1, 0.
|
|
1085
|
+
const nextScore = (existing?.score ?? 0) + reciprocalRankScore(index + 1, 0.3);
|
|
924
1086
|
fused.set(row.source_path, {
|
|
925
1087
|
sourcePath: row.source_path,
|
|
926
1088
|
kind: existing?.kind ?? row.kind,
|
|
@@ -928,12 +1090,13 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
928
1090
|
score: nextScore + (existing ? 0 : (signals?.exactBoost ?? 0)),
|
|
929
1091
|
vectorRank: existing?.vectorRank,
|
|
930
1092
|
textRank: index + 1,
|
|
1093
|
+
...(existing ? pickTemporalResultMetadata(existing) : {}),
|
|
931
1094
|
});
|
|
932
1095
|
});
|
|
933
1096
|
signalRows.forEach((row, index) => {
|
|
934
1097
|
const existing = fused.get(row.source_path);
|
|
935
1098
|
const signals = docSignals.get(row.source_path);
|
|
936
|
-
const nextScore = (existing?.score ?? 0) + reciprocalRankScore(index + 1, 0.
|
|
1099
|
+
const nextScore = (existing?.score ?? 0) + reciprocalRankScore(index + 1, 0.2);
|
|
937
1100
|
fused.set(row.source_path, {
|
|
938
1101
|
sourcePath: row.source_path,
|
|
939
1102
|
kind: existing?.kind ?? row.kind,
|
|
@@ -942,6 +1105,7 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
942
1105
|
vectorRank: existing?.vectorRank,
|
|
943
1106
|
textRank: existing?.textRank,
|
|
944
1107
|
signalRank: index + 1,
|
|
1108
|
+
...(existing ? pickTemporalResultMetadata(existing) : {}),
|
|
945
1109
|
});
|
|
946
1110
|
});
|
|
947
1111
|
return {
|
|
@@ -956,21 +1120,33 @@ function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
|
956
1120
|
function tryProjectLexicalFastPath(input) {
|
|
957
1121
|
const { queryIntent } = input;
|
|
958
1122
|
const primaryKeywordStep = buildProjectKeywordSearchPlan(input.query)[0];
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1123
|
+
const normalizedFileQuery = path.basename(input.query.trim()).toLowerCase();
|
|
1124
|
+
const normalizedFileStem = path.parse(normalizedFileQuery).name;
|
|
1125
|
+
const explicitFileMatches = /\.[a-z0-9]{1,8}$/iu.test(normalizedFileQuery)
|
|
1126
|
+
? Array.from(input.docSignals.keys()).filter((sourcePath) => {
|
|
1127
|
+
const candidateName = path.basename(sourcePath).toLowerCase();
|
|
1128
|
+
return candidateName === normalizedFileQuery
|
|
1129
|
+
|| path.parse(candidateName).name === normalizedFileStem;
|
|
1130
|
+
})
|
|
1131
|
+
: [];
|
|
1132
|
+
if (explicitFileMatches.length !== 1
|
|
1133
|
+
&& (queryIntent.strongTerms.length === 0
|
|
1134
|
+
|| queryIntent.weakTerms.length > 0
|
|
1135
|
+
|| !primaryKeywordStep
|
|
1136
|
+
|| primaryKeywordStep.substringTerms.length > 0)) {
|
|
963
1137
|
return null;
|
|
964
1138
|
}
|
|
965
1139
|
const fullCoverage = Array.from(input.docSignals.entries()).filter(([, signals]) => signals.strongCoverageRatio === 1);
|
|
966
1140
|
const pathMatches = fullCoverage.filter(([, signals]) => signals.fileNameHits >= queryIntent.strongTerms.length
|
|
967
1141
|
|| signals.pathHits >= queryIntent.strongTerms.length);
|
|
968
1142
|
const phraseMatches = fullCoverage.filter(([, signals]) => signals.phraseMatch);
|
|
969
|
-
const confidentSourcePath =
|
|
970
|
-
?
|
|
971
|
-
:
|
|
972
|
-
?
|
|
973
|
-
:
|
|
1143
|
+
const confidentSourcePath = explicitFileMatches.length === 1
|
|
1144
|
+
? explicitFileMatches[0]
|
|
1145
|
+
: pathMatches.length === 1
|
|
1146
|
+
? pathMatches[0]?.[0]
|
|
1147
|
+
: phraseMatches.length === 1
|
|
1148
|
+
? phraseMatches[0]?.[0]
|
|
1149
|
+
: null;
|
|
974
1150
|
if (!confidentSourcePath) {
|
|
975
1151
|
return null;
|
|
976
1152
|
}
|
|
@@ -1197,6 +1373,53 @@ function matchesAllSubstrings(db, sourcePath, substringTerms) {
|
|
|
1197
1373
|
function escapeLikePattern(term) {
|
|
1198
1374
|
return term.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
1199
1375
|
}
|
|
1376
|
+
function detectTemporalQueryIntent(query) {
|
|
1377
|
+
const normalized = query.trim().toLowerCase();
|
|
1378
|
+
const date = /\b(\d{4}-\d{2}-\d{2})\b/u.exec(normalized)?.[1] ?? null;
|
|
1379
|
+
const historical = /(?:\bwhy\b|\brationale\b|\bhistor(?:y|ical)\b|\bprevious(?:ly)?\b|\bpast\b|\bformerly\b|\brollback\b|\brevert(?:ed)?\b|\bused to\b|为什么|原因|历史|过去|之前|曾经|当时|回退|回滚|反复|演化|旧方案)/iu.test(normalized);
|
|
1380
|
+
if (historical || date) {
|
|
1381
|
+
return { mode: "historical", date };
|
|
1382
|
+
}
|
|
1383
|
+
const current = /(?:\bcurrent(?:ly)?\b|\bnow\b|\bpresent\b|\bfinal\b|\blatest\b|\bas of today\b|当前|现在|目前|最终|现行|如今|为准|怎么工作|如何工作)/iu.test(normalized);
|
|
1384
|
+
return { mode: current ? "current" : "neutral", date };
|
|
1385
|
+
}
|
|
1386
|
+
function calculateTemporalChunkBoost(input) {
|
|
1387
|
+
const effectiveState = input.state ?? input.documentState;
|
|
1388
|
+
let score = 0;
|
|
1389
|
+
if (input.intent.mode === "current") {
|
|
1390
|
+
if (effectiveState === "current" || effectiveState === "accepted")
|
|
1391
|
+
score += 0.035;
|
|
1392
|
+
else if (effectiveState === "historical")
|
|
1393
|
+
score -= 0.025;
|
|
1394
|
+
else if (effectiveState === "superseded")
|
|
1395
|
+
score -= 0.04;
|
|
1396
|
+
}
|
|
1397
|
+
else if (input.intent.mode === "historical") {
|
|
1398
|
+
if (effectiveState === "historical")
|
|
1399
|
+
score += 0.035;
|
|
1400
|
+
else if (effectiveState === "superseded")
|
|
1401
|
+
score += 0.02;
|
|
1402
|
+
}
|
|
1403
|
+
else if (effectiveState === "current" || effectiveState === "accepted") {
|
|
1404
|
+
score += 0.012;
|
|
1405
|
+
}
|
|
1406
|
+
else if (effectiveState === "superseded") {
|
|
1407
|
+
score -= 0.025;
|
|
1408
|
+
}
|
|
1409
|
+
if (input.intent.date && input.dated === input.intent.date) {
|
|
1410
|
+
score += 0.05;
|
|
1411
|
+
}
|
|
1412
|
+
return score;
|
|
1413
|
+
}
|
|
1414
|
+
function pickTemporalResultMetadata(entry) {
|
|
1415
|
+
return {
|
|
1416
|
+
...(entry.documentKind !== undefined ? { documentKind: entry.documentKind } : {}),
|
|
1417
|
+
...(entry.documentState !== undefined ? { documentState: entry.documentState } : {}),
|
|
1418
|
+
...(entry.state !== undefined ? { state: entry.state } : {}),
|
|
1419
|
+
...(entry.dated !== undefined ? { dated: entry.dated } : {}),
|
|
1420
|
+
...(entry.headingPath !== undefined ? { headingPath: entry.headingPath } : {}),
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1200
1423
|
function rerankProjectSearchCandidates(candidates, docSignals, limit) {
|
|
1201
1424
|
const remaining = [...candidates];
|
|
1202
1425
|
const selected = [];
|
|
@@ -1212,8 +1435,8 @@ function rerankProjectSearchCandidates(candidates, docSignals, limit) {
|
|
|
1212
1435
|
const uncoveredStrongTerms = (signals?.strongMatchedTerms ?? []).filter((term) => !coveredStrongTerms.has(term));
|
|
1213
1436
|
const uncoveredTerms = (signals?.matchedTerms ?? []).filter((term) => !coveredTerms.has(term));
|
|
1214
1437
|
const adjustedScore = candidate.score
|
|
1215
|
-
+ uncoveredStrongTerms.length * 0.
|
|
1216
|
-
+ uncoveredTerms.length * 0.
|
|
1438
|
+
+ uncoveredStrongTerms.length * 0.015
|
|
1439
|
+
+ uncoveredTerms.length * 0.003
|
|
1217
1440
|
+ Math.max(routeCount - 1, 0) * 0.003;
|
|
1218
1441
|
if (adjustedScore > bestScore) {
|
|
1219
1442
|
bestScore = adjustedScore;
|
|
@@ -1249,6 +1472,27 @@ function rerankProjectSearchCandidates(candidates, docSignals, limit) {
|
|
|
1249
1472
|
kind: next.kind,
|
|
1250
1473
|
snippet: next.snippet,
|
|
1251
1474
|
score: next.score,
|
|
1475
|
+
...pickTemporalResultMetadata(next),
|
|
1476
|
+
...(process.env.CLAW_SEARCH_RANKING_DIAGNOSTICS === "1"
|
|
1477
|
+
? {
|
|
1478
|
+
_ranking: {
|
|
1479
|
+
vectorRank: next.vectorRank ?? null,
|
|
1480
|
+
textRank: next.textRank ?? null,
|
|
1481
|
+
signalRank: next.signalRank ?? null,
|
|
1482
|
+
baseLexicalBoost: (nextSignals?.exactBoost ?? 0)
|
|
1483
|
+
- (nextSignals?.titleMatchScore ?? 0)
|
|
1484
|
+
- (nextSignals?.entityTitleScore ?? 0)
|
|
1485
|
+
- (nextSignals?.documentTypeScore ?? 0)
|
|
1486
|
+
+ (nextSignals?.genericPenalty ?? 0),
|
|
1487
|
+
titleMatchScore: nextSignals?.titleMatchScore ?? 0,
|
|
1488
|
+
entityTitleScore: nextSignals?.entityTitleScore ?? 0,
|
|
1489
|
+
documentTypeScore: nextSignals?.documentTypeScore ?? 0,
|
|
1490
|
+
genericPenalty: nextSignals?.genericPenalty ?? 0,
|
|
1491
|
+
matchedTerms: nextSignals?.matchedTerms ?? [],
|
|
1492
|
+
strongMatchedTerms: nextSignals?.strongMatchedTerms ?? [],
|
|
1493
|
+
},
|
|
1494
|
+
}
|
|
1495
|
+
: {}),
|
|
1252
1496
|
});
|
|
1253
1497
|
}
|
|
1254
1498
|
return selected;
|
|
@@ -1258,6 +1502,7 @@ function buildProjectSearchSignals(input) {
|
|
|
1258
1502
|
const normalizedContent = input.content.toLowerCase();
|
|
1259
1503
|
const normalizedPath = input.sourcePath.toLowerCase();
|
|
1260
1504
|
const fileName = path.basename(input.sourcePath).toLowerCase();
|
|
1505
|
+
const title = extractDocumentTitle(input.content, fileName);
|
|
1261
1506
|
const lowerTerms = input.queryIntent.terms.map((term) => term.toLowerCase());
|
|
1262
1507
|
const lowerStrongTerms = input.queryIntent.strongTerms.map((term) => term.toLowerCase());
|
|
1263
1508
|
const lowerWeakTerms = input.queryIntent.weakTerms.map((term) => term.toLowerCase());
|
|
@@ -1276,12 +1521,11 @@ function buildProjectSearchSignals(input) {
|
|
|
1276
1521
|
const phraseMatch = normalizedQuery.length > 0
|
|
1277
1522
|
&& (normalizedContent.includes(normalizedQuery.toLowerCase()) || normalizedPath.includes(normalizedQuery.toLowerCase()));
|
|
1278
1523
|
const weakOnlyPenalty = strongMatchedTerms.size === 0 && weakMatchedTerms.size > 0 ? 0.012 : 0;
|
|
1279
|
-
const missingStrongPenalty = lowerStrongTerms.length > 0 && strongMatchedTerms.size === 0
|
|
1280
|
-
? 0.035
|
|
1281
|
-
: lowerStrongTerms.length > 1 && strongMatchedTerms.size === 1
|
|
1282
|
-
? 0.01
|
|
1283
|
-
: 0;
|
|
1284
1524
|
const indexFilePenalty = isIndexLikeDocName(fileName) ? 0.06 : 0;
|
|
1525
|
+
const titleMatchScore = calculateTitleMatchScore(normalizedQuery, title);
|
|
1526
|
+
const entityTitleScore = calculateEntityTitleScore(input.queryIntent.entityPhrases, title);
|
|
1527
|
+
const documentTypeScore = calculateDocumentTypeScore(normalizedQuery, input.sourcePath, title);
|
|
1528
|
+
const genericPenalty = calculateGenericDocumentPenalty(normalizedQuery, input.sourcePath, fileName, title);
|
|
1285
1529
|
return {
|
|
1286
1530
|
matchedTerms: Array.from(matchedTerms),
|
|
1287
1531
|
strongMatchedTerms: Array.from(strongMatchedTerms),
|
|
@@ -1292,6 +1536,10 @@ function buildProjectSearchSignals(input) {
|
|
|
1292
1536
|
pathHits,
|
|
1293
1537
|
phraseMatch,
|
|
1294
1538
|
strongCoverageRatio,
|
|
1539
|
+
titleMatchScore,
|
|
1540
|
+
entityTitleScore,
|
|
1541
|
+
documentTypeScore,
|
|
1542
|
+
genericPenalty,
|
|
1295
1543
|
exactBoost: strongMatchedTerms.size * 0.016
|
|
1296
1544
|
+ weakMatchedTerms.size * 0.004
|
|
1297
1545
|
+ coverageRatio * 0.008
|
|
@@ -1300,13 +1548,117 @@ function buildProjectSearchSignals(input) {
|
|
|
1300
1548
|
+ fileNameHits * 0.025
|
|
1301
1549
|
+ pathHits * 0.01
|
|
1302
1550
|
+ (phraseMatch ? 0.018 : 0)
|
|
1551
|
+
+ titleMatchScore
|
|
1552
|
+
+ entityTitleScore
|
|
1553
|
+
+ documentTypeScore
|
|
1303
1554
|
- weakOnlyPenalty
|
|
1304
|
-
-
|
|
1305
|
-
-
|
|
1555
|
+
- indexFilePenalty
|
|
1556
|
+
- genericPenalty,
|
|
1306
1557
|
};
|
|
1307
1558
|
}
|
|
1559
|
+
function calculateEntityTitleScore(entityPhrases, title) {
|
|
1560
|
+
const normalizedTitle = normalizeComparableTitle(title);
|
|
1561
|
+
let score = 0;
|
|
1562
|
+
for (const phrase of entityPhrases) {
|
|
1563
|
+
const normalizedPhrase = normalizeComparableTitle(phrase);
|
|
1564
|
+
if (normalizedPhrase.length < 3) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
if (normalizedPhrase === normalizedTitle) {
|
|
1568
|
+
score = Math.max(score, 0.14);
|
|
1569
|
+
}
|
|
1570
|
+
else if (normalizedTitle.includes(normalizedPhrase) || normalizedPhrase.includes(normalizedTitle)) {
|
|
1571
|
+
score = Math.max(score, 0.09);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return score;
|
|
1575
|
+
}
|
|
1576
|
+
function normalizeComparableTitle(value) {
|
|
1577
|
+
return value.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
1578
|
+
}
|
|
1308
1579
|
function isIndexLikeDocName(fileName) {
|
|
1309
|
-
return fileName === "contents.md"
|
|
1580
|
+
return fileName === "contents.md"
|
|
1581
|
+
|| fileName === "summary.md"
|
|
1582
|
+
|| fileName === "index.md"
|
|
1583
|
+
|| fileName === "readme.md"
|
|
1584
|
+
|| fileName === "project-truth.md";
|
|
1585
|
+
}
|
|
1586
|
+
function extractDocumentTitle(content, fileName) {
|
|
1587
|
+
const heading = content
|
|
1588
|
+
.split(/\r?\n/u, 40)
|
|
1589
|
+
.find((line) => /^#{1,3}\s+\S/u.test(line.trim()));
|
|
1590
|
+
return (heading?.replace(/^#{1,3}\s+/u, "") ?? path.parse(fileName).name).trim().toLowerCase();
|
|
1591
|
+
}
|
|
1592
|
+
function calculateTitleMatchScore(query, title) {
|
|
1593
|
+
const queryFeatures = buildTitleLexicalFeatures(query);
|
|
1594
|
+
const titleFeatures = buildTitleLexicalFeatures(title);
|
|
1595
|
+
if (queryFeatures.size === 0 || titleFeatures.size === 0) {
|
|
1596
|
+
return 0;
|
|
1597
|
+
}
|
|
1598
|
+
let matched = 0;
|
|
1599
|
+
for (const feature of titleFeatures) {
|
|
1600
|
+
if (queryFeatures.has(feature)) {
|
|
1601
|
+
matched += 1;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (matched < 2) {
|
|
1605
|
+
return 0;
|
|
1606
|
+
}
|
|
1607
|
+
const titleCoverage = matched / titleFeatures.size;
|
|
1608
|
+
return Math.min(0.075, matched * 0.006 + titleCoverage * 0.035);
|
|
1609
|
+
}
|
|
1610
|
+
function buildTitleLexicalFeatures(value) {
|
|
1611
|
+
const features = new Set();
|
|
1612
|
+
for (const asciiWord of value.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/gu) ?? []) {
|
|
1613
|
+
features.add(`a:${asciiWord}`);
|
|
1614
|
+
}
|
|
1615
|
+
for (const sequence of value.match(/[\u4e00-\u9fff]+/gu) ?? []) {
|
|
1616
|
+
const characters = Array.from(sequence);
|
|
1617
|
+
for (let size = 2; size <= 3; size += 1) {
|
|
1618
|
+
for (let index = 0; index <= characters.length - size; index += 1) {
|
|
1619
|
+
features.add(`c:${characters.slice(index, index + size).join("")}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return features;
|
|
1624
|
+
}
|
|
1625
|
+
function calculateDocumentTypeScore(query, sourcePath, title) {
|
|
1626
|
+
const normalizedPath = sourcePath.replaceAll("\\", "/").toLowerCase();
|
|
1627
|
+
let score = 0;
|
|
1628
|
+
if (/(?:必须|不应该|优先|规则)/u.test(query) && /(?:规则|规范|指南)/u.test(title)) {
|
|
1629
|
+
score += 0.025;
|
|
1630
|
+
}
|
|
1631
|
+
if (/(?:有哪些|哪些|名单|清单|在哪里定义|哪里定义)/u.test(query) && /(?:名单|清单|目录|索引)/u.test(title)) {
|
|
1632
|
+
score += 0.025;
|
|
1633
|
+
}
|
|
1634
|
+
if (/(?:章节|第[一二三四五六七八九十百]+章)/u.test(query) && /第[一二三四五六七八九十百]+章/u.test(title)) {
|
|
1635
|
+
score += 0.025;
|
|
1636
|
+
}
|
|
1637
|
+
if (/(?:边界|允许|禁止|分工)/u.test(query) && /(?:边界|规范|规则|分工)/u.test(title)) {
|
|
1638
|
+
score += 0.02;
|
|
1639
|
+
}
|
|
1640
|
+
if (/(?:为什么|为何|决定|取舍)/u.test(query) && /(?:adr|方案|决策|裁定)/iu.test(title)) {
|
|
1641
|
+
score += 0.012;
|
|
1642
|
+
}
|
|
1643
|
+
if (/(?:现在|当前|最终|由谁|运行时)/u.test(query) && normalizedPath.includes("/.claw/truth/")) {
|
|
1644
|
+
score += 0.006;
|
|
1645
|
+
}
|
|
1646
|
+
return Math.min(score, 0.04);
|
|
1647
|
+
}
|
|
1648
|
+
function calculateGenericDocumentPenalty(query, sourcePath, fileName, title) {
|
|
1649
|
+
const normalizedPath = sourcePath.replaceAll("\\", "/").toLowerCase();
|
|
1650
|
+
const explicitlyRequestsPlanning = /(?:计划|规划|路线|规格|实施方案|复盘)/u.test(query);
|
|
1651
|
+
const planningLikePath = /\/(?:plans?|specs?|development-plans|[^/]*开发[^/]*计划)(?:\/|$)/iu.test(normalizedPath);
|
|
1652
|
+
if (planningLikePath && !explicitlyRequestsPlanning) {
|
|
1653
|
+
return 0.04;
|
|
1654
|
+
}
|
|
1655
|
+
if (/第[一二三四五六七八九十百]+章/u.test(title) && !/(?:章节|第[一二三四五六七八九十百]+章)/u.test(query)) {
|
|
1656
|
+
return 0.025;
|
|
1657
|
+
}
|
|
1658
|
+
if (/(?:总纲|索引|总览)/u.test(title) && !/(?:总纲|索引|总览|整体|全部)/u.test(query)) {
|
|
1659
|
+
return 0.035;
|
|
1660
|
+
}
|
|
1661
|
+
return isIndexLikeDocName(fileName) ? 0.01 : 0;
|
|
1310
1662
|
}
|
|
1311
1663
|
function reciprocalRankScore(rank, weight) {
|
|
1312
1664
|
return weight * (1 / (40 + rank));
|