Aggregation queries

Summary: Group and aggregate query results

Simple mapping

The `APPLY` clause allows you to apply a simple mapping function to a result set that is returned based on the query expression.

[code example]

Here is a more detailed explanation of the query syntax:

1. **Query expression**: you can use the same query expressions as you would use with the [`FT.SEARCH`]() command. You can substitute `query_expr` with any of the expressions explained in the articles of this [query topic](). Vector search queries are an exception. You can't combine a vector search with an aggregation query.
2. **Loaded fields**: if field values weren't already loaded into the aggregation pipeline, you can force their presence via the `LOAD` clause. This clause takes the number of fields (`n`), followed by the field names (`"field_1" .. "field_n"`).
3. **Mapping function**: this mapping function operates on the field values. A specific field is referenced as `@field_name` within the function expression. The result is returned as `result_field`.

The following example shows you how to calculate a discounted price for new bicycles:

#### Code Examples

Foundational: Apply mapping functions to transform field values using APPLY when you need to calculate derived values

**Difficulty:** Beginner

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

The field `__key` is a built-in field.

The output of this query is:

[code example]

Grouping with aggregation

The previous example did not group the data. You can group and aggregate data based on one or many criteria in the following way:

[code example]

Here is an explanation of the additional constructs:

1. **Grouping**: you can group by one or many fields. Each ordered sequence of field values then defines one group. It's also possible to group by values that resulted from a previous `APPLY ... AS`.
2. **Aggregation**: you must replace `AGG_FUNC` with one of the supported aggregation functions (e.g., `SUM` or `COUNT`). A complete list of functions is available in the [aggregations reference documentation](). Replace `aggregated_result_field` with a value of your choice.

The following query shows you how to group by the field `condition` and apply a reduction based on the previously derived `price_category`. The expression `@price<1000` causes a bicycle to have the price category `1` if its price is lower than 1000 USD. Otherwise, it has the price category `0`. The output is the number of affordable bicycles grouped by price category.

#### Code Examples

Grouping with aggregation: Use GROUPBY with REDUCE to group results by field values and apply aggregation functions when you need to summarize data across groups

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

[code example]

You can also create more complex aggregation pipelines with [FT.AGGREGATE](). Applying multiple reduction functions under one `GROUPBY` clause is possible. In addition, you can also chain groupings and mix in additional mapping steps (e.g., `GROUPBY ... REDUCE ... APPLY ... GROUPBY ... REDUCE`)

Aggregating without grouping

You can't use an aggregation function outside of a `GROUPBY` clause, but you can construct your pipeline in a way that the aggregation happens on a single group that spans all documents. If your documents don't share a common attribute, you can add it via an extra `APPLY` step.

Here is an example that adds a type attribute `bicycle` to each document before counting all documents with that type:

#### Code Examples

Aggregating without grouping: Create a dummy grouping field with APPLY to aggregate across all documents when you need a single aggregate result

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

The result is:

[code example]

Grouping without aggregation

It's sometimes necessary to group your data without applying a mathematical aggregation function. If you need a grouped list of values, then the `TOLIST` function is helpful.

The following example shows how to group all bicycles by `condition`:

#### Code Examples

Grouping without aggregation: Use TOLIST to collect grouped values without applying mathematical functions when you need to organize results by category

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

The output of this query is:

[code example]

FT.AGGREGATE index "query_expr" LOAD n "field_1" .. "field_n" APPLY "function_expr" AS "result_field"

FT.AGGREGATE idx:bicycle "@condition:{new}" LOAD 2 "__key" "price" APPLY "@price - (@price * 0.1)" AS "discounted"

AggregationResult res1 = db.FT().Aggregate(
            "idx:bicycle",
            new AggregationRequest("@condition:{new}")
                .Load(new FieldName("__key"), new FieldName("price"))
                .Apply("@price - (@price * 0.1)", "discounted")
        );
        Console.WriteLine(res1.TotalResults);   // >>> 5

        for (int i = 0; i < res1.TotalResults; i++)
        {
            Row res1Row = res1.GetRow(i);

            Console.WriteLine(
                $"Key: {res1Row["__key"]}, Price: {res1Row["price"]}, Discounted: {res1Row["discounted"]}"
            );
        }
        // >>> Key: bicycle:0, Price: 270, Discounted: 243
        // >>> Key: bicycle:5, Price: 810, Discounted: 729
        // >>> Key: bicycle:6, Price: 2300, Discounted: 2070
        // >>> Key: bicycle:7, Price: 430, Discounted: 387
        // >>> Key: bicycle:8, Price: 1200, Discounted: 1080

