feat(memory): add search score explanations (#5102)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Chirag Arora
2026-06-05 21:30:10 +05:30
committed by GitHub
co-authored by kartik-mem0
parent d817aa9c12
commit a44855af9e
11 changed files with 220 additions and 14 deletions
+16 -4
View File
@@ -1132,6 +1132,7 @@ class Memory(MemoryBase):
filters: Optional[Dict[str, Any]] = None,
threshold: float = 0.1,
rerank: bool = False,
explain: bool = False,
**kwargs,
):
"""
@@ -1162,6 +1163,7 @@ class Memory(MemoryBase):
- {"NOT": [filter1]} - logical NOT
threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -1221,11 +1223,12 @@ class Memory(MemoryBase):
"encoded_ids": encoded_ids,
"sync_type": "sync",
"threshold": threshold,
"explain": explain,
"advanced_filters": bool(filters and self._has_advanced_operators(filters)),
},
)
original_memories = self._search_vector_store(query, effective_filters, limit, threshold)
original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
# Apply reranking if enabled and reranker is available
if rerank and self.reranker and original_memories:
@@ -1341,7 +1344,7 @@ class Memory(MemoryBase):
return True
return False
def _search_vector_store(self, query, filters, limit, threshold=0.1):
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
# Guard against None threshold (backward compat)
if threshold is None:
threshold = 0.1
@@ -1396,6 +1399,7 @@ class Memory(MemoryBase):
entity_boosts=entity_boosts,
threshold=threshold,
top_k=limit,
explain=explain,
)
# Step 9: Format results
@@ -1433,6 +1437,8 @@ class Memory(MemoryBase):
if not memory_item_dict.get("metadata"):
memory_item_dict["metadata"] = {}
memory_item_dict["metadata"].update(additional_metadata)
if explain and "score_details" in scored:
memory_item_dict["score_details"] = scored["score_details"]
original_memories.append(memory_item_dict)
@@ -2568,6 +2574,7 @@ class AsyncMemory(MemoryBase):
filters: Optional[Dict[str, Any]] = None,
threshold: float = 0.1,
rerank: bool = False,
explain: bool = False,
**kwargs,
):
"""
@@ -2598,6 +2605,7 @@ class AsyncMemory(MemoryBase):
- {"NOT": [filter1]} - logical NOT
threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -2659,11 +2667,12 @@ class AsyncMemory(MemoryBase):
"encoded_ids": encoded_ids,
"sync_type": "async",
"threshold": threshold,
"explain": explain,
"advanced_filters": bool(filters and self._has_advanced_operators(filters)),
},
)
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold)
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
# Apply reranking if enabled and reranker is available
if rerank and self.reranker and original_memories:
@@ -2782,7 +2791,7 @@ class AsyncMemory(MemoryBase):
return True
return False
async def _search_vector_store(self, query, filters, limit, threshold=0.1):
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
if threshold is None:
threshold = 0.1
@@ -2836,6 +2845,7 @@ class AsyncMemory(MemoryBase):
entity_boosts=entity_boosts,
threshold=threshold,
top_k=limit,
explain=explain,
)
# Step 9: Format results
@@ -2872,6 +2882,8 @@ class AsyncMemory(MemoryBase):
if not memory_item_dict.get("metadata"):
memory_item_dict["metadata"] = {}
memory_item_dict["metadata"].update(additional_metadata)
if explain and "score_details" in scored:
memory_item_dict["score_details"] = scored["score_details"]
original_memories.append(memory_item_dict)
+24 -6
View File
@@ -63,6 +63,7 @@ def score_and_rank(
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.
@@ -79,6 +80,14 @@ def score_and_rank(
- 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.
"""
@@ -109,13 +118,22 @@ def score_and_rank(
raw_combined = semantic_score + bm25_score + entity_boost
combined = min(raw_combined / max_possible, 1.0)
scored.append(
{
"id": mem_id_str,
"score": combined,
"payload": result.get("payload"),
scored_result = {
"id": mem_id_str,
"score": combined,
"payload": result.get("payload"),
}
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,
}
)
scored.append(scored_result)
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]