Enable fuzzy full-text search with @hana.fullTextIndex, use OData $search, and tune FUZZY() thresholds.
@hana.fullTextIndex$search and contains() predicatesLIKE '%search%' scans every row, every character. HANA full-text search uses an inverted index — the same technique as Elasticsearch — that answers text searches in milliseconds even on 100 million rows. It also handles synonyms, inflections, and typos.
entity Products {
key ID : UUID;
name : String @hana.fullTextIndex;
description : LargeString @hana.fullTextIndex: {
fuzzySearchIndex: true, // tolerate typos
text_analysis: true, // language-aware tokenization
fast: true // FAST index (better performance, more space)
};
technicalSpecs : String; // not indexed — not searched
}
Once the full-text index is in place, standard OData $search works:
GET /catalog/Products?$search=machine learning
CAP translates $search into HANA CONTAINS(*) automatically for full-text indexed entities.
# Exact phrase
GET /catalog/Products?$filter=contains(description,'machine learning')
# Fuzzy search (0.8 = 80% similarity threshold)
GET /catalog/Products?$filter=search.ismatch('machin lernig','description','full','false','0.8')
const db = await cds.connect.to('db')
this.on('searchProducts', async (req) => {
const { query, fuzzy = 0.8 } = req.data
const results = await db.run(`
SELECT TOP 20
ID, name, description,
SCORE() AS relevance
FROM PRODUCTS
WHERE CONTAINS(name, description, ?, FUZZY(${fuzzy}))
ORDER BY SCORE() DESC
`, [query])
return results
})
| Parameter | Range | Effect |
|---|---|---|
FUZZY(0.9) |
0.0–1.0 | Higher = stricter match |
FUZZY(0.7, 'textSearch=compare') |
— | Error-tolerant text matching |
FUZZY(0.8, 'similarCalculationMode=compare') |
— | Character comparison mode |
entity Articles {
title : String @hana.fullTextIndex: { text_analysis: true };
// text_analysis enables:
// - stemming (run/running/ran → run)
// - stop word filtering (the, a, is)
// - language detection per document
}
-- After text_analysis is enabled
SELECT * FROM ARTICLES
WHERE CONTAINS(title, 'running', LINGUISTIC);
-- Matches: run, running, ran, runner (stemmed matches)
| Scenario | Recommendation |
|---|---|
| Short strings (< 100 chars) | @hana.fullTextIndex (basic) |
| Long text, typo tolerance | @hana.fullTextIndex: { fuzzySearchIndex: true } |
| Multilingual content | text_analysis: true |
| Maximum performance | fast: true (uses more disk space) |
| Structured data (IDs, codes) | Don't use full-text index; use standard index instead |
GET /catalog/SearchResults?$search=enterprise&$select=type,title,snippet
// Handler aggregates results from multiple entities
this.on('READ', 'SearchResults', async (req) => {
const { '$search': query } = req._.req.query
const [products, docs, faq] = await Promise.all([
db.run(`SELECT ID,'product' AS type,name AS title FROM PRODUCTS WHERE CONTAINS(name,?)`, [query]),
db.run(`SELECT ID,'doc' AS type,title FROM DOCUMENTS WHERE CONTAINS(title,description,?)`, [query]),
db.run(`SELECT ID,'faq' AS type,question AS title FROM FAQ WHERE CONTAINS(question,answer,?)`, [query])
])
return [...products, ...docs, ...faq]
.sort((a, b) => (b.SCORE || 0) - (a.SCORE || 0))
.slice(0, 20)
})
Full-text index annotations are ignored on SQLite. Test $search on HANA Dev instance:
# Run against HANA (not SQLite)
CDS_ENV=staging cds watch
# Then test: GET http://localhost:4004/catalog/Products?$search=machine
@hana.fullTextIndex: { fuzzySearchIndex: true } to title and description on BooksGET /catalog/Books?$search=cloud programmingprogaming (typo) — should still return CAP booksSCORE() to the result by wrapping the OData call with a native SQL handler@hana.fullTextIndex annotation enables HANA full-text indexing$search=term maps to CONTAINS(*) automaticallyFUZZY(0.8) threshold: 0.0 = anything matches, 1.0 = exact only