Skip to content

feat(memory): enable query expansion in hybrid search mode#57711

Closed
ckybit wants to merge 2 commits into
openclaw:mainfrom
ckybit:feat/memory-hybrid-query-expansion
Closed

feat(memory): enable query expansion in hybrid search mode#57711
ckybit wants to merge 2 commits into
openclaw:mainfrom
ckybit:feat/memory-hybrid-query-expansion

Conversation

@ckybit

@ckybit ckybit commented Mar 30, 2026

Copy link
Copy Markdown

Summary

  • Problem: In hybrid search mode, the raw conversational query is passed directly to FTS, which AND-joins every token. Stop words like "what", "that", "about" must ALL appear in the indexed text for a match, causing zero keyword results for most natural-language queries (e.g. "what was that thing about API keys").
  • Why it matters: The FTS component in hybrid mode is effectively broken for conversational queries, making hybrid search degrade to vector-only in practice. This reduces recall for exact lexical matches that FTS excels at.
  • What changed: Extract meaningful keywords via extractKeywords (already used in FTS-only mode) before the hybrid keyword search. Vector search continues to receive the original query for semantic matching.
  • What did NOT change (scope boundary): FTS-only mode behavior is unchanged. Vector search path is unchanged. No config or schema changes. No new dependencies.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: When query expansion was introduced in feat: FTS fallback + query expansion for memory search #18304, it was scoped to the FTS-only fallback path. The hybrid path was not updated to benefit from keyword extraction.
  • Missing detection / guardrail: No test coverage for conversational query recall in hybrid mode.
  • Prior context: extractKeywords and stop-word lists already exist in packages/memory-host-sdk/src/host/query-expansion.ts and are imported in manager.ts, but only used in the if (!this.provider) branch.
  • Why this regressed now: It was always this way since the FTS-only expansion was added; not a regression but a missing enhancement.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
  • Target test or file: extensions/memory-core/src/memory/manager.hybrid-query-expansion.test.ts
  • Scenario the test should lock in: Conversational queries with stop words return 0 FTS results with raw query, but return correct results after keyword extraction.
  • Why this is the smallest reliable guardrail: Tests searchKeyword directly with indexed content, proving the AND-join problem and the keyword extraction fix.

User-visible / Behavior Changes

memory_search in hybrid mode now returns better keyword-side results for conversational queries. Previously, queries like "what was that thing about API keys" would produce zero FTS hits (only vector results); now the FTS component correctly matches on "API" and "keys".

Diagram (if applicable)

Before (hybrid mode):
  "what was that thing about API keys"
    → FTS: "what" AND "was" AND "that" AND "thing" AND "about" AND "API" AND "keys"
    → 0 keyword results (stop words not in indexed text)

After (hybrid mode):
  "what was that thing about API keys"
    → extractKeywords → ["API", "keys"]
    → FTS: "API" AND "keys"
    → keyword results found ✓

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS (darwin 24.6.0)
  • Runtime/container: Node.js v22.22.1
  • Model/provider: N/A (FTS path, no embedding calls)
  • Integration/channel (if any): N/A
  • Relevant config (redacted): default memory-core with hybrid enabled

Steps

  1. Index a memory file containing "Store API keys in environment variables"
  2. Search with conversational query "what was that thing about API keys"
  3. Observe keyword-side results in hybrid merge

Expected

  • Keyword search returns the indexed chunk

Actual

  • Before: 0 keyword results (AND-join of all tokens including stop words fails)
  • After: keyword results found correctly

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

New test file manager.hybrid-query-expansion.test.ts with 5 test cases covering English, Chinese, and edge cases.

Human Verification (required)

  • Verified scenarios: All 5 new tests pass; all 44 existing manager/hybrid/search tests pass; pnpm check clean; pnpm build clean.
  • Edge cases checked: Empty keyword extraction (falls back to original query); Chinese conversational queries; pure keyword queries without stop words.
  • What you did not verify: Full end-to-end with a live embedding provider and real gateway session.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: Keyword extraction may be too aggressive for some queries, reducing FTS precision.