res1, err := rdb.FTAggregateWithArgs(ctx,
		"idx:bicycle",
		"@condition:{new}",
		&redis.FTAggregateOptions{
			Apply: []redis.FTAggregateApply{
				{
					Field: "@price - (@price * 0.1)",
					As:    "discounted",
				},
			},
			Load: []redis.FTAggregateLoad{
				{Field: "__key"},
				{Field: "price"},
			},
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(len(res1.Rows)) // >>> 5

	sort.Slice(res1.Rows, func(i, j int) bool {
		return res1.Rows[i].Fields["__key"].(string) <
			res1.Rows[j].Fields["__key"].(string)
	})

	for _, row := range res1.Rows {
		fmt.Printf(
			"__key=%v, discounted=%v, price=%v\n",
			row.Fields["__key"],
			row.Fields["discounted"],
			row.Fields["price"],
		)
	}
	// >>> __key=bicycle:0, discounted=243, price=270
	// >>> __key=bicycle:5, discounted=729, price=810
	// >>> __key=bicycle:6, discounted=2070, price=2300
	// >>> __key=bicycle:7, discounted=387, price=430
	// >>> __key=bicycle:8, discounted=1080, price=1200

AggregateArgs<String, String> agg1Args = AggregateArgs.<String, String> builder().load("__key").load("price")
                    .apply("@price - (@price * 0.1)", "discounted").build();

            CompletableFuture<AggregationReply<String, String>> agg1 = asyncCommands
                    .ftAggregate("idx:bicycle", "@condition:{new}", agg1Args).thenApply(result -> {
                        result.getReplies().get(0).getResults().stream()
                                .sorted((doc1, doc2) -> doc1.getFields().get("__key").compareTo(doc2.getFields().get("__key")))
                                .forEach(doc -> {
                                    System.out.printf("Key: %s, Price: %s, Discounted: %s\n", doc.getFields().get("__key"),
                                            doc.getFields().get("price"), doc.getFields().get("discounted"));
                                });
                        // >>> Key: bicycle:0, Price: 270, Discounted: 243
                        // >>> Key: bicycle:5, Price: 810, Discounted: 729
                        // >>> Key: bicycle:6, Price: 2300, Discounted: 2070
                        // >>> Key: bicycle:7, Price: 430, Discounted: 387
                        // >>> Key: bicycle:8, Price: 1200, Discounted: 1080
                        return result;
                    }).toCompletableFuture();

AggregateArgs<String, String> agg1Args = AggregateArgs.<String, String> builder().load("__key").load("price")
                    .apply("@price - (@price * 0.1)", "discounted").build();

            Mono<AggregationReply<String, String>> agg1 = reactiveCommands
                    .ftAggregate("idx:bicycle", "@condition:{new}", agg1Args).doOnNext(result -> {
                        result.getReplies().get(0).getResults().stream()
                                .sorted((doc1, doc2) -> doc1.getFields().get("__key").compareTo(doc2.getFields().get("__key")))
                                .forEach(doc -> {
                                    System.out.printf("Key: %s, Price: %s, Discounted: %s\n", doc.getFields().get("__key"),
                                            doc.getFields().get("price"), doc.getFields().get("discounted"));
                                });
                        // >>> Key: bicycle:0, Price: 270, Discounted: 243
                        // >>> Key: bicycle:5, Price: 810, Discounted: 729
                        // >>> Key: bicycle:6, Price: 2300, Discounted: 2070
                        // >>> Key: bicycle:7, Price: 430, Discounted: 387
                        // >>> Key: bicycle:8, Price: 1200, Discounted: 1080
                    });

AggregationResult res1 = jedis.ftAggregate("idx:bicycle",
            new AggregationBuilder("@condition:{new}")
                    .load("__key", "price")
                    .apply("@price - (@price * 0.1)", "discounted")
        );

        List<Row> rows1 = res1.getRows();
        System.out.println(rows1.size());   // >>> 5

        for (int i = 0; i < rows1.size(); i++) {
            System.out.println(rows1.get(i));
        }
        // >>> {__key=bicycle:0, discounted=243, price=270}
        // >>> {__key=bicycle:5, discounted=729, price=810}
        // >>> {__key=bicycle:6, discounted=2070, price=2300}
        // >>> {__key=bicycle:7, discounted=387, price=430}
        // >>> {__key=bicycle:8, discounted=1080, price=1200}

const res1 = await client.ft.aggregate('idx:bicycle', '@condition:{new}', {
  LOAD: ['__key', 'price'],
  APPLY: {
    expression: '@price - (@price * 0.1)',
    AS: 'discounted'
  }
});

console.log(res1.results.length); // >>> 5
console.log(res1.results); // >>>
//[
//  [Object: null prototype] { __key: 'bicycle:0', price: '270' },
//  [Object: null prototype] { __key: 'bicycle:5', price: '810' },
//  [Object: null prototype] { __key: 'bicycle:6', price: '2300' },
//  [Object: null prototype] { __key: 'bicycle:7', price: '430' },
//  [Object: null prototype] { __key: 'bicycle:8', price: '1200' }
//]

search = Search(r, index_name="idx:bicycle")
aggregate_request = AggregateRequest(query='@condition:{new}') \
    .load('__key', 'price') \
    .apply(discounted='@price - (@price * 0.1)')
res = search.aggregate(aggregate_request)
print(len(res.rows)) # >>> 5
print(res.rows) # >>> [['__key', 'bicycle:0', ...
#[['__key', 'bicycle:0', 'price', '270', 'discounted', '243'],
# ['__key', 'bicycle:5', 'price', '810', 'discounted', '729'],
# ['__key', 'bicycle:6', 'price', '2300', 'discounted', '2070'],
# ['__key', 'bicycle:7', 'price', '430', 'discounted', '387'],
# ['__key', 'bicycle:8', 'price', '1200', 'discounted', '1080']]

FT.AGGREGATE index "query_expr" ...  GROUPBY n "field_1" .. "field_n" REDUCE AGG_FUNC m "@field_param_1" .. "@field_param_m" AS "aggregated_result_field"

FT.AGGREGATE idx:bicycle "*" LOAD 1 price APPLY "@price<1000" AS price_category GROUPBY 1 @condition REDUCE SUM 1 "@price_category" AS "num_affordable"

AggregationResult res2 = db.FT().Aggregate(
            "idx:bicycle",
            new AggregationRequest("*")
                .Load(new FieldName("price"))
                .Apply("@price<1000", "price_category")
                .GroupBy(
                    "@condition",
                    Reducers.Sum("@price_category").As("num_affordable")
                )
        );
        Console.WriteLine(res2.TotalResults);   // >>> 3

        for (int i = 0; i < res2.TotalResults; i++)
        {
            Row res2Row = res2.GetRow(i);

            Console.WriteLine(
                $"Condition: {res2Row["condition"]}, Num affordable: {res2Row["num_affordable"]}"
            );
        }
        // >>> Condition: refurbished, Num affordable: 1
        // >>> Condition: used, Num affordable: 1
        // >>> Condition: new, Num affordable: 3

res2, err := rdb.FTAggregateWithArgs(ctx,
		"idx:bicycle", "*",
		&redis.FTAggregateOptions{
			Load: []redis.FTAggregateLoad{
				{Field: "price"},
			},
			Apply: []redis.FTAggregateApply{
				{
					Field: "@price<1000",
					As:    "price_category",
				},
			},
			GroupBy: []redis.FTAggregateGroupBy{
				{
					Fields: []interface{}{"@condition"},
					Reduce: []redis.FTAggregateReducer{
						{
							Reducer: redis.SearchSum,
							Args:    []interface{}{"@price_category"},
							As:      "num_affordable",
						},
					},
				},
			},
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(len(res2.Rows)) // >>> 3

	sort.Slice(res2.Rows, func(i, j int) bool {
		return res2.Rows[i].Fields["condition"].(string) <
			res2.Rows[j].Fields["condition"].(string)
	})

	for _, row := range res2.Rows {
		fmt.Printf(
			"condition=%v, num_affordable=%v\n",
			row.Fields["condition"],
			row.Fields["num_affordable"],
		)
	}
	// >>> condition=new, num_affordable=3
	// >>> condition=refurbished, num_affordable=1
	// >>> condition=used, num_affordable=1

AggregateArgs<String, String> agg2Args = AggregateArgs.<String, String> builder().load("price")
                    .apply("@price<1000", "price_category").groupBy(GroupBy.<String, String> of("condition")
                            .reduce(Reducer.<String, String> sum("@price_category").as("num_affordable")))
                    .build();

            CompletableFuture<AggregationReply<String, String>> agg2 = asyncCommands.ftAggregate("idx:bicycle", "*", agg2Args)
                    .thenApply(result -> {
                        result.getReplies().get(0).getResults().stream().sorted(
                                (doc1, doc2) -> doc1.getFields().get("condition").compareTo(doc2.getFields().get("condition")))
                                .forEach(doc -> {
                                    System.out.printf("Condition: %s, Num Affordable: %s\n", doc.getFields().get("condition"),
                                            doc.getFields().get("num_affordable"));
                                });
                        // >>> Condition: new, Num Affordable: 3
                        // >>> Condition: refurbished, Num Affordable: 1
                        // >>> Condition: used, Num Affordable: 1
                        return result;
                    }).toCompletableFuture();

AggregateArgs<String, String> agg2Args = AggregateArgs.<String, String> builder().load("price")
                    .apply("@price<1000", "price_category").groupBy(GroupBy.<String, String> of("condition")
                            .reduce(Reducer.<String, String> sum("@price_category").as("num_affordable")))
                    .build();

            Mono<AggregationReply<String, String>> agg2 = reactiveCommands.ftAggregate("idx:bicycle", "*", agg2Args)
                    .doOnNext(result -> {
                        result.getReplies().get(0).getResults().stream().sorted(
                                (doc1, doc2) -> doc1.getFields().get("condition").compareTo(doc2.getFields().get("condition")))
                                .forEach(doc -> {
                                    System.out.printf("Condition: %s, Num Affordable: %s\n", doc.getFields().get("condition"),
                                            doc.getFields().get("num_affordable"));
                                });
                        // >>> Condition: new, Num Affordable: 3
                        // >>> Condition: refurbished, Num Affordable: 1
                        // >>> Condition: used, Num Affordable: 1
                    });

AggregationResult res2 = jedis.ftAggregate("idx:bicycle",
            new AggregationBuilder("*")
                    .load("price")
                    .apply("@price<1000", "price_category")
                    .groupBy("@condition",
                        Reducers.sum("@price_category").as("num_affordable"))
        );

        List<Row> rows2 = res2.getRows();
        System.out.println(rows2.size());   // >>> 3

        for (int i = 0; i < rows2.size(); i++) {
            System.out.println(rows2.get(i));
        }
        // >>> {condition=refurbished, num_affordable=1}
        // >>> {condition=used, num_affordable=1}
        // >>> {condition=new, num_affordable=3}

const res2 = await client.ft.aggregate('idx:bicycle', '*', {
  LOAD: ['@price'],
  STEPS: [{
      type: FT_AGGREGATE_STEPS.APPLY,
      expression: '@price<1000',
      AS: 'price_category'
    },{
      type: FT_AGGREGATE_STEPS.GROUPBY,
      properties: '@condition',
      REDUCE:[{
        type: FT_AGGREGATE_GROUP_BY_REDUCERS.SUM,
        property: '@price_category',
        AS: 'num_affordable'
      }]
    }]
});
console.log(res2.results.length); // >>> 3
console.log(res2.results); // >>>
//[[Object: null prototype] { condition: 'refurbished', num_affordable: '1' },
//  [Object: null prototype] { condition: 'used', num_affordable: '1' },
//  [Object: null prototype] { condition: 'new', num_affordable: '3' }
//]

search = Search(r, index_name="idx:bicycle")
aggregate_request = AggregateRequest(query='*') \
    .load('price') \
    .apply(price_category='@price<1000') \
    .group_by('@condition', reducers.sum('@price_category').alias('num_affordable'))
res = search.aggregate(aggregate_request)
print(len(res.rows)) # >>> 3
print(res.rows) # >>>
#[['condition', 'refurbished', 'num_affordable', '1'],
# ['condition', 'used', 'num_affordable', '1'],
# ['condition', 'new', 'num_affordable', '3']]

1) "3"
2) 1) "condition"
   1) "refurbished"
   2) "num_affordable"
   3) "1"
