Skip to content

Hybrid Search

This document explains AutoMem’s hybrid search system, which combines semantic, lexical, graph, temporal, and metadata signals to retrieve and rank memories. Current canonical benchmark claims are 87.00% on LongMemEval full with 97.00% recall@5, and 84.74% on LoCoMo full. See Benchmarks for the experiment log and methodology links.

For information about the memory structure being searched, see Memory Model. For details on relationship types used in graph traversal, see Relationship Types. For API usage, see Recall Operations.


AutoMem’s recall system combines results from two complementary data stores and applies multi-dimensional scoring to rank results by relevance:

sequenceDiagram
    participant Client
    participant API as "/recall endpoint"
    participant Qdrant as "Qdrant Vector DB"
    participant Falkor as "FalkorDB Graph"
    participant Scorer as "_compute_metadata_score()"

    Client->>API: "GET /recall?query=X&tags=Y"

    par Vector Search
        API->>Qdrant: "search(embedding, limit)"
        Qdrant-->>API: "Similar vectors with scores"
    and Keyword Search
        API->>Falkor: "MATCH WHERE content CONTAINS X"
        Falkor-->>API: "Keyword matches with scores"
    end

    API->>API: "Merge and deduplicate results"

    loop For each result
        API->>Scorer: "compute score with all factors"
        Scorer-->>API: "Weighted final score"
    end

    API->>API: "Sort by final score DESC"
    API->>Falkor: "Fetch relationships for top results"
    Falkor-->>API: "Related memory edges"

    API-->>Client: "Ranked results with context"

graph TB
    Query["Search Query"]

    subgraph "Search Strategies"
        Vector["Vector Similarity<br/>Qdrant cosine search<br/>Weight: 0.35"]
        Keyword["Keyword Match<br/>FalkorDB text search<br/>Weight: 0.35"]
        Metadata["Metadata Field Match<br/>match_type=metadata results<br/>Weight: 0.35"]
        Tag["Tag Overlap<br/>Prefix/exact matching<br/>Weight: 0.20"]
        Importance["Importance Score<br/>User-assigned value<br/>Weight: 0.10"]
        Confidence["Confidence Score<br/>Classification confidence<br/>Weight: 0.05"]
        Recency["Recency Score<br/>Configurable decay curve<br/>Weight: 0.10"]
        Exact["Exact Match<br/>Query in metadata<br/>Weight: 0.20"]
    end

    Result["Final Weighted Score"]

    Query --> Vector
    Query --> Keyword
    Query --> Metadata
    Query --> Tag
    Query --> Importance
    Query --> Confidence
    Query --> Recency
    Query --> Exact

    Vector --> Result
    Keyword --> Result
    Metadata --> Result
    Tag --> Result
    Importance --> Result
    Confidence --> Result
    Recency --> Result
    Exact --> Result

Vector search uses Qdrant to find memories with semantically similar embeddings. Query text is converted to a vector using the configured embedding provider, then a cosine similarity search retrieves the top candidates.

The _vector_search() function handles both explicit embeddings (passed by client) and on-demand generation. If Qdrant is unavailable, the system gracefully degrades to graph-only mode.

Key behaviors:

  • Over-fetches vector candidates (RECALL_VECTOR_OVERFETCH, default 4×, capped by RECALL_VECTOR_FETCH_CAP=200) before hybrid re-ranking so high-importance or exact-match memories are not cut on raw cosine similarity alone; response size is still trimmed to limit
  • Excludes internal artifact types listed in RECALL_EXCLUDED_TYPES (default MetaPattern) from ranked results
  • Returns empty list if no query text and no explicit embedding provided
  • Applies tag filters via _build_qdrant_tag_filter()
  • Deduplicates results using seen_ids set
  • Attaches relations by calling fetch_relations() for each hit

Keyword search performs text matching in FalkorDB’s graph store using Cypher queries. The system extracts keywords from the query, searches content and tags, and assigns scores based on match frequency.

The keyword scoring algorithm rewards both individual keyword matches and phrase matches:

Match TypeLocationScore
Single keywordcontent field+2
Single keywordtags array+1
Full phrasecontent field+2
Full phrasetags array+1

Fallback behavior: If the normalized query is empty or "*", the system calls _graph_trending_results() to return high-importance memories sorted by the sort parameter (time_asc, time_desc, updated_asc, updated_desc, or default importance ordering).

Graph traversal leverages FalkorDB’s typed relationship edges to find connected memories. This enables multi-hop reasoning and bridge discovery.

