Combining Semantic & Keyword Search: A Hybrid Search Tutorial with Agent Platform Vector Search
Source notebook
Repo path:
embeddings/hybrid-search.ipynb· Open on GitHub · intermediate
Builds sparse and hybrid Vector Search indexes for Google Merch Shop product search.
Summary
This notebook teaches how hybrid search combines semantic search with token-based search to improve retrieval quality. It loads Google Merch Shop product titles, builds TF-IDF sparse embeddings, uploads index data to Cloud Storage, and creates a Vector Search index. It then adds dense embeddings from gemini-embedding-001 and queries a hybrid index using HybridQuery with RRF ranking.
Key code patterns
TF-IDF sparse embedding wrapper
vectorizer = TfidfVectorizer()
vectorizer.fit_transform(df.title.tolist())
def get_sparse_embedding(text):
tfidf_vector = vectorizer.transform([text])
values, dims = [], []
for i, tfidf_value in enumerate(tfidf_vector.data):
values.append(float(tfidf_value))
dims.append(int(tfidf_vector.indices[i]))
return {"values": values, "dimensions": dims}Converts word-level TF-IDF output into the sparse_embedding values and dimensions format required by Vector Search.
Sparse index data file
items = []
for i in range(len(df)):
title = df.title[i]
sparse_embedding = get_sparse_embedding(title)
items.append({"id": i, "title": title, "sparse_embedding": sparse_embedding})
with open("items.json", "w") as f:
for item in items:
f.write(f"{item}\n")
! gsutil cp items.json $BUCKET_URIBuilds per-item sparse embeddings and stages the index input file in Cloud Storage.
Create and deploy Vector Search index
aiplatform.init(project=PROJECT_ID, location=LOCATION)
my_sparse_index = aiplatform.MatchingEngineIndex.create_tree_ah_index(
display_name=f"vs-tokensearch-index-{UID}",
contents_delta_uri=BUCKET_URI,
dimensions=768,
approximate_neighbors_count=10,
)
my_index_endpoint = aiplatform.MatchingEngineIndexEndpoint.create(
display_name=f"vs-hybridsearch-index-endpoint-{UID}",
public_endpoint_enabled=True,
)Creates a Tree-AH Vector Search index from Cloud Storage and exposes it through a public index endpoint.
Dense Gemini embedding wrapper
embed_client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)
def get_dense_embedding(text):
response = embed_client.models.embed_content(
model="gemini-embedding-001",
contents=text,
config=EmbedContentConfig(output_dimensionality=768),
)
return response.embeddings[0].valuesGenerates 768-dimensional dense text embeddings for semantic search.
Hybrid query with RRF alpha
query_dense_emb = get_dense_embedding(query_text)
query_sparse_emb = get_sparse_embedding(query_text)
query = HybridQuery(
dense_embedding=query_dense_emb,
sparse_embedding_dimensions=query_sparse_emb["dimensions"],
sparse_embedding_values=query_sparse_emb["values"],
rrf_ranking_alpha=0.5,
)
response = my_index_endpoint.find_neighbors(
deployed_index_id=DEPLOYED_HYBRID_INDEX_ID,
queries=[query],
num_neighbors=10,
)Combines dense and sparse retrieval signals in one query and balances them with Reciprocal Rank Fusion.
Models & APIs used
- Models: gemini-embedding-001
- APIs / services: Agent Platform API, Agent Platform Vector Search, Cloud Storage
- SDKs / libraries:
google-cloud-aiplatform,google-cloud-storage,google-genai,pandas,scikit-learn
When to use this
Use this pattern when product, document, or catalog search needs both semantic matching and exact keyword matching for out-of-domain terms.
Gotchas & caveats
- Vector Search deployments can take up to 30 minutes, and the tutorial estimates 1 to 1.5 hours because deployment waits occur twice.
- An existing Google Cloud project and enabled Agent Platform API are required.
- The Colab authentication cell applies only when running in Google Colab.
- Colab Enterprise deployment may output a timeout error that the notebook says can be ignored.
- Semantic search alone can miss out-of-domain SKUs, new product names, and proprietary codenames.
- Indexes, Index Endpoints, and Cloud Storage buckets should be deleted after the tutorial to avoid unexpected costs.
- rrf_ranking_alpha of 1 or omitted uses dense results only, 0 uses sparse results only, and values from 0 to 1 merge both.
Best practices
- Store both embedding and sparse_embedding on each item when building a hybrid index.
- Fit the sparse vectorizer on the corpus before generating item and query sparse embeddings.
- Use rrf_ranking_alpha to control the weight between dense and sparse search results.
- Consider subword tokenizers, BM25, or SPLADE instead of basic word-level TF-IDF for production requirements.
- Use fast Vector Search retrieval followed by reranking for higher-quality production retrieval or recommender systems.
- Clean up Indexes, Index Endpoints, and Cloud Storage buckets after finishing the tutorial.
Related
- Concepts: Embeddings & Vector Search · Applied Use Cases
- Entities: Google GenAI SDK · Vertex AI SDK · Vector Search · Cloud Storage · Gemini
- Area: Embeddings & Vector Search Notebooks
- Best practices: Embeddings & Vector Search - Best Practices · Applied Use Cases - Best Practices