3) 1) "condition"
   1) "used"
   2) "num_affordable"
   3) "1"
4) 1) "condition"
   1) "new"
   2) "num_affordable"
   3) "3"

FT.AGGREGATE idx:bicycle "*" APPLY "'bicycle'" AS type GROUPBY 1 @type REDUCE COUNT 0 AS num_total

AggregationResult res3 = db.FT().Aggregate(
            "idx:bicycle",
            new AggregationRequest("*")
                .Apply("'bicycle'", "type")
                .GroupBy("@type", Reducers.Count().As("num_total"))
        );
        Console.WriteLine(res3.TotalResults);   // >>> 1

        Row res3Row = res3.GetRow(0);
        Console.WriteLine($"Type: {res3Row["type"]}, Num total: {res3Row["num_total"]}");
        // >>> Type: bicycle, Num total: 10

res3, err := rdb.FTAggregateWithArgs(ctx,
		"idx:bicycle", "*",
		&redis.FTAggregateOptions{
			Apply: []redis.FTAggregateApply{
				{
					Field: "'bicycle'",
					As:    "type",
				},
			},
			GroupBy: []redis.FTAggregateGroupBy{
				{
					Fields: []interface{}{"@type"},
					Reduce: []redis.FTAggregateReducer{
						{
							Reducer: redis.SearchCount,
							As:      "num_total",
						},
					},
				},
			},
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(len(res3.Rows)) // >>> 1

	for _, row := range res3.Rows {
		fmt.Printf(
			"type=%v, num_total=%v\n",
			row.Fields["type"],
			row.Fields["num_total"],
		)
	}
	// type=bicycle, num_total=10

AggregateArgs<String, String> agg3Args = AggregateArgs.<String, String> builder().apply("'bicycle'", "type")
                    .groupBy(GroupBy.<String, String> of("type").reduce(Reducer.<String, String> count().as("num_total")))
                    .build();

            CompletableFuture<AggregationReply<String, String>> agg3 = asyncCommands.ftAggregate("idx:bicycle", "*", agg3Args)
                    .thenApply(result -> {
                        result.getReplies().get(0).getResults().stream()
                                .sorted((doc1, doc2) -> doc1.getFields().get("type").compareTo(doc2.getFields().get("type")))
                                .forEach(doc -> {
                                    System.out.printf("Type: %s, Total Count: %s\n", doc.getFields().get("type"),
                                            doc.getFields().get("num_total"));
                                });
                        // >>> Type: bicycle, Total Count: 10
                        return result;
                    }).toCompletableFuture();

AggregateArgs<String, String> agg3Args = AggregateArgs.<String, String> builder().apply("'bicycle'", "type")
                    .groupBy(GroupBy.<String, String> of("type").reduce(Reducer.<String, String> count().as("num_total")))
                    .build();

            Mono<AggregationReply<String, String>> agg3 = reactiveCommands.ftAggregate("idx:bicycle", "*", agg3Args)
                    .doOnNext(result -> {
                        result.getReplies().get(0).getResults().stream()
                                .sorted((doc1, doc2) -> doc1.getFields().get("type").compareTo(doc2.getFields().get("type")))
                                .forEach(doc -> {
                                    System.out.printf("Type: %s, Total Count: %s\n", doc.getFields().get("type"),
                                            doc.getFields().get("num_total"));
                                });
                        // >>> Type: bicycle, Total Count: 10
                    });

AggregationResult res3 = jedis.ftAggregate("idx:bicycle",
            new AggregationBuilder("*")
                    .apply("'bicycle'", "type")
                    .groupBy("@type", Reducers.count().as("num_total"))
        );

        List<Row> rows3 = res3.getRows();
        System.out.println(rows3.size());   // >>> 1

        for (int i = 0; i < rows3.size(); i++) {
            System.out.println(rows3.get(i));
        }
        // >>> {type=bicycle, num_total=10}

const res3 = await client.ft.aggregate('idx:bicycle', '*', {
  STEPS: [{
      type: FT_AGGREGATE_STEPS.APPLY,
      expression: "'bicycle'",
      AS: 'type'
    }, {
      type: FT_AGGREGATE_STEPS.GROUPBY,
      properties: '@type',
      REDUCE: [{
        type: FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT,
        property: null,
        AS: 'num_total'
      }]
    }]
});
console.log(res3.results.length); // >>> 1
console.log(res3.results); // >>>
//[ [Object: null prototype] { type: 'bicycle', num_total: '10' } ]

search = Search(r, index_name="idx:bicycle")
aggregate_request = AggregateRequest(query='*') \
    .apply(type="'bicycle'") \
    .group_by('@type', reducers.count().alias('num_total'))
res = search.aggregate(aggregate_request)
print(len(res.rows)) # >>> 1
print(res.rows) # >>> [['type', 'bicycle', 'num_total', '10']]

FT.AGGREGATE idx:bicycle "*" LOAD 1 "__key" GROUPBY 1 "@condition" REDUCE TOLIST 1 "__key" AS bicycles

res4, err := rdb.FTAggregateWithArgs(ctx,
		"idx:bicycle", "*",
		&redis.FTAggregateOptions{
			Load: []redis.FTAggregateLoad{
				{Field: "__key"},
			},
			GroupBy: []redis.FTAggregateGroupBy{
				{
					Fields: []interface{}{"@condition"},
					Reduce: []redis.FTAggregateReducer{
						{
							Reducer: redis.SearchToList,
							Args:    []interface{}{"__key"},
							As:      "bicycles",
						},
					},
				},
			},
		},
	).Result()

	if err != nil {
		panic(err)
	}

	fmt.Println(len(res4.Rows)) // >>> 3

	sort.Slice(res4.Rows, func(i, j int) bool {
		return res4.Rows[i].Fields["condition"].(string) <
			res4.Rows[j].Fields["condition"].(string)
	})

	for _, row := range res4.Rows {
		rowBikes := row.Fields["bicycles"].([]interface{})
		bikes := make([]string, len(rowBikes))

		for i, rowBike := range rowBikes {
			bikes[i] = rowBike.(string)
		}

		sort.Slice(bikes, func(i, j int) bool {
			return bikes[i] < bikes[j]
		})

		fmt.Printf(
			"condition=%v, bicycles=%v\n",
			row.Fields["condition"],
			bikes,
		)
	}
	// >>> condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8]
	// >>> condition=refurbished, bicycles=[bicycle:9]
	// >>> condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4]

// The `TOLIST` reducer is not currently available in Lettuce.

AggregationResult res4 = jedis.ftAggregate("idx:bicycle",
            new AggregationBuilder("*")
                    .load("__key")
                    .groupBy("@condition",
                        Reducers.to_list("__key").as("bicycles"))
        );

        List<Row> rows4 = res4.getRows();
        System.out.println(rows4.size());   // >>> 3

        for (int i = 0; i < rows4.size(); i++) {
            System.out.println(rows4.get(i));
        }
        // >>> {condition=refurbished, bicycles=[bicycle:9]}
        // >>> {condition=used, bicycles=[bicycle:3, bicycle:4, bicycle:1, bicycle:2]}
        // >>> {condition=new, bicycles=[bicycle:7, bicycle:0, bicycle:5, bicycle:6, bicycle:8]}

const res4 = await client.ft.aggregate('idx:bicycle', '*', {
  LOAD: ['__key'],
  STEPS: [{
      type: FT_AGGREGATE_STEPS.GROUPBY,
      properties: '@condition',
      REDUCE: [{
        type: FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST,
        property: '__key',
        AS: 'bicycles'
      }]
    }]
});
console.log(res4.results.length); // >>> 3
console.log(res4.results); // >>>
//[[Object: null prototype] {condition: 'refurbished', bicycles: [ 'bicycle:9' ]},
//  [Object: null prototype] {condition: 'used', bicycles: [ 'bicycle:1', 'bicycle:2', 'bicycle:3', 'bicycle:4' ]},
//  [Object: null prototype] {condition: 'new', bicycles: [ 'bicycle:5', 'bicycle:6', 'bicycle:7', 'bicycle:0', 'bicycle:8' ]}]

search = Search(r, index_name="idx:bicycle")
aggregate_request = AggregateRequest(query='*') \
    .load('__key') \
    .group_by('@condition', reducers.tolist('__key').alias('bicycles'))
res = search.aggregate(aggregate_request)
print(len(res.rows)) # >>> 3
print(res.rows) # >>>
#[['condition', 'refurbished', 'bicycles', ['bicycle:9']],
# ['condition', 'used', 'bicycles', ['bicycle:1', 'bicycle:2', 'bicycle:3', 'bicycle:4']],
# ['condition', 'new', 'bicycles', ['bicycle:5', 'bicycle:6', 'bicycle:7', 'bicycle:0', 'bicycle:8']]]

1) "3"
2) 1) "condition"
   1) "refurbished"
   2) "bicycles"
   3) 1) "bicycle:9"