The fetch_relations() helper queries all relationships for a given memory and returns a list of related memory summaries. Each relationship includes:

  • Target memory ID and summary
  • Relationship type (e.g., "PREFERS_OVER")
  • Strength value (metadata field on relationship edge)

Multi-hop patterns:

  1. Direct relations — Single-hop from seed memory
  2. Bridges — Memories that connect two or more seed results
  3. Entity expansion — Following entity:<type>:<slug> tags to related memories

Temporal scoring boosts memories that align with time-based query constraints. The system supports both absolute time ranges and relative expressions.

Supported time expressions:

  • Relative: last hour, last day, last week, last month, last year
  • Relative: this hour, this day, this week, this month, this year
  • Relative: next hour, next day, next week, next month, next year
  • Absolute: before 2025-02-01, after 2025-01-15
  • Range: between 2025-01-15 and 2025-01-20
  • Range: 2025-01-15 to 2025-01-20

Tag filters support both exact matching and prefix matching for hierarchical tag namespaces. The system normalizes tags to lowercase and computes prefixes for efficient filtering.

Tag prefix system: AutoMem automatically computes tag prefixes for efficient hierarchical filtering. For example, the tag "entity:people:sarah" generates prefixes: ["entity", "entity:people", "entity:people:sarah"].

Example queries:

tags=slack&tag_match=prefix
→ Matches: slack:*, slack:U123:*, slack:channel-ops
tags=entity:people&tag_match=prefix&tag_mode=all
→ Matches: entity:people:*, entity:people:sarah, entity:people:john
tags=project&tags=decision&tag_mode=all
→ Matches: Memories tagged with both "project" AND "decision"

Three metadata fields contribute to the final score: importance, confidence, and recency.

Recency calculation: Recency decays from the memory’s timestamp. The curve and window are configurable: with the defaults (SEARCH_RECENCY_CURVE=linear, SEARCH_RECENCY_WINDOW_DAYS=180) the score is max(0.0, 1.0 - (age_days / 180.0)), so memories older than 180 days score 0.0. Setting SEARCH_RECENCY_CURVE=exp switches to 0.5 ** (age_days / window), treating the window as a half-life.

Default behavior:

  • Missing or non-numeric importance contributes 0.0
  • Missing or non-numeric confidence contributes 0.0
  • Recency reads timestamp; a missing or unparseable timestamp contributes 0.0

The final score for each memory result combines ten weighted components. The weights are configurable via environment variables.

final_score =
vector_similarity × SEARCH_WEIGHT_VECTOR (default: 0.35)
+ keyword_score × SEARCH_WEIGHT_KEYWORD (default: 0.35)
+ metadata_match_score × SEARCH_WEIGHT_METADATA (default: 0.35)
+ tag_match_score × SEARCH_WEIGHT_TAG (default: 0.20)
+ importance × SEARCH_WEIGHT_IMPORTANCE (default: 0.10)
+ confidence × SEARCH_WEIGHT_CONFIDENCE (default: 0.05)
+ recency_score × SEARCH_WEIGHT_RECENCY (default: 0.10)
+ exact_match_score × SEARCH_WEIGHT_EXACT (default: 0.20)
+ relation_strength × SEARCH_WEIGHT_RELATION (default: 0.25)
+ relevance_score × SEARCH_WEIGHT_RELEVANCE (default: 0.0)
+ context_bonus (unweighted)

Note: Component weights are relative contributions that sum to 1.95, so the raw weighted sum can exceed 1.0. It is not renormalized — final_score is stored and sorted as computed, and the context bonus is added on top of it without a weight. Treat scores as a ranking signal, not a probability.

ComponentDefault WeightConfigurableDescription
Vector35%SEARCH_WEIGHT_VECTORSemantic similarity from Qdrant
Keyword35%SEARCH_WEIGHT_KEYWORDLexical matching score
Metadata35%SEARCH_WEIGHT_METADATAMetadata field content match (non-zero when match_type=metadata)
Tag20%SEARCH_WEIGHT_TAGTag filter matching
Importance10%SEARCH_WEIGHT_IMPORTANCEUser-assigned priority
Confidence5%SEARCH_WEIGHT_CONFIDENCEClassification certainty
Recency10%SEARCH_WEIGHT_RECENCYAge decay from timestamp (linear over 180 days by default)
Exact20%SEARCH_WEIGHT_EXACTExact phrase match boost
Relation25%SEARCH_WEIGHT_RELATIONGraph relationship strength
Relevance0%SEARCH_WEIGHT_RELEVANCEConsolidation decay relevance_score (experimental, off by default)

