<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Sayanteka_24]]></title><description><![CDATA[Sayanteka_24]]></description><link>https://sayanteka24.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 05 Sep 2026 02:06:38 GMT</lastBuildDate><atom:link href="https://sayanteka24.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Build Travel AI Agents Using Phidata and Qdrant]]></title><description><![CDATA[I have seen the travel industry undergoing a massive shift in the last few years and AI now sits at the center of that transformation. Nearly every company is racing to build the next personal travel concierge: a system that understands your preferen...]]></description><link>https://sayanteka24.hashnode.dev/how-to-build-travel-ai-agents-using-phidata-and-qdrant</link><guid isPermaLink="true">https://sayanteka24.hashnode.dev/how-to-build-travel-ai-agents-using-phidata-and-qdrant</guid><category><![CDATA[Phidata]]></category><category><![CDATA[qdrant]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[sayanteka chakraborty]]></dc:creator><pubDate>Wed, 10 Dec 2025 18:10:27 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><em>I have seen the travel industry undergoing a massive shift in the last few years and AI now sits at the center of that transformation. Nearly every company is racing to build the next personal travel concierge: a system that understands your preferences, budget, and travel style, and plans the perfect trip with a single prompt.</em></p>
</blockquote>
<p>Instead of curated, high-quality itineraries, users often receive:</p>
<ul>
<li><p>Hotels that don’t exist</p>
</li>
<li><p>Restaurants that closed years ago</p>
</li>
<li><p>Generic “copy-paste” trip plans</p>
</li>
<li><p>Recommendations that don’t match their budget or interests</p>
</li>
</ul>
<p>The core issue is that most of these systems rely purely on the language model without connecting it to real-world travel data.</p>
<p>When you converse with a language model, it’s astonishingly good at grasping what you intend to do: it can reason, summarize, and even pick up what you want to say from just a few words. But there is one enormous limitation: if there’s no actual, fact-checked information for the model to get its data from, it then begins to guess. And that is where misinformation or half-correct recommendations start creeping in.</p>
<p>That is exactly why <strong>RAG (Retrieval Augmented Generation)</strong> changes the game. Instead of letting the model rely only on what it “remembers,” RAG makes it pause, understand your request, and then actually look things up — searching through reliable databases, documents, or sources. The model doesn’t imagine answers; it grounds them in real data before presenting anything to you.</p>
<blockquote>
<p><strong><em>But here’s the thing:</em></strong></p>
<p><strong><em>RAG alone still won’t give you a great trip.</em></strong></p>
</blockquote>
<p>A memorable journey isn’t a bunch of search results stitched together. You want pacing that fits your energy, timing that respects your style, and trade-offs that make sense for you. Maybe you are a sunrise person. Maybe you like slow mornings and lively nights. Maybe food matters more than museums for you. A plain RAG system won’t understand that.</p>
<p>That is where multi-agent AI systems completely change the experience.</p>
<p>Think of it like having your own personal travel team.</p>
<p>One agent gets to know you: your vibe, budget, preferences, what kind of moments you like.</p>
<p>Another becomes your hotel hunter, filtering out noise and only bringing the stays that match your mood to you.</p>
<p>A third one becomes your explorer, diving into attractions, food spots, hidden gems, nightlife, and even offbeat places you didn’t know existed.</p>
<p>Then there’s the orchestrator agent, the planner with the big map who takes everything the others found and puts it into a day-by-day itinerary that feels like it was tailored just for you.</p>
<p>The bottom line?</p>
<p>Not a robotic list, nor a hollow summary. But a trip that reflects you: your pace, your preferences, your travel personality. It feels less like using a tool… and more like having a personal travel concierge that knows you better with each conversation.</p>
<blockquote>
<p><em>And the best part is that you can build a system like this and even outperform many commercial AI planners using</em> <strong><em>Phidata + Qdrant,</em></strong> <em>with no large engineering team or complex infrastructure.</em></p>
</blockquote>
<p>To demonstrate the full workflow, this guide walks you through a working prototype built for <strong>Goa, India</strong>, featuring:</p>
<ul>
<li><p>A <strong>hotel agent</strong> (dense + sparse search) using Goibibo datasets</p>
</li>
<li><p>A <strong>discovery agent</strong> that surfaces must-visit attractions using real traveler review datasets</p>
</li>
<li><p>A <strong>planner agent</strong> that combines both and generates a personalized itinerary</p>
</li>
</ul>
<h3 id="heading-architecture"><strong>Architecture</strong></h3>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*JVBN8qH7KjTVcju3Zu1-CA.png" alt /></p>
<p><strong>System Architecture: Phidata Agents Integrated with Qdrant Vector DB</strong></p>
<p>Our system currently consists of three specialized agents, each responsible for a different part of the travel-planning pipeline. Instead of one model doing everything, each agent performs a focused task and all of this orchestration is made possible using the <strong>Phidata (Phi) agentic framework</strong>.</p>
<p>Phidata allows us to register tools, define agents, and orchestrate how they talk to each other. Instead of manually routing queries, it handles intent-detection, tool-calling, inter-agent communication, and final response synthesis behind the scenes.</p>
<p><strong><em>Hotel Agent</em></strong></p>
<ul>
<li><p>It uses hotel_search_tool, which retrieves hotel information from the <a target="_blank" href="https://www.kaggle.com/datasets/PromptCloudHQ/hotels-on-goibibo">Goibibo dataset</a> stored in <a target="_blank" href="https://qdrant.tech/"><strong>Qdrant</strong></a> by combining dense MiniLM embeddings for semantic search with sparse TF-IDF vectors for accurate keyword matching.</p>
</li>
<li><p>It applies filters such as minimum rating, star category, and so on.</p>
</li>
</ul>
<p><strong><em>Discovery Agent</em></strong></p>
<ul>
<li><p>It uses discover_places_tool to recommend places to visit in Goa based on descriptions from real travelers.</p>
</li>
<li><p>It leverages information from <a target="_blank" href="https://www.kaggle.com/datasets/ritvik1909/indian-places-to-visit-reviews-data">Indian Places to Visit Reviews Dataset stored in <strong>Qdrant</strong>.</a></p>
</li>
<li><p>The tool performs hybrid search, retrieves the most relevant places, and then reranks using CrossEncoder to ensure highly accurate matches</p>
</li>
</ul>
<p><strong><em>Planner Agent</em></strong></p>
<ul>
<li><p>It is the “CEO” of our agent system which combines hotel recommendations + places + sequencing into a proper itinerary.</p>
</li>
<li><p>It reads user intent and decides</p>
</li>
<li><p><strong>Do they need a hotel?</strong> If yes, calls hotel_search_tool.</p>
</li>
<li><p><strong>Do they want sightseeing?</strong> If yes, calls discover_places_tool.</p>
</li>
<li><p><strong>Do they want a full itinerary?</strong> If yes, calls both, then analyses results and drafts a final response.</p>
</li>
</ul>
<p><strong><em>Why Is This Architecture So Strong?</em></strong></p>
<ul>
<li><p>Modular: Each agent is responsible for one domain, thereby reducing hallucinations.</p>
</li>
<li><p>Powered by RAG: Both hotels and places rely on real datasets stored in Qdrant.</p>
</li>
<li><p>Context-Aware: The planner agent uses tool outputs + reasoning, producing coherent itineraries.</p>
</li>
<li><p>Extendable: New agents can be easily integrated into the current system like food agent, budget optimizer agent, flight search agent, and so on.</p>
</li>
</ul>
<h3 id="heading-building-the-travel-knowledge-base"><strong>Building the Travel Knowledge Base</strong></h3>
<p><strong><em>Dataset Collection</em></strong></p>
<p>We use two Kaggle datasets: the <a target="_blank" href="https://www.kaggle.com/datasets/PromptCloudHQ/hotels-on-goibibo"><strong>Goibibo Hotels Dataset</strong></a> for hotel listings and the <a target="_blank" href="https://www.kaggle.com/datasets/ritvik1909/indian-places-to-visit-reviews-data"><strong>Indian Places to Visit Reviews Dataset</strong></a> for attraction-level reviews and places to visit.</p>
<p>Since Kaggle requires authentication, I’ll begin by configuring the API inside our Colab environment:</p>
<pre><code class="lang-plaintext"># Create the .kaggle directory
!mkdir -p ~/.kaggle
# Copy kaggle.json from correct path
!cp "/content/kaggle.json" ~/.kaggle/
# Set correct permissions
!chmod 600 ~/.kaggle/kaggle.json
</code></pre>
<p>Once authenticated, we download and unzip both datasets.</p>
<p><strong>Goibibo Hotels Dataset (for hotel recommendation agent):</strong> This dataset provides hotel names, amenities, ratings, room types, and locality data used to build the hybrid semantic–keyword search for the hotel agent.</p>
<pre><code class="lang-plaintext"># Download the Goibibo hotels dataset from Kaggle
!kaggle datasets download -d PromptCloudHQ/hotels-on-goibibo
# Extract into folder "goibibo_hotels"
!unzip hotels-on-goibibo.zip -d goibibo_hotels
</code></pre>
<p><strong>Indian Places to Visit Reviews Dataset (for discovery agent):</strong> This dataset contains traveler reviews and place descriptions from across India. We filter all records belonging to <strong>Goa</strong> and create dense + sparse embeddings for the discovery agent.</p>
<pre><code class="lang-plaintext"># Download the reviews-based tourism dataset from Kaggle
!kaggle datasets download -d ritvik1909/indian-places-to-visit-reviews-data
# Extract into folder "place_reviews"
!unzip indian-places-to-visit-reviews-data.zip -d place_reviews
</code></pre>
<p><strong><em>Data Analysis</em></strong></p>
<p>Before building the retrieval engine and AI agents, the first step is to prepare our travel datasets.</p>
<p>Load &amp; clean the Goibibo Hotel Dataset</p>
<pre><code class="lang-plaintext">import pandas as pd
df = pd.read_csv("goibibo_hotels/goibibo_com-travel_sample.csv")
# Normalize column names
df.columns = (
df.columns
.str.strip()
.str.lower()
.str.replace(" ", "_")
)
# Filter rows where state = Goa
goa_hotel_df = df[df["state"].str.lower() == "goa"]
goa_hotel_df.head(3)
Next, we keep only the hotel attributes relevant for retrieval and filtering.
columns_to_keep = [
"property_id",
"property_name",
"hotel_facilities",
"address",
"locality",
"city",
"state",
"hotel_star_rating",
"site_review_rating",
"site_review_count",
"room_type"
]
final_cols = [col for col in columns_to_keep if col in goa_hotel_df.columns]
goa_hotels = goa_hotel_df[final_cols].copy()
# Remove rows with missing values
goa_hotels = goa_hotels.dropna()
goa_hotels.head(2)
</code></pre>
<p>This cleaned dataset becomes the <strong>knowledge base</strong> for the hotel search agent.</p>
<p>Load &amp; clean the Tourist Places Review Dataset</p>
<pre><code class="lang-plaintext">import pandas as pd
df = pd.read_csv("place_reviews/Review_db.csv")
df["City"] = df["City"].astype(str).str.strip().str.lower()
Define the list of Goa cities and localities present in the dataset.
goa_places = [
"agonda", "alto-porvorim", "amboli", "anjuna", "assagao", "baga", "bardez",
"benaulim", "calangute", "canacona", "candolim", "chapora", "divar island",
"dona paula", "margao", "marmagao", "mapusa", "nuvem", "old goa",
"panjim", "porvorim", "quepem", "saligao", "sangolda", "sanguem",
"sanquelim", "vasco da gama", "varca", "velsao", "verna", "vagator"
]
</code></pre>
<p>Filter the dataset to only include entries from these regions:</p>
<pre><code class="lang-plaintext">goa_places = df[df["City"].isin(goa_places)].copy()
# Keep only one row per city to prevent duplicates
goa_places = goa_places.drop_duplicates(
subset=["City"], keep="first"
).reset_index(drop=True)
goa_places.head(2)
</code></pre>
<p>This dataset feeds into the <strong>place discovery agent</strong>, helping it retrieve tourist attractions, food spots, beaches, and nightlife areas from across Goa.</p>
<p><strong><em>Embedding Models</em></strong></p>
<p>To power semantic search and hybrid retrieval, we’ll prepare two embedding models:</p>
<ul>
<li><p>Dense Embedding Model (SentenceTransformer)</p>
</li>
<li><p>Sparse Embedding Model (TF-IDF)</p>
</li>
</ul>
<p><strong><em>Dense Embeddings</em></strong></p>
<pre><code class="lang-plaintext">from sentence_transformers import SentenceTransformer
dense_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
</code></pre>
<p>This model (MiniLM-L6-v2) creates <strong>384-dimensional semantic vectors</strong>, allowing the system to understand meaning such as:</p>
<ul>
<li><p>“beach resort” ≈ “seaside hotel”</p>
</li>
<li><p>“nightlife places” ≈ “clubs”, “party spots”</p>
</li>
</ul>
<p>Dense vectors retrieve hotels or places <strong>based on meaning</strong>, even if the exact keywords are not present.</p>
<p><strong><em>Sparse Embeddings</em></strong></p>
<pre><code class="lang-plaintext">from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer()
</code></pre>
<p>TF-IDF creates <strong>keyword-based sparse vectors</strong>.</p>
<p>Advantages:</p>
<ul>
<li><p>Captures exact keyword matches (“spa”, “pool”, “breakfast included”)</p>
</li>
<li><p>Helps retrieve items that match specific phrases</p>
</li>
<li><p>Crucial for precision filtering</p>
</li>
</ul>
<p>Using <strong>both dense + sparse</strong> gives a powerful hybrid search that is more accurate than either alone.</p>
<p><strong><em>Connecting to Qdrant Vector Database</em></strong></p>
<pre><code class="lang-plaintext">from qdrant_client import QdrantClient
Q_client = QdrantClient(
url="https://2ddc31f6-2586-4530-a046-770d105c0382.europe-west3-0.gcp.cloud.qdrant.io:6333",
api_key="YOUR API KEY"
)
</code></pre>
<p>We initialize the Qdrant client that connects to a cloud-hosted instance.</p>
<p><strong>Why Qdrant?</strong></p>
<p>For me, <a target="_blank" href="https://qdrant.tech/">Qdrant</a> is not just another vector database; it’s the missing puzzle piece that has made modern AI search both practical and powerful at the same time. I wanted something that captured those small, vital details people actually type in.</p>
<p>Qdrant does exactly that.</p>
<p>With hybrid search allowing semantic intelligence and keyword precision to live together in the same collection, it doesn’t force me to choose between “smart” and “accurate”. And with HNSW under its hood, I get lightning-fast search even when the dataset grows to millions of travel listings, hotels, or attractions.</p>
<p>The thing I like most is the way Qdrant handles metadata. It does it easily, whether I want to narrow the results by price, rating, city, vibe, or even a personal preference like ‘quiet neighborhoods.That gives flexibility to my travel planning system to behave almost like a human researcher: aware and able to adapt to real-world constraints.</p>
<p><strong>Create Qdrant Collections (Hotels + Places)</strong></p>
<p>Two separate collections store hotels and tourist attractions.</p>
<p><strong><em>Hotels Collection</em></strong></p>
<pre><code class="lang-plaintext">from qdrant_client.models import VectorParams, Distance, SparseVectorParams
Q_client.recreate_collection(
collection_name="goa_hotels",
vectors_config={
"dense": VectorParams(size=384, distance=Distance.COSINE),
},
sparse_vectors_config={
"sparse": SparseVectorParams()
}
)
print("Created Qdrant collection: goa_hotels")
</code></pre>
<p><strong><em>Places Collection</em></strong></p>
<pre><code class="lang-plaintext">Q_client.recreate_collection(
collection_name="goa_places",
vectors_config={
"dense": VectorParams(size=384, distance=Distance.COSINE)
},
sparse_vectors_config={
"sparse": SparseVectorParams()
}
)
print("Created Qdrant collection: goa_places")
</code></pre>
<h3 id="heading-uploading-vector-embeddings-to-qdrant-hotels-places"><strong><em>Uploading Vector Embeddings to Qdrant (Hotels + Places)</em></strong></h3>
<p><strong><em>Hotels Embedding Upload</em></strong></p>
<p>Creates <strong>dense vectors</strong> using hotel facilities column and <strong>sparse vectors</strong> using TF-IDF over room types.</p>
<pre><code class="lang-plaintext">from qdrant_client.models import SparseVector, PointStruct

