

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Still calibrating
click for more info
Not enough gems
Cost: 6 gems
1: Keyword vs. Semantic Search
incomplete
2: Hybrid Search
incomplete
3: Score Normalization
incomplete
4: Weighted Combination
incomplete
5: Reciprocal Rank Fusion
incomplete
Back
ctrl+,
Next
ctrl+.
This lesson's interactive features are locked, please to keep using them
So if keyword search is better in some cases and semantic search is better in others... which should we use?
The answer is both! It's called Hybrid Search – we run both search methods and then combine their results intelligently.
We expect different search methods to perform better on different types of queries. Here are some of the common ones:
Before we hop into the assignment, I want you to run both keyword and semantic search using the following commands and think about which method gave you better results and why. Then try to come up with your own queries that you think would be better suited for each method and see if you are correct.
uv run cli/semantic_search_cli.py search_chunked "gods among mortals"
uv run cli/keyword_search_cli.py bm25search "gods among mortals"
uv run cli/semantic_search_cli.py search_chunked "Bear DiCaprio"
uv run cli/keyword_search_cli.py bm25search "Bear DiCaprio"
Let's start stubbing out the code for hybrid search.
import argparse
def main() -> None:
parser = argparse.ArgumentParser(description="Hybrid Search CLI")
parser.add_subparsers(dest="command", help="Available commands")
args = parser.parse_args()
match args.command:
case _:
parser.print_help()
if __name__ == "__main__":
main()
import os
from .keyword_search import InvertedIndex
from .semantic_search import ChunkedSemanticSearch
class HybridSearch:
def __init__(self, documents: list[dict]) -> None:
self.documents = documents
self.semantic_search = ChunkedSemanticSearch()
self.semantic_search.load_or_create_chunk_embeddings(documents)
self.idx = InvertedIndex()
if not os.path.exists(self.idx.index_path):
self.idx.build()
self.idx.save()
def _bm25_search(self, query: str, limit: int) -> list[dict]:
self.idx.load()
return self.idx.bm25_search(query, limit)
def weighted_search(self, query: str, alpha: float, limit: int = 5) -> list[dict]:
raise NotImplementedError("Weighted hybrid search is not implemented yet.")
def rrf_search(self, query: str, k: int, limit: int = 10) -> list[dict]:
raise NotImplementedError("RRF hybrid search is not implemented yet.")
You can structure your code however you like, and yours likely doesn't look exactly like mine. Take some time to refactor your code so that it's organized in a way that makes sense to you.
Run and submit the CLI tests once you are satisfied with the basic structure.