AI-assisted PR (developed with Claude). Fully tested locally: pnpm checkpnpm buildpnpm test:extension memory-core ✅ (7 pre-existing QMD failures unrelated to this change). Author has reviewed and understands all code changes.

Made with Cursor

Query expansion (stop-word removal and keyword extraction) was only
active in the FTS-only fallback path. In hybrid mode the raw
conversational query was passed directly to FTS, which AND-joins every
token — stop words like "what", "that", "about" had to appear in the
indexed text for a match, producing zero keyword results for most
natural-language queries.

Extract keywords via `extractKeywords` before the hybrid keyword
search, matching the existing FTS-only behaviour. Vector search
continues to receive the original query for semantic matching.

Made-with: Cursor
@greptile-apps

greptile-apps Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real bug in hybrid search mode: conversational queries were passed verbatim to FTS, causing buildFtsQuery to AND-join every token — including stop words like "what", "that", "about" — which are never present in indexed content, resulting in zero keyword hits. The one-line fix applies the existing extractKeywords helper (already used in FTS-only mode) to strip stop words before the hybrid keyword search, while leaving vector search on the original query untouched.

Key observations:

  • The core fix is correct and well-motivated; the extractKeywords call mirrors what FTS-only mode already does.
  • FTS-only mode searches each extracted keyword individually then OR-merges the results; the new hybrid path AND-joins the extracted keywords via buildFtsQuery. This means multi-keyword queries where no single document contains all extracted terms will still return zero FTS hits in hybrid mode. This is defensible (vector search covers broader recall), but the asymmetry is undocumented.
  • The test named "falls back to original query when no keywords are extracted" does not test the fallback branch — it asserts keywords.length > 0 and exercises the normal extraction path. The actual fallback (all-stop-word input yielding an empty keyword array) is not covered by any test.

Confidence Score: 5/5

Safe to merge — the core fix is correct, backward-compatible, and the remaining findings are minor test quality issues.

The one-line production change in manager.ts is straightforward and directly mirrors an already-proven pattern from FTS-only mode. Both findings are P2: the AND-vs-OR semantics difference is a defensible design choice given vector search covers broad recall, and the misleading test name does not affect runtime behaviour. No data integrity, security, or primary-path correctness issues were found.

The test file manager.hybrid-query-expansion.test.ts has a mislabelled test case and a gap in fallback-path coverage, but neither blocks the merge.

Important Files Changed

Filename Overview
extensions/memory-core/src/memory/manager.ts Adds extractKeywords call before the hybrid FTS search so stop words are stripped from conversational queries, fixing the AND-join over-restriction. Core logic is correct; minor note that AND semantics over extracted keywords differ from the FTS-only mode's per-keyword OR approach.
extensions/memory-core/src/memory/manager.hybrid-query-expansion.test.ts New test file with 5 cases covering English, Chinese, and edge cases. One test is misleadingly named but exercises the wrong code path; the true fallback path (all-stop-word query) is not covered.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager.hybrid-query-expansion.test.ts
Line: 130-149

Comment:
**Misleading test name and missing fallback coverage**

The test is titled "falls back to original query when no keywords are extracted", but the body immediately asserts `expect(keywords.length).toBeGreaterThan(0)` — confirming that keywords _were_ extracted from `"API PostgreSQL"`. This means the fallback branch (`keywords.length === 0 → use cleaned`) is never exercised. There is no test for the actual fallback path, which would be triggered by a query composed entirely of stop words.

Consider splitting into two tests: one that covers AND-joining multiple extracted keywords, and one that genuinely tests the fallback by passing a query where all tokens are stop words, asserts the returned keywords array is empty, and verifies the search still returns a defined (non-throwing) result.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager.ts
Line: 383-386

Comment:
**AND-join semantics differ from FTS-only mode's per-keyword OR approach**

In FTS-only mode each keyword is searched independently and result sets are union-merged (OR semantics). In the new hybrid code, keywords are space-joined and passed to `buildFtsQuery`, which AND-joins every token (e.g. `"API keys"` becomes `"API" AND "keys"`).