3) 1) "condition"
   1) "used"
   2) "bicycles"
   3) 1) "bicycle:1"
      1) "bicycle:2"
      2) "bicycle:3"
      3) "bicycle:4"
4) 1) "condition"
   1) "new"
   2) "bicycles"
   3) 1) "bicycle:0"
      1) "bicycle:5"
      2) "bicycle:6"
      3) "bicycle:8"
      4) "bicycle:7"


Combined queries

Summary: Combine query expressions

AND

The binary operator ` ` (space) is used to intersect the results of two or more expressions.

[code example]

If you want to perform an intersection based on multiple values within a specific text field, then you should use the following simplified notion:

[code example]

The following example shows you a query that finds bicycles in new condition and in a price range from 500 USD to 1000 USD:

#### Code Examples

AND operator: Combine multiple query expressions using space to find documents matching all criteria when you need to narrow results with multiple filters

**Difficulty:** Beginner

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

You might also be interested in bicycles for kids. The query below shows you how to combine a full-text search with the criteria from the previous query:

#### Code Examples

AND with full-text: Combine full-text search with structured queries using AND when you need to find documents with specific text and matching field values

**Difficulty:** Beginner

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

OR

You can use the binary operator `|` (vertical bar) to perform a union.

[code example]

The logical `AND` takes precedence over `OR` when using dialect version two. The expression `expr1 expr2 | expr3 expr4` means `(expr1 expr2) | (expr3 expr4)`. Version one of the query dialect behaves differently. Using parentheses in query strings is advised to ensure the order is clear.

If you want to perform the union based on multiple values within a single tag or text field, then you should use the following simplified notion:

[code example]

[code example]

The following query shows you how to find used bicycles that contain either the word 'kids' or 'small':

#### Code Examples

OR operator: Combine multiple expressions using the pipe operator to find documents matching any criteria when you need to broaden results with alternatives

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

The previous query searches across all text fields. The following example shows you how to limit the search to the description field:

#### Code Examples

OR within a field: Use OR to search for multiple values within a single field when you need to match any of several options in one field

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

If you want to extend the search to new bicycles, then the below example shows you how to do that:

#### Code Examples

OR across multiple fields: Combine OR operators across different fields when you need to match alternatives in multiple field criteria

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

NOT

A minus (`-`) in front of a query expression negates the expression.

[code example]

If you want to exclude new bicycles from the search within the previous price range, you can use this query:

#### Code Examples

NOT operator: Negate query expressions using minus to exclude documents matching criteria when you need to filter out unwanted results

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Numeric filter

The [FT.SEARCH]() command allows you to combine any query expression with a numeric filter.

[code example]

Please see the [range query article]() to learn more about numeric range queries and such filters.

Pre-filter for a KNN  vector query

You can use a simple or more complex query expression with logical operators as a pre-filter in a KNN vector query.

[code example]

Here is an example:

#### Code Examples

Vector pre-filtering: Use structured queries as pre-filters before KNN vector search when you need to find similar items within a specific subset of data

**Difficulty:** Advanced

**Redis CLI:**

[code example]

**Available in:** Redis CLI, JavaScript (Node.js), Python

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

The [vector search article]() provides further details about vector queries in general.

FT.SEARCH index "@text_field:( value1 value2 ... )"

FT.SEARCH idx:bicycle "@price:[500 1000] @condition:{new}"

const res1 = await client.ft.search('idx:bicycle', '@price:[500 1000] @condition:{new}');
console.log(res1.total); // >>> 1
console.log(res1); // >>>
//{
//  total: 1,
//  documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]
//}

q = Query("@price:[500 1000] @condition:{new}")
res = index.search(q)
print(res.total) # >>> 1