The context-profile bonus is an eleventh, unweighted term: when context_tags, context_types, priority_ids, or context keywords are supplied, _compute_context_bonus() adds its own fixed sub-weights (tag 0.45, type 0.25, keyword 0.2, anchor 0.9) directly to the total.

Note: Weights are relative contributions (sum = 1.95). The weighted sum is used as-is — there is no renormalization step.

flowchart TD
    subgraph sources ["Data Source Scores"]
        VS["Vector Search<br/>Qdrant similarity<br/>0.0 - 1.0"]
        KS["Keyword Search<br/>TF-IDF score<br/>Normalized"]
        MS["Metadata Search<br/>Field content match<br/>0.0 - 1.0"]
        GS["Graph Score<br/>Importance fallback"]
    end

    subgraph metadata ["Metadata Sources"]
        TagSrc["Query tags vs<br/>memory tags"]
        ImpSrc["memory.importance<br/>field"]
        ConfSrc["memory.confidence<br/>field"]
        RecSrc["memory.timestamp"]
        ExSrc["Query phrase vs<br/>memory content"]
        RelSrc["memory.relevance_score<br/>from consolidation"]
        CtxSrc["context_tags / context_types /<br/>priority_ids / context"]
    end

    subgraph weights ["Weight Application"]
        VW["× SEARCH_WEIGHT_VECTOR<br/>0.35"]
        KW["× SEARCH_WEIGHT_KEYWORD<br/>0.35"]
        MW["× SEARCH_WEIGHT_METADATA<br/>0.35"]
        TagW["× SEARCH_WEIGHT_TAG<br/>0.20"]
        IW["× SEARCH_WEIGHT_IMPORTANCE<br/>0.10"]
        ConfW["× SEARCH_WEIGHT_CONFIDENCE<br/>0.05"]
        RecW["× SEARCH_WEIGHT_RECENCY<br/>0.10"]
        ExW["× SEARCH_WEIGHT_EXACT<br/>0.20"]
        RW["× SEARCH_WEIGHT_RELATION<br/>0.25"]
        RelW["× SEARCH_WEIGHT_RELEVANCE<br/>0.0"]
        CtxW["context bonus<br/>added unweighted"]
    end

    subgraph combination ["Score Combination"]
        Sum["SUM all weighted<br/>components"]
    end

    subgraph output ["Final Ranking"]
        Sort["Sort by final_score DESC"]
        Dedup["Deduplicate by memory_id<br/>using seen_ids set"]
        Limit["Apply limit parameter"]
    end

    Results["Ranked results array"]

    VS --> VW
    KS --> KW
    MS --> MW
    TagSrc --> TagW
    ImpSrc --> IW
    ConfSrc --> ConfW
    RecSrc --> RecW
    ExSrc --> ExW
    GS --> RW
    RelSrc --> RelW
    CtxSrc --> CtxW

    VW --> Sum
    KW --> Sum
    MW --> Sum
    TagW --> Sum
    IW --> Sum
    ConfW --> Sum
    RecW --> Sum
    ExW --> Sum
    RW --> Sum
    RelW --> Sum
    CtxW --> Sum

    Sum --> Sort
    Sort --> Dedup
    Dedup --> Limit
    Limit --> Results

Multi-hop reasoning enables AutoMem to find memories that are indirectly related to the query through intermediate connections.

Bridge discovery identifies memories that connect multiple seed results, revealing hidden relationships.

Configuration:

  • expand_relations=true — Enable relation expansion (default: false, opt-in)
  • expand_min_strength — Minimum relationship strength (0.0-1.0)
  • expand_min_importance — Minimum target memory importance (0.0-1.0)
  • RECALL_EXPANSION_LIMIT — Maximum expanded results (default: 25)

Bridge scoring: A bridge memory’s score is the sum of its relationship strengths to all seed memories. Higher scores indicate stronger connections.

Entity expansion follows entity tags to find related memories. This enables queries like “What is Sarah’s sister’s job?” to work across multiple memory hops.

How it works:

  1. Execute initial recall to get seed results
  2. For each seed result, extract entity names from existing entity:* tags using _extract_entities_from_results()
  3. Convert extracted entity names to entity:<type>:<slug> search tags
  4. Query FalkorDB/Qdrant for memories with matching entity tags
  5. Merge entity-expanded results with seed results
  6. Apply expansion limits and filters

Configuration:

  • expand_entities=true — Enable entity expansion (default: false)
  • entity_expansion=true — Alias for expand_entities
  • Entity expansion respects original tag filters for context scoping

