RAG for Hotel and Hostel Chains: The Retrieval Fixes That Moved Accuracy

A hostel chain's property pages all read alike, so search kept handing our voice agent the wrong hostel. We moved search into Postgres and measured every knob. Answers found went from 80.4% to 89.2%, and the search step from 545 ms to 2 ms.

A caller rings a hostel chain's booking line and asks, "hello sir, Manali hostel dorm bed how much, one night?" The chain runs hundreds of properties, and their pages read almost the same: one template, one brand name, the same words like "property", "dorm" and "check-in". Any hotel group that writes its property pages from a template has this problem. If search hands the agent the page for the hostel in one Manali neighbourhood instead of the next, the agent quotes the wrong price, and sounds sure of it.

On a booking line, a price is a promise. Someone either honours the wrong number or walks it back with the guest on the phone. So accuracy came first in this project. We cared about cost and latency too, but we wouldn't take a cheaper or faster search that got even one more price wrong.

This post covers how we measured retrieval for our hotel voice agent, why we moved search into the Postgres we already run, and the tuning that took answers found from 80.4% to 89.2%. Along the way, the search step fell from 545 ms to 2 ms.

Freeze a benchmark before you change anything

We froze a benchmark before touching any infrastructure, so we could score every idea on the same data.

  • The corpus: real documents from eight customer knowledge bases, exported with the embeddings they already had.
  • 240 graded questions in four groups. On the hostel chain's knowledge base, they look like this:
    • Easy questions use the document's own words: "What is the property code for the Kalpa hostel?"
    • ASR questions are what speech-to-text hands us: "sir in wayanad which place hostel is there, available?"
    • Vague questions share no keywords with the answer: "the lodging near the famous suspension bridge in Rishikesh"
    • Aggregate questions need several records at once: "Which properties are in Bangalore, and how many are there?"
  • 74 real caller questions from September call transcripts.

A question counts as a hit only when the passages we hand to the agent contain the answer. Round one scored 17 setups on 388 questions. The parameter sweep scored 77 settings on 240 questions, 18,480 retrieved contexts in all.

The hybrid search options we tested

We tested every option that could run hybrid (vector plus keyword) search, all at the settings our voice agents use in production:

SetupAnswers foundEasyASRVagueAggregateSearch p50
Pinecone hybrid (our old setup)80.4%97.9%87.5%58.3%60.4%545 ms
Valkey86.7%95.8%85.4%85.4%70.8%1.6 ms
Postgres + pgvector89.2%95.8%95.8%81.2%77.1%2.2 ms
Redis 8 FT.HYBRID90.4%95.8%95.8%83.3%81.2%1.2 ms

Redis and its FT.HYBRID command came out ahead, but not by a significant margin (p = 0.63). Postgres with pgvector was already in our stack. It meant no new service and no extra bill, and customer data stays in our own database in India. We also dropped Pinecone's bill of about $50 a month.

The biggest gains came on the questions callers ask: vague questions went from 58% to 81%, aggregate from 60% to 77%, and misheard broken English from 87.5% to 95.8%.

How the retrieval pipeline works

At upload, we split each document into chunks of about 1,000 characters at sentence boundaries and drop duplicates. Each chunk sits in one kb_chunks table with its 1,536-dimension embedding (HNSW index) and a generated tsvector (GIN index).

On every search:

  1. Keyword search starts at once, while we're still embedding the question. Embedding takes 270 ms to 1 s, and the keyword query finishes inside that window.
  2. Vector search finds the nearest chunks by cosine distance.
  3. Reciprocal Rank Fusion (k = 60) merges the two lists. A chunk ranked high in both wins, and a chunk ranked high in one still makes the cut.
  4. The distance cutoff applies only to vector results. A keyword match on an exact property code counts even when its embedding sits far from the question.
  5. Neighbour stitching adds about 150 characters from each adjacent chunk when the budget allows, so an answer split across a chunk boundary still arrives whole.
  6. We label passages as reference data, not instructions. We planted instructions in test documents. Before the label, the agent obeyed them 4 times out of 4. After, 0 out of 4.