FT.SEARCH idx:bicycle "kids (@price:[500 1000] @condition:{used})"

const res2 = await client.ft.search('idx:bicycle', 'kids @price:[500 1000] @condition:{used}');
console.log(res2.total); // >>> 1
console.log(res2); // >>>
// {
//   total: 1,
//   documents: [ { id: 'bicycle:2', value: [Object: null prototype] } ]
// }

q = Query("kids @price:[500 1000] @condition:{used}")
res = index.search(q)
print(res.total) # >>> 1

FT.SEARCH index "@text_field:( value1 | value2 | ... )"

FT.SEARCH index "@tag_field:{ value1 | value2 | ... }"

FT.SEARCH idx:bicycle "(kids | small) @condition:{used}"

const res3 = await client.ft.search('idx:bicycle', '(kids | small) @condition:{used}');
console.log(res3.total); // >>> 2
console.log(res3); // >>>
//{
//  total: 2,
//  documents: [
//    { id: 'bicycle:2', value: [Object: null prototype] },
//    { id: 'bicycle:1', value: [Object: null prototype] }
//  ]
//}

q = Query("(kids | small) @condition:{used}")
res = index.search(q)
print(res.total) # >>> 2

FT.SEARCH idx:bicycle "@description:(kids | small) @condition:{used}"

const res4 = await client.ft.search('idx:bicycle', '@description:(kids | small) @condition:{used}');
console.log(res4.total); // >>> 2
console.log(res4); // >>>
//{
//  total: 2,
//  documents: [
//    { id: 'bicycle:2', value: [Object: null prototype] },
//    { id: 'bicycle:1', value: [Object: null prototype] }
//  ]
//}

q = Query("@description:(kids | small) @condition:{used}")
res = index.search(q)
print(res.total) # >>> 0

FT.SEARCH idx:bicycle "@description:(kids | small) @condition:{new | used}"

const res5 = await client.ft.search('idx:bicycle', '@description:(kids | small) @condition:{new | used}');
console.log(res5.total); // >>> 3
console.log(res5); // >>>
//{
//  total: 3,
//  documents: [
//    { id: 'bicycle:1', value: [Object: null prototype] },
//    { id: 'bicycle:0', value: [Object: null prototype] },
//    { id: 'bicycle:2', value: [Object: null prototype] }
//  ]
//}

q = Query("@description:(kids | small) @condition:{new | used}")
res = index.search(q)
print(res.total) # >>> 0

FT.SEARCH idx:bicycle "@price:[500 1000] -@condition:{new}"

const res6 = await client.ft.search('idx:bicycle', '@price:[500 1000] -@condition:{new}');
console.log(res6.total); // >>> 2
console.log(res6); // >>>
//{
//  total: 2,
//  documents: [
//    { id: 'bicycle:2', value: [Object: null prototype] },
//    { id: 'bicycle:9', value: [Object: null prototype] }
//  ]
//}

q = Query("@price:[500 1000] -@condition:{new}")
res = index.search(q)
print(res.total) # >>> 2

FT.SEARCH index "expr" FILTER numeric_field start end

FT.SEARCH index "(filter_expr)=>[KNN num_neighbours @field $vector]" PARAMS 2 vector "binary_data" DIALECT 2

FT.SEARCH idx:bikes_vss "(@price:[500 1000] @condition:{new})=>[KNN 3 @vector $query_vector]" PARAMS 2 "query_vector" "Z\xf8\x15:\xf23\xa1\xbfZ\x1dI>\r\xca9..." DIALECT 2

const res7 = await client.ft.search('idx:bicycle',
  '(@price:[500 1000] -@condition:{new})=>[KNN 3 @vector $query_vector]', {
    PARAMS: { query_vector: vector_query },
    DIALECT: 2
  }
);
console.log(res7.total); // >>> 2
console.log(res7); // >>>
//{
//  total: 2,
//  documents: [
//    { id: 'bicycle:2', value: [Object: null prototype] },
//    { id: 'bicycle:9', value: [Object: null prototype] }
//  ]
//}

q = Query("(@price:[500 1000] -@condition:{new})=>[KNN 3 @vector $query_vector]").dialect(2)
# put query string here
res = index.search(q,{ 'query_vector': query_vector })
print(res.total) # >>> 2


Exact match queries

Summary: Perform simple exact match queries

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:

[code example]

As described in the [article about range queries](), you can also use the `FILTER` argument:

[code example]

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

#### Code Examples

Foundational: Query numeric fields for exact matches using range syntax or equality operators when you need to find documents with specific numeric values

**Difficulty:** Beginner

**Commands:** FT.SEARCH

**Complexity:**
- FT.SEARCH: O(N)

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Tag field

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

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:

[code example]

The curly brackets are mandatory for tag queries.

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

#### Code Examples

Foundational: Query tag fields for exact matches using curly bracket syntax when you need to find documents with specific categorical values

**Difficulty:** Beginner

**Commands:** FT.SEARCH

**Complexity:**
- FT.SEARCH: O(N)

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Use double quotes and [DIALECT 2](#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:

#### Code Examples

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

**Difficulty:** Advanced

**Commands:** FT.CREATE, JSON.SET, FT.SEARCH

**Complexity:**
- FT.CREATE: O(K)
- JSON.SET: O(M+N)
- FT.SEARCH: O(N)

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

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:

[code example]

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':

#### Code Examples

Text fields: Query text fields for exact phrase matches using escaped double quotes when you need to find documents containing specific multi-word phrases

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

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

FT.SEARCH index "*" FILTER field start end

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

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

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
                    });

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

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

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

> 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\"}"

SearchResult res3 = db.FT().Search(
            "idx:bicycle",
            new("@condition:{new}")
        );
        Console.WriteLine(res3.TotalResults);   // >>> 4

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

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
                    });

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

const res5 = await client.ft.search('idx:bicycle', '@condition:{new}');
console.log(res5.total); // >>> 5

res = index.search(Query("@condition:{new}"))
print(res.total)
# >>> 5

> FT.CREATE idx:email ON JSON PREFIX 1 key: SCHEMA $.email AS email TAG
OK
> JSON.SET key:1 $ '{"email": "test@redis.com"}'
OK
> FT.SEARCH idx:email '@email:{"test@redis.com"}' DIALECT 2
1) (integer) 1
2) "key:1"
3) 1) "$"
   2) "{\"email\":\"test@redis.com\"}"

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\": \"test@redis.com\"}");

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

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": "test@redis.com",
		},
	).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

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("\"test@redis.com\"")));
                    }).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("\"test@redis.com\"")))).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();

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\": \"test@redis.com\"}");

        SearchResult res4 = jedis.ftSearch("idx:email",
            RediSearchUtil.escapeQuery("@email{test@redis.com}"),
            new FTSearchParams().dialect(2)
        );
        System.out.println(res4.getTotalResults());

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: 'test@redis.com' });

try {
    const res6 = await client.ft.search('idx:email', 'test@redis.com', { DIALECT: 2 });
    console.log(res6);
} catch (err) {
    console.log("'test@redis.com' syntax not yet supported.");
}

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": "test@redis.com"}')

try:
    res = idx_email.search(Query("test@redis.com").dialect(2))
    print(res)
except Exception:
    print("'test@redis.com' syntax not yet supported.")

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\"}"

SearchResult res5 = db.FT().Search(
            "idx:bicycle",
            new("@description:\"rough terrain\"")
        );
        Console.WriteLine(res5.TotalResults);   // >>> 1

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

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
                    });

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

