Exact match queries

Perform simple exact match queries

An exact match query allows you to select all documents where a field matches a specific value.

You can use exact match queries on several field types. The query syntax varies depending on the type.

The examples in this article use a schema with the following fields:

Field name Field type
description TEXT
condition TAG
price NUMERIC

You can find more details about creating the index and loading the demo data in the quick start guide.

Numeric field

To perform an exact match query on a numeric field, you need to construct a range query with the same start and end value:

FT.SEARCH index "@field:[value value]"

or

FT.SEARCH index "@field:[value]" DIALECT 2 # requires v2.10

or

FT.SEARCH index "@field==value" DIALECT 2 # requires v2.10

As described in the article about range queries, you can also use the FILTER argument:

FT.SEARCH index "*" FILTER field start end

The following examples show you how to query for bicycles with a price of exactly 270 USD:

Foundational: Query numeric fields for exact matches using range syntax or equality operators when you need to find documents with specific numeric values
> FT.SEARCH idx:bicycle "@price:[270 270]"
1) (integer) 1
2) "bicycle:0"
3) 1) "$"
   2) "{\"pickup_zone\":\"POLYGON((-74.0610 40.7578, ...

> FT.SEARCH idx:bicycle "@price:[270]" # requires v2.10
1) (integer) 1
2) "bicycle:0"
3) 1) "$"
   2) "{\"pickup_zone\":\"POLYGON((-74.0610 40.7578, ...

> FT.SEARCH idx:bicycle "@price==270" # requires v2.10
1) (integer) 1
2) "bicycle:0"
3) 1) "$"
   2) "{\"pickup_zone\":\"POLYGON((-74.0610 40.7578, ...

> FT.SEARCH idx:bicycle "*" FILTER price 270 270
1) (integer) 1
2) "bicycle:0"
3) 1) "$"
   2) "{\"pickup_zone\":\"POLYGON((-74.0610 40.7578, ...
res = index.search(Query("@price:[270 270]"))
print(res.total)
# >>> 1

try:
    res = index.search(Query("@price:[270]")) # not yet supported in redis-py
    print(res.total)
    # >>> 1
    assert res.total == 1
except Exception:
    print("'@price:[270]' syntax not yet supported.")

try:
    res = index.search(Query("@price==270")) # not yet supported in redis-py
    print(res.total)
    # >>> 1
    assert res.total == 1
except Exception:
    print("'@price==270' syntax not yet supported.")

query = Query("*").add_filter(NumericFilter("price", 270, 270))
res = index.search(query)
print(res.total)
# >>> 1
const res1 = await client.ft.search('idx:bicycle', '@price:[270 270]');
console.log(res1.total); // >>> 1

try {
    const res2 = await client.ft.search('idx:bicycle', '@price:[270]');
    console.log(res2.total); // >>> 1
    assert.strictEqual(res2.total, 1);
} catch (err) {
    console.log("'@price:[270]' syntax not yet supported.");
}

try {
    const res3 = await client.ft.search('idx:bicycle', '@price==270');
    console.log(res3.total); // >>> 1
    assert.strictEqual(res3.total, 1);
} catch (err) {
    console.log("'@price==270' syntax not yet supported.");
}