points = []

for idx, row in goa_hotels.iterrows():

    # Dense
    dense_vec = dense_model.encode(str(row["hotel_facilities"])).tolist()

    # Sparse TF-IDF
    tfidf.fit([str(row["room_type"])])
    sparse_row = tfidf.transform([str(row["room_type"])])
    sparse_vec = SparseVector(
        indices=sparse_row.indices.tolist(),
        values=sparse_row.data.tolist()
    )

    # Build point
    points.append(
        PointStruct(
            id=int(idx),
            vector={
                "dense": dense_vec,
                "sparse": sparse_vec
            },
            payload=row.to_dict()
        )
    )

Q_client.upsert(collection_name="goa_hotels", points=points)
print(f"Inserted {len(points)} hybrid hotel vectors ")
</code></pre>
<p>Adds payload indexes for: city,hotel_star_rating,site_review_rating</p>
<pre><code class="lang-plaintext">from qdrant_client.models import PayloadSchemaType

Q_client.create_payload_index(
    collection_name="goa_hotels",
    field_name="city",
    field_schema=PayloadSchemaType.KEYWORD
)

Q_client.create_payload_index(
    collection_name="goa_hotels",
    field_name="hotel_star_rating",
    field_schema=PayloadSchemaType.INTEGER
)

Q_client.create_payload_index(
    collection_name="goa_hotels",
    field_name="site_review_rating",
    field_schema=PayloadSchemaType.FLOAT
)

