RediSearch is a powerful text search and secondary indexing engine, built on top of Redis as a Redis module.

Unlike Redis search libraries, it does not use Redis’ internal data structures. Using its own highly optimized data structures and algorithms, RediSearch allows for advanced search features, with high performance and a small memory footprint. It can perform simple text searches as well as complex structured queries, filtering by numeric properties and geographical distances

You can create a RediSearch client on a given index name from a Redis instance.

1 Client commands

1.1 Config

Retrieves, describes and sets runtime configuration options.


FT.CONFIG <GET|HELP> {option}
FT.CONFIG SET {option} {value}
                
            
Copy
const redis = Ax.ext.db.getRedisClient();
let client = redis.getRediSearch('index1');
console.log(client.config('SET MINPREFIX 1'));
console.log(client.config('SET xMINPREFIX 1'));
console.log(client.config('GET MINPREFIX'));
console.log(client.config('GET xMINPREFIX'));
console.log(client.config('GET *'));
true
false
1
null
{FORKGC_SLEEP_BEFORE_EXIT=0, FORK_GC_RUN_INTERVAL=10, MIN_PHONETIC_TERM_LEN=3, SAFEMODE=true, MAXEXPANSIONS=200, CURSOR_MAX_IDLE=300000, MAXDOCTABLESIZE=1000000, ON_TIMEOUT=return, GC_POLICY=fork, FRISOINI=null, FORK_GC_CLEAN_THRESHOLD=0, _MAX_RESULTS_TO_UNSORTED_MODE=1000, INDEX_THREADS=8, NOGC=false, TIMEOUT=500, SEARCH_THREADS=20, FORK_GC_RETRY_INTERVAL=10, EXTLOAD=null, MINPREFIX=1, GCSCANSIZE=100, CONCURRENT_WRITE_MODE=false}
Notice the config returns
  • boolean for SET if command is successul or false if not.
  • a string value for GET on a single valid parameter
  • a null value for GET on a single invalid parameter
  • a map for GET on a multiple options (*)

2 Indexes

Creates an index with the given spec. The index name will be used in all the key names so keep it short!

The grammar in Redis language to create an index is shown below.

FT.CREATE {index} 
    [MAXTEXTFIELDS] [TEMPORARY {seconds}] [NOOFFSETS] [NOHL] [NOFIELDS] [NOFREQS]
    [STOPWORDS {num} {stopword} ...]
    SCHEMA {field} [TEXT [NOSTEM] [WEIGHT {weight}] [PHONETIC {matcher}] | NUMERIC | GEO | TAG [SEPARATOR {sep}] ] [SORTABLE][NOINDEX] ...                
            

To add elements to and index:

FT.ADD {index} {docId} {score} 
  [NOSAVE]
  [REPLACE [PARTIAL]]
  [LANGUAGE {language}] 
  [PAYLOAD {payload}]
  [IF {condition}]
  FIELDS {field} {value} [{field} {value}...]                
            

And finally to search:

FT.SEARCH {index} {query} [NOCONTENT] [VERBATIM] [NOSTOPWORDS] [WITHSCORES] [WITHPAYLOADS] [WITHSORTKEYS]
  [FILTER {numeric_field} {min} {max}] ...
  [GEOFILTER {geo_field} {lon} {lat} {raius} m|km|mi|ft]
  [INKEYS {num} {key} ... ]
  [INFIELDS {num} {field} ... ]
  [RETURN {num} {field} ... ]
  [SUMMARIZE [FIELDS {num} {field} ... ] [FRAGS {num}] [LEN {fragsize}] [SEPARATOR {separator}]]
  [HIGHLIGHT [FIELDS {num} {field} ... ] [TAGS {open} {close}]]
  [SLOP {slop}] [INORDER]
  [LANGUAGE {language}]
  [EXPANDER {expander}]
  [SCORER {scorer}]
  [PAYLOAD {payload}]
  [SORTBY {field} [ASC|DESC]]
  [LIMIT offset num]                
            

We can use built in classes to do the same task.

Copy
<script>

    // =============================================================
    // Get a RediSearch client for a given index named "tables"
    // =============================================================
    var client = Ax.ext.db.getRedisClient().getRediSearch("tables");
    
    // =============================================================
    // Drop index (ignoring error if not exists)
    // =============================================================
    client.dropIndex(true);

    // =============================================================
    // Create a simple schema
    // =============================================================
    var sc = client.createSchema()
        .addTextField("tabname", options => {
        	options.weight = 5.0;
        	options.phonetic = "en";
        	options.type = "NUMERIC";
        })
        .addSortableNumericField("nrows");
            
    // =============================================================
    // Create the schema in the index
    // =============================================================
    client.createIndex(sc);
    
    // =============================================================
    // Load some data
    // =============================================================
    var rs = Ax.db.executeQuery("SELECT tabid, tabname, nrows FROM systables");
    for (var row of rs) {
    	client.addDocument("tabid", row);
    }
    
    // =============================================================
    // Query
    // =============================================================
    var results = client.search("sys*", q => {
    	q.limit(0, 5)
    });
    
    // =============================================================
    // The result is an Iterable<Document> SearchResult object
    // =============================================================

    for (var document of results) {
    	console.log(document.getId() + " " + document.getScore() + " " + document.getProperties());
    }

</script>
116 1 [tabname=sysbldiprovided, nrows=2.0]
115 1 [tabname=sysbldirequired, nrows=1.0]
114 1 [tabname=sysbldregistered, nrows=3.0]
113 1 [tabname=sysbldobjkinds, nrows=21.0]
112 1 [tabname=sysbldobjdepends, nrows=1273.0]

3 Suggestions

3.1 SUGADD

FT.SUGADD {key} {string} {score} [INCR] [PAYLOAD {payload}]           
    
Copy
const client = Ax.ext.db.getRedisClient().getRediSearch('key');
    // parameters (word to add, score, payload, incr)
    client.addSuggestion('word',1.0,'payload',true);
    client.addSuggestion('word 2',1.0,'payload',true);

Adds a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestions dictionaries to the user.

3.2 SUGGET

FT.SUGGET {key} {prefix} [FUZZY] [MAX num]
    
Copy
const client = Ax.ext.db.getRedisClient()
    // parameters (key, word to search , fuzzy , maximum output words)
    // fuzzy: if true, we do a fuzzy prefix search, including prefixes at Levenshtein distance of 1 from the prefix sent
    client.sugget('key', 'word', true, 10);

Gets completion suggestions for a prefix.

3.3 SUGDEL

FT.SUGDEL {key} {string}
    
Copy
const client = Ax.ext.db.getRedisClient().getRediSearch('key');
    // parameters (word to delete)
    client.delSuggestion('word');

Deletes a string from a suggestion index.

3.4 SUGLEN

FT.SUGLEN {key}
    
Copy
const client = Ax.ext.db.getRedisClient().getRediSearch('key');
    let len =client.getSuggestionLength('word');
    
    // or 
    len = Ax.ext.db.getRedisClient().suglen("sug_test")

Gets the size of an auto-complete suggestion dictionary

4 Synonyms

RediSearch allows the use of synonyms in the queries. Synonyms can be loaded and updated, but never deleted.

4.1 Add synonyms

In order to add a synonyms data structure into the REDIS index, the native function is:

FT.SYNADD <index name> <term1> <term2> ...

In Ax, the implementation is:

Copy
const client = Ax.ext.db.getRedisClient().getRediSearch('key');
client.addSynonym(['boy','child','baby']);

In this example, all three words are synonyms to eachother, at the same level, and will be returned whenever a query points to a word of the synonym dictionary. The function returns the index of the synonym list created. This index can be used to add more synonyms later.

4.2 Update synonyms

After creating a synonym list, this can be updated with more synonyms. In Redis native language, the function is:

FT.SYNUPDATE <index name> <synonym group id> <term1> <term2> ...

The 'synonym group id' corresponds to the index returned at the creation of the synonyms list. In Ax, the instructions are as follows:

Copy
const client = Ax.ext.db.getRedisClient().getRediSearch('key');
const idx = client.addSynonym(['boy','child','baby']);
client.updateSynonym(idx, 'man');

These instructions would create the synonym group, return the index, and then update this list adding one more word.

Note that the synonyms will be available only for words indexed after the creation or update of the synonyms list. Word indexed before the addition of the synonym group will not be affected when performing a query.

5 Example

Create an index with products and apply some queries to it. Let's see the results!