// FILTER is not supported
// const res4 = await client.ft.search('idx:bicycle', '*', {
//   FILTER: {
//       field: 'price',
//       min: 270,
//       max: 270,
//   }
// });
// console.log(res4.total); // >>> 1
        SearchResult res1 = jedis.ftSearch("idx:bicycle", "@price:[270 270]");
        System.out.println(res1.getTotalResults()); // >>> 1

        List<Document> docs1 = res1.getDocuments();

        for (int i = 0; i < docs1.size(); i++) {
            System.out.println(docs1.get(i).getId());
        }
        // >>> bicycle:0

        SearchResult res2 = jedis.ftSearch("idx:bicycle",
            "*",
            FTSearchParams.searchParams()
                    .filter("price", 270, 270)
        );
        System.out.println(res2.getTotalResults()); // >>> 1

        List<Document> docs2 = res2.getDocuments();

        for (int i = 0; i < docs2.size(); i++) {
            System.out.println(docs2.get(i).getId());
        }
        // >>> bicycle:0
            CompletableFuture<SearchReply<String, String>> priceResults = asyncCommands.ftSearch("idx:bicycle", "@price:[270]")

                    .thenApply(res -> {

                        res.getResults().forEach(doc -> {
                            System.out.printf("ID: %s\n", doc.getId());
                        });
                        // >>> ID: bicycle:0

                        return res;
                    }).toCompletableFuture();
            Mono<SearchReply<String, String>> priceResults = reactiveCommands.ftSearch("idx:bicycle", "@price:[270]")
                    .doOnNext(res -> {
                        res.getResults().forEach(doc -> {
                            System.out.printf("ID: %s\n", doc.getId());
                        });
                        // >>> ID: bicycle:0
                    });
	res1, err := rdb.FTSearch(ctx,
		"idx:bicycle", "@price:[270 270]",
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res1.Total) // >>> 1

	for _, doc := range res1.Docs {
		fmt.Println(doc.ID)
	}
	// >>> bicycle:0

	res2, err := rdb.FTSearchWithArgs(ctx,
		"idx:bicycle",
		"*",
		&redis.FTSearchOptions{
			Filters: []redis.FTSearchFilter{
				{
					FieldName: "price",
					Min:       270,
					Max:       270,
				},
			},
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res2.Total) // >>> 1

	for _, doc := range res2.Docs {
		fmt.Println(doc.ID)
	}
	// >>> bicycle:0
        SearchResult res1 = db.FT().Search(
            "idx:bicycle",
            new("@price:[270 270]")
        );
        Console.WriteLine(res1.TotalResults); //    >>> 1

        SearchResult res2 = db.FT().Search(
            "idx:bicycle",
            new Query().AddFilter(
                new Query.NumericFilter("price", 270, 270)
            )
        );
        Console.WriteLine(res2.TotalResults); //    >>> 1

Tag field

A tag is a short sequence of text, for example, "new" or "Los Angeles".

Important:
If you need to query for short texts, use a tag query instead of a full-text query. Tag fields are more space-efficient for storing index entries and often lead to lower query complexity for exact match queries.

You can construct a tag query for a single tag in the following way:

FT.SEARCH index "@field:{tag}"
Note:
The curly brackets are mandatory for tag queries.

This short example shows you how to query for new bicycles:

Foundational: Query tag fields for exact matches using curly bracket syntax when you need to find documents with specific categorical values
> FT.SEARCH idx:bicycle "@condition:{new}"
 1) (integer) 5
 2) "bicycle:0"
 3) 1) "$"
    2) "{\"pickup_zone\":\"POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, -74.0610 40.6678, -74.0610 40.7578))\",\"store_location\":\"-74.0060,40.7128\",\"brand\":\"Velorim\",\"model\":\"Jigger\",\"price\":270,\"description\":\"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids\xe2\x80\x99 pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.\",\"condition\":\"new\"}"
 4) "bicycle:5"
 5) 1) "$"
    2) "{\"pickup_zone\":\"POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, -0.1778 51.4024, -0.1778 51.5524))\",\"store_location\":\"-0.1278,51.5074\",\"brand\":\"Breakout\",\"model\":\"XBN 2.1 Alloy\",\"price\":810,\"description\":\"The XBN 2.1 Alloy is our entry-level road bike \xe2\x80\x93 but that\xe2\x80\x99s not to say that it\xe2\x80\x99s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano\xe2\x80\x99s, this is a bike which doesn\xe2\x80\x99t break the bank and delivers craved performance.\",\"condition\":\"new\"}"
 6) "bicycle:6"
 7) 1) "$"
    2) "{\"pickup_zone\":\"POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, 2.1767 48.5516, 2.1767 48.9016))\",\"store_location\":\"2.3522,48.8566\",\"brand\":\"ScramBikes\",\"model\":\"WattBike\",\"price\":2300,\"description\":\"The WattBike is the best e-bike for people who still feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one charge. It\xe2\x80\x99s great for tackling hilly terrain or if you just fancy a more leisurely ride. With three working modes, you can choose between E-bike, assisted bicycle, and normal bike modes.\",\"condition\":\"new\"}"
 8) "bicycle:7"
 9) 1) "$"
    2) "{\"pickup_zone\":\"POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, 13.3260 52.2700, 13.3260 52.5700))\",\"store_location\":\"13.4050,52.5200\",\"brand\":\"Peaknetic\",\"model\":\"Secto\",\"price\":430,\"description\":\"If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.\",\"condition\":\"new\"}"
