SynapseKit — 8 lines (2 imports + 6 functional)
BM25 is hybrid-first. There is no standalone keyword retriever — you must initialise an embedding model and vector store even with bm25_weight=1.0. Zero extra installs, but 5 extra lines compared to LangChain.
from synapsekit.retrieval import HybridSearchRetriever, Retriever, InMemoryVectorStore
from synapsekit.embeddings import SynapsekitEmbeddings
emb = SynapsekitEmbeddings(model="all-MiniLM-L6-v2", use_gpu=False)
r = Retriever(InMemoryVectorStore(emb))
hybrid = HybridSearchRetriever(r, bm25_weight=1.0, vector_weight=0.0)
hybrid.add_documents(DOCS)
await r.add(DOCS)
results = await hybrid.retrieve(QUERY, top_k=3)
Trade-offs
✓ rank-bm25 installs automatically — no hidden dependencies
✓ BM25 weight configurable 0.0–1.0 for hybrid blending
✗ Cannot do pure BM25 without instantiating an embedding model
✗ async API requires await — extra friction in sync contexts
⚠ Silent runtime failure: BM25Retriever is in langchain-community but requires rank-bm25 installed separately. The import succeeds. BM25Retriever.from_texts() raises ModuleNotFoundError: No module named 'rank_bm25' at runtime if missing. Add rank-bm25 to your requirements file explicitly.
LangChain — 3 lines (1 import + 2 functional)
The cleanest BM25 API. from_texts() takes a list of strings directly. invoke() returns Document objects with .page_content. Three lines. But requires pip install rank-bm25 separately.
from langchain_community.retrievers import BM25Retriever
r = BM25Retriever.from_texts(DOCS, k=3)
results = [doc.page_content for doc in r.invoke(QUERY)]
from langchain_community.retrievers import BM25Retriever
r = BM25Retriever.from_texts(DOCS, k=3)
# ✗ RuntimeError: ModuleNotFoundError: No module named 'rank_bm25'
LlamaIndex — 9 lines (3 imports + 6 functional)
Requires llama-index-retrievers-bm25 as a separate package. Fails at import time (honest). Needs Settings.llm = None to suppress LLM/embedding warnings. Documents must be wrapped in Document objects and parsed into nodes before BM25 can index them.
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core import Document, Settings
from llama_index.core.node_parser import SentenceSplitter
Settings.llm = None
Settings.embed_model = None
nodes = SentenceSplitter(chunk_size=512).get_nodes_from_documents(
[Document(text=d) for d in DOCS])
r = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=3)
results = [n.text for n in r.retrieve(QUERY)]