Relevant source files
The following files were used as context for generating this wiki page:
- mem0-ts/src/oss/src/memory/index.ts
- mem0/memory/main.py
- mem0/utils/scoring.py
- integrations/openclaw/recall.ts
- integrations/claude-code-plugin/core/memory_core.py
- integrations/agent-plugin-core/python/memory_core.py
- integrations/codex-plugin/core/memory_core.py
- integrations/antigravity-plugin/core/memory_core.py
- integrations/kimi-plugin/core/memory_core.py
- integrations/cursor-plugin/core/memory_core.py
- integrations/mem0-agent-plugin/core/memory_core.py
- mem0-ts/src/oss/src/utils/scoring.ts
- mem0/proxy/main.py
- mem0-ts/src/oss/src/vector_stores/memory.ts
- mem0-ts/src/oss/src/vector_stores/qdrant.ts
- integrations/hermes-plugin-mem0/init.py
- mem0-ts/src/oss/src/utils/bm25.ts
- mem0/vector_stores/valkey.py
- mem0/reranker/huggingface_reranker.py
- mem0/vector_stores/vertex_ai_vector_search.py
- mem0/vector_stores/redis.py
- mem0/configs/prompts.py
- mem0/vector_stores/milvus.py
- mem0-ts/src/oss/src/vector_stores/milvus.ts
- mem0/reranker/llm_reranker.py
- mem0/reranker/zero_entropy_reranker.py
- mem0/utils/lemmatization.py
Overview
Multi-Signal Retrieval combines vector similarity, BM25 keyword matching, entity boosting, and threshold filtering to surface relevant memories. By integrating multiple scoring dimensions, the retrieval engine resolves semantic nuance that single-pass vector search often misses.
Sources: mem0/memory/main.py:1648-1701, mem0/utils/scoring.py:60-139
Hybrid Search Architecture
BM25 Parameter Selection and Score Normalization
Query-length-adaptive sigmoid parameters are retrieved via get_bm25_params, which adjusts midpoint and steepness based on the term count of the lemmatized query to account for higher raw BM25 score distributions in longer inputs.
def get_bm25_params(query: str, *, lemmatized: Optional[str] = None) -> tuple:
"""Get BM25 sigmoid parameters based on query length.
Longer queries tend to have higher raw BM25 scores, so we adjust
the sigmoid midpoint and steepness accordingly.
Returns:
(midpoint, steepness) for sigmoid normalization.
"""
if lemmatized is None:
from mem0.utils.lemmatization import lemmatize_for_bm25
lemmatized = lemmatize_for_bm25(query)
num_terms = len(lemmatized.split()) if lemmatized else 1
if num_terms <= 3:
return 5.0, 0.7
elif num_terms <= 6:
return 7.0, 0.6
elif num_terms <= 9:
return 9.0, 0.5
elif num_terms <= 15:
return 10.0, 0.5
else:
return 12.0, 0.5
Sources: mem0/utils/scoring.py:16-41
Raw unbounded BM25 scores are converted to a normalized range of [0, 1] via a logistic sigmoid function using normalize_bm25.
def normalize_bm25(raw_score: float, midpoint: float, steepness: float) -> float:
"""Normalize BM25 score to [0, 1] using logistic sigmoid.
Args:
raw_score: Raw BM25 score (unbounded, typically 0-20+).
midpoint: Score at which sigmoid outputs 0.5.
steepness: Controls how quickly sigmoid transitions.
Returns:
Normalized score in range [0, 1].
"""
return 1.0 / (1.0 + math.exp(-steepness * (raw_score - midpoint)))
Sources: mem0/utils/scoring.py:43-55
Additive Scoring and Threshold Gates
Candidate memories are ranked and combined in score_and_rank. The threshold parameter gates the semantic score prior to combining signals, excluding candidates below the threshold even if lexical or entity boosts would otherwise raise them.
def score_and_rank(
semantic_results: List[Dict[str, Any]],
bm25_scores: Dict[str, float],
entity_boosts: Dict[str, float],
threshold: float,
top_k: int,
explain: bool = False,
) -> List[Dict[str, Any]]:
"""Score candidates additively and return top-k results.
For each candidate:
semantic_score is taken from the result's score field.
combined = (semantic + bm25 + entity_boost) / max_possible
Threshold gates the semantic score BEFORE combining -- candidates
below the threshold are excluded even if BM25/entity would boost them.
The divisor adapts based on which signals are active:
- Semantic only: max_possible = 1.0
- Semantic + BM25: max_possible = 2.0
- Semantic + BM25 + entity: max_possible = 2.5
- Semantic + entity (no BM25): max_possible = 1.5
Args:
semantic_results: Candidate memories from vector search.
bm25_scores: Normalized keyword scores keyed by memory ID.
entity_boosts: Entity-link boosts keyed by memory ID.
threshold: Minimum semantic score required before hybrid scoring.
top_k: Maximum number of results to return.
explain: Include score_details in each result when true.
Returns:
List of scored result dicts sorted by combined score descending.
"""
has_bm25 = bool(bm25_scores)
has_entity = bool(entity_boosts)
max_possible = 1.0
if has_bm25:
max_possible += 1.0
if has_entity:
max_possible += ENTITY_BOOST_WEIGHT
Sources: mem0/utils/scoring.py:60-102
The divisor (max_possible) adapts dynamically depending on which active signals contribute to the score calculation:
| Active Signals | Max Possible Divisor (max_possible) |
|---|---|
| Semantic only | 1.0 |
| Semantic + BM25 | 2.0 |
| Semantic + entity (no BM25) | 1.5 |
| Semantic + BM25 + entity | 2.5 |
Sources: mem0/utils/scoring.py:77-101
Vector Store Delegation and Chat Retrieval
Vector search backends such as GoogleMatchingEngine handle similarity searches by embedding the query vector and executing search, subsequently wrapping payloads into Document and score pairs.
def similarity_search_with_score(
self,
query: str,
k: int = 5,
filter: Optional[Dict] = None,
) -> List[Tuple[Document, float]]:
"""Return documents most similar to query with scores."""
logger.debug("Starting similarity search with score")
logger.debug("Query: %s", query)
logger.debug("k: %d", k)
logger.debug("Filter: %s", filter)
embedding = self.embedder.embed_query(query)
results = self.search(query=query, vectors=embedding, top_k=k, filters=filter)
docs_and_scores = [
(Document(page_content=result.payload.get("text", ""), metadata=result.payload), result.score)
for result in results
]
logger.debug("Found %d results", len(docs_and_scores))
return docs_and_scores
Sources: mem0/vector_stores/vertex_ai_vector_search.py:609-630
Note
GoogleMatchingEngine explicitly does not support index reset operations, logging a warning and returning immediately when reset is called.
def reset(self):
"""
Reset the Google Matching Engine index.
"""
logger.warning("Reset operation is not supported for Google Matching Engine")
passSources: mem0/vector_stores/vertex_ai_vector_search.py:642-647
Chat proxy completions utilize _fetch_relevant_memories to pass the last 6 message turns from the conversation history into the client's search API.
def _fetch_relevant_memories(self, messages, user_id, agent_id, run_id, filters, top_k):
# Currently, only pass the last 6 messages to the search API to prevent long query
message_input = [f"{message['role']}: {message['content']}" for message in messages][-6:]
# TODO: Make it better by summarizing the past conversation
return self.mem0_client.search(
query="\n".join(message_input),
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
filters=filters,
top_k=top_k,
)Sources: mem0/proxy/main.py:163-174
Lexical Lemmatization Pipeline
spaCy-Based Text Normalization
Query strings and payload texts are processed by lemmatize_for_bm25 in mem0/utils/lemmatization.py to standardize token forms for BM25 matching. The pipeline loads a spaCy model via get_nlp_lemma and converts the input text to lowercase before tokenization.
def lemmatize_for_bm25(text: str) -> str:
"""Lemmatize text for BM25 matching.
Returns space-joined lemmas for full-text search. Falls back to
the original text if spaCy is unavailable.
"""
from mem0.utils.spacy_models import get_nlp_lemma
nlp = get_nlp_lemma()
if nlp is None:
return text
doc = nlp(text.lower())Sources: mem0/utils/lemmatization.py:22-34
Tokens identified as punctuation (token.is_punct) or stopwords (token.is_stop) are filtered out. For remaining tokens, the lemmatizer extracts token.lemma_ and verifies that it is alphanumeric via lemma.isalnum().
Note
If the spaCy lemma model returns None, lemmatize_for_bm25 skips tokenization entirely and returns the raw input text unmodified.
if nlp is None:
return textSources: mem0/utils/lemmatization.py:31-32
Ambiguity Handling for -ing Forms
To prevent context-dependent lemmatization discrepancies (such as treating "meeting" as a noun versus a verb), the pipeline inspects the raw token text. If token.text ends with "ing", differs from the computed lemma, and is alphanumeric, the original surface form is appended to the token list alongside its lemma.
# This handles noun/verb ambiguity (meeting/meet, attending/attend).
if token.text.endswith("ing") and token.text != lemma and token.text.isalnum():
tokens.append(token.text)Sources: mem0/utils/lemmatization.py:46-48
The resulting filtered tokens are joined with single spaces and returned as a unified string for full-text keyword indexing and search evaluation.
return " ".join(tokens)Sources: mem0/utils/lemmatization.py:50-50
BM25 Scoring and Normalization
Query-Length-Adaptive Sigmoid Parameters
Raw BM25 scores are unbounded and typically range from zero past twenty depending on corpus characteristics and term frequency distributions. To scale these lexical values into a standardized range, Mem0 applies a logistic sigmoid normalization function. Because longer search queries inherently accumulate higher raw term frequency scores, the engine adapts its normalization midpoint and steepness dynamically based on the term count of the lemmatized query string.
def get_bm25_params(query: str, *, lemmatized: Optional[str] = None) -> tuple:
"""Get BM25 sigmoid parameters based on query length.
Longer queries tend to have higher raw BM25 scores, so we adjust
the sigmoid midpoint and steepness accordingly.
Returns:
(midpoint, steepness) for sigmoid normalization.
"""
if lemmatized is None:
from mem0.utils.lemmatization import lemmatize_for_bm25
lemmatized = lemmatize_for_bm25(query)
num_terms = len(lemmatized.split()) if lemmatized else 1
if num_terms <= 3:
return 5.0, 0.7
elif num_terms <= 6:
return 7.0, 0.6
elif num_terms <= 9:
return 9.0, 0.5
elif num_terms <= 15:
return 10.0, 0.5
else:
return 12.0, 0.5
Sources: mem0/utils/scoring.py:16-41
The query term count determines the sigmoid configuration thresholds according to specific length boundaries:
Query Term Count (num_terms) | Midpoint | Steepness |
|---|---|---|
1 to 3 | 5.0 | 0.7 |
4 to 6 | 7.0 | 0.6 |
7 to 9 | 9.0 | 0.5 |
10 to 15 | 10.0 | 0.5 |
Greater than 15 | 12.0 | 0.5 |
Sources: mem0/utils/scoring.py:31-40
Note
If the optional lemmatized query parameter is omitted when calling get_bm25_params, the function invokes lemmatize_for_bm25 directly on the raw query string to compute term lengths accurately.
if lemmatized is None:
from mem0.utils.lemmatization import lemmatize_for_bm25
lemmatized = lemmatize_for_bm25(query)Sources: mem0/utils/scoring.py:25-28
Logistic Sigmoid Transformation
Once the midpoint and steepness parameters are resolved for a given query, normalize_bm25 transforms the raw score into a bounded interval between zero and one using the logistic function formula.
def normalize_bm25(raw_score: float, midpoint: float, steepness: float) -> float:
"""Normalize BM25 score to [0, 1] using logistic sigmoid.
Args:
raw_score: Raw BM25 score (unbounded, typically 0-20+).
midpoint: Score at which sigmoid outputs 0.5.
steepness: Controls how quickly sigmoid transitions.
Returns:
Normalized score in range [0, 1].
"""
return 1.0 / (1.0 + math.exp(-steepness * (raw_score - midpoint)))Sources: mem0/utils/scoring.py:43-54
def normalize_bm25(raw_score: float, midpoint: float, steepness: float) -> float:
return 1.0 / (1.0 + math.exp(-steepness * (raw_score - midpoint)))Sources: mem0/utils/scoring.py:43-54
Entity Boosting Mechanisms
Entity Weight Configuration and Additive Scoring
Candidate memories passing the semantic threshold can be reinforced through entity-linking boosts keyed by memory identifier strings. The scoring engine defines a constant weight modifier for entity associations, setting ENTITY_BOOST_WEIGHT to 0.5 to calibrate how strongly matched entity links influence the final aggregated score alongside semantic and BM25 signals.
ENTITY_BOOST_WEIGHT = 0.5Sources: mem0/utils/scoring.py:57-57
During the score_and_rank execution, the engine dynamically determines the normalization divisor (max_possible) based on which active signals are present in the current retrieval pass. This divisor ensures that combined scores remain bounded correctly between zero and one regardless of whether lexical or entity signals are enabled.
| Active Signals | Divisor (max_possible) Formula | Maximum Possible Score |
|---|---|---|
| Semantic only | 1.0 | 1.0 |
| Semantic + BM25 | 1.0 + 1.0 | 2.0 |
| Semantic + Entity | 1.0 + ENTITY_BOOST_WEIGHT | 1.5 |
| Semantic + BM25 + Entity | 1.0 + 1.0 + ENTITY_BOOST_WEIGHT | 2.5 |
Sources: mem0/utils/scoring.py:77-82, mem0/utils/scoring.py:97-102
Warning
The threshold check is applied strictly to the vector search semantic_score before hybrid combining takes place. Candidates falling below the configured threshold are discarded immediately, preventing BM25 or entity boosts from rescuing low-similarity memories.
Threshold gates the semantic score BEFORE combining -- candidates
below the threshold are excluded even if BM25/entity would boost them.Sources: mem0/utils/scoring.py:74-75
Explanation and Score Auditing
When the explain flag is enabled on score_and_rank, each returned result dictionary includes a score_details sub-dictionary that breaks down individual component contributions. This audit dictionary exposes the exact intermediate values computed during entity boosting and normalization.
if explain:
scored_result["score_details"] = {
"semantic_score": semantic_score,
"bm25_score": bm25_score,
"entity_boost": entity_boost,
"raw_score": raw_combined,
"max_possible_score": max_possible,
"final_score": combined,
"threshold": threshold,
}Sources: mem0/utils/scoring.py:126-135
scored_result["score_details"] = {
"semantic_score": semantic_score,
"bm25_score": bm25_score,
"entity_boost": entity_boost,
"raw_score": raw_combined,
"max_possible_score": max_possible,
"final_score": combined,
"threshold": threshold,
}Sources: mem0/utils/scoring.py:127-134
Tip
The final combined score is capped at 1.0 using min(raw_combined / max_possible, 1.0) to guarantee that score distributions do not violate upper bounds when multiple signals peak simultaneously.
raw_combined = semantic_score + bm25_score + entity_boost
combined = min(raw_combined / max_possible, 1.0)Sources: mem0/utils/scoring.py:118-119
Temporal Decay and Recency Weighting
Additive Hybrid Scoring Components
The hybrid scoring architecture in mem0/utils/scoring.py combines semantic similarity scores from vector search with normalized BM25 keyword matching scores and entity-linking boosts. Each candidate memory's identifier is checked against active signal dictionaries to aggregate raw component values before normalization.
mem_id_str = str(mem_id)
bm25_score = bm25_scores.get(mem_id_str, 0.0)
entity_boost = entity_boosts.get(mem_id_str, 0.0)
raw_combined = semantic_score + bm25_score + entity_boostSources: mem0/utils/scoring.py:114-118
Note
Candidate memory entries lacking an id field or failing to meet the minimum semantic threshold are excluded from score aggregation entirely, regardless of their auxiliary keyword or entity boost values.
mem_id = result.get("id")
if mem_id is None:
continue
semantic_score = result.get("score") or 0.0
if semantic_score < threshold:
continueSources: mem0/utils/scoring.py:106-112
Score Auditing and Explanation Details
When explain=True is passed to score_and_rank, the resulting dictionary contains a comprehensive breakdown of every individual contribution and scaling factor used during score computation.
if explain:
scored_result["score_details"] = {
"semantic_score": semantic_score,
"bm25_score": bm25_score,
"entity_boost": entity_boost,
"raw_score": raw_combined,
"max_possible_score": max_possible,
"final_score": combined,
"threshold": threshold,
}Sources: mem0/utils/scoring.py:126-135
Multi-Signal Rank Fusion
Final Score Aggregation and Ranking
The score_and_rank function aggregates the individual components by summing the semantic similarity score, the normalized BM25 keyword score, and the entity boost. The raw combined sum is divided by the dynamic max_possible denominator and clamped to an upper bound of 1.0.
raw_combined = semantic_score + bm25_score + entity_boost
combined = min(raw_combined / max_possible, 1.0)Sources: mem0/utils/scoring.py:118-119
Once all valid candidates are processed, the collection of scored result dictionaries is sorted in descending order by their final normalized score, and truncated to the requested top_k limit.
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]Sources: mem0/utils/scoring.py:138-139
Caution
Candidates that do not contain an id attribute are skipped silently during iteration, and those falling below the configured similarity threshold are filtered out before combining occurs.
mem_id = result.get("id")
if mem_id is None:
continue
semantic_score = result.get("score") or 0.0
if semantic_score < threshold:
continueSources: mem0/utils/scoring.py:106-112
Related
On This Page
OverviewHybrid Search ArchitectureBM25 Parameter Selection and Score NormalizationAdditive Scoring and Threshold GatesVector Store Delegation and Chat RetrievalLexical Lemmatization PipelinespaCy-Based Text NormalizationAmbiguity Handling for -ing FormsBM25 Scoring and NormalizationQuery-Length-Adaptive Sigmoid ParametersLogistic Sigmoid TransformationEntity Boosting MechanismsEntity Weight Configuration and Additive ScoringExplanation and Score AuditingTemporal Decay and Recency WeightingAdditive Hybrid Scoring ComponentsScore Auditing and Explanation DetailsMulti-Signal Rank FusionFinal Score Aggregation and RankingRelated