print("Indexes created successfully ")
</code></pre>
<p><strong><em>Tourist Places Embedding Upload</em></strong></p>
<p>Creates a combined text block using Place + Review + City.</p>
<pre><code class="lang-plaintext">goa_places["text"] = (
goa_places["Place"].astype(str) + " - " +
goa_places["Review"].astype(str) + " - Located in " +
goa_places["City"].astype(str)
)
</code></pre>
<p>Generates both dense + sparse vectors from the text column.</p>
<pre><code class="lang-plaintext">from tqdm import tqdm
from qdrant_client.models import PointStruct, SparseVector

points = []

for i, row in tqdm(goa_places.iterrows(), total=len(goa_places)):

    text = row['text']

    #Dense
    dense_vec = dense_model.encode(text).tolist()


    # Sparse TF-IDF
    tfidf.fit([text])
    sparse_tf = tfidf.transform([text])
    sparse_vec = SparseVector(
        indices=sparse_tf.indices.tolist(),
        values=sparse_tf.data.tolist()
    )

    # Build Point
    points.append(
        PointStruct(
            id=i,
            vector={
                "dense": dense_vec,
                "sparse": sparse_vec
            },
            payload=row.to_dict()
        )
    )


Q_client.upsert(
    collection_name="goa_places",
    points=points
)

print(f"Inserted {len(points)} hybrid tourist goa places vectors ")
</code></pre>
<h3 id="heading-reranker-model"><strong><em>Reranker Model</em></strong></h3>
<p>After Qdrant finds the initial matches, the CrossEncoder Reranker helps decide which ones actually fit the user’s intent. It reads your query and result together, understands subtle meaning and context, and gives each item a single accuracy score. This final reranking makes the results more relevant and personalized.</p>
<p><strong><em>Load a Reranker Model</em></strong></p>
<pre><code class="lang-plaintext">from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
</code></pre>
<h3 id="heading-building-the-hotel-search-tool"><strong>Building the Hotel Search Tool</strong></h3>
<p>The Hotel Search Tool is responsible for retrieving, filtering, and ranking hotels from the <strong>Goa Hotels vector database</strong> stored in Qdrant.</p>
<p><strong><em>Retrieving Information About Hotels from Qdrant</em></strong></p>
<pre><code class="lang-plaintext">from qdrant_client.models import (
Filter, FieldCondition, MatchValue, Range, SparseVector
)

def retrieve_candidates(query, min_stars=0, min_rating=0, top_k=10):

    # Dense vector
    q_dense = dense_model.encode(query).tolist()

    # Sparse TF-IDF
    sparse_q = tfidf.transform([query])
    sparse_vec = SparseVector(
        indices=sparse_q.indices.tolist(),
        values=sparse_q.data.tolist()
    )

    # Filters
    hotel_filter = Filter(
        must=[
            FieldCondition(key="city", match=MatchValue(value="Goa")),
            FieldCondition(key="hotel_star_rating", range=Range(gte=min_stars)),
            FieldCondition(key="site_review_rating", range=Range(gte=min_rating))
        ]
    )

    # Dense search
    dense_results = Q_client.search(
        collection_name="goa_hotels",
        query_vector={"name": "dense", "vector": q_dense},
        query_filter=hotel_filter,
        limit=top_k
    )

    # Sparse search
    sparse_results = Q_client.search(
        collection_name="goa_hotels",
        query_vector={"name": "sparse", "vector": sparse_vec},
        query_filter=hotel_filter,
        limit=top_k
    )

    # Combine IDs
    unique_ids = list({r.id for r in dense_results + sparse_results})

    candidates = []
    for pid in unique_ids:
        rec = Q_client.retrieve("goa_hotels", [pid])[0]
        candidates.append({
            "id": pid,
            "hotel": rec.payload.get("property_name"),
            "locality": rec.payload.get("locality"),
            "stars": rec.payload.get("hotel_star_rating"),
            "rating": rec.payload.get("site_review_rating"),
            "facilities": rec.payload.get("hotel_facilities")
        })

    return candidates
</code></pre>
<p><strong><em>Reranking Results</em></strong></p>
<pre><code class="lang-plaintext">def rerank(query, candidates):
    sentences = [
        f"{c['hotel']} {c['locality']} {c['facilities']}"
        for c in candidates
    ]

    scores = reranker.predict([(query, s) for s in sentences])

    for i, score in enumerate(scores):
        candidates[i]["rerank_score"] = float(score)

    return sorted(candidates, key=lambda x: x["rerank_score"], reverse=True)
</code></pre>
<p><strong><em>Search Pipeline</em></strong></p>
<pre><code class="lang-plaintext">def search_hotels(query, min_stars=0, min_rating=0, top_k=10):

    # Retrieve dense+sparse candidates -
    candidates = retrieve_candidates(query, min_stars, min_rating, top_k)

    if not candidates:
        return []

    # Rerank retrieved candidates
    ranked = rerank(query, candidates)


    results = [
        {
            "hotel": c["hotel"],
            "stars": float(c["stars"]),
            "rating": float(c["rating"]),
            "locality": c["locality"]
        }
        for c in ranked[:top_k]
    ]

    return results,candidates
</code></pre>
<p><strong><em>Phidata Tool Wrapper</em></strong></p>
<p>This is the function the <strong>Hotel Expert Agent</strong> calls.</p>
<pre><code class="lang-plaintext">def hotel_search_tool(query: str, min_stars: float = 0, min_rating: float = 0):

    results, _ = search_hotels(
        query=query,
        min_stars=min_stars,
        min_rating=min_rating,
        top_k=5
    )

    if not results:
        return "No hotels found matching criteria."

    formatted = "\n".join([
        f" {r['hotel']} —  {r['stars']} | Rating: {r['rating']} |  {r['locality']}"
        for r in results
    ])

    return "Here are the best matches:\n\n" + formatted
