Skip to content

Candidate Matching

generate_candidate_matches is the notebook layer: DataFrame in, DataFrame out, for interactive analysis. rank_similarity is the service layer it's built on: plain embedding lists in, (corpus_index, score) pairs out, for callers already working in their own data types (e.g. a live matching service).

matchminer_ai.matching.match

Candidate match generation helpers.

generate_candidate_matches

generate_candidate_matches(query_df: DataFrame, corpus_df: DataFrame, *, k: int | None = 20) -> pd.DataFrame

Generate top-k candidate matches by ranking corpus items for each query item.

Parameters:

Name Type Description Default
query_df DataFrame

Query-side DataFrame with embeddings. Must contain: - patient_id or space_trial_id: str - embedding : array-like

required
corpus_df DataFrame

Corpus-side DataFrame with embeddings. Must contain: - patient_id or space_trial_id: str - embedding : array-like

required
k int | None

Number of top matches to return per query entity. If None, return all corpus items per query (sorted by similarity).

20

Returns:

Type Description
DataFrame

Ranked top-k match pairs including: - patient_id or space_trial_id (from query_df) - patient_id or space_trial_id (from corpus_df) - similarity_score : float - rank : int (1 = highest similarity per query)

Source code in src/matchminer_ai/matching/match.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def generate_candidate_matches(
    query_df: pd.DataFrame,
    corpus_df: pd.DataFrame,
    *,
    k: int | None = 20,
) -> pd.DataFrame:
    """
    Generate top-k candidate matches by ranking corpus items for each query item.

    Parameters
    ----------
    query_df : pd.DataFrame
        Query-side DataFrame with embeddings.
        Must contain:
            - patient_id or space_trial_id: str
            - embedding : array-like
    corpus_df : pd.DataFrame
        Corpus-side DataFrame with embeddings.
        Must contain:
            - patient_id or space_trial_id: str
            - embedding : array-like
    k : int | None, default 20
        Number of top matches to return per query entity. If None, return all
        corpus items per query (sorted by similarity).

    Returns
    -------
    pd.DataFrame
        Ranked top-k match pairs including:
            - patient_id or space_trial_id (from query_df)
            - patient_id or space_trial_id (from corpus_df)
            - similarity_score : float
            - rank : int (1 = highest similarity per query)
    """
    # Validate required schema for IDs and embeddings.
    query_id_col = _resolve_id_column(query_df, "query_df")
    corpus_id_col = _resolve_id_column(corpus_df, "corpus_df")
    if "embedding" not in query_df.columns:
        raise ValueError("query_df must include embedding")
    if "embedding" not in corpus_df.columns:
        raise ValueError("corpus_df must include embedding")

    query_ids = query_df[query_id_col].astype(str).tolist()
    corpus_ids = corpus_df[corpus_id_col].astype(str).tolist()

    ranked = rank_similarity(query_df["embedding"], corpus_df["embedding"], k=k)

    records: list[dict[str, object]] = [
        {
            query_id_col: query_id,
            corpus_id_col: corpus_ids[corpus_idx],
            "similarity_score": score,
            "rank": rank,
        }
        for query_id, matches in zip(query_ids, ranked)
        for rank, (corpus_idx, score) in enumerate(matches, start=1)
    ]

    result = pd.DataFrame.from_records(records)
    if result.empty:
        return pd.DataFrame(
            columns=[query_id_col, corpus_id_col, "similarity_score", "rank"]
        )

    return result

rank_similarity

rank_similarity(query_embeddings, corpus_embeddings, *, k: int | None = 20) -> list[list[tuple[int, float]]]

Rank corpus embeddings by cosine similarity, for each query embedding.

Parameters:

Name Type Description Default
query_embeddings Sequence[array - like]

Query-side embedding vectors.

required
corpus_embeddings Sequence[array - like]

Corpus-side embedding vectors.

required
k int | None

Number of top matches to return per query row. If None, return all corpus rows per query (sorted by similarity).

20

Returns:

Type Description
list[list[tuple[int, float]]]

Per query row, a list of (corpus_index, similarity_score) pairs into corpus_embeddings, sorted by similarity descending and truncated to the top k.

Source code in src/matchminer_ai/matching/match.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def rank_similarity(
    query_embeddings,
    corpus_embeddings,
    *,
    k: int | None = 20,
) -> list[list[tuple[int, float]]]:
    """
    Rank corpus embeddings by cosine similarity, for each query embedding.

    Parameters
    ----------
    query_embeddings : Sequence[array-like]
        Query-side embedding vectors.
    corpus_embeddings : Sequence[array-like]
        Corpus-side embedding vectors.
    k : int | None, default 20
        Number of top matches to return per query row. If None, return all
        corpus rows per query (sorted by similarity).

    Returns
    -------
    list[list[tuple[int, float]]]
        Per query row, a list of (corpus_index, similarity_score) pairs into
        `corpus_embeddings`, sorted by similarity descending and truncated to
        the top `k`.
    """
    if k is not None and k <= 0:
        raise ValueError("k must be a positive integer or None")

    # Parse embeddings and ensure consistent vector dimensionality.
    query_matrix = _stack_embeddings(query_embeddings)
    corpus_matrix = _stack_embeddings(corpus_embeddings)
    if query_matrix.numel() == 0 or corpus_matrix.numel() == 0:
        return [[] for _ in query_embeddings]

    # Normalize embeddings and compute cosine similarity matrix.
    query_norm = _normalize_rows(query_matrix)
    corpus_norm = _normalize_rows(corpus_matrix)
    similarity = query_norm @ corpus_norm.T

    num_corpus = similarity.shape[1]
    top_k = num_corpus if k is None else min(k, num_corpus)

    results: list[list[tuple[int, float]]] = []
    for row_idx in range(similarity.shape[0]):
        scores = similarity[row_idx]
        if top_k == 0:
            results.append([])
            continue
        # stable=True guarantees ties break by corpus order, deterministically
        # across runs/devices; torch.topk has no stability guarantee at all.
        ranked_idx = torch.argsort(scores, descending=True, stable=True)[:top_k]
        results.append([(int(idx), float(scores[int(idx)])) for idx in ranked_idx])

    return results