refract
Smart similarity search that understands your query and your data.
refract is a Python library that replaces static cosine similarity with a dynamic, context-aware mixture of similarity metrics -- weighted based on the nature of your query and the geometry of your search space. Every result comes with a provenance trace explaining exactly how it was scored.
flowchart LR
Q["Query"] --> QA["Query Analyzer"]
C["Corpus"] --> SA["Space Analyzer"]
QA --> R["Router"]
SA --> R
R --> |"weights"| F["Fusion Engine"]
M["Metrics\n cosine | bm25 | mahalanobis | euclidean"] --> F
F --> RES["Ranked Results\n+ Provenance"]
The problem with cosine similarity
Cosine similarity is the default for vector search. But it assumes the embedding space is flat and isotropic, that all dimensions contribute equally, and that the same metric works for every query type.
None of these are true. Transformer embeddings are anisotropic. Hierarchical relationships are not linear. The right notion of "similar" for "sort list python" is different from "what are the philosophical implications of determinism".
What refract does
Instead of score(x, y) = cosine(x, y), refract computes:
score(x, y | query, space) = sum_i w_i(query, space) * sim_i(x, y)
The weights w_i are determined dynamically by analyzing:
- Query type -- keyword, natural language, code, or structured data
- Search space geometry -- density, variance, anisotropy of the candidate pool
- Discriminability -- which metrics actually separate candidates for this query
Why not just...
| Approach | What it does | refract difference |
|---|---|---|
| Cosine similarity | Single static metric | Refract dynamically routes across multiple metrics |
| Hybrid search (BM25 + dense) | Static weights (e.g., 0.7/0.3) | Weights are dynamic and space-aware |
| Rerankers (cross-encoders) | Post-processes a fixed candidate set | Refract changes how scoring happens, not just the order after |
| Learning to rank | Learns feature weights | Refract works out of the box (heuristic), with optional learned routing |
| FAISS / Qdrant / Pinecone | ANN indexing & search | Infrastructure layer -- refract operates above these |
Install
# Core (numpy, scipy, scikit-learn, rank_bm25 only)
pip install refract-search
# With local embeddings
pip install "refract-search[sentence-transformers]"
# With OpenAI embeddings
pip install "refract-search[openai]"
# Everything
pip install "refract-search[all]"
Quickstart
import refract
docs = [
"Sort a Python list using the sorted() built-in.",
"Neural networks learn representations of data.",
"Retrieve relevant documents from a large corpus.",
"Use cosine similarity to measure vector closeness.",
]
results = refract.search("how do I sort things in Python", docs)
for r in results:
print(f"{r.score:.3f} {r.text}")
Output:
0.726 Sort a Python list using the sorted() built-in function.
0.357 Python decorators modify function behavior at definition time.
0.333 Use cosine similarity to measure vector closeness in embedding space.
Score provenance
Every result explains why it ranked where it did:
result = results[0]
print(result.provenance)
# Provenance(score=0.726, router='heuristic', query_type='natural_language',
# density='medium',
# [cosine=0.895x0.50, bm25=0.877x0.15, mahalanobis=0.412x0.25, euclidean=0.513x0.10])
With an embedder
from refract.embedders.sentence_transformers import SentenceTransformerEmbedder
embedder = SentenceTransformerEmbedder("all-MiniLM-L6-v2")
results = refract.search("machine learning fundamentals", docs, embedder=embedder)
With pre-computed vectors
import numpy as np
query_vec = np.random.randn(384)
corpus_vecs = np.random.randn(100, 384)
results = refract.search(query_vec, corpus_vecs)
Batch search
Amortize corpus analysis across multiple queries:
results = refract.search_batch(
["query one", "query two", "query three"],
documents,
top_k=5,
)
# Returns: list[list[SearchResult]]
Custom metric
from refract.metrics import BaseMetric
import numpy as np
class MyDomainMetric(BaseMetric):
name = "my_metric"
def score(self, query_vec: np.ndarray, candidate_vec: np.ndarray) -> float:
return float(np.dot(query_vec, candidate_vec))
results = refract.search(query, docs, metrics=["cosine", "bm25", MyDomainMetric()])
Custom router
from refract.routing import BaseRouter
class MyRouter(BaseRouter):
name = "my_router"
def route(self, query_profile, space_profile, available_metrics):
if query_profile.query_type == "code":
return {"cosine": 0.7, "bm25": 0.3}
return {"cosine": 0.5, "mahalanobis": 0.3, "bm25": 0.2}
results = refract.search(query, docs, router=MyRouter())
Train a learned router from relevance feedback
Use your own judged queries to learn when each metric should matter more:
from refract.routing import LearnedRouter
queries = [
"how to sort a list in Python",
"neural network architecture",
"vector similarity embedding",
]
relevance = {
0: {0, 16},
1: {1, 8, 15},
2: {3, 11, 19},
}
router = LearnedRouter(["cosine", "bm25", "mahalanobis", "euclidean"])
report = router.fit_from_relevance(
queries=queries,
corpus=docs,
relevance=relevance,
top_k=5,
)
print(report)
print(report.metric_quality)
fit_from_relevance() automatically:
- Builds query + space features
- Measures how well each metric ranks the relevant documents
- Converts those per-query metric scores into target routing weights
- Trains a small gating network to predict those weights later
Use a trained router
from refract.routing import LearnedRouter
router.save("learned_router.pkl")
trained_router = LearnedRouter.load("learned_router.pkl")
results = refract.search(
"how do I sort things in Python",
docs,
router=trained_router,
)
Evaluate learning
You can evaluate the learned router directly, then benchmark it against heuristic routing:
evaluation = trained_router.evaluate_from_relevance(
queries=queries,
corpus=docs,
relevance=relevance,
top_k=5,
)
print(evaluation.router_ndcg_at_k, evaluation.oracle_ndcg_at_k)
from refract.benchmark import BenchmarkHarness, CustomDataset
dataset = CustomDataset(
name="my_eval",
queries=queries,
corpus=docs,
relevance=relevance,
)
harness = BenchmarkHarness()
heuristic = harness.run(dataset, compare_cosine_baseline=False)[0]
learned = harness.run(dataset, router=trained_router, compare_cosine_baseline=False)[0]
Use as a RAG retrieval step
import refract
def retrieve(query: str, knowledge_base: list[str], top_k: int = 5) -> list[str]:
results = refract.search(query, knowledge_base, top_k=top_k)
return [r.text for r in results]
# Feed into your LLM
context = retrieve("What is the refund policy?", documents)
Benchmarking
Prove it works on your data:
from refract.benchmark import BenchmarkHarness, CustomDataset
dataset = CustomDataset(
name="my_data",
queries=["query 1", "query 2"],
corpus=["doc 1", "doc 2", "doc 3"],
relevance={0: {0}, 1: {1, 2}},
)
harness = BenchmarkHarness()
results = harness.run(dataset, compare_cosine_baseline=True)
for r in results:
print(f"{r.method:20s} NDCG@10={r.ndcg_at_10:.3f} Recall@10={r.recall_at_10:.3f}")
Routing modes
| Mode | When to use | Training required |
|---|---|---|
HeuristicRouter (default) |
Always -- good out of the box | No |
LearnedRouter |
When you have relevance feedback data and want adaptive routing | Yes |
CompositeRouter |
Blend multiple routers | Depends |
BaseRouter subclass |
Full custom control | You decide |
Works with
refract is not a vector database or RAG framework. It makes the scoring step smarter. Use it with:
- Vector DBs: FAISS, Qdrant, Pinecone, Weaviate, Milvus, Chroma
- RAG frameworks: LangChain, LlamaIndex, Haystack
- Embeddings: OpenAI, Cohere, sentence-transformers, any custom model
Design principles
- Progressive complexity. Five lines to get started. Full control available.
- Embedding-agnostic. Bring your own vectors; embedders are optional extras.
- Explainable by default. Every score comes with a provenance trace.
- Pluggable everywhere. Metrics, routers, and embedders all follow stable interfaces.
- No reinventing the wheel. Does not store vectors. Does not orchestrate LLMs. One job: smarter scoring.
Project structure
src/refract/
search.py # Main API: refract.search(), refract.search_batch()
types.py # SearchResult, Provenance, QueryProfile, SpaceProfile
analysis/ # Query type detection + space geometry analysis
metrics/ # Cosine, Euclidean, Mahalanobis, BM25 + registry
routing/ # HeuristicRouter, LearnedRouter, CompositeRouter
fusion/ # Weighted score fusion with provenance
embedders/ # SentenceTransformer, OpenAI, Cohere (optional)
benchmark/ # Evaluation harness with NDCG, Recall, MRR
Examples
| Example | Description |
|---|---|
quickstart.py |
5-line usage demo |
rag_pipeline.py |
RAG retrieval step |
code_search.py |
Code similarity + query type detection |
custom_metric.py |
Plug in your own metric |
compare_cosine.py |
Side-by-side vs vanilla cosine |
benchmark_demo.py |
Evaluation harness demo |
train_learned_router.py |
Train a learned router from judged queries |
evaluate_learned_router.py |
Compare heuristic vs learned routing |
vector_db_integration.py |
FAISS/Qdrant integration pattern |
Contributing
See CONTRIBUTING.md for development setup and guidelines.
Roadmap
0.1.0-- Core API, heuristic router, cosine / euclidean / mahalanobis / BM250.2.0-- Learned router with relevance-driven training and evaluation (you are here)0.3.0-- BEIR benchmark harness with published results1.0.0-- Stable API, comprehensive benchmarks, documentation site
License
Metadata
Release files for refract-search 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| refract_search-0.2.0.tar.gz | 57.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| refract_search-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.5 kB
Release files / refract_search-0.2.0.tar.gz
| Download URL | refract_search-0.2.0.tar.gz |
|---|---|
| Size | 57.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3fa2cd630e625b553a2207beee6686c50947b607d7094066e73e060d4484f49f
|
|
BLAKE2b-256 checksum How to use checksums |
95c80312cde7a895a8a6e61bb4798df9813af3ee30dba72befbf7dd5d3f51dfd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 11, 2026.
Transparency logRelease files / refract_search-0.2.0-py3-none-any.whl
| Download URL | refract_search-0.2.0-py3-none-any.whl |
|---|---|
| Size | 48.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
92ed6664f31dbad6727c377fe0ddcb8e353f62eb0bcba67cd5df8a9f586b6b9c
|
|
BLAKE2b-256 checksum How to use checksums |
c724ab8f494489e2a7cbacc1e5890820fdc8a796d5bb8fc89e5afdafe0ca1e71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 11, 2026.
Transparency log