</code></pre>
<h3 id="heading-building-the-discover-places-tool"><strong>Building the Discover Places Tool</strong></h3>
<p>The Discover Places Tool is responsible for retrieving, filtering, and ranking hotels from the <strong>Goa Places vector database</strong> stored in Qdrant.</p>
<p><strong><em>Retrieving Information About Tourist Places from Qdrant</em></strong></p>
<pre><code class="lang-plaintext">from qdrant_client.models import Filter, FieldCondition, MatchValue, Range, SparseVector
def retrieve_places(query, top_k=10):


    q_dense = dense_model.encode(query).tolist()


    sparse_q = tfidf.transform([query])
    sparse_vec = SparseVector(
        indices=sparse_q.indices.tolist(),
        values=sparse_q.data.tolist()
    )


    dense_results = Q_client.search(
        collection_name="goa_places",
        query_vector={"name": "dense", "vector": q_dense},
        limit=top_k
    )

    sparse_results = Q_client.search(
        collection_name="goa_places",
        query_vector={"name": "sparse", "vector": sparse_vec},
        limit=top_k
    )


    ids = list({r.id for r in dense_results + sparse_results})

    # Retrieve full records
    candidates = []
    for pid in ids:
        rec = Q_client.retrieve("goa_places", [pid])[0]
        candidates.append({
            "id": pid,
            "text": rec.payload.get("text"),

        })

    return candidates
</code></pre>
<p><strong><em>Search Pipeline</em></strong></p>
<pre><code class="lang-plaintext">def search_places(query, top_k=10):

    candidates = retrieve_places(query, top_k)

    if not candidates:
        return []

    ranked = rerank_places(query, candidates)



    return ranked[:top_k]
</code></pre>
<p><strong><em>Phidata Tool Wrapper</em></strong></p>
<p>This is the function the <strong>Goa Discovery Agent</strong> calls.</p>
<pre><code class="lang-plaintext">def discover_places_tool(query: str):

    results = search_places(query, top_k=5)

    if not results:
        return "No relevant places found."

    formatted = "\n".join([
        f"{r['text']}..."
        for r in results
    ])

    return "Suggested places:\n\n" + formatted
</code></pre>
<h3 id="heading-building-travel-agents-with-phidata"><strong>Building Travel Agents With Phidata</strong></h3>
<p>After configuring the Qdrant hybrid search pipeline and preparing both datasets (Goibibo hotels + Indian tourist review dataset), the final step is assembling the <strong>multi-agent travel planning system</strong> using <a target="_blank" href="https://docs.phidata.com/introduction"><strong>Phidata</strong>.</a></p>
<p><strong><em>Define Hotel Agent</em></strong></p>
<pre><code class="lang-plaintext">from phi.agent import Agent
from phi.model.openai import OpenAIChat
import os
os.environ["OPENAI_API_KEY"] = "Your API KEY"
hotel_agent = Agent(
name="Hotel Expert",
model=OpenAIChat(id="gpt-4o-mini"),
description="Recommends hotels in Goa.",
tools=[hotel_search_tool],
instructions=[
"If user asks for hotels or stays, call hotel_search_tool.",
],
markdown=True,
show_tool_calls=True
)
</code></pre>
<p><strong><em>Define Discovery Agent</em></strong></p>
<pre><code class="lang-plaintext">discovery_agent = Agent(
name="Goa Discovery Agent",
model=OpenAIChat(id="gpt-4o-mini"),
description="Suggests attractions, nightlife, beaches and food spots in Goa.",
tools=[discover_places_tool],
instructions=[
"If user asks about things to do, places to visit, or nightlife, call discover_places_tool.",
],
markdown=True,
show_tool_calls=True
)
</code></pre>
<p><strong><em>Define Orchestrator</em></strong></p>
<pre><code class="lang-plaintext">planner = Agent(
name="Goa Planner",
model=OpenAIChat(id="gpt-4o"),
description="Creates travel itineraries: hotels + places + pacing.",
instructions=[
"1. Detect whether user needs hotels, sightseeing, or both.",
"2. Call hotel_search_tool if hotel preferences present.",
"3. Call discover_places_tool for attractions.",
"4. Combine results into a final formatted itinerary with day-by-day plan.",
"Do not show raw function output - rewrite into a travel-friendly summary."
],
tools=[hotel_search_tool, discover_places_tool],
show_tool_calls=True,
markdown=True
)
</code></pre>
<h3 id="heading-how-the-agents-work-together"><strong>How the Agents Work Together</strong></h3>
<p><img src="https://miro.medium.com/v2/resize:fit:1014/1*oZdltmb9tWmsDbtOuOdaKA.png" alt /></p>
<p><strong>End -to-End Flow of Phidata-Based Agents Using Qdrant for Vector Search</strong></p>
<p>You can send a prompt like:</p>
<p><em>“Plan a 4-day luxury Goa trip. Prefer Candolim or Baga. Want pool-facing hotel and nightlife. Min Stars=4.”</em></p>
<p>The <strong>Planner Agent</strong> analyzes the request and decides:</p>
<ul>
<li><p>The user needs <strong>hotels</strong> (location + stars + pool-facing)</p>
</li>
<li><p>The user needs <strong>nightlife</strong></p>
</li>
<li><p>The user needs a <strong>structured itinerary</strong></p>
</li>
</ul>
<p>Phidata allows this agent to <strong>automatically call tools</strong> based on the intent.</p>
<p>The Planner triggers a tool call and has information related to a curated list of luxury pool-facing hotels, a list of attractions/nightlife spots and user preferences (location, luxury style).</p>
<p>The Planner then:</p>
<ul>
<li><p>Structures everything into a 4-day plan</p>
</li>
<li><p>Balances mornings, afternoons, nightlife</p>
</li>
<li><p>Places hotels strategically</p>
</li>
<li><p>Matches nearby attractions</p>
</li>
<li><p>Formats it into a <strong>clean travel-friendly itinerary</strong></p>
</li>
</ul>
<p>The final output is <em>not</em> raw tool output. It is rewritten using the LLM’s reasoning capability.</p>
<p><strong><em>Agent Output</em></strong></p>
<pre><code class="lang-plaintext">response = planner.run(
    "Plan a 4-day luxury Goa trip. Prefer Candolim or Baga. Want pool-facing hotel and nightlife. Min_Stars=4"
)

print(response.content)
</code></pre>
<pre><code class="lang-plaintext">Running:
 - hotel_search_tool(query=Goa Candolim or Baga pool-facing luxury, min_stars=4)
 - discover_places_tool(query=Goa Candolim Baga nightlife attractions)

### 4-Day Luxury Goa Itinerary

#### Day 1: Arrive in Goa and Settle In
- **Hotel Check-in**: Choose from these recommended luxury hotels:
  - **Cygnett Inn Celestiial** (4 stars, Candolim)
  - **Treehouse The Palatium** (4 stars, Candolim)
  - **White Pearl Suites** (5 stars, Baga)
  - **Vivanta by Taj - Fort Aguada** (5 stars, Candolim)
  - **Cidade De Goa** (5 stars, Panjim)
- **Evening**: Explore the nearby beach area. Relax by the pool and enjoy a fine dining experience at your hotel.

#### Day 2: Beach Day and Nightlife
- **Morning**: Visit **Baga Beach**. Enjoy the sun, sand, and sea. Great for family bonding, couple trips, and making new friends.
- **Afternoon**: Return to the hotel for a refreshing dip in the pool and grab a delicious lunch.
- **Nightlife**: Dive into Goa's vibrant nightlife scene at clubs in Baga and Candolim. Enjoy cocktails and music at the local hotspots.

#### Day 3: Cultural Exploration
- **Morning**: Explore the **Houses of Goa Museum** in Porvorim. Discover its unique architecture and fascinating exhibits.
- **Afternoon**: Visit the **Our Lady of Assumption Church** in Velsao for some quiet reflection and to witness the local culture.
- **Evening**: Dinner at one of the eateries in Candolim or Baga, followed by a leisurely beachside stroll.

