mirror of
https://github.com/thedotmack/claude-mem.git
synced 2026-09-20 04:23:02 +08:00
fix(search): hydrate Chroma matches in relevance order, not by date (#3881)
A search returns the newest of up to 100 semantic candidates rather than the most relevant ones, so an older observation that matches precisely loses to a newer, vaguely related one — including when the query is the older observation's own verbatim text. Raising the limit surfaces it again, which shows it was a candidate all along and was dropped by the date cut rather than by relevance. hybridSemanticHydrate asks Chroma for 100 matches, which arrive in distance order, then hydrates them with orderBy 'date_desc'. That re-sorts the ranking away before the limit is applied. The same file already does this correctly in performChromaSemanticSearch, which hydrates with orderBy 'relevance' — the mode that preserves the caller-provided id order. Use 'relevance' at the three call sites that pass ids straight from Chroma: searchObservations, getTimelineByQuery, and searchChromaForTimeline. The last one selects the single timeline anchor, where 'date_desc' picked the most recent candidate rather than the top-ranked one, disagreeing with the FTS fallback directly beneath it that returns the best match. Closes #3876
This commit is contained in:
@@ -129,7 +129,7 @@ export class SearchManager {
|
||||
|
||||
private async searchChromaForTimeline(query: string, project?: string, platformSource?: string): Promise<ObservationSearchResult[]> {
|
||||
return this.hybridSemanticHydrate(query, 'observation', project, platformSource, (ids) =>
|
||||
this.sessionStore.getObservationsByIds(ids, { orderBy: 'date_desc', limit: 1, project, platformSource })
|
||||
this.sessionStore.getObservationsByIds(ids, { orderBy: 'relevance', limit: 1, project, platformSource })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -924,7 +924,7 @@ export class SearchManager {
|
||||
try {
|
||||
const limit = options.limit || 20;
|
||||
results = await this.hybridSemanticHydrate(query, 'observation', options.project, options.platformSource, (ids) =>
|
||||
this.sessionStore.getObservationsByIds(ids, { orderBy: 'date_desc', limit, project: options.project, platformSource: options.platformSource })
|
||||
this.sessionStore.getObservationsByIds(ids, { orderBy: 'relevance', limit, project: options.project, platformSource: options.platformSource })
|
||||
);
|
||||
} catch (chromaError) {
|
||||
const errorObject = chromaError instanceof Error ? chromaError : new Error(String(chromaError));
|
||||
@@ -1103,7 +1103,7 @@ export class SearchManager {
|
||||
logger.debug('SEARCH', 'Using hybrid semantic search for timeline query', {});
|
||||
try {
|
||||
results = await this.hybridSemanticHydrate(query, 'observation', project, platformSource, (ids) =>
|
||||
this.sessionStore.getObservationsByIds(ids, { orderBy: 'date_desc', limit, project, platformSource })
|
||||
this.sessionStore.getObservationsByIds(ids, { orderBy: 'relevance', limit, project, platformSource })
|
||||
);
|
||||
} catch (chromaError) {
|
||||
const errorObject = chromaError instanceof Error ? chromaError : new Error(String(chromaError));
|
||||
|
||||
@@ -102,6 +102,67 @@ describe('SearchManager platform-scoped Chroma hydration', () => {
|
||||
expect(result.observations).toEqual([observation]);
|
||||
});
|
||||
|
||||
it('hydrates Chroma observation matches in relevance order, not by date', async () => {
|
||||
// Chroma returns up to 100 candidates already ranked by distance. Hydrating
|
||||
// them with orderBy 'date_desc' discards that ranking and yields the N
|
||||
// newest candidates instead of the N most relevant, so an older exact match
|
||||
// loses to a newer vague one. 'relevance' preserves the caller-provided id
|
||||
// order (tests/services/sqlite/get-observations-by-ids-relevance.test.ts).
|
||||
// performChromaSemanticSearch already does this; these two paths did not.
|
||||
const olderExactMatch = 11;
|
||||
const newerVagueMatch = 22;
|
||||
const now = Date.now();
|
||||
|
||||
const makeManager = (getObservationsByIds: any) => new SearchManager(
|
||||
{
|
||||
searchObservations: mock(() => []),
|
||||
searchSessions: mock(() => []),
|
||||
searchUserPrompts: mock(() => []),
|
||||
} as any,
|
||||
{
|
||||
getObservationsByIds,
|
||||
getSessionSummariesByIds: mock(() => []),
|
||||
getUserPromptsByIds: mock(() => []),
|
||||
} as any,
|
||||
{
|
||||
queryChroma: mock(() => Promise.resolve({
|
||||
// Chroma's own order: the exact match ranks first despite being older.
|
||||
ids: [olderExactMatch, newerVagueMatch],
|
||||
distances: [0.05, 0.4],
|
||||
metadatas: [
|
||||
{ sqlite_id: olderExactMatch, doc_type: 'observation', created_at_epoch: now - 86_400_000 },
|
||||
{ sqlite_id: newerVagueMatch, doc_type: 'observation', created_at_epoch: now },
|
||||
],
|
||||
})),
|
||||
} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
);
|
||||
|
||||
const searchHydrate = mock(() => []);
|
||||
await makeManager(searchHydrate).searchObservations({ query: 'exact phrase', limit: 1 });
|
||||
expect(searchHydrate).toHaveBeenCalledWith(
|
||||
[olderExactMatch, newerVagueMatch],
|
||||
expect.objectContaining({ orderBy: 'relevance' })
|
||||
);
|
||||
|
||||
const timelineHydrate = mock(() => []);
|
||||
await makeManager(timelineHydrate).getTimelineByQuery({ query: 'exact phrase', limit: 1 });
|
||||
expect(timelineHydrate).toHaveBeenCalledWith(
|
||||
[olderExactMatch, newerVagueMatch],
|
||||
expect.objectContaining({ orderBy: 'relevance' })
|
||||
);
|
||||
|
||||
// timeline() picks a single anchor via searchChromaForTimeline; the anchor
|
||||
// should be the top-ranked match, not merely the most recent one.
|
||||
const anchorHydrate = mock(() => []);
|
||||
await makeManager(anchorHydrate).timeline({ query: 'exact phrase' });
|
||||
expect(anchorHydrate).toHaveBeenCalledWith(
|
||||
[olderExactMatch, newerVagueMatch],
|
||||
expect.objectContaining({ orderBy: 'relevance' })
|
||||
);
|
||||
});
|
||||
|
||||
it('passes platformSource into Chroma session where filter and SQLite hydration', async () => {
|
||||
const session = {
|
||||
id: 6,
|
||||
|
||||
Reference in New Issue
Block a user