惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
量子位
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog
A
About on SuperTechFans
腾讯CDC
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
Last Week in AI
Last Week in AI
H
Help Net Security
WordPress大学
WordPress大学
博客园 - 司徒正美
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Tailwind CSS Blog
博客园 - 【当耐特】
S
SegmentFault 最新的问题
美团技术团队
M
MIT News - Artificial intelligence
L
LangChain Blog
博客园 - 聂微东

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(memory): use per-keyword FTS search in hybrid mode #3...
openclaw-clo · 2026-06-16 · via Recent Commits to openclaw:main

@@ -71,6 +71,7 @@ const FTS_TABLE = "chunks_fts";

7171

const EMBEDDING_CACHE_TABLE = "embedding_cache";

7272

const MEMORY_INDEX_MANAGER_CACHE_KEY = Symbol.for("openclaw.memoryIndexManagerCache");

7373

export const EMBEDDING_PROBE_CACHE_TTL_MS = 30_000;

74+

const KEYWORD_FALLBACK_SEARCH_TERM_LIMIT = 6;

7475

const log = createSubsystemLogger("memory");

7576

type MemoryIndexManagerPurpose = "default" | "status" | "cli";

7677

type MemoryEmbeddingProviderRequirement = {

@@ -88,6 +89,8 @@ type EmbeddingProbeCacheEntry = {

8889

expireAtMs: number;

8990

};

909192+

type KeywordSearchHit = MemorySearchResult & { id: string; textScore: number };

93+9194

const EMBEDDING_PROBE_CACHE = new Map<string, EmbeddingProbeCacheEntry>();

92959396

export async function closeAllMemoryIndexManagers(): Promise<void> {

@@ -689,7 +692,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem

689692

return [];

690693

}

691694692-

const fullQueryResults = await this.searchKeyword(

695+

const keywordResults = await this.searchKeywordWithFallback(

693696

cleaned,

694697

candidates,

695698

{

@@ -700,47 +703,9 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem

700703

log.warn(`memory search: FTS keyword query failed: ${formatErrorMessage(err)}`);

701704

return [];

702705

});

703-

const resultSets =

704-

fullQueryResults.length > 0

705-

? [fullQueryResults]

706-

: await Promise.all(

707-

// Fallback: broaden recall for conversational queries when the

708-

// exact AND query is too strict to return any results.

709-

(() => {

710-

const keywords = extractKeywords(cleaned, {

711-

ftsTokenizer: this.settings.store.fts.tokenizer,

712-

});

713-

const searchTerms = keywords.length > 0 ? keywords : [cleaned];

714-

return searchTerms.map((term) =>

715-

this.searchKeyword(

716-

term,

717-

candidates,

718-

{ boostFallbackRanking: true },

719-

sourceFilterList,

720-

).catch((err: unknown) => {

721-

log.warn(

722-

`memory search: FTS per-keyword query failed for "${term}": ${formatErrorMessage(err)}`,

723-

);

724-

return [];

725-

}),

726-

);

727-

})(),

728-

);

729-730-

// Merge and deduplicate results, keeping highest score for each chunk

731-

const seenIds = new Map<string, (typeof resultSets)[0][0]>();

732-

for (const results of resultSets) {

733-

for (const result of results) {

734-

const existing = seenIds.get(result.id);

735-

if (!existing || result.score > existing.score) {

736-

seenIds.set(result.id, result);

737-

}

738-

}

739-

}

740706741-

const merged = [...seenIds.values()];

742707

const decayed = await applyTemporalDecayToHybridResults({

743-

results: merged,

708+

results: keywordResults,

744709

temporalDecay: hybrid.temporalDecay,

745710

workspaceDir: this.workspaceDir,

746711

});

@@ -751,7 +716,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem

751716

// If FTS isn't available, hybrid mode cannot use keyword search; degrade to vector-only.

752717

const loadKeywordResults = async () =>

753718

hybrid.enabled && this.fts.enabled && this.fts.available

754-

? await this.searchKeyword(

719+

? await this.searchKeywordWithFallback(

755720

cleaned,

756721

candidates,

757722

{ boostFallbackRanking: true },

@@ -824,8 +789,8 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem

824789

}

825790826791

// Hybrid defaults can produce keyword-only matches below minScore after

827-

// weighting. If strict vector+keyword results are empty, preserve the FTS

828-

// matches; FTS already established lexical relevance.

792+

// BM25 normalization and textWeight scaling. Preserve FTS-backed lexical

793+

// hits when they are the only relevant results.

829794

const relaxedMinScore = 0;

830795

const keywordKeys = new Set(

831796

keywordResults.map(

@@ -910,7 +875,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem

910875

limit: number,

911876

options?: { boostFallbackRanking?: boolean },

912877

sourceFilterList?: MemorySource[],

913-

): Promise<Array<MemorySearchResult & { id: string; textScore: number }>> {

878+

): Promise<KeywordSearchHit[]> {

914879

if (!this.fts.enabled || !this.fts.available) {

915880

return [];

916881

}

@@ -927,7 +892,63 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem

927892

bm25RankToScore,

928893

boostFallbackRanking: options?.boostFallbackRanking,

929894

});

930-

return results.map((entry) => entry as MemorySearchResult & { id: string; textScore: number });

895+

return results.map((entry) => entry as KeywordSearchHit);

896+

}

897+898+

private async searchKeywordWithFallback(

899+

query: string,

900+

limit: number,

901+

options: { boostFallbackRanking?: boolean } | undefined,

902+

sourceFilterList: MemorySource[],

903+

): Promise<KeywordSearchHit[]> {

904+

const fullQueryResults = await this.searchKeyword(

905+

query,

906+

limit,

907+

options,

908+

sourceFilterList,

909+

).catch(() => []);

910+

if (fullQueryResults.length > 0) {

911+

return fullQueryResults;

912+

}

913+914+

// Broaden recall for conversational queries when the exact AND query is too

915+

// strict, but cap the number of extra FTS probes so long prompts cannot fan

916+

// out into unbounded sqlite work.

917+

const fallbackTerms = this.resolveKeywordFallbackTerms(query);

918+

if (fallbackTerms.length === 0) {

919+

return [];

920+

}

921+922+

const resultSets = await Promise.all(

923+

fallbackTerms.map((term) =>

924+

this.searchKeyword(term, limit, options, sourceFilterList).catch(() => []),

925+

),

926+

);

927+

return this.mergeKeywordSearchHits(resultSets);

928+

}

929+930+

private resolveKeywordFallbackTerms(query: string): string[] {

931+

const keywords = extractKeywords(query, {

932+

ftsTokenizer: this.settings.store.fts.tokenizer,

933+

}).filter((term) => term !== query);

934+

return keywords.slice(0, KEYWORD_FALLBACK_SEARCH_TERM_LIMIT);

935+

}

936+937+

private mergeKeywordSearchHits(resultSets: KeywordSearchHit[][]): KeywordSearchHit[] {

938+

const seenIds = new Map<string, KeywordSearchHit>();

939+

for (const results of resultSets) {

940+

for (const result of results) {

941+

const existing = seenIds.get(result.id);

942+

if (

943+

!existing ||

944+

result.textScore > existing.textScore ||

945+

(result.textScore === existing.textScore && result.score > existing.score)

946+

) {

947+

seenIds.set(result.id, result);

948+

}

949+

}

950+

}

951+

return [...seenIds.values()].toSorted((a, b) => b.score - a.score);

931952

}

932953933954

private mergeHybridResults(params: {