#### Day 4: Relaxation and Departure
- **Morning**: Head to the **Benz Celebrity Wax Museum** in Calangute and marvel at the fine statues and dine at the adjoining restaurant.
- **Afternoon**: Sign up for one of the **Best Shore Trips** in Marmagao to explore secret beaches and enjoy the ocean.
- **Evening**: Arrange for a late check-out, enjoy the hotel amenities one last time, and prepare for your departure.

This itinerary combines luxury accommodation with the excitement of Goan nightlife and the tranquility of its cultural landmarks. Enjoy your trip!
</code></pre>
<p>The above output shows how a multi-agent system eliminates hallucinations by grounding every answer in <strong>real Qdrant-stored hotel and attraction data</strong>, not model guesses.</p>
<p>The Hotel Agent and Discovery Agent fetch only <strong>verified results</strong> using hybrid dense + sparse search plus CrossEncoder reranking. The Planner Agent then combines these factual results into a coherent, personalized itinerary ensuring relevance and zero fabricated recommendations.</p>
<h3 id="heading-link-to-code"><strong>Link to Code</strong></h3>
<p>Colab: <a target="_blank" href="https://colab.research.google.com/drive/1-4lTpB8MowluSJnfiPdNnoLSk26zhL04?usp=sharing">Travel Agent.ipynb</a></p>
<p>GitHub: <a target="_blank" href="https://github.com/sayanteka24/Travel-Agent">https://github.com/sayanteka24/Travel-Agent</a></p>
<h3 id="heading-summary"><strong>Summary</strong></h3>
<p>In this project, I built a travel-planning system that finally behaves the way I want AI tools to: really grounded, dependable, and (almost) devoid of hallucinations. Because each suggestion is pulled from Qdrant, and not out of the model’s imagination, the planner remains fact-based and personalized. The result sounds less like an AI making guesses and more like a real assistant setting up a trip for the traveler. I hope you found my tutorial helpful.</p>
<h3 id="heading-references"><strong>References</strong></h3>
<h2 id="heading-home-qdranthttpsqdranttechdocumentationsourcepostpage-1c4327c4f2de"><a target="_blank" href="https://qdrant.tech/documentation/?source=post_page-----1c4327c4f2de---------------------------------------"><strong>Home - Qdrant</strong></a></h2>
<h3 id="heading-qdrant-is-an-open-source-vector-database-and-vector-search-engine-written-in-rust-it-provides-fast-and-scalable-vectorhttpsqdranttechdocumentationsourcepostpage-1c4327c4f2de"><a target="_blank" href="https://qdrant.tech/documentation/?source=post_page-----1c4327c4f2de---------------------------------------">Qdrant is an Open-Source Vector Database and Vector Search Engine written in Rust. It provides fast and scalable vector…</a></h3>
<p><a target="_blank" href="https://qdrant.tech/documentation/?source=post_page-----1c4327c4f2de---------------------------------------">qdrant.tech</a></p>
<p><a target="_blank" href="https://github.com/agno-agi/phidata">https://github.com/agno-agi/phidata</a></p>
<p><a target="_blank" href="https://github.com/qdrant/qdrant">https://github.com/qdrant/qdrant</a></p>
<p><a target="_blank" href="https://www.kaggle.com/datasets">https://www.kaggle.com/datasets</a></p>
<p><a target="_blank" href="https://docs.phidata.com/introduction">https://docs.phidata.com/introduction</a></p>
]]></content:encoded></item><item><title><![CDATA[How to Use Sparse Vectors to Power E-commerce Recommendations With Qdrant]]></title><description><![CDATA[1. Introduction

In e-commerce, success hinges on one thing: showing the right product to the right user at the right time. Whether it’s search results, recommendations, or personalized feeds, every interaction shapes how a customer feels about a bra...]]></description><link>https://sayanteka24.hashnode.dev/how-to-use-sparse-vectors-to-power-e-commerce-recommendations-with-qdrant</link><guid isPermaLink="true">https://sayanteka24.hashnode.dev/how-to-use-sparse-vectors-to-power-e-commerce-recommendations-with-qdrant</guid><category><![CDATA[vector embeddings]]></category><category><![CDATA[qdrant]]></category><category><![CDATA[sparsevectors]]></category><category><![CDATA[dense vectors]]></category><dc:creator><![CDATA[sayanteka chakraborty]]></dc:creator><pubDate>Thu, 30 Oct 2025 14:28:04 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-1-introduction"><strong>1. Introduction</strong></h2>
<blockquote>
<p><em>In e-commerce, success hinges on one thing: showing the right product to the right user at the right time. Whether it’s search results, recommendations, or personalized feeds, every interaction shapes how a customer feels about a brand. And behind that experience lie three pillars: relevance, personalization, and multilingual understanding.</em></p>
</blockquote>
<p><strong>Relevance:</strong></p>
<p>When a shopper searches for <em>“waterproof hiking backpack,”</em> the system shouldn’t serve random fashion bags or travel rucksacks. It should surface backpacks designed for trekking and hiking, tuned to the user’s intent, not just their keywords.</p>
<p><strong>Personalization:</strong></p>
<p>Now imagine two users typing <em>“smartwatch.”</em> User A has been exploring fitness trackers, so they should see smartwatches with heart-rate monitoring, GPS, and long battery life. User B, however, has been browsing luxury accessories, so the system should recommend high-end models from Apple or TAG Heuer.</p>
<p>This level of intelligence comes from learning patterns hidden in browsing history, cart behavior, and demographic context.</p>
<p><strong>Multilingual Understanding:</strong></p>
<p>A global shopper base means diverse languages. If a user in India searches for <em>“चाय का कप”</em> (tea cup), they should instantly see the same results as someone searching <em>“tea mug”</em> in English.</p>
<p>To deliver this kind of smart retrieval, we need to move beyond simple keyword matching. Queries and product descriptions must be represented as <strong>vectors</strong> — mathematical representations that capture meaning and relationships between words while preserving important keyword signals.</p>
<p>That’s where <a target="_blank" href="https://qdrant.tech/documentation/">Qdrant</a> comes in. Acting as the engine for intelligent search and recommendation, Qdrant stores, indexes, and searches these vector representations efficiently at scale. With built-in support for sparse, dense, and hybrid retrieval, it enables developers to build real-time product recommendations, personalized search, and multilingual discovery systems that truly understand what users mean — not just what they type.</p>
<h3 id="heading-2-lexical-retrieval-vs-dense-retrieval-vs-sparse-retrieval"><strong>2. Lexical Retrieval vs Dense Retrieval vs Sparse Retrieval</strong></h3>
<p><strong>2.1 Lexical Retrieval</strong></p>
<p>Lexical retrieval converts text into <strong>bag-of-words vectors</strong>, where each word is treated as a separate dimension and assigned a statistical weight using models such as TF-IDF or BM25. Only the words that appear in the document have non-zero weights. Only words that appear in the document have non-zero weights; words absent from the document receive a weight of zero.</p>
<p>In this approach:</p>
<ul>
<li><p>Common words across documents get low weight because they appear frequently and carry less distinguishing power.</p>
</li>
<li><p>Rare words get high weight, since they occur less often and are assumed to be more informative.</p>
</li>
</ul>
<p><em>Example:</em></p>
<p><em>User Query: “Waterproof hiking backpack”</em></p>
<p><em>Product A:“40L trekking backpack with rain cover”<br />Product B:“Alpine brand leather handbag”<br />Product C:“Travel rucksack for camping trips”</em></p>
<p><em>Product B might score high because “alpine” is a rare word, even though it has nothing to do with hiking. Product C might also rank high because of the keyword “rucksack”, despite being related to general travelling and not hiking. Understanding that “trekking backpack” (Product A) is semantically the closest match to “hiking backpack” is missing here.</em></p>
<p><strong>2.2 Dense Retrieval</strong></p>
<p>Dense Retrieval represents text, such as user queries and product descriptions, as <strong>dense vectors (embeddings)</strong> in a low-dimensional space. Instead of relying on exact word matching, neural models like BERT, GPT capture the semantic meaning of text, so that texts with similar meanings have similar vector representations, even when they use completely different words.</p>
<p><em>Example:</em></p>
<p><em>User Query: “Waterproof hiking backpack”</em></p>
<p><em>Product A:“40L trekking backpack with rain cover”<br />Product B:“Alpine brand leather handbag”<br />Product C: “Travel rucksack for camping trips”</em></p>
<p><em>Model will give high score to Product A which is correct as it’s a backpack meant for trekking and includes a rain cover. Product B is clearly irrelevant, and the model will give it a low similarity score. Product C, however, will get a high score even though it’s a travel rucksack, not a waterproof hiking backpack. This happens because the dense model interprets “hiking backpack” and “travel rucksack” as semantically close as both represent bags for carrying items outdoors. As a result it fails to capture the specific intent that the user wants a waterproof technical hiking bag, not a travel bag for clothes.</em></p>
<p>Also, we need to remember that dense models are computationally expensive.</p>
<p><strong>2.3 Sparse Retrieval</strong></p>
<p>Sparse retrieval represents the new generation of search models that combine the precision of lexical retrieval (TF-IDF, BM25) with the semantic depth of dense retrieval (BERT, GPT). Instead of assigning fixed statistical weights to words, sparse neural models such as <strong>SPLADE</strong> and <strong>miniCOIL</strong> use transformer-based architectures to learn which terms are truly important and how they relate contextually to others.</p>
<p>Because they leverage sparse representations and rely on lightweight indexing structures (like inverted lists) rather than computationally expensive vector similarity searches, sparse retrieval is more efficient and scalable than dense models — especially during query time.</p>
<p><em>Example:</em></p>
<p><em>User Query: “Waterproof hiking backpack”</em></p>
<p><em>Product A:“40L trekking backpack with rain cover”<br />Product B:“Alpine brand leather handbag”<br />Product C: “Travel rucksack for camping trips”</em></p>
<p><em>Product C, which was wrongly boosted by dense retrieval in the above example, will now get a lower score since it lacks matching terms, though “hiking backpack” and “travel rucksack” are semantically close. Product A will get the highest score. Product B will get the lowest score</em></p>
<h3 id="heading-3-splade-vs-minicoil"><strong>3. SPLADE vs miniCOIL</strong></h3>
<blockquote>
<p><em>Both SPLADE and miniCOIL are sparse neural models, that is, they represent text as high-dimensional, sparse vectors where most word dimensions are zero, but the important words have learned weights capturing semantic meaning.</em></p>
</blockquote>
<p><strong>3.1 SPLADE</strong></p>
<p>SPLADE stands for Sparse Lexical and Expansion Model for Information Retrieval. It is a sparse neural model in which queries and documents are represented as vectors with mostly zero values, just like TF-IDF or BM25, but the non-zero weights are learned using transformer-based models like BERT.</p>
<p>SPLADE also supports term expansion, meaning the model can assign weights to tokens that do not explicitly appear in the document but are contextually related to those that do. This powerful feature allows the model to better capture the semantic context of the document and improve retrieval performance.</p>
<p>For example:</p>
<p>Input: “wireless noise cancelling headphones”<br />SPLADE output:</p>
<pre><code class="lang-plaintext">#dummy weights assigned for explanation
{
  wireless: 1.0,
  headphones: 0.9,
  bluetooth: 0.8,
  audio: 0.7,
  music: 0.6,
  sound: 0.6,
  earphones: 0.5,
  noise: 0.6,
  cancelling: 0.2
}
</code></pre>
<ul>
<li><p>Assigns learned weights that show how important each word is.</p>
</li>
<li><p>Adds additional semantic terms not present in input/document such as bluetooth, music, sound ,earphones and audio.</p>
</li>
</ul>
<p><strong>3.2 miniCOIL</strong></p>
<p>miniCOIL, unlike SPLADE, does not expand documents with new terms; it only reweights the existing words based on their contextual importance.</p>
<p>As a result, the index size remains smaller and more compact than that of SPLADE, enabling faster and more efficient retrieval with higher precision. However, since miniCOIL focuses solely on exact terms and lacks semantic expansion, its recall is lower compared to SPLADE, which can retrieve a broader range of semantically related results.</p>
<h3 id="heading-4-qdrant-architecture"><strong>4. Qdrant Architecture</strong></h3>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*cQGXpdwb0X8C0n1wmAdAog.png" alt /></p>
<p><strong>Source: Author</strong></p>
<p>The diagram above represents a high-level overview of some of the main components of <a target="_blank" href="https://qdrant.tech/">Qdrant</a>.</p>
<p><strong>4.1 Collection</strong></p>
<p>A collection is like a table or index where related vectors are stored together. Example: A collection named <em>products_collection</em> might store all product vectors for an e-commerce catalog.</p>
<p><strong>4.2 Distance Metrics</strong></p>
<p>Qdrant uses distance metrics to compare vectors and measure similarity, particularly in the case of dense vector representations. Common metrics include cosine, dot product, and Euclidean distance. The metric is defined when the collection is created.</p>
<p><strong>4.3 Point &amp; Payload</strong></p>
<p>Each point represents a single data entry that contains an ID, a vector (dense or sparse), and an optional payload (metadata).</p>
<pre><code class="lang-plaintext">{
  "id": 101,
  "vector": [0.12, -0.88, 0.55, ...],
  "payload": {
    "title": "Waterproof Hiking Backpack 30L",
    "brand": "WildTrail",
    "category": "Backpacks",
    "price": 1999,
    "language": "en",
    "rating": 4.5,
    "in_stock": true
  }
}
</code></pre>
<p>The JSON object above represents a point in Qdrant. The vector is a numerical representation of the product’s title or description. Each point corresponds to one product in the search system. When many such points are stored together, they form a collection named <em>products_collection</em>.</p>
<p>A payload is the additional JSON metadata attached to a vector, used for filtering, ranking, or applying business logic during search.</p>
<h3 id="heading-5-product-search-using-qdranthttpsqdranttech"><strong>5. Product Search Using</strong> <a target="_blank" href="https://qdrant.tech/"><strong>Qdrant</strong></a></h3>
<blockquote>
<p><em>Let us now explore a comparative study of lexical search, dense retrieval, and sparse neural retrieval using Qdrant for an e-commerce use case.</em></p>
</blockquote>
<p>Install the following Python libraries:</p>
<pre><code class="lang-plaintext">! pip install qdrant-client
! pip install sentence-transformers
! pip install transformers
</code></pre>
<p>Load the required Python libraries:</p>
<pre><code class="lang-plaintext">from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct, SparseVector,VectorParams, Distance
from sklearn.feature_extraction.text import TfidfVectorizer
from sentence_transformers import SentenceTransformer
from transformers import AutoModelForMaskedLM, AutoTokenizer
import torch
import numpy as np
</code></pre>
<p>Initialize the Qdrant client:</p>
<pre><code class="lang-plaintext">client = QdrantClient(":memory:")
</code></pre>
<p>Create dummy data for e-commerce:</p>
<pre><code class="lang-plaintext">products = [
    {"id": "S001", "title": "Nike Air Zoom Pegasus", "description": "Lightweight breathable running shoes", "brand": "Nike", "category": "Running", "price": 8999},
    {"id": "S002", "title": "Adidas Ultraboost 5.0", "description": "Cushioned marathon runner with energy return", "brand": "Adidas", "category": "Running", "price": 12999},
    {"id": "S003", "title": "ASICS Gel-Kayano Stability", "description": "Comfortable long distance support running shoes", "brand": "ASICS", "category": "Running", "price": 11999},
    {"id": "S004", "title": "Puma Velocity Nitro 2", "description": "Nitro foam responsive daily trainer", "brand": "Puma", "category": "Running", "price": 8499},
    {"id": "S005", "title": "Skechers GoWalk", "description": "Soft cushioned walking shoes", "brand": "Skechers", "category": "Walking", "price": 4999},
    {"id": "S006", "title": "Clarks Oxford Leather", "description": "Classic formal leather shoes", "brand": "Clarks", "category": "Formal", "price": 7499},
    {"id": "S007", "title": "Woodland Trek Waterproof", "description": "Rugged waterproof trekking boots", "brand": "Woodland", "category": "Outdoor", "price": 6999},
    {"id": "S008", "title": "Converse Chuck Taylor", "description": "Iconic casual canvas sneakers", "brand": "Converse", "category": "Casual", "price": 3999},
    {"id": "S009", "title": "Nike Metcon Trainer", "description": "Stable cross-training gym shoes", "brand": "Nike", "category": "Training", "price": 7999},
    {"id": "S010", "title": "Adidas Terrex Trail GTX", "description": "Waterproof trail running shoes with grip", "brand": "Adidas", "category": "Trail", "price": 13999}
]

