Feat: Add Hybrid Search (Vector + FTS) - #1475
Conversation
## Change
- range_index_creation_threshold: 10,000 → 1,000
- vector_index_creation_threshold: 10,000 → 1,000
## Reason
Neo4j vector and range indexes are now created at 1,000 nodes instead of
10,000, enabling index-based search for smaller collections. This is
particularly beneficial for:
1. Test environments with limited data (e.g., test datasets with ~5,000 nodes)
2. Development environments where early index creation aids debugging
3. Production workloads with many small sessions that benefit from
indexed queries
4. FTS (Full-Text Search) index creation, which is triggered alongside
range index creation for Derivative collections
## Files Changed
- packages/server/src/memmachine_server/common/vector_graph_store/neo4j_vector_graph_store.py
- Neo4jVectorGraphStoreParams.range_index_creation_threshold
- Neo4jVectorGraphStoreParams.vector_index_creation_threshold
Signed-off-by: Jeoungahn Park <arsd098@gmail.com>
## Change
- Add `_create_fts_index_if_not_exists()` method for FTS index creation
- FTS index is automatically created when collection exceeds threshold (1,000 nodes)
- FTS index targets `content` property on all NODE collections
- Index creation runs in background using `asyncio.create_task()`
## Reason
Full-Text Search enables efficient text-based retrieval on node content.
FTS indexes are created alongside range indexes for all NODE collections,
allowing text search queries to scale with data growth. This is particularly
beneficial for:
1. Text-based retrieval queries that don't use vector embeddings
2. Hybrid search scenarios combining FTS with vector similarity search
3. Development and test environments where quick text search is needed
## Files Changed
- packages/server/src/memmachine_server/common/vector_graph_store/neo4j_vector_graph_store.py
- Added `_create_fts_index_if_not_exists()` method
- Added FTS index creation call in `add_nodes()` method (EntityType.NODE)
- FTS index naming: `fts_{sanitized_collection}_content`
- FTS analyzer: `standard`
Signed-off-by: Jeoungahn Park <arsd098@gmail.com>
## Change
- Add `use_fts` boolean parameter to `search_scored()` API endpoint
- Implement `_search_scored_hybrid()` for Vector + FTS hybrid search
- Add `_search_fts()` method for Neo4j Full-Text Search on Derivative nodes
- Add `_escape_lucene_query()` static method for Lucene special character escaping
- Hybrid search returns Vector results (top-k) + FTS results (up to 10, deduped)
- FTS follows same path as Vector Search: Derivative → DERIVED_FROM → Episode
## Reason
Hybrid Search combines vector similarity search with keyword-based full-text
search to improve retrieval quality. The API flag allows clients to
opt-in to hybrid search without changing query patterns. This addresses:
1. Keyword match queries where vector similarity fails (exact term matching)
2. Hybrid retrieval scenarios benefiting from both semantic and lexical search
3. Flexible API design allowing Vector-only or Hybrid mode via single flag
FTS integration uses existing FTS indexes on Derivative collections, ensuring
consistent behavior with vector search (same DERIVED_FROM relation traversal,
same Episode.uid for dedup). Deduplication preserves Vector result order and
appends unique FTS results, returning top-k + (0~10) episodes.
## Files Changed
- packages/common/src/memmachine_common/api/spec.py
- packages/server/src/memmachine_server/server/api_v2/service.py
- packages/server/src/memmachine_server/main/memmachine.py
- packages/server/src/memmachine_server/episodic_memory/episodic_memory.py
- packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py
- Added `use_fts` parameter to `search_scored()` method (line 258)
- Added `_search_scored_hybrid()` method for hybrid search orchestration
- Added `_search_fts()` method for FTS-only search on Derivative nodes
- Added `_escape_lucene_query()` static method for query escaping
- FTS index name: `fts_{sanitized_derivative_collection}_content`
- Dedup strategy: UID-based, preserves Vector result order
Signed-off-by: Jeoungahn Park <arsd098@gmail.com>
## Change - Add `use_fts: bool = False` to `QueryParam` in `agent_api.py` - Pass `use_fts=query.use_fts` to `query_memory()` in `memmachine_retriever.py` ## Reason Enables retrieval-agent to support hybrid search (Vector + FTS) via the existing `use_fts` flag. Agents can now opt-in to hybrid search without code changes — just pass `use_fts=True` in QueryParam. ## Files Changed - packages/server/src/memmachine_server/retrieval_agent/common/agent_api.py - packages/server/src/memmachine_server/retrieval_agent/agents/memmachine_retriever.py Signed-off-by: Jeoungahn Park <arsd098@gmail.com>
|
We will retire the neo4j based episodic memory implementation. The new implementation is based on vector store and sql DB, the code is under https://github.com/MemMachine/MemMachine/tree/main/packages/server/src/memmachine_server/episodic_memory/event_memory. Can you also add the FTS to the new implementation? |
I think a text index with the new system would require a new ABC or interface. It would not be appropriate to add text search functionality to the VectorStore/Collection API. |
|
Thank you for the thoughtful feedback(@malatewang @edwinyyyu). |
The embedder was hardcoded to OpenAI text-embedding-3-small at 1536 dims, so reproducing benchmarks run under a different embedder (e.g. Qwen3-Embedding-4B at 2560 dims, per PR MemMachine#1475) was impossible. Add --embedding-model / --embedding-dimensions / --embedding-base-url to both ingest and search, and give the embedder its own OpenAI-compatible client keyed by EMBEDDING_API_KEY (falling back to OPENAI_API_KEY), so embeddings can come from a hosted Qwen endpoint while answer generation and the judge keep using OpenAI. Defaults reproduce the previous behavior exactly. The model+dimensions flow into the cache signature, so switching embedders yields a fresh cache with no collisions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: RonNiles <ron.niles@memverge.com>
To A/B our RRF fusion against PR MemMachine#1475's original method on identical data, add a `fusion` option to LongTermMemory.search_scored (default "rrf", unchanged behavior). "append" restores the pre-RRF logic — extracted into _search_scored_hybrid_append — which keeps the vector list and appends the FTS top-10 (dedup by uid), so FTS augments rather than competes for slots and never displaces a vector hit. Expose it via a --fusion flag on the search harness. This isolates the fusion-method variable, which the harness makes material: it feeds all returned episodes to the answer model, so append (100 vector + 10 FTS) adds context whereas RRF (re-ranked, truncated to 100) can drop vector hits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: RonNiles <ron.niles@memverge.com> Signed-off-by: RonNiles <28164584+RonNiles@users.noreply.github.com>
|
First let's get CI to pass. Please review the attached gist, which covers most of the issues, and apply it if you find it satisfactory: https://gist.github.com/RonNiles/b299f1826d58a0f33ab91ca01a53b479 |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thanks @RonNiles.
For this PR, we could move forward with option 1 first, with option 2 as our final development target. Would you agree with this approach? |
I think #1 is the best way because it limits the scope of this PR and the refactoring and expanded backend support required by #2 is best done in a separate PR. Also, we will not retire neo4j right away, and legacy support of neo4j should be possible into the future because many users monitor the neo4j database directly for things like determining whether ingestion is complete before beginning search, so we don't want to break those use cases. Here is my wish list for the rest of the PR:
|
Purpose of the change
This PR introduces Hybrid Search capability to MemMachine, combining vector similarity search with keyword-based Full-Text Search (FTS) to significantly improve retrieval accuracy. The implementation addresses the fundamental limitation of pure vector search where exact keyword matches (error messages, model names, technical terms, identifiers) are often missed due to semantic embedding dilution.
This PR enables clients to opt-in to hybrid retrieval via a simple use_fts flag without changing query patterns.
Description
1. API Extension - Added
use_fts: boolparameter across the API stack to enable hybrid search mode. Defaultfalsemaintains backward compatibility.2. FTS Index Setup - FTS indexes are auto-created during ingest. The index name follows the pattern
fts_{sanitized_derivative_collection}_contentand is indexed on the Derivative node's content field. No manual index creation is required; the index is ready for use when hybrid search is enabled.3. FTS Search Implementation - New
_search_fts()method (packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py:363-460) executes Neo4j Full-Text Search queries on Derivative nodes usingdb.index.fulltext.queryNodes(). It follows theDERIVED_FROMrelation to retrieve Episode nodes, returning FTS scores and Episode objects with metadata extracted from Neo4j.4. Hybrid Search Orchestration - New
_search_scored_hybrid()method (packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py:295-349) runs Vector Search and FTS in parallel viaasyncio.gather()(no latency overhead). Results are merged with UID-based deduplication: Vector results (top-k, RRF-reranked) are preserved in order, and unique FTS results (top-10) are appended. Returns up totop-k + 10episodes.5. Query Escaping Utility - Static method
_escape_lucene_query()(packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py:351-361) escapes Lucene special characters (+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /) to prevent query syntax errors on user input containing special characters.Motivation: Vector search excels at semantic similarity but struggles with exact keyword matches (error messages like
CUDA_ERROR_OUT_OF_MEMORY, model names likeRTX-4090, acronyms likeAPI/HTTP, function/class names). FTS complements vector search by providing precise term matching.Dependencies: No new external dependencies. Uses existing Neo4j FTS indexes (auto-created on ingest), asyncio, and vector search infrastructure.
Summary of Changes
This PR adds Hybrid Search functionality by integrating Neo4j Full-Text Search (FTS) indexes with existing vector search infrastructure. The key changes include:
Fixes/Closes
N/A (New feature)
Type of change
[Please delete options that are not relevant.]
How Has This Been Tested?
Test Environment:
longmemeval_m_cleaned.json[Please delete options that are not relevant.]
Usage Example:
When data is ingested, FTS indexes are automatically created. To enable hybrid search, pass
use_fts=Truein theQueryParamwhen calling the search function:Test Results: [Attach logs, screenshots, or relevant output]
Key Findings:
Checklist
[Please delete options that are not relevant.]
Maintainer Checklist
Screenshots/Gifs
N/A (Backend feature - no UI changes)
Further comments
Known Limitations: