Skip to main content

Keyword Search

Keyword search ranks records by how well their text matches your keywords, using BM25 over a text index. Use it for search boxes, exact product names, error codes and identifiers: anything where the words themselves matter more than their meaning. It works the same on RushDB Cloud and on a self-hosted engine, and needs no embedding model.

To combine keywords with meaning in one ranking, add a text leg to a vector search: see Hybrid Search.


Declare a text index​

A text index covers the records of one label and either one property, or every string of the record with property: '*' (strings inside arrays and nested objects included). It is built online; searches are accepted once its status is ready.

db.text_indexes.create({"label": "Article", "property": "body"})

# every string of the record
db.text_indexes.create({"label": "Ticket", "property": "*", "consistency": "immediate"})

for index in db.text_indexes.find().data:
print(index["name"], index["status"], index["pending"])
FieldRequiredDescription
labelyesLabel of the records to index.
propertyyesA property name, or * for every string of the record.
namenoUnique within the project. Defaults to <label>.<property>.text (all in place of *).
consistencynoeventual (default) or immediate, see below.

suggestName({ label, property }) (Python suggest_name) returns the default name a new index would get. Drop an index by name with delete(name).

Consistency​

consistencyWhen a search sees a committed writeCost
eventual (default)Shortly after the commit: a background applier indexes changes within about 250 ms.Writes and searches never wait for indexing.
immediateAlways: a search first indexes every queued change (read-your-writes).The first search after a burst of writes pays for indexing them.

Each index reports pending, the changes it has not applied yet.


db.records.textSearch() / POST /api/v1/search/text returns full records, best first. labels and where select the candidates before ranking, so a record outside the filter never takes a top slot, and total counts every match, not just the page.

result = db.records.text_search({
"query": "graph database",
"labels": ["Article"],
"where": {"status": "published"},
"limit": 10,
})

print(result.total, result.meta["textSearch"])
for record in result.data:
print(f"[{record.score:.3f}] {record.get('title')}")
ParameterTypeDefaultDescription
querystring—Keywords, in the syntax below. A blank query matches nothing.
indexstring—The text index, by name. Otherwise the one ready text index of the first label.
propertystring—Narrows the choice of text index to one over this property.
labelsstring[]—Labels to search within.
whereobject—Filter applied before ranking; every where operator works.
skipnumber0Results to skip.
limitnumber10Maximum results.
allTermsbooleanfalseEvery unmarked keyword must match (AND) instead of any (OR).
phrasebooleanfalseMatch the whole query as one phrase; the syntax below is not interpreted.

The result reports matches (records matching the keywords among the filtered ones) and pending (changes the index had not applied when the search ran): TypeScript result.textSearch, Python result.meta["textSearch"], REST meta.textSearch.

Query syntax​

SyntaxMeaningExample
a bAny term may match (OR); records matching more terms rank higher.quick fox
"a b"Phrase: the terms adjacent, in order."brown fox"
+aa must match.+graph database
-aa must not match.graph -relational
a AND b, a OR b, NOT aBoolean operators (upper case).graph AND NOT sql
( … )Grouping.(graph OR network) AND storage

Wrap terms containing :, ^, *, [, ], {, }, ! or \ in double quotes.

Scores​

__score is a BM25 score. It is comparable only within one result set: don't compare it across queries or indexes, and don't add it to vector scores. Hybrid search fuses the two rankings for you.


See also​