const res7 = await client.ft.search('idx:bicycle', '@description:"rough terrain"');
console.log(res7.total); // >>> 1 (Result{1 total, docs: [Document {'id': 'bicycle:8'...)

res = index.search(Query("@description:\"rough terrain\""))
print(res.total)
# >>> 1 (Result{1 total, docs: [Document {'id': 'bicycle:8'...)


Full-text search

Summary: Perform a full-text search

Single word

To search for a word (or word stem) across all text fields, you can construct the following simple query:

[code example]

Instead of searching across all text fields, you might want to limit the search to a specific text field.

[code example]

Words that occur very often in natural language, such as `the` or `a` for the English language, aren't indexed and will not return a search result. You can find further details in the [stop words article]().

The following example searches for all bicycles that have the word 'kids' in the description:

#### Code Examples

Foundational: Search for single words across text fields using simple word queries when you need to find documents containing specific terms

**Difficulty:** Beginner

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Phrase

A phrase is a sentence, sentence fragment, or small group of words. You can find further details about how to find exact phrases in the [exact match article]().

Word prefix

You can also search for words that match a given prefix.

[code example]

[code example]

The prefix needs to be at least two characters long.

Here is an example that shows you how to search for bicycles with a brand that starts with 'ka':

#### Code Examples

Prefix matching: Search for words with a specific prefix using wildcard syntax when you need to find documents with words starting with certain characters

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Word suffix

Similar to the prefix, it is also possible to search for words that share the same suffix.

[code example]

You can also combine prefix- and suffix-based searches within a query expression.

[code example]

Here is an example that finds all brands that end with 'bikes':

#### Code Examples

Suffix/infix matching: Search for words with a specific suffix or infix using wildcard syntax when you need to find documents with words ending or containing certain characters

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Fuzzy search

A fuzzy search allows you to find documents with words that approximately match your search term. To perform a fuzzy search, you wrap search terms with pairs of `%` characters. A single pair represents a (Levenshtein) distance of one, two pairs represent a distance of two, and three pairs, the maximum distance, represents a distance of three.

Here is the command that searches across all text fields with a distance of one:

[code example]

The following example finds all documents that contain a word that has a distance of one to the incorrectly spelled word 'optamized'. You can see that this matches the word 'optimized'.

#### Code Examples

Fuzzy search: Search for words with approximate matches using fuzzy search with single % delimiters when you need to find documents despite spelling variations or typos

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

If you want to increase the maximum word distance to two, you can use the following query:

#### Code Examples

Fuzzy search: Search for words with larger approximate matches using fuzzy search with double %% delimiters when you need to tolerate greater spelling variations

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, C# (Synchronous), Go, Java (Asynchronous - Lettuce), Java (Reactive - Lettuce), Java (Synchronous - Jedis), JavaScript (Node.js), Python

**C# (Synchronous):**

[code example]

**Go:**

[code example]

**Java (Asynchronous - Lettuce):**

[code example]

**Java (Reactive - Lettuce):**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Fuzzy search on a specific attribute

To perform a fuzzy search on a specific text field, use the `@field:(...)` syntax with the fuzzy term inside parentheses.

[code example]

Do not use quotes around the fuzzy term when targeting a specific field. Wrapping the term in quotes (for example, `@field:("%word%")`) converts the query into an exact match search, and the `%` characters are treated as literal characters rather than fuzzy operators.

The following example shows the correct way to perform a fuzzy search with distance one on the `description` field:

[code example]

For a fuzzy search with distance two on a specific field:

[code example]

You can also combine fuzzy searches on multiple fields using boolean operators:

[code example]

Unicode considerations

Redis Search only supports Unicode characters in the [basic multilingual plane](https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane); U+0000 to U+FFFF. Unicode characters beyond U+FFFF, such as Emojis, are not supported and would not be retrieved by queries including such characters in the following use cases:

* Querying TEXT fields with Prefix/Suffix/Infix
* Querying TEXT fields with fuzzy

Examples:

[code example]

FT.SEARCH idx:bicycle "@description: kids"

SearchResult res1 = db.FT().Search(
           "idx:bicycle",
           new("@description: kids")
       );
        Console.WriteLine(res1);    // >>> 2

res1, err := rdb.FTSearch(ctx,
		"idx:bicycle", "@description: kids",
	).Result()

	if err != nil {
		panic(err)
	}

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

	sort.Slice(res1.Docs, func(i, j int) bool {
		return res1.Docs[i].ID < res1.Docs[j].ID
	})

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

CompletableFuture<SearchReply<String, String>> descriptionResults = asyncCommands
                    .ftSearch("idx:bicycle", "@description: kids").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:1
                        // >>> ID: bicycle:2
                        return res;
                    }).toCompletableFuture();

Mono<SearchReply<String, String>> descriptionResults = reactiveCommands
                    .ftSearch("idx:bicycle", "@description: kids").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:1
                            // >>> ID: bicycle:2
                        });
                    });

SearchResult res1 = jedis.ftSearch("idx:bicycle", "@description: kids");
        System.out.println(res1.getTotalResults()); // >>> 2

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

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

const res1 = await client.ft.search('idx:bicycle', '@description: kids');
console.log(res1.total); // >>> 2

res = index.search(Query("@description: kids"))
print(res.total)
# >>> 2

SearchResult res2 = db.FT().Search(
            "idx:bicycle",
            new("@model: ka*")
        );
        Console.WriteLine(res2.TotalResults);   // >>> 1

res2, err := rdb.FTSearch(ctx,
		"idx:bicycle", "@model: ka*",
	).Result()

	if err != nil {
		panic(err)
	}

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

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

CompletableFuture<SearchReply<String, String>> startsWithResults = asyncCommands
                    .ftSearch("idx:bicycle", "@model: ka*").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:4
                        return res;
                    }).toCompletableFuture();

Mono<SearchReply<String, String>> startsWithResults = reactiveCommands.ftSearch("idx:bicycle", "@model: ka*")
                    .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:4
                        });
                    });

SearchResult res2 = jedis.ftSearch("idx:bicycle", "@model: ka*");
        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:4

const res2 = await client.ft.search('idx:bicycle', '@model: ka*');
console.log(res2.total); // >>> 1

res = index.search(Query("@model: ka*"))
print(res.total)
# >>> 1

SearchResult res3 = db.FT().Search(
            "idx:bicycle",
            new("@brand: *bikes")
        );
        Console.WriteLine(res3.TotalResults);   // >>> 2

res3, err := rdb.FTSearch(ctx,
		"idx:bicycle", "@brand: *bikes",
	).Result()

	if err != nil {
		panic(err)
	}

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

	sort.Slice(res3.Docs, func(i, j int) bool {
		return res3.Docs[i].ID < res3.Docs[j].ID
	})
	for _, doc := range res3.Docs {
		fmt.Println(doc.ID)
	}
	// >>> bicycle:4
	// >>> bicycle:6

CompletableFuture<SearchReply<String, String>> endsWithResults = asyncCommands
                    .ftSearch("idx:bicycle", "@brand: *bikes").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:4
                        // >>> ID: bicycle:6
                        return res;
                    }).toCompletableFuture();

Mono<SearchReply<String, String>> endsWithResults = reactiveCommands.ftSearch("idx:bicycle", "@brand: *bikes")
                    .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:4
                            // >>> ID: bicycle:6
                        });
                    });