texts = [p["title"] + " " + p["description"] for p in products]
</code></pre>
<p><strong>5.1 Lexical Search(TF-IDF)</strong></p>
<pre><code class="lang-plaintext">vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(texts)
print(tfidf_matrix.shape)

#create collection
client.recreate_collection(
    collection_name="products_collection",
    vectors_config={},
    sparse_vectors_config={"tfidf": models.SparseVectorParams(
            index=models.SparseIndexParams(
                on_disk=False,
            )
        )
    },

)

#insert products data
client.upsert(
    collection_name="products_collection",
    points=[
        models.PointStruct(
            id=i,
            payload=product,
            vector={
                "tfidf": models.SparseVector(
                    indices=tfidf_matrix[i].tocoo().col.tolist(),
                    values=tfidf_matrix[i].tocoo().data.tolist()
                )
            }
        )

        for i, product in enumerate(products)
    ]
)
</code></pre>
<p>Output:</p>
<p>(10, 62)</p>
<p>No of unique words(Bag -of-words)=62</p>
<p>No of products=10</p>
<p>indices are the positions of non-zero terms in the vector and values are the importance weights for each index.</p>
<pre><code class="lang-plaintext">query_text = "waterproof running shoes"
query_vector = vectorizer.transform([query_text]).tocoo()
print(query_vector.shape)
print(query_vector.col.tolist()) 
print(query_vector.data.tolist()) 