10) "bicycle:8"
11) 1) "$"
    2) "{\"pickup_zone\":\"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))\",\"store_location\":\"2.1734, 41.3851\",\"brand\":\"nHill\",\"model\":\"Summit\",\"price\":1200,\"description\":\"This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\xe2\x80\x99re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.\",\"condition\":\"new\"}"
res = index.search(Query("@condition:{new}"))
print(res.total)
# >>> 5
const res5 = await client.ft.search('idx:bicycle', '@condition:{new}');
console.log(res5.total); // >>> 5
        SearchResult res3 = jedis.ftSearch("idx:bicycle", "@condition:{new}");
        System.out.println(res3.getTotalResults()); // >>> 5

        List<Document> docs3 = res3.getDocuments();

        for (int i = 0; i < docs3.size(); i++) {
            System.out.println(docs3.get(i).getId());
        }
        // >>> bicycle:5
        // >>> bicycle:0
        // >>> bicycle:6
        // >>> bicycle:7
        // >>> bicycle:8
            CompletableFuture<SearchReply<String, String>> conditionResults = asyncCommands
                    .ftSearch("idx:bicycle", "@condition:{new}").thenApply(res -> {
                        res.getResults().stream().sorted((doc1, doc2) -> doc1.getId().compareTo(doc2.getId())).forEach(doc -> {
                            System.out.printf("ID: %s\n", doc.getId());
                        });
                        // >>> ID: bicycle:0
                        // >>> ID: bicycle:5
                        // >>> ID: bicycle:6
                        // >>> ID: bicycle:7
                        // >>> ID: bicycle:8

                        return res;
                    }).toCompletableFuture();
            Mono<SearchReply<String, String>> conditionResults = reactiveCommands.ftSearch("idx:bicycle", "@condition:{new}")
                    .doOnNext(res -> {
                        res.getResults().stream().sorted((doc1, doc2) -> doc1.getId().compareTo(doc2.getId())).forEach(doc -> {
                            System.out.printf("ID: %s\n", doc.getId());
                        });
                        // >>> ID: bicycle:0
                        // >>> ID: bicycle:5
                        // >>> ID: bicycle:6
                        // >>> ID: bicycle:7
                        // >>> ID: bicycle:8
                    });
	res3, err := rdb.FTSearch(ctx,
		"idx:bicycle", "@condition:{new}",
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res3.Total) // >>> 5

	docs := res3.Docs
	slices.SortFunc(docs, func(a, b redis.Document) int {
		return strings.Compare(a.ID, b.ID)
	})

	for _, doc := range docs {
		fmt.Println(doc.ID)
	}
	// >>> bicycle:0
	// >>> bicycle:5
	// >>> bicycle:6
	// >>> bicycle:7
	// >>> bicycle:8
        SearchResult res3 = db.FT().Search(
            "idx:bicycle",
            new("@condition:{new}")
        );
        Console.WriteLine(res3.TotalResults);   // >>> 4

Use double quotes and DIALECT 2 for exact match queries involving tags that contain special characters. As of v2.10, the only character that needs escaping in queries involving double-quoted tags is the double-quote character. Here's an example of using double-quoted tags that contain special characters:

Tag fields: Query tag fields with special characters using double-quoted syntax and DIALECT 2 when you need to match tags containing symbols or spaces
> FT.CREATE idx:email ON JSON PREFIX 1 key: SCHEMA $.email AS email TAG
OK
> JSON.SET key:1 $ '{"email": "[email protected]"}'
OK
> FT.SEARCH idx:email '@email:{"[email protected]"}' DIALECT 2
1) (integer) 1
2) "key:1"
3) 1) "$"
   2) "{\"email\":\"[email protected]\"}"