This means a multi-keyword query that extracts several terms will produce zero FTS hits if no single document contains all extracted terms — whereas FTS-only mode would still surface documents matching any individual keyword.

Given that vector search covers broad semantic recall in hybrid mode this is defensible and arguably more precise, but the asymmetry compared to FTS-only mode is worth documenting in a code comment, and a test covering a multi-keyword, multi-document scenario would help lock in the intended AND-join behaviour.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(memory): enable query expansion in ..." | Re-trigger Greptile

Comment on lines +130 to +149
rows,
query: keywords.join(" "),
});
// Expanded query should return at least as many results as raw
expect(expandedResults.length).toBeGreaterThanOrEqual(rawResults.length);
});

it("falls back to original query when no keywords are extracted", async () => {
const keywords = extractKeywords("API PostgreSQL");
// Pure keywords without stop words — extractKeywords returns them as-is
expect(keywords.length).toBeGreaterThan(0);

const results = await runSearch({
rows: indexedRows,
query: keywords.join(" "),
});
// "API" AND "PostgreSQL" — only row 1 has "API" but not "PostgreSQL",
// so this may return 0. The point is the code doesn't break.
expect(results).toBeDefined();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading test name and missing fallback coverage

The test is titled "falls back to original query when no keywords are extracted", but the body immediately asserts expect(keywords.length).toBeGreaterThan(0) — confirming that keywords were extracted from "API PostgreSQL". This means the fallback branch (keywords.length === 0 → use cleaned) is never exercised. There is no test for the actual fallback path, which would be triggered by a query composed entirely of stop words.

Consider splitting into two tests: one that covers AND-joining multiple extracted keywords, and one that genuinely tests the fallback by passing a query where all tokens are stop words, asserts the returned keywords array is empty, and verifies the search still returns a defined (non-throwing) result.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager.hybrid-query-expansion.test.ts
Line: 130-149

Comment:
**Misleading test name and missing fallback coverage**

The test is titled "falls back to original query when no keywords are extracted", but the body immediately asserts `expect(keywords.length).toBeGreaterThan(0)` — confirming that keywords _were_ extracted from `"API PostgreSQL"`. This means the fallback branch (`keywords.length === 0 → use cleaned`) is never exercised. There is no test for the actual fallback path, which would be triggered by a query composed entirely of stop words.

Consider splitting into two tests: one that covers AND-joining multiple extracted keywords, and one that genuinely tests the fallback by passing a query where all tokens are stop words, asserts the returned keywords array is empty, and verifies the search still returns a defined (non-throwing) result.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1745ffe: split into two tests — "AND-joins multiple extracted keywords" and "falls back to original query when all tokens are stop words" (exercises the keywords.length === 0 branch).

Comment on lines +383 to +386
const keywords = extractKeywords(cleaned, {
ftsTokenizer: this.settings.store.fts.tokenizer,
});
const keywordQuery = keywords.length > 0 ? keywords.join(" ") : cleaned;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 AND-join semantics differ from FTS-only mode's per-keyword OR approach

In FTS-only mode each keyword is searched independently and result sets are union-merged (OR semantics). In the new hybrid code, keywords are space-joined and passed to buildFtsQuery, which AND-joins every token (e.g. "API keys" becomes "API" AND "keys").

This means a multi-keyword query that extracts several terms will produce zero FTS hits if no single document contains all extracted terms — whereas FTS-only mode would still surface documents matching any individual keyword.

Given that vector search covers broad semantic recall in hybrid mode this is defensible and arguably more precise, but the asymmetry compared to FTS-only mode is worth documenting in a code comment, and a test covering a multi-keyword, multi-document scenario would help lock in the intended AND-join behaviour.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager.ts
Line: 383-386

Comment:
**AND-join semantics differ from FTS-only mode's per-keyword OR approach**

In FTS-only mode each keyword is searched independently and result sets are union-merged (OR semantics). In the new hybrid code, keywords are space-joined and passed to `buildFtsQuery`, which AND-joins every token (e.g. `"API keys"` becomes `"API" AND "keys"`).

This means a multi-keyword query that extracts several terms will produce zero FTS hits if no single document contains all extracted terms — whereas FTS-only mode would still surface documents matching any individual keyword.

Given that vector search covers broad semantic recall in hybrid mode this is defensible and arguably more precise, but the asymmetry compared to FTS-only mode is worth documenting in a code comment, and a test covering a multi-keyword, multi-document scenario would help lock in the intended AND-join behaviour.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1745ffe: added a comment documenting the intentional AND-join semantics in hybrid mode vs the per-keyword OR approach in FTS-only mode.

- Document intentional AND-join semantics in hybrid mode vs OR in
  FTS-only mode (vector search covers broad recall).
- Split misleading "falls back" test into two: one for AND-join of
  multiple keywords, one for genuine fallback on all-stop-word input.

Made-with: Cursor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1745ffe86b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const keywords = extractKeywords(cleaned, {
ftsTokenizer: this.settings.store.fts.tokenizer,
});
const keywordQuery = keywords.length > 0 ? keywords.join(" ") : cleaned;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve numeric constraints in hybrid keyword expansion

When hybrid search now switches to keywords.join(" "), numeric-only terms can be silently dropped before FTS runs. extractKeywords filters pure numbers, so a query like error 404 becomes error (if at least one other keyword remains), and searchKeyword no longer requires 404, which broadens matches and can surface irrelevant chunks for version/error-code lookups. Before this change, the raw hybrid query kept all tokens and enforced both terms through buildFtsQuery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch. extractKeywords filtering pure numeric tokens is its existing behaviour (already active in FTS-only mode since #18304) — this PR only reuses the same function in the hybrid path. In hybrid mode, vector search still receives the original query including numeric terms, so semantic recall for error codes/versions is preserved. If we want to refine numeric handling in extractKeywords itself, that's a good candidate for a follow-up issue. Not addressing in this PR to keep scope tight.

@ckybit

ckybit commented Mar 31, 2026

Copy link
Copy Markdown
Author

CI Status Note

All CI failures on this PR are pre-existing on main — the latest main CI run also fails (and more broadly: check, build-artifacts, build-smoke all fail on main but pass on this PR).

The failures here are:

  1. extension-fast-memory-core — known QMD test flake in qmd-manager.slugified-paths.test.ts (pre-existing, not introduced by this PR)
  2. checks-node-test-* / checks-windows-node-test-* — Vitest sharding infra error (--shard <count> must be smaller than count of test files), also not related to this PR

All PR-specific checks pass cleanly: checkbuild-artifactsbuild-smokecheck-additionalsecurity-fast


@vignesh07 This PR is ready for review. It's a small, scoped fix (net +9 lines in production code) that brings keyword extraction to the hybrid search path, matching what FTS-only mode already does. All bot review feedback has been addressed. Would appreciate your review when you have a moment. Thanks!

@ckybit

ckybit commented Mar 31, 2026

Copy link
Copy Markdown
Author

Hi @vincentkoc — hope you don't mind the ping. This is a small scoped fix for the memory module (net +9 lines in production code) that brings keyword extraction to the hybrid search path. It's been open for about a day and all bot review feedback has been addressed. Would really appreciate a look when you have a moment. Thanks for all the work on memory-core!

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

This PR is a valid small fix, but it is superseded by open #39555, which now carries forward the same hybrid query-expansion behavior and broadens the fix to bounded per-keyword hybrid keyword fan-out with regression coverage. Current main does not yet implement either fix, so the best path is to keep #39555 as canonical.

Best possible solution:

Close this PR as superseded and use #39555 as the canonical implementation/review path, because it carries forward the same hybrid query-expansion intent while addressing the broader per-keyword hybrid recall bug with bounded fan-out and tests.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex review notes: model gpt-5.5, reasoning high; reviewed against f256eeba431b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant