<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx" -->

---
title: Your ODC Semantic Search Cannot Find ERR-BM25–003. Here...
description: Semantic search fails on exact identifiers like error codes because embedding models encode meaning, not surface form. This post details building a hybrid...
canonical: https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Your ODC Semantic Search Cannot Find ERR-BM25–003. Here Is the Fix. | daily.dev
og:description: Semantic search fails on exact identifiers like error codes because embedding models encode meaning, not surface form. This post details building a hybrid...
og:url: https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx
og:image: https://api.daily.dev/og/posts/QPQWI2Unx.png
og:image:alt: Your ODC Semantic Search Cannot Find ERR-BM25–003. Here Is the Fix.
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Your ODC Semantic Search Cannot Find ERR-BM25–003. Here Is the Fix.

**[ITNEXT](https://daily.dev/sources/itnext)** · 15 min read · 0 upvotes · 0 comments

## Summary

Semantic search fails on exact identifiers like error codes because embedding models encode meaning, not surface form. This post details building a hybrid retrieval system inside OutSystems Developer Cloud (ODC) that combines BM25 keyword search with vector-based semantic search, fused via Reciprocal Rank Fusion (RRF). The implementation uses a C# External Logic library (BM25Engine) with a Porter stemmer, three new ODC entities (BM25Term, BM25Posting, BM25Stats), and a timer-based DocumentFrequency recomputation strategy to avoid ingestion-time drift. Testing on exact error codes, field names, and version strings showed BM25 surfacing the correct definition chunks that semantic search missed entirely. The hybrid approach preserves semantic search behavior for conceptual queries while closing the exact-term gap, with no UI changes required. Both BM25Engine and the updated RAG Knowledge Base app are available on the OutSystems Forge.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://itnext.io/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix-a0d836f28312>

## Questions this post answers

### Why does semantic search fail to find exact error codes or field names in a RAG pipeline?

Semantic search encodes meaning, not surface form. An exact identifier like ERR-BM25-003 is not a concept, so the embedding model has no basis to place it near the correct chunk — it returns thematically adjacent content instead. BM25 handles this by matching on token frequency and rarity, splitting hyphenated codes into separate tokens (err, bm25, 003) that can be indexed and matched precisely.

_Teams building RAG search over technical documentation track gaps like this on daily.dev before they reach production._

### What is Reciprocal Rank Fusion and how does it combine BM25 and vector search results?

Reciprocal Rank Fusion (RRF) combines ranked result lists without normalizing raw scores. Each chunk receives a contribution of 1/(k + rank) from each retrieval leg it appears in, where k=60. A chunk appearing in both the BM25 and vector legs gets two additive contributions and scores higher than a chunk found by only one leg. No score calibration between BM25 and cosine similarity scales is needed.

_Developers choosing between retrieval fusion strategies for their search stack find comparative discussions on daily.dev._

### Why use a timer to recompute BM25 DocumentFrequency instead of incrementing it at ingestion time?

Incrementing DocumentFrequency at ingestion accumulates drift from concurrent ingestion workflows, retry double-counting, and chunk deletions — all producing incorrect counts that compound silently. A timer recomputes from source truth on every run using COUNT(DISTINCT DocumentChunkId) GROUP BY BM25TermId. The tradeoff is a bounded staleness window between timer ticks, which is acceptable for corpora that don't change by the second.

_Engineers maintaining search index consistency in production pipelines share operational patterns like this on daily.dev._

## Similar posts on daily.dev

- [Semantic Search in OutSystems Developer Cloud](https://daily.dev/posts/semantic-search-in-outsystems-developer-cloud-j13qfe4qz) · ITNEXT · 1 upvotes · 0 comments
- [Why Vector Search Alone Isn't Enough: Hybrid Retrieval for RAG](https://daily.dev/posts/why-vector-search-alone-isn-t-enough-hybrid-retrieval-for-rag-si650vuzf) · InfoQ · 0 upvotes · 0 comments
- [I Stopped Using Vector Databases. My RAG Got Better.](https://daily.dev/posts/i-stopped-using-vector-databases-my-rag-got-better--msjc5zpqk) · Medium · 0 upvotes · 0 comments
- [Hybrid Search Explained: Combining Vector and Keyword Retrieval](https://daily.dev/posts/hybrid-search-explained-combining-vector-and-keyword-retrieval-csaektsg6) · BigData Boutique blog · 0 upvotes · 0 comments
- [Your Retrieval Metrics Are Measured Against an Incomplete Answer Key](https://daily.dev/posts/your-retrieval-metrics-are-measured-against-an-incomplete-answer-key-noroy7smo) · ITNEXT · 0 upvotes · 0 comments

---

Tags: [#rag](https://daily.dev/tags/rag), [#vector-search](https://daily.dev/tags/vector-search)

[View this post on daily.dev](https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Your ODC Semantic Search Cannot Find ERR-BM25–003. Here Is the Fix.","url":"https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx"},"datePublished":"2026-08-07T08:06:47.514Z","dateModified":"2026-08-07T08:07:22.726Z","description":"Semantic search fails on exact identifiers like error codes because embedding models encode meaning, not surface form. This post details building a hybrid...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/5196131f1b615846af2d090df0a139be?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/5196131f1b615846af2d090df0a139be?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"ITNEXT","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"ITNEXT","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/itnext2","url":"https://daily.dev/sources/itnext"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"rag,vector-search","timeRequired":"PT15M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"ITNEXT","item":"https://daily.dev/sources/itnext"},{"@type":"ListItem","position":3,"name":"Your ODC Semantic Search Cannot Find ERR-BM25–003. Here Is the Fix."}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/your-odc-semantic-search-cannot-find-err-bm25-003-here-is-the-fix--qpqwi2unx#faq","mainEntity":[{"@type":"Question","name":"Why does semantic search fail to find exact error codes or field names in a RAG pipeline?","acceptedAnswer":{"@type":"Answer","text":"Semantic search encodes meaning, not surface form. An exact identifier like ERR-BM25-003 is not a concept, so the embedding model has no basis to place it near the correct chunk — it returns thematically adjacent content instead. BM25 handles this by matching on token frequency and rarity, splitting hyphenated codes into separate tokens (err, bm25, 003) that can be indexed and matched precisely. Teams building RAG search over technical documentation track gaps like this on daily.dev before they reach production."}},{"@type":"Question","name":"What is Reciprocal Rank Fusion and how does it combine BM25 and vector search results?","acceptedAnswer":{"@type":"Answer","text":"Reciprocal Rank Fusion (RRF) combines ranked result lists without normalizing raw scores. Each chunk receives a contribution of 1/(k + rank) from each retrieval leg it appears in, where k=60. A chunk appearing in both the BM25 and vector legs gets two additive contributions and scores higher than a chunk found by only one leg. No score calibration between BM25 and cosine similarity scales is needed. Developers choosing between retrieval fusion strategies for their search stack find comparative discussions on daily.dev."}},{"@type":"Question","name":"Why use a timer to recompute BM25 DocumentFrequency instead of incrementing it at ingestion time?","acceptedAnswer":{"@type":"Answer","text":"Incrementing DocumentFrequency at ingestion accumulates drift from concurrent ingestion workflows, retry double-counting, and chunk deletions — all producing incorrect counts that compound silently. A timer recomputes from source truth on every run using COUNT(DISTINCT DocumentChunkId) GROUP BY BM25TermId. The tradeoff is a bounded staleness window between timer ticks, which is acceptable for corpora that don't change by the second. Engineers maintaining search index consistency in production pipelines share operational patterns like this on daily.dev."}}]}
```