SearchResult res3 = jedis.ftSearch("idx:bicycle", "@brand: *bikes");
        System.out.println(res3.getTotalResults()); // >>> 2

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

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

const res3 = await client.ft.search('idx:bicycle', '@brand: *bikes');
console.log(res3.total); // >>> 2

res = index.search(Query("@brand: *bikes"))
print(res.total)
# >>> 2

SearchResult res4 = db.FT().Search(
            "idx:bicycle",
            new("%optamized%")
        );
        Console.WriteLine(res4.TotalResults);   // >>> 1

res4, err := rdb.FTSearch(ctx,
		"idx:bicycle", "%optamized%",
	).Result()

	if err != nil {
		panic(err)
	}

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

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

CompletableFuture<SearchReply<String, String>> fuzzyResults = asyncCommands.ftSearch("idx:bicycle", "%optamized%")
                    .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:3
                        return res;
                    }).toCompletableFuture();

Mono<SearchReply<String, String>> fuzzyResults = reactiveCommands.ftSearch("idx:bicycle", "%optamized%")
                    .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:3
                        });
                    });

SearchResult res4 = jedis.ftSearch("idx:bicycle", "%optamized%");
        System.out.println(res4.getTotalResults()); // >>> 1

        List<Document> docs4 = res4.getDocuments();

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

const res4 = await client.ft.search('idx:bicycle', '%optamized%');
console.log(res4); // >>> { total: 1, documents: [ { id: 'bicycle:3', value: [Object: null prototype] } ]}

res = index.search(Query("%optamized%"))
print(res)
# >>> Result{1 total, docs: [Document {'id': 'bicycle:3', 'payload': None, 'json': '{"pickup_zone":"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))","store_location":"-80.1918,25.7617","brand":"Eva","model":"Eva 291","price":3400,"description":"The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!","condition":"used"}'}]}

SearchResult res5 = db.FT().Search(
            "idx:bicycle",
            new("%%optamised%%")
        );
        Console.WriteLine(res5.TotalResults);   // >>> 1

res5, err := rdb.FTSearch(ctx,
		"idx:bicycle", "%%optamised%%",
	).Result()

	if err != nil {
		panic(err)
	}

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

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

CompletableFuture<SearchReply<String, String>> fuzzierResults = asyncCommands
                    .ftSearch("idx:bicycle", "%%optamised%%").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:3
                        return res;
                    }).toCompletableFuture();

Mono<SearchReply<String, String>> fuzzierResults = reactiveCommands.ftSearch("idx:bicycle", "%%optamised%%")
                    .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:3
                        });
                    });

SearchResult res5 = jedis.ftSearch("idx:bicycle", "%%optamised%%");
        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:3

const res5 = await client.ft.search('idx:bicycle', '%%optamised%%');
console.log(res5); // >>> { total: 1, documents: [ { id: 'bicycle:3', value: [Object: null prototype] } ]}

res = index.search(Query("%%optamised%%"))
print(res)
# >>> Result{1 total, docs: [Document {'id': 'bicycle:3', 'payload': None, 'json': '{"pickup_zone":"POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, -80.2433 25.6967, -80.2433 25.8067))","store_location":"-80.1918,25.7617","brand":"Eva","model":"Eva 291","price":3400,"description":"The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!","condition":"used"}'}]}

FT.SEARCH idx:bicycle "@description:(%optamized%) | @model:(%jigar%)"

redis> FT.CREATE idx SCHEMA tag TAG text TEXT
OK
redis> HSET doc:1 tag '😀😁🙂' text '😀😁🙂'
(integer) 2
redis> HSET doc:2 tag '😀😁🙂abc' text '😀😁🙂abc'
(integer) 2
redis> FT.SEARCH idx '@text:(*😀😁🙂)' NOCONTENT
1) (integer) 0
redis> FT.SEARCH idx '@text:(*😀😁🙂*)' NOCONTENT
1) (integer) 0
redis> FT.SEARCH idx '@text:(😀😁🙂*)' NOCONTENT
1) (integer) 0

redis> FT.SEARCH idx '@text:(%😀😁🙃%)' NOCONTENT
1) (integer) 0


Geospatial queries

Summary: Query based on geographic data

Radius

You can construct a radius query by passing the center coordinates (longitude, latitude), the radius, and the distance unit to the [FT.SEARCH]() command.

[code example]

Allowed units are `m`, `km`, `mi`, and `ft`.

The following query finds all bicycle stores within a radius of 20 miles around London:

#### Code Examples

Foundational: Query geographic locations within a radius using center coordinates and distance when you need to find nearby points of interest

**Difficulty:** Beginner

**Redis CLI:**

[code example]

**Available in:** Redis CLI, Go, Java (Synchronous - Jedis), JavaScript (Node.js), Python

**Go:**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

Shape

The only supported shapes are points and polygons. You can query for polygons or points that either contain or are within a given geometric shape.

[code example]

Here is a more detailed explanation of this query:

1. **Field name**: you need to replace `geo_shape_field` with the `GEOSHAPE` field's name on which you want to query.
2. **Spatial operator**: spatial operators define the relationship between the shapes in the database and the shape you are searching for. You can either use `WITHIN`, `CONTAINS`, `INTERSECTS`, or `DISJOINT`. `WITHIN` finds any shape in the database that is inside the given shape. `CONTAINS` queries for any shape that surrounds the given shape. `INTERSECTS` finds any shape that has coordinates in common with the provided shape. `DISJOINT` finds any shapes that have nothing in common with the provided shape. `INTERSECTS` and `DISJOINT` were introduced in v2.10.
3. **Parameter**: the query refers to a parameter named `shape`. You can use any parameter name here. You need to use the `PARAMS` clause to set the parameter value. The value follows the [well-known text representation of a geometry](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry). Supported types are `POINT(x y)` and `POLYGON((x1 y1, x2 y2, ...))`.
4. **Dialect**: Shape-based queries are not supported in `DIALECT 1`.

The following example query verifies if a bicycle is within a pickup zone:

#### Code Examples

Spatial operators: Query geometric shapes using CONTAINS to test which shapes enclose another shape when you need to find bounding regions

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, Go, Java (Synchronous - Jedis), JavaScript (Node.js), Python

**Go:**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

If you want to find all pickup zones that are approximately within Europe, then you can use the following query:

#### Code Examples

Spatial operators: Query geometric shapes using the WITHIN operator to find shapes contained in a region when you need to identify items within geographic boundaries

**Difficulty:** Intermediate

**Redis CLI:**

[code example]

**Available in:** Redis CLI, Go, Java (Synchronous - Jedis), JavaScript (Node.js), Python

**Go:**

[code example]

**Java (Synchronous - Jedis):**

[code example]

**JavaScript (Node.js):**

[code example]

**Python:**

[code example]

FT.SEARCH index "@geo_field:[lon lat radius unit]"

FT.SEARCH idx:bicycle "@store_location:[-0.1778 51.5524 20 mi]"

res1, err := rdb.FTSearchWithArgs(ctx,
		"idx:bicycle", "@store_location:[$lon $lat $radius $units]",
		&redis.FTSearchOptions{
			Params: map[string]interface{}{
				"lon":    -0.1778,
				"lat":    51.5524,
				"radius": 20,
				"units":  "mi",
			},
			DialectVersion: 2,
		},
	).Result()

	if err != nil {
		panic(err)
	}

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

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