Tuning results, knob by knob

The sweep replaced several guesses with numbers:

  • The number of chunks sent to the agent mattered most. Going from 3 chunks to 20 took answers found from 65.8% to 89.6%. For chat, raising the budget from 5 chunks and 5,000 characters to 12 chunks and 12,000 characters took it from 69.6% to 87.9%.
  • Neighbour stitching added another 4.6 points, from 87.5% to 92.1%.
  • The distance cutoff did nothing. No cutoff value beat having none, at any chunk count.

Template pages and repeated words

Hostel and hotel chains write every property page from the same template, so the same words show up on every page: the brand name, "property", "hostel", "dorm". In one knowledge base, the brand name and "property" appeared in 35 of 35 chunks.

That broke keyword search. Postgres's ts_rank_cd scores how often the query words appear, but it doesn't weight a word by how rare it is (it has no IDF, unlike BM25). Say a caller asks, "brand-name property in bangalore, which one is there?" Only "bangalore" narrows the answer, and it appears in just 2 chunks. Every chunk matched the brand name and "property", so the Bangalore pages sank down the keyword list and lost in fusion. The agent found the right brand in the wrong city.

So before the keyword search runs, we count how many of the agent's chunks contain each query word. A word found in more than half of them can't tell pages apart, and we leave it out of the keyword query. "bangalore" stays, and the brand name and "property" drop out. Three details keep this safe:

  • The vector half still sees the whole question, so the search loses no meaning. We prune only the keyword half.
  • We skip small knowledge bases. Below 4 chunks, every word looks common.
  • We cache word counts for 5 minutes, so the check adds no noticeable latency.

It ships as an "Ignore common words" switch on each agent's search tool, and the chat search log shows which words it left out.

164 of our 240 test questions had a common word dropped. Correct final chat answers rose from 67.5% to 71.7% (11 fixed, 1 broken). At voice settings, answers found rose from 90.0% to 91.7%, and vague questions from 89.6% to 95.8%. The voice gain is smaller and not yet statistically significant.

Cold starts on a voice call

We ran the benchmarks warm, and production calls often aren't. On our small staging database (about 22 MB/s disk), the first search of an uncached knowledge base took 1.3 to 3 seconds. On a voice call, the guest hears that as dead air. We fixed it without a new database:

  • Warm on connect. When a call starts, a background probe pulls the agent's chunks into Postgres's cache before the caller finishes saying hello.
  • Overlap the slow steps. Keyword search runs during embedding, and each process caches question embeddings (512 entries).
  • Stop reading vectors you don't need. The keyword query computed a vector distance it never used, reading about 6 KB of out-of-row (TOAST) data per match. Removing it cost nothing.

Warm searches now take about 8 ms. Hosted vector databases had the same cold-start problem (874 to 1,686 ms). Our next step is pgvector's halfvec type, which uses half the memory with no recall loss in our tests.

What didn't work

  • Cross-encoder reranking added about a second of latency with no clear gain in accuracy. That's too slow for voice.
  • LLM reranking with our own small model added 2.3 points at 0.43 s. We're keeping it on the bench for chat.
  • Better search can't make up for missing content. Many real callers asked about early check-in and luggage storage, and no document covered either. So we shipped a "how to write for the knowledge base" guide next to the upload screen.

Shipping it safely

Everything sits behind a per-account flag, and a backfill script copies existing documents over from Pinecone. An agent switches only once every one of its documents has arrived, so no agent ever searches half its knowledge base. Turning the flag off sends it back to Pinecone within 30 seconds.

What we'd tell another team

  1. Freeze a benchmark before you pick a vendor, and include misheard and vague questions. When the answer is a price, score accuracy first.
  2. Check what your existing database can do. pgvector, full-text search and RRF matched a dedicated engine.
  3. Measure every knob. The distance cutoff did nothing. The chunk budget, stitching and dropping repeated words did the work.

Osvi's hotel agents run this search on phone calls, on WhatsApp and in website chat. If you run a hotel or hostel group and want to see how it handles your own property pages, talk to us.

RAG for Hotel and Hostel Chains: What Worked | Osvi AI