# Searching for similar documents
result = client.search(
    collection_name="products_collection",
    query_vector=models.NamedSparseVector(
        name="tfidf",
        vector=models.SparseVector(
            indices=query_vector.col.tolist(),
            values=query_vector.data.tolist(),
        ),
    ),

    limit=3,
)

result
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">(1, 62)
[40, 41, 58]
[0.594703257324028, 0.4292602362938498, 0.6797526647723708]
</code></pre>
<p>The above output means that the word <em>“waterproof”</em> appears at index <strong>40</strong> with a weight of <strong>0.59</strong>, <em>“running”</em> appears at index <strong>41</strong> with a weight of <strong>0.43</strong>, and so on.</p>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*ipmC4TK3wtDon7draXjeew.png" alt /></p>
<p><strong>5.2 Dense Retrieval</strong></p>
<pre><code class="lang-plaintext">bert_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

client.recreate_collection(
    collection_name="products_collection",
    vectors_config={
        "dense": models.VectorParams(   
            size=384,                 
            distance=models.Distance.COSINE
        )
    }
)
</code></pre>
<p>The collection is created to store 384 dimensional dense vectors using cosine similarity for semantic search.</p>
<pre><code class="lang-plaintext">client.upsert(
    collection_name="products_collection",
    points=[
        models.PointStruct(
            id=i,
            payload=product,
            vector={
                "dense": bert_model.encode(product["title"] + " " + product["description"]).tolist()
            }
        )
        for i, product in enumerate(products)
    ]
)
</code></pre>
<pre><code class="lang-plaintext">query_text = "waterproof running shoes"
query_vector = bert_model.encode(query_text).tolist()


result = client.search(
    collection_name="products_collection",
    query_vector=models.NamedVector(
        name="dense",           
        vector=query_vector      
    ),

    limit=3
)

result
</code></pre>
<p>Output:</p>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*0N5EIR4pQLbTSDZyZynwOg.png" alt /></p>
<p><strong>5.3 Sparse Neural Retrieval(SPLADE)</strong></p>
<pre><code class="lang-plaintext">splade_model_name = "naver/splade-cocondenser-ensembledistil"
splade_tokenizer = AutoTokenizer.from_pretrained(splade_model_name)
splade_model = AutoModelForMaskedLM.from_pretrained(splade_model_name)

client.recreate_collection(
    collection_name="products_collection",
    vectors_config={},
    sparse_vectors_config={"splade": models.SparseVectorParams()}
)



points = []
for i, product in enumerate(products):
    splade_vec = encode_splade(product["title"] + " " + product["description"])
    points.append(
        models.PointStruct(
            id=i,
            payload=product,
            vector={"splade": splade_vec}
        )
    )

client.upsert(collection_name="products_collection", points=points)
</code></pre>
<pre><code class="lang-plaintext">query_text = "waterproof running shoes"

def encode_splade(text):
    inputs = splade_tokenizer(text, return_tensors="pt", truncation=True)
    with torch.no_grad():
        logits = splade_model(**inputs).logits.squeeze()
        max_values, _ = torch.max(torch.relu(logits), dim=0)
    max_values = max_values.numpy()
    indices = np.nonzero(max_values)[0]
    values = max_values[indices]
    return models.SparseVector(indices=indices.tolist(), values=values.tolist())


query_splade_vec = encode_splade(query_text)


result = client.search(
    collection_name="products_collection",
    query_vector=models.NamedSparseVector(
        name="splade",  
        vector=models.SparseVector(
            indices=query_splade_vec.indices,
            values=query_splade_vec.values
        ),
    ),
    limit=3,

)


for item in result:
    print(item)
</code></pre>
<p>Output:</p>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*VQ2m8075GTmCW3PpedRorA.png" alt /></p>
<p><strong>5.4 Ranked Results Comparison</strong></p>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*5MZkCvhpxPTLF4ftDy1J6Q.png" alt /></p>
<p>Lexical retrieval matches exact keywords such as “waterproof” and “running” but lacks semantic understanding. As a result, it may rank <em>Woodland Trek Waterproof</em> above <em>Adidas Terrex Trail GTX</em> simply because of keyword overlap.</p>
<p>Dense vector retrieval captures semantic meaning but may overlook exact keyword matches.</p>
<p>Sparse neural vector representations (such as SPLADE) combine both strengths — they understand exact terms (e.g., “waterproof”) as well as semantically related ones (e.g., “GTX”, “trail”). Additionally, they offer faster retrieval than dense semantic models, making them both efficient and highly relevant for search applications.</p>
<h3 id="heading-6-hybrid-retrieval-for-multilingual-search-why-hybrid-approach"><strong>6. Hybrid Retrieval for Multilingual Search -Why Hybrid Approach?</strong></h3>
<blockquote>
<p><em>Sparse neural models excel at keyword matching while maintaining contextual relevance, but they do not capture cross-language semantics or translation. Dense multilingual embeddings, on the other hand, capture meaning and context across different languages but often lack precision in keyword matching.</em></p>
<p><em>A hybrid retrieval approach combines the best of both worlds — leveraging the contextual and lexical precision of sparse neural models with the semantic and multilingual understanding of dense embeddings. This results in search outcomes that are both precise and meaningfully aligned across languages.</em></p>
</blockquote>
<p>Let’s understand this through a practical e-commerce use case:</p>
<p>Multilingual E-commerce Product Dataset:</p>
<pre><code class="lang-plaintext">products = [
    {
        "id": 1,
        "title": "Waterproof hiking backpack 40L",
        "desc": "Durable backpack with rain cover, ideal for trekking and outdoor use",
        "brand": "TrailPro", "category": "Backpacks", "price": 3499, "lang": "en"
    },
    {
        "id": 2,
        "title": "Leather fashion handbag for women",
        "desc": "Premium leather handbag for daily use",
        "brand": "Modish", "category": "Handbags", "price": 4999, "lang": "en"
    },
    {
        "id": 3,
        "title": "जलरोधक ट्रेकिंग बैकपैक 35L",
        "desc": "बारिश के लिए रेन कवर सहित हल्का ट्रेकिंग बैग",
        "brand": "TrailPro", "category": "Backpacks", "price": 3299, "lang": "hi"
    },
    {
        "id": 4,
        "title": "ब्लूटूथ वायरलेस हेडफ़ोन",
        "desc": "लंबी बैटरी लाइफ वाले शोर रद्द करने वाले हेडफ़ोन",
        "brand": "SoundBeats", "category": "Headphones", "price": 2599, "lang": "hi"
    },
    {
        "id": 5,
        "title": "Bluetooth noise-cancelling headphones",
        "desc": "Wireless over-ear headphones with long battery life and ANC",
        "brand": "SoundBeats", "category": "Headphones", "price": 2699, "lang": "en"
    },
]
</code></pre>
<p>Loading Multilingual Dense and Sparse Neural Models for Hybrid Retrieval:</p>
<pre><code class="lang-plaintext">from transformers import AutoTokenizer, AutoModelForMaskedLM
dense_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
tokenizer = AutoTokenizer.from_pretrained("naver/splade_v2_distil")
sparse_model = AutoModelForMaskedLM.from_pretrained("naver/splade_v2_distil")