SearchResult res1 = jedis.ftSearch("idx:bicycle",
            "@store_location:[$lon $lat $radius $units]",
            FTSearchParams.searchParams()
                    .addParam("lon", -0.1778)
                    .addParam("lat", 51.5524)
                    .addParam("radius", 20)
                    .addParam("units", "mi")
                    .dialect(2)
        );
        System.out.println(res1.getTotalResults()); // >>> 1

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

        for (Document document : docs1) {
            System.out.println(document.getId());
        }
        // >>> bicycle:5

const res1= await client.ft.search('idx:bicycle', '@store_location:[-0.1778 51.5524 20 mi]');
console.log(res1.total); // >>> 1
console.log(res1); // >>> {total: 1, documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]}

params_dict = {"lon": -0.1778, "lat": 51.5524, "radius": 20, "units": "mi"}
q = Query("@store_location:[$lon $lat $radius $units]").dialect(2)
res = index.search(q, query_params=params_dict)
print(res)
# >>> Result{1 total, docs: [Document {'id': 'bicycle:5', ...

FT.SEARCH index "@geo_shape_field:[{WITHIN|CONTAINS|INTERSECTS|DISJOINT} $shape] PARAMS 2 shape "shape_as_wkt" DIALECT 3

FT.SEARCH idx:bicycle "@pickup_zone:[CONTAINS $bike]" PARAMS 2 bike "POINT(-0.1278 51.5074)" DIALECT 2

res2, err := rdb.FTSearchWithArgs(ctx,
		"idx:bicycle",
		"@pickup_zone:[CONTAINS $bike]",
		&redis.FTSearchOptions{
			Params: map[string]interface{}{
				"bike": "POINT(-0.1278 51.5074)",
			},
			DialectVersion: 3,
		},
	).Result()

	if err != nil {
		panic(err)
	}

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

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

SearchResult res2 = jedis.ftSearch("idx:bicycle",
            "@pickup_zone:[CONTAINS $bike]",
            FTSearchParams.searchParams()
                    .addParam("bike", "POINT(-0.1278 51.5074)")
                    .dialect(3)
        );
        System.out.println(res2.getTotalResults());   // >>> 1

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

        for (Document document : docs2) {
            System.out.println(document.getId());
        }
        // >>> bicycle:5

const params_dict_geo2 = { bike: 'POINT(-0.1278 51.5074)' };
const q_geo2 = '@pickup_zone:[CONTAINS $bike]';
const res2 = await client.ft.search('idx:bicycle', q_geo2, { PARAMS: params_dict_geo2, DIALECT: 3 });
console.log(res2.total); // >>> 1
console.log(res2); // >>> {total: 1, documents: [ { id: 'bicycle:5', value: [Object: null prototype] } ]}

params_dict = {"bike": "POINT(-0.1278 51.5074)"}
q = Query("@pickup_zone:[CONTAINS $bike]").dialect(3)
res = index.search(q, query_params=params_dict)
print(res.total) # >>> 1

FT.SEARCH idx:bicycle "@pickup_zone:[WITHIN $europe]" PARAMS 2 europe "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))" DIALECT 2

res3, err := rdb.FTSearchWithArgs(ctx,
		"idx:bicycle",
		"@pickup_zone:[WITHIN $europe]",
		&redis.FTSearchOptions{
			Params: map[string]interface{}{
				"europe": "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))",
			},
			DialectVersion: 3,
		},
	).Result()

	if err != nil {
		panic(err)
	}

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

	sort.Slice(res3.Docs, func(i, j int) bool {
		return res3.Docs[i].ID < res3.Docs[j].ID
	})

	for _, doc := range res3.Docs {
		fmt.Println(doc.ID)
	}
	// >>> bicycle:5
	// >>> bicycle:6
	// >>> bicycle:7
	// >>> bicycle:8
	// >>> bicycle:9

SearchResult res3 = jedis.ftSearch("idx:bicycle",
            "@pickup_zone:[WITHIN $europe]",
            FTSearchParams.searchParams()
                    .addParam("europe", "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))")
                    .dialect(3)
        );
        System.out.println(res3.getTotalResults()); // >>> 5

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

        for (Document document : docs3) {
            System.out.println(document.getId());
        }
        // >>> bicycle:5
        // >>> bicycle:6
        // >>> bicycle:7
        // >>> bicycle:8
        // >>> bicycle:9

const params_dict_geo3 = { europe: 'POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))' };
const q_geo3 = '@pickup_zone:[WITHIN $europe]';
const res3 = await client.ft.search('idx:bicycle', q_geo3, { PARAMS: params_dict_geo3, DIALECT: 3 });
console.log(res3.total); // >>> 5
console.log(res3); // >>>
// {
//   total: 5,
//   documents: [
//     { id: 'bicycle:5', value: [Object: null prototype] },
//     { id: 'bicycle:6', value: [Object: null prototype] },
//     { id: 'bicycle:7', value: [Object: null prototype] },
//     { id: 'bicycle:8', value: [Object: null prototype] },
//     { id: 'bicycle:9', value: [Object: null prototype] }
//   ]
// }

params_dict = {"europe": "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))"}
q = Query("@pickup_zone:[WITHIN $europe]").dialect(3)
res = index.search(q, query_params=params_dict)
print(res.total) # >>> 5


Querying data

Summary: Understand how to query, search, and aggregate Redis data

Summary: Perform simple exact match queries

Exact match queries

Summary: Perform numeric range queries

Range queries

Summary: Perform a full-text search

Full-text search

Summary: Query based on geographic data

Geospatial queries

Summary: Query for data based on vector embeddings

Vector search

Summary: Combine query expressions

Combined queries

Summary: Group and aggregate query results

Aggregation queries

Content

Redis Open Source distinguishes between the [FT.SEARCH]() and [FT.AGGREGATE]() query commands. You should use [FT.SEARCH]() if you want to perform selections and projections only. If you also need to apply mapping functions, group, or aggregate data, use the [FT.AGGREGATE]() command.

* **Selection**: A selection allows you to return all documents that fulfill specific criteria.
* **Projection**: Projections are used to return specific fields of the result set. You can also map/project to calculated field values.
* **Aggregation**: Aggregations collect and summarize data across several fields.

Here is a short SQL comparison using the [bicycle dataset](./data/bicycles.txt):

|Type| SQL | Redis |
|----------| --- | ----------- |
| Selection | `SELECT * FROM bicycles WHERE price >= 1000` | `FT.SEARCH idx:bicycle "@price:[1000 +inf]"` |
| Simple projection | `SELECT id, price FROM bicycles` | `FT.SEARCH idx:bicycle "*" RETURN 2 __key, price` |
| Calculated projection| `SELECT id, price-price*0.1 AS discounted FROM bicycles`| `FT.AGGREGATE idx:bicycle "*" LOAD 2 __key price APPLY "@price-@price*0.1" AS discounted`|
| Aggregation | `SELECT condition, AVG(price) AS avg_price FROM bicycles GROUP BY condition` | `FT.AGGREGATE idx:bicycle "*" GROUPBY 1 @condition REDUCE AVG 1 @price AS avg_price` |

The following articles provide an overview of how to query data with the [FT.SEARCH]() command:

* [Exact match queries]()
* [Range queries]()
* [Full-text search ]()
* [Geospatial queries]()
* [Vector search]()
* [Combined queries]()

You can find further details about aggregation queries with [FT.AGGREGATE]() in the following article:

* [Aggregation queries]()