Skip to content

Commit a820c14

Browse files
committed
fix(memory-core): preserve vector fallback without FTS
1 parent 297e158 commit a820c14

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

extensions/memory-core/src/memory/manager.async-search.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,60 @@ describe("memory search async sync", () => {
5454
expect(queryMock).toHaveBeenCalledTimes(1);
5555
});
5656

57+
it("keeps vector-only results unweighted when FTS is unavailable", async () => {
58+
const vectorResult = {
59+
id: "memory-alpha",
60+
path: "memory/alpha.md",
61+
startLine: 1,
62+
endLine: 1,
63+
score: 0.9,
64+
snippet: "Alpha memory",
65+
source: "memory" as const,
66+
};
67+
const mergeHybridResults = vi.fn(async () => []);
68+
const manager = Object.create(MemoryIndexManager.prototype) as MemoryIndexManager;
69+
Object.assign(manager as unknown as Record<string, unknown>, {
70+
providerRequirement: { mode: "optional", provider: "openai" },
71+
providerLifecycle: { mode: "ready" },
72+
provider: { id: "mock", model: "mock-embed" },
73+
settings: {
74+
sync: { onSearch: false },
75+
query: {
76+
minScore: 0.8,
77+
maxResults: 5,
78+
hybrid: {
79+
enabled: true,
80+
candidateMultiplier: 2,
81+
vectorWeight: 0.5,
82+
textWeight: 0.5,
83+
temporalDecay: { enabled: false, halfLifeDays: 30 },
84+
},
85+
},
86+
},
87+
sources: new Set(["memory"]),
88+
dirty: false,
89+
sessionsDirty: false,
90+
workspaceDir: "",
91+
drainReindex: vi.fn(async () => {}),
92+
hasIndexedContentForSearch: vi.fn(async () => true),
93+
warmSession: vi.fn(),
94+
sync: vi.fn(async () => {}),
95+
ensureProviderInitialized: vi.fn(async () => {}),
96+
assertRequiredProviderAvailable: vi.fn(),
97+
refreshSearchIndexIdentityDirty: vi.fn(async () => ({ status: "valid" })),
98+
searchKeywordWithFallback: vi.fn(async () => []),
99+
embedQueryWithRetry: vi.fn(async () => [1, 0]),
100+
searchVector: vi.fn(async () => [vectorResult]),
101+
hasKeywordSearchAvailable: vi.fn(async () => false),
102+
mergeHybridResults,
103+
});
104+
105+
const results = await manager.search("alpha");
106+
107+
expect(results).toEqual([vectorResult]);
108+
expect(mergeHybridResults).not.toHaveBeenCalled();
109+
});
110+
57111
it("waits for in-flight search sync during close", async () => {
58112
let releaseSync = () => {};
59113
const pendingSync = new Promise<void>((resolve) => {

extensions/memory-core/src/memory/manager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,11 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
780780
})
781781
: [];
782782

783-
if (!hybrid.enabled) {
783+
const keywordSearchAvailable = await this.hasKeywordSearchAvailable();
784+
// Hybrid ranking requires a keyword side. When FTS is unavailable, preserve
785+
// the current vector-only fallback so vector scores are not downweighted by
786+
// empty keyword results.
787+
if (!hybrid.enabled || !keywordSearchAvailable) {
784788
return vectorResults.filter((entry) => entry.score >= minScore).slice(0, maxResults);
785789
}
786790

0 commit comments

Comments
 (0)