Copy
<script>
    // =================================================================================
    // Check if redis exists and if it is ready
    // =================================================================================
    console.log(Ax.ext.db.existRedis());
    console.log(Ax.ext.db.getRedisClient().isReady());
</script>
true
true

Create the index.

Copy
<script>
    // =================================================================================
    // Get a RediSearch client for a given index named "test_products"
    // =================================================================================
    var client = Ax.ext.db.getRedisClient().getRediSearch("test_products");
    
    // =================================================================================
    // Drop index (ignoring error if not exists)
    // =================================================================================
    client.dropIndex(true);

    // =================================================================================
    // Create the schema and create the index
    // =================================================================================
    let sc = client.createSchema()
      .addTextField("product_code", 5.0)
      .addTextField("product_name", 5.0)
      .addTextField("brand_code", 1.0)
      .addTextField("brand_desc", 1.0)

    let indexOptions =  client.createIndexOptions()
                        .setNoStopwords();

    client.createIndex(sc, indexOptions);

</script>

Add some products.

Copy
<script>
    const redis = Ax.ext.db.getRedisClient();
    var client = redis.getRediSearch("test_products");
    
    var rs = new Ax.rs.Reader().memory(options => {
    	options.setColumnNames([ "product_id", "product_code", "product_name", "brand_code", "brand_desc" ]);
    });
    rs.rows().add([ 1, 'TS001', 'Red Tshirt', 'BILL', 'Billabong' ]);
    rs.rows().add([ 2, 'TS002', 'Green Tshirt', 'BILL', 'Billabong' ]);
    rs.rows().add([ 3, 'TS003', 'Blue Tshirt', 'BILL', 'Billabong' ]);
    rs.rows().add([ 4, 'TS004', 'Red Tshirt', 'DES', 'Desigual' ]);
    rs.rows().add([ 5, 'TS005', 'Blue Tshirt', 'DES', 'Desigual' ]);
    rs.rows().add([ 6, 'JN001', 'Blue Jeans', 'DES', 'Desigual' ]);
    rs.rows().add([ 7, 'TS006', 'Blue Tshirt', 'QKS', 'Quicksilver' ]);


    for (var product of rs) {
        // We can escape special characters, for example:
        product.product_name = redis.escape(product.product_name);
        
        // Add the document to redis index
    	client.addDocument("product_id", product);
    }
    
</script>

Search products where product_name matches "blu*".

Copy
<script>
    const redis = Ax.ext.db.getRedisClient();
    var client = redis.getRediSearch("test_products");
    
    // First 2 products where product_name matches "blu*" ordered by brand_code ascending ("true" argument)
    let results = client.search("@product_name:blu*", options => {
        options.limit(0, 2)
        options.setSortBy('brand_code',true)
    });
    
    for (let document of results) {
        console.log(document.properties);
    }
    
</script>
[brand_code=BILL, product_code=TS003, product_name=Blue Tshirt, brand_desc=Billabong]
[brand_code=DES, product_code=TS005, product_name=Blue Tshirt, brand_desc=Desigual]

Aggregation. Group brands with products where product_name matches "blu*", sorted by brand concurrence and brand code.

Copy
<script>
    const redis = Ax.ext.db.getRedisClient();
    var client = redis.getRediSearch("test_products");
    
    let builder = new Ax.redis.redisearch.AggregationBuilder("@product_name:blu*");
    let sorters = [ Ax.redis.redisearch.SortedField.desc('@brand_concurrence') , Ax.redis.redisearch.SortedField.asc('@brand_code') ]
    let reducer = Ax.redis.redisearch.Reducers.count().as('brand_concurrence'); 
        builder.groupBy(['@brand_code','@brand_desc'],[reducer]);
        builder.filter('@brand_code')
        builder.sortBy(5,sorters)
        builder.limit(5)
    
    var results = client.aggregate(builder);
    console.log(results.getResults())
    
</script>
[{
  "brand_code": "DES",
  "brand_concurrence": "2",
  "brand_desc": "Desigual"
}, {
  "brand_code": "BILL",
  "brand_concurrence": "1",
  "brand_desc": "Billabong"
}, {
  "brand_code": "QKS",
  "brand_concurrence": "1",
  "brand_desc": "Quicksilver"
}]