feat(memory): add search score explanations (#5102)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
co-authored by
kartik-mem0
parent
d817aa9c12
commit
a44855af9e
+16
-4
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user