schema = (
    TagField("$.email", as_name="email")
)

idx_email = r.ft("idx:email")
idx_email.create_index(
    schema,
    definition=IndexDefinition(prefix=["key:"], index_type=IndexType.JSON),
)
r.json().set('key:1', Path.root_path(), '{"email": "[email protected]"}')

try:
    res = idx_email.search(Query("[email protected]").dialect(2))
    print(res)
except Exception:
    print("'[email protected]' syntax not yet supported.")
await client.ft.create('idx:email', {
  '$.email': {
    type: SCHEMA_FIELD_TYPE.TAG,
    AS: 'email'
  }
}, {
  ON: 'JSON',
  PREFIX: 'key:'
})

await client.json.set('key:1', '$', { email: '[email protected]' });

try {
    const res6 = await client.ft.search('idx:email', '[email protected]', { DIALECT: 2 });
    console.log(res6);
} catch (err) {
    console.log("'[email protected]' syntax not yet supported.");
}
        SchemaField[] emailSchema = {
            TextField.of("$.email").as("email")
        };

        jedis.ftCreate("idx:email", 
            new FTCreateParams()
                    .addPrefix("key:")
                    .on(IndexDataType.JSON),
            emailSchema
        );

        jedis.jsonSet("key:1", Path2.ROOT_PATH, "{\"email\": \"[email protected]\"}");
        
        SearchResult res4 = jedis.ftSearch("idx:email",
            RediSearchUtil.escapeQuery("@email{[email protected]}"),
            new FTSearchParams().dialect(2)
        );
        System.out.println(res4.getTotalResults());
            List<FieldArgs<String>> emailSchema = Arrays
                    .asList(TagFieldArgs.<String> builder().name("$.email").as("email").build());
            CreateArgs<String, String> emailCreateArgs = CreateArgs.<String, String> builder().on(CreateArgs.TargetType.JSON)
                    .withPrefix("key:").build();

            CompletableFuture<SearchReply<String, String>> emailResults = asyncCommands
                    .ftCreate("idx:email", emailCreateArgs, emailSchema).thenCompose(res -> {
                        System.out.println(res); // >>> OK
                        return asyncCommands.jsonSet("key:1", JsonPath.ROOT_PATH,
                                parser.createJsonObject().put("email", parser.createJsonValue("\"[email protected]\"")));
                    }).thenCompose(res2 -> {
                        System.out.println(res2); // >>> OK
                        return asyncCommands.ftSearch("idx:email", "@email:{test\\@redis\\.com}");
                    }).thenApply(res3 -> {
                        System.out.println(res3.getResults().size()); // >>> 1
                        System.out.println(res3.getResults().get(0).getId());
                        // >>> key:1
                        return res3;
                    }).toCompletableFuture();
            List<FieldArgs<String>> emailSchema = Arrays
                    .asList(TagFieldArgs.<String> builder().name("$.email").as("email").build());

            CreateArgs<String, String> emailCreateArgs = CreateArgs.<String, String> builder().on(CreateArgs.TargetType.JSON)
                    .withPrefix("key:").build();

            Mono<Void> emailResults = reactiveCommands.ftCreate("idx:email", emailCreateArgs, emailSchema).doOnNext(res -> {
                System.out.println(res); // >>> OK
            }).flatMap(res -> reactiveCommands.jsonSet("key:1", JsonPath.ROOT_PATH,
                    parser.createJsonObject().put("email", parser.createJsonValue("\"[email protected]\"")))).doOnNext(res -> {
                        System.out.println(res); // >>> OK
                    }).flatMap(res -> reactiveCommands.ftSearch("idx:email", "@email:{test\\@redis\\.com}")).doOnNext(res -> {
                        System.out.println(res.getResults().size()); // >>> 1
                        System.out.println(res.getResults().get(0).getId());
                        // >>> key:1
                    }).then();
	res4, err := rdb.FTCreate(ctx,
		"idx:email",
		&redis.FTCreateOptions{
			OnJSON: true,
			Prefix: []interface{}{"key:"},
		},
		&redis.FieldSchema{
			FieldName: "$.email",
			As:        "email",
			FieldType: redis.SearchFieldTypeTag,
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res4) // >>> OK

	res5, err := rdb.JSONSet(ctx, "key:1", "$",
		map[string]interface{}{
			"email": "[email protected]",
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res5) // >>> OK

	res6, err := rdb.FTSearch(ctx, "idx:email",
		"@email:{test\\@redis\\.com}",
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res6.Total) // >>> 1
        Schema emailSchema = new Schema()
            .AddTagField(new FieldName("$.email", "email"));

        FTCreateParams emailParams = new FTCreateParams()
            .AddPrefix("key:")
            .On(IndexDataType.JSON);

        db.FT().Create("idx:email", emailParams, emailSchema);

        db.JSON().Set("key:1", "$", "{\"email\": \"[email protected]\"}");


        SearchResult res4 = db.FT().Search(
            "idx:email",
            new Query("@email:{test\\@redis\\.com}").Dialect(2)
        );
        Console.WriteLine(res4.TotalResults);   // >>> 1

Full-text field

A detailed explanation of full-text queries is available in the full-text queries documentation. You can also query for an exact match of a phrase within a text field:

FT.SEARCH index "@field:\"phrase\""
Important:

The phrase must be wrapped by escaped double quotes for an exact match query.

You can't use a phrase that starts with a stop word.

Here is an example for finding all bicycles that have a description containing the exact text 'rough terrain':

Text fields: Query text fields for exact phrase matches using escaped double quotes when you need to find documents containing specific multi-word phrases
FT.SEARCH idx:bicycle "@description:\"rough terrain\""
1) (integer) 1
2) "bicycle:8"
3) 1) "$"
   2) "{\"pickup_zone\":\"POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, 1.9450 41.1987, 1.9450 41.4301))\",\"store_location\":\"2.1734, 41.3851\",\"brand\":\"nHill\",\"model\":\"Summit\",\"price\":1200,\"description\":\"This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail in mountains on the weekends or you\xe2\x80\x99re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.\",\"condition\":\"new\"}"
