FTSearch: Domino's Three-Tier Full-Text Search API (NotesDatabase / NotesView / NotesDocumentCollection)
You’ve got an NSF holding half a million orders, a user types “invoice” into your portal’s search box, and you need to return the matching documents. Domino’s built-in answer is FTSearch, the text-indexed search method.
Read the docs and you’ll find FTSearch lives on three classes: NotesDatabase, NotesView, and NotesDocumentCollection. Same method name, three different return types — one gives you a collection, one gives you a Long count, one returns nothing. Why design it that way? Which one do you actually call?
This is Part 1 of a three-part Domino search series — the semantic differences between the tiers, the query syntax operators, index management and its load updall -x counterpart, and one trap that lets buggy-looking code pass dev tests then quietly degrade production performance.
TL;DR
- FTSearch lives on three classes and means three different things:
NotesDatabase.FTSearchreturns a freshNotesDocumentCollection;NotesView.FTSearchfilters the view object in place and returns aLong;NotesDocumentCollection.FTSearchreduces the collection in place and returns nothing. - Missing FT index is not an error — the method still runs. The official docs literally say “works, but less efficiently”; in production that means a lot slower.
max=0doesn’t mean “no limit” — it means “default cap”, which is 5,000 documents. Going beyond requires a notes.ini override.- Query operators must be uppercase (AND / OR / NOT). Lowercase variants are treated as literal search terms — silent failure mode.
- The
*wildcard only works at the end of a term, never at the start or in the middle. CreateFTIndexonly works on local databases — server-side indexes go through admin viaload updall -x.- The three tiers map to three workflow shapes: DB-level for “search the whole database”, View-level for “filter what the user sees in the UI”, Collection-level for “structured criteria first, then text filter”.
Why three tiers?
Domino’s search API has three entry points not because it’s over-designed but because there are three different things you might already be holding:
| What you have | Method to call | What it does |
|---|---|---|
A NotesDatabase and want to search the whole thing | db.FTSearch(...) | Sweeps the database, returns a new collection |
A NotesView and want to filter what the user sees | view.FTSearch(...) | View itself becomes filtered; subsequent GetFirstDocument only sees matches |
A NotesDocumentCollection from a prior step | collection.FTSearch(...) | Trims the collection in place, keeping only matches |
All three share the same underlying full-text index (if the database has one) and the same query syntax. The only thing that varies is what goes in and what comes out.
NotesDatabase.FTSearch — search the whole database
Full signature:
Set ndc = db.FTSearch(query$, maxdocs%, [sortoptions%], [otheroptions%])| Parameter | Type | Required | Meaning |
|---|---|---|---|
query$ | String | ✅ | Full-text query string, may include operators / wildcards |
maxdocs% | Integer | ✅ | Max results to return. 0 = default cap (5,000) |
sortoptions% | Integer | optional | Sort order, constants below |
otheroptions% | Integer | optional | Extra flags, additively combined |
Returns: a NotesDocumentCollection, described verbatim in the docs as “A collection of documents that match the full-text query, sorted by the selected option”.
sortoptions constants
| Constant | Value | Behavior |
|---|---|---|
FT_SCORES | 8 | Default, sorts by relevance score |
FT_DATE_ASC | 64 | Sorts by document date ascending |
FT_DATE_DES | 32 | Sorts by document date descending |
FT_DATECREATED_ASC | 1543 | Sorts by creation date ascending |
FT_DATECREATED_DES | 1542 | Sorts by creation date descending |
otheroptions constants (combine by addition)
| Constant | Value | Behavior |
|---|---|---|
FT_FUZZY | 16384 | Fuzzy match, fault-tolerant |
FT_STEMS | 512 | Stem search (running matches run / ran) |
FT_THESAURUS | 1024 | Expand to synonyms |
FT_DATABASE | 8192 | Include Domino databases |
FT_FILESYSTEM | 4096 | Include non-database files |
Example — find documents containing “invoice”, sort by score, cap at 50, with stem search:
Dim session As New NotesSessionDim db As NotesDatabaseSet db = session.CurrentDatabase
Dim docs As NotesDocumentCollectionSet docs = db.FTSearch("invoice", 50, FT_SCORES, FT_STEMS)
Print "Found " & docs.Count & " documents"
Dim doc As NotesDocumentSet doc = docs.GetFirstDocument()Do Until doc Is Nothing Print doc.GetItemValue("Subject")(0) & _ " (score: " & doc.FTSearchScore & ")" Set doc = docs.GetNextDocument(doc)LoopNote doc.FTSearchScore — every document object carries this property after an FTSearch, integer 0–100, higher means more relevant.
NotesView.FTSearch — filter the view in place
numDocs& = view.FTSearch(query$, maxDocs%)Two big differences from the DB-level version:
- Returns
Long, not a collection — you get a count of “how many entries match” - The view object itself is mutated — subsequent
GetFirstDocument/GetNextDocumentcalls on that view only see the matching documents
To restore the full view, call view.Clear():
Dim view As NotesViewSet view = db.GetView("(AllByCustomer)")
Dim n As Longn = view.FTSearch("Acme AND status=open", 0)Print "Matched " & n & " entries in view"
' Walk filtered resultsDim doc As NotesDocumentSet doc = view.GetFirstDocument()Do Until doc Is Nothing Print " " & doc.GetItemValue("CustomerName")(0) Set doc = view.GetNextDocument(doc)Loop
' Restore the viewCall view.Clear()When the database has an FT index: results are sorted by relevance, descending. Without an index: matching documents keep the view’s original sort order; only the filtering happens.
This tier fits UI scenarios — a user types a keyword into a view’s filter box and the view live-updates — because the mutation target is the view object itself, no extra collection bookkeeping.
NotesDocumentCollection.FTSearch — chain a text filter on existing matches
Call collection.FTSearch(query$, maxDocs%)Returns void; the collection is reduced in place. The docs say “reduces the collection to those documents that match”.
Useful for chaining — first narrow by structured criteria with db.Search() (formula-based), then text-filter the survivors:
' Step 1: find all Type="Order" docs with Total > 1000Dim orders As NotesDocumentCollectionSet orders = db.Search( _ "Type = ""Order"" & Total > 1000", _ Nothing, 0)
' Step 2: within those orders, find ones containing "rush"Call orders.FTSearch("rush", 0)
Print "Large rush orders: " & orders.CountAfter filtering, the collection is sorted by relevance descending, and FTSearchScore is available on each document.
⚠️ The collection-level FTSearch has no sortopt / options parameters — only the default behavior. For custom sort, fall back to DB-level or sort the result yourself.
Query syntax cheat sheet
From the Domino full-text query operators reference, the operators you’ll actually use:
Logical operators (must be uppercase)
| Operator | Usage | Meaning |
|---|---|---|
AND | invoice AND paid | Both terms must be present |
OR | invoice OR receipt | Either term |
NOT | invoice NOT cancelled | Exclude |
Lowercase and / or get treated as literal search terms — silent failure. Always UCase() your operators before they hit the query string.
Field-scoped searches
[FieldName] CONTAINS value[FieldName] = value[FieldName] IS PRESENT ' field is non-blank[FieldName] > 100 ' numeric / date comparison[Status] = "Open" AND [Total] > 500[_CreationDate] and [_RevisionDate] are built-in metadata pseudo-fields.
Wildcards
| Syntax | Meaning | Constraint |
|---|---|---|
bench* | benchmark / benchmarks / benchmarking | * only at end of term, never start or middle |
b?nch | bench / banch / … | ? matches a single character |
Proximity operators
documents PARAGRAPH revenue ' both words in same paragraphdocuments SENTENCE revenue ' both words in same sentenceExact phrase + case
"exact phrase here" ' double quotes for literal phraseEXACTCASE Hello ' case-sensitive matchingTERMWEIGHT 2 important ' weighted term (affects score)Managing the FT index
Check whether the database is indexed
If Not db.IsFTIndexed Then Print "Warning: no FT index, search will be slow"End IfIsFTIndexed is a read-only Boolean; the database must be open first.
Building an index programmatically (local only)
CreateFTIndex takes an options bitmask, additively combined:
| Flag | Value | Meaning |
|---|---|---|
FTINDEX_ATTACHED_FILES | 1 | Index attachment text content |
FTINDEX_ENCRYPTED_FIELDS | 2 | Index encrypted fields |
FTINDEX_ALL_BREAKS | 4 | Index sentence / paragraph breaks (for PARAGRAPH / SENTENCE operators) |
FTINDEX_CASE_SENSITIVE | 8 | Case-sensitive index (for EXACTCASE) |
FTINDEX_ATTACHED_BIN_FILES | 16 | Index attachment binary (PDF / Office) |
' Build an index that captures paragraph breaks + case sensitivity' If one already exists, drop and recreateCall db.CreateFTIndex( _ FTINDEX_ALL_BREAKS + FTINDEX_CASE_SENSITIVE, _ True)⚠️ This only works on local databases. Server-side databases must be indexed by admin via load updall -x <dbname> or the database property dialog.
Update / remove
' Incremental updateCall db.UpdateFTIndex(False)
' Create if missing (local only)Call db.UpdateFTIndex(True)
' Drop the indexCall db.RemoveFTIndex()Difference between UpdateFTIndex(True) and CreateFTIndex: the former updates an existing index, creates only if missing; the latter forces a fresh build. Use the former for incremental scenarios.
Auto-update frequency
The db.FTIndexFrequency property controls how often the server auto-updates the index, with constants matching the DB property dialog (Immediate / Hourly / Daily / Scheduled).
Three common traps
1. Missing index doesn’t complain
The most insidious one — write FTSearch, it “works” in dev, then crushes production. The official docs literally say: “If the database is not full-text indexed, this method works, but less efficiently”.
Defense: production code should always check:
If Not db.IsFTIndexed Then ' Two options: ' (a) try to build one (local only, usually not applicable in prod) ' (b) raise an error so admin can react — don't silently degrade Error 1001, "DB " & db.FilePath & " has no FT index — performance will degrade"End If2. max=0 is not “unlimited”
Many developers assume db.FTSearch(query, 0) means “give me everything” — what they actually get is the first 5,000 results. The default cap is 5,000; raising it requires the FT_MAX_SEARCH_RESULTS notes.ini setting.
Defense: for large result sets in production, paginate (sort by a field + cursor through), or switch to DQL — which has no such cap, covered in the follow-up.
3. View.FTSearch leaves the view filtered
' ❌ Wrong: view object never cleared, next caller inherits filter stateCall view.FTSearch("urgent", 0)' ...process results...' Done, view object falls out of scope
' ✅ Right: explicitly clear when finishedCall view.FTSearch("urgent", 0)' ...process results...Call view.Clear()Server-side agents usually get away with this (view object is GC’d at scope end), but XPages or classic web apps that reuse view objects across requests will show users stale filter state from the previous search.
Connecting to the other search mechanisms
FTSearch isn’t Domino’s only search path — it’s the text-indexed path. The follow-up db.Search covers formula-based search (no index needed, brute-force scan), and the third article compares FTSearch / db.Search / DQL side-by-side as a decision guide, cross-linked with the DQL trilogy.
Domino 14 also integrated @FTSearch() as a term inside DQL queries — you can now embed a full-text predicate directly inside DQL. That relationship is detailed in the third article.
What about Java and SSJS?
| Language | Counterpart |
|---|---|
| LotusScript | NotesDatabase.FTSearch / NotesView.FTSearch / NotesDocumentCollection.FTSearch |
| Java | lotus.domino.Database.FTSearch() / View.FTSearch() / DocumentCollection.FTSearch() (drop the Notes prefix; remember to .recycle()) |
| SSJS | database.FTSearch(...) / view.FTSearch(...) / documentCollection.FTSearch(...) (directly callable inside XPages, returns wrapper objects) |
The API surface is consistent across languages — query syntax, sortopt, and options constants all share the same values (in Java they’re qualified as Database.FT_SCORES etc.).