def encode_splade(text):
    inputs = splade_tokenizer(text, return_tensors="pt", truncation=True)
    with torch.no_grad():
        logits = splade_model(**inputs).logits.squeeze()
        max_values, _ = torch.max(torch.relu(logits), dim=0)
    max_values = max_values.numpy()
    indices = np.nonzero(max_values)[0]
    values = max_values[indices]
    return models.SparseVector(indices=indices.tolist(), values=values.tolist())
</code></pre>
<p>Defining Collection:</p>
<pre><code class="lang-plaintext">from qdrant_client import QdrantClient

client = QdrantClient(":memory:")
client.recreate_collection(
    collection_name="products_collection_multilingual",
    vectors_config={"dense": VectorParams(size=384, distance=Distance.COSINE)},
    sparse_vectors_config={"sparse": models.SparseVectorParams()}
)
</code></pre>
<p>Adding data to collection:</p>
<pre><code class="lang-plaintext">from qdrant_client.models import PointStruct, SparseVector, VectorParams, Distance
points = []
for i, product in enumerate(products):
    points.append(
        models.PointStruct(
            id=i,
            vector={"dense": dense_model.encode(product["title"] + " " + product["desc"]).tolist(),"sparse":encode_splade(product["title"] + " " + product["desc"])},

            payload={
                "title": product["title"],
                "brand": product["brand"],
                "category": product["category"],
                "price": product["price"],
                "lang": product["lang"]
            }
        )
    )
client.upsert(collection_name="products_collection_multilingual", points=points)
</code></pre>
<p>Searching Query:</p>
<pre><code class="lang-plaintext">query_text = "earphones"


query_splade_vec = encode_splade(query_text)


result_sparse = client.search(
    collection_name="products_collection_multilingual",
    query_vector=models.NamedSparseVector(
        name="sparse",
        vector=models.SparseVector(
            indices=query_splade_vec.indices,
            values=query_splade_vec.values
        ),
    ),
    limit=3,

)

query_vector = dense_model.encode(query_text).tolist()


result_dense = client.search(
    collection_name="products_collection_multilingual",
    query_vector=models.NamedVector(
        name="dense",
        vector=query_vector
    ),

    limit=3
)


-------------Helper Functions-------------
def normalize_scores(points):
    """
    Normalize scores from any retriever (dense or sparse) to [0, 1].
    Works even if scores vary wildly in scale or sign.
    """
    if not points:
        return {}

    scores = [p.score for p in points]
    min_s, max_s = min(scores), max(scores)


    denom = (max_s - min_s) if (max_s - min_s) != 0 else 1e-9


    return {p.id: (p.score - min_s) / denom for p in points}


def hybrid_score_fusion(result_sparse, result_dense, w_sparse=0.5, w_dense=0.5):
    """
    Combine sparse and dense search results using normalized weighted fusion.
    Parameters:
        w_sparse : float - weight for sparse retriever
        w_dense  : float - weight for dense retriever
    Returns:
        list[tuple(id, combined_score)] sorted descending
    """

    sparse_norm = normalize_scores(result_sparse.points if hasattr(result_sparse, "points") else result_sparse)
    dense_norm  = normalize_scores(result_dense.points  if hasattr(result_dense,  "points")  else result_dense)

    all_ids = set(sparse_norm.keys()) | set(dense_norm.keys())
    combined = {}

    for pid in all_ids:
        s_score = sparse_norm.get(pid, 0.0)
        d_score = dense_norm.get(pid, 0.0)
        combined[pid] = (w_sparse * s_score) + (w_dense * d_score)


    fused = sorted(combined.items(), key=lambda x: x[1], reverse=True)
    return fused


fusion = hybrid_score_fusion(result_sparse, result_dense, w_sparse=0.4, w_dense=0.6)
print("\nHYBRID RESULTS (Normalized Score Fusion):")
for pid, score in fusion:
    doc = client.retrieve("products_collection_multilingual", ids=[pid])[0]
    print(f"→ {doc.payload['title']} | ₹{doc.payload['price']} | combined_score={score:.4f}")
</code></pre>
<p>Output:</p>
<p>Press enter or click to view image in full size</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1050/1*kJAdOHieHmshV_q2hEA-Cg.png" alt /></p>
<p>The user query is processed through a sparse neural model for keyword precision and semantic understanding, and a dense multilingual model for semantic and cross-language comprehension.</p>
<p>Since both models produce scores on different scales, the scores are normalized to a common range [0,1] to ensure fair contribution from both systems during result fusion.</p>
<p>Weighted score blending then combines the lexical precision of the sparse model with the semantic depth of the dense model, producing a unified and balanced ranking.</p>
<p>The final hybrid results outperform individual models, demonstrating cross-lingual accuracy, context-aware retrieval, and high precision for multilingual product search in e-commerce.</p>
<h3 id="heading-7-conclusion"><strong>7. Conclusion</strong></h3>
<p>In modern e-commerce, users expect search results that <em>think like they do</em> — relevant, personalized, and multilingual. Traditional keyword-based search (lexical retrieval) hits exact matches but misses intent. Dense vector search captures meaning but can overlook crucial keywords and domain nuances. Sparse neural retrieval brings the best of both worlds — combining keyword precision with semantic intelligence to deliver results that feel natural and context-aware.</p>
<p>This is where <a target="_blank" href="https://qdrant.tech/documentation/">Qdrant</a> steps in as the engine behind next-generation product discovery. It unifies sparse, dense, and hybrid retrieval in a single, developer-friendly framework: no complex setup, no trade-offs. By fusing sparse inverted indexing with optimized ANN search, Qdrant makes hybrid queries blazing fast, often returning results in milliseconds even across millions of products.</p>
<p>For developers, this means you can build intelligent, low-latency, multilingual search systems that scale effortlessly — whether you’re recommending sneakers, gadgets, or luxury travel gear. Qdrant lets you move beyond search that just <em>matches words</em> to search that truly <em>understands meaning.</em></p>
<h3 id="heading-8-references"><strong>8. References</strong></h3>
<ul>
<li><p><a target="_blank" href="https://qdrant.tech/documentation/">Qdrant Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/qdrant/qdrant">Qdrant GitHub Repository</a></p>
</li>
<li><p><a target="_blank" href="https://qdrant.tech/articles/hybrid-search/">Hybrid Search: Combining Full-Text and Vector Search in Qdrant</a></p>
</li>
<li><p><a target="_blank" href="https://qdrant.tech/articles/sparse-vectors/">Sparse Vectors in Qdrant</a></p>
</li>
</ul>
<p><strong>Note:</strong> This article was first published on Medium at <a target="_blank" href="https://medium.com/p/39b813d78684">https://medium.com/p/39b813d78684</a> .</p>
]]></content:encoded></item></channel></rss>