Entity types extracted:

  • entity:people:<name> — People mentioned
  • entity:tools:<name> — Tools and technologies
  • entity:projects:<name> — Projects and repositories
  • entity:concepts:<name> — Concepts and ideas
  • entity:organizations:<name> — Organizations

The recall endpoint orchestrates the entire hybrid search process:

Key decision points:

  1. Embedding generation: Skip if explicit embedding parameter provided
  2. Vector vs Keyword: Vector search requires Qdrant; keyword search always available
  3. Trending fallback: If query is empty or "*", use _graph_trending_results()
  4. Expansion order: Bridges first, then entity expansion
  5. Filter application: Seed results never filtered; expanded results respect min thresholds

VariableDefaultDescription
SEARCH_WEIGHT_VECTOR0.35Vector similarity contribution
SEARCH_WEIGHT_KEYWORD0.35Keyword match contribution
SEARCH_WEIGHT_METADATA0.35Metadata field content match contribution
SEARCH_WEIGHT_TAG0.20Tag match contribution
SEARCH_WEIGHT_IMPORTANCE0.10Importance field contribution
SEARCH_WEIGHT_CONFIDENCE0.05Confidence field contribution
SEARCH_WEIGHT_RECENCY0.10Recency decay contribution
SEARCH_WEIGHT_EXACT0.20Exact phrase match boost
SEARCH_WEIGHT_RELATION0.25Graph relationship strength contribution
SEARCH_WEIGHT_RELEVANCE0.0Consolidation decay relevance_score contribution (experimental; 0.0 = no-op)
SEARCH_RECENCY_WINDOW_DAYS180Recency window — decay reaches zero (linear) or halves (exp) at this age
SEARCH_RECENCY_CURVElinearRecency decay shape: linear or exp

Note on weight configuration: Default weights are relative contributions that sum to 1.95, and the sum is not renormalized. To customize, maintain relative proportions.

VariableDefaultDescription
RECALL_EXPANSION_LIMIT25Maximum expanded results (bridges + entities)
RECALL_RELATION_LIMIT5Maximum relations fetched per memory
RECALL_VECTOR_OVERFETCH4Vector candidate pool multiplier before hybrid re-rank (1 = legacy)
RECALL_VECTOR_FETCH_CAP200Absolute cap on over-fetched vector candidates
RECALL_EXCLUDED_TYPESMetaPatternComma-separated types excluded from recall and vector sync counts
RECALL_METADATA_SEARCH_ENABLEDtrueAdmit bounded metadata-sidecar candidates during recall
ParameterTypeDefaultDescription
querystringSearch query text
embeddingfloat[]Pre-computed embedding vector
tagsstring[]Tag filters (comma-separated)
tag_modeenum”any”Match mode: "any" or "all"
tag_matchenum”prefix”Match type: "prefix" or "exact"
exclude_tagsstring[]Tags to exclude
time_querystringTemporal expression or ISO range
expand_relationsbooleanfalseEnable bridge discovery
expand_entitiesbooleanfalseEnable entity expansion
expand_min_strengthfloatMinimum relation strength filter
expand_min_importancefloatMinimum target importance filter
limitinteger5Maximum seed results
sortenum”score”Sort order: "score", "time_asc", "time_desc", "updated_asc", "updated_desc"

AutoMem’s current canonical results are sourced from the main repository’s May 2026 benchmark verification trail, not from hand-maintained prose. The headline results are:

BenchmarkScopeScoreRetrievalNotes
LongMemEval full500 questions87.00% (435/500)recall@5 97.00% (485/500)gpt-5-mini answerer, pinned gpt-5.4-mini-2026-03-17 judge
LoCoMo full10 conversations, 1,986 questions84.74% (1683/1986)Pinned gpt-5.4-mini-2026-03-17 judge, 0 skips/errors

For category breakdowns, canary runs, exploratory signals, and reproduction links, see Benchmarks.

Typical response times for different query patterns:

Query TypeTypical LatencyNotes
Vector-only (Qdrant)20-50msSemantic similarity only
Keyword-only (FalkorDB)30-80msGraph keyword search
Hybrid (both stores)50-150msCombined vector + keyword
With bridge expansion100-300msIncludes multi-hop traversal
With entity expansion150-400msIncludes entity tag queries

Optimization tips:

  • Use explicit embedding parameter to skip generation (saves 200-500ms)
  • Set tight expand_min_strength filters to reduce expansion overhead
  • Use limit parameter to reduce result set size
  • Enable Qdrant for semantic search; fallback to keyword-only is slower but functional