res = index.search(Query("@description:\"rough terrain\""))
print(res.total)
# >>> 1 (Result{1 total, docs: [Document {'id': 'bicycle:8'...)
const res7 = await client.ft.search('idx:bicycle', '@description:"rough terrain"');
console.log(res7.total); // >>> 1 (Result{1 total, docs: [Document {'id': 'bicycle:8'...)
        SearchResult res5 = jedis.ftSearch("idx:bicycle",
            "@description:\"rough terrain\""
        );
        System.out.println(res5.getTotalResults()); // >>> 1

        List<Document> docs5 = res5.getDocuments();

        for (int i = 0; i < docs5.size(); i++) {
            System.out.println(docs5.get(i).getId());
        }
        // >>> bicycle:8
            CompletableFuture<SearchReply<String, String>> textMatchResults = asyncCommands
                    .ftSearch("idx:bicycle", "@description:\"rough terrain\"").thenApply(res -> {
                        res.getResults().forEach(doc -> {
                            System.out.printf("ID: %s\n", doc.getId());
                        });
                        // >>> ID: bicycle:8

                        return res;
                    }).toCompletableFuture();
            Mono<SearchReply<String, String>> textMatchResults = reactiveCommands
                    .ftSearch("idx:bicycle", "@description:\"rough terrain\"").doOnNext(res -> {
                        res.getResults().forEach(doc -> {
                            System.out.printf("ID: %s\n", doc.getId());
                        });
                        // >>> ID: bicycle:8
                    });
	res7, err := rdb.FTSearch(ctx,
		"idx:bicycle", "@description:\"rough terrain\"",
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(res7.Total) // >>> 1

	for _, doc := range res7.Docs {
		fmt.Println(doc.ID)
	}
	// >>> bicycle:8
        SearchResult res5 = db.FT().Search(
            "idx:bicycle",
            new("@description:\"rough terrain\"")
        );
        Console.WriteLine(res5.TotalResults);   // >>> 1
RATE THIS PAGE
Back to top ↑