SearchWithParamsTest.java

package redis.clients.jedis.modules.search;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static redis.clients.jedis.util.AssertUtil.assertOK;
import static redis.clients.jedis.util.RedisConditions.ModuleVersion.SEARCH_MOD_VER_80M3;

import java.util.*;
import java.util.stream.Collectors;

import io.redis.test.annotations.SinceRedisVersion;
import io.redis.test.utils.RedisVersion;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;

import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.RedisProtocol;
import redis.clients.jedis.args.GeoUnit;
import redis.clients.jedis.args.SortingOrder;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.json.Path;
import redis.clients.jedis.search.*;
import redis.clients.jedis.search.schemafields.*;
import redis.clients.jedis.search.schemafields.GeoShapeField.CoordinateSystem;
import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
import redis.clients.jedis.modules.RedisModuleCommandsTestBase;
import redis.clients.jedis.util.RedisConditions;
import redis.clients.jedis.util.RedisVersionUtil;

@ParameterizedClass
@MethodSource("redis.clients.jedis.commands.CommandsTestsParameters#respVersions")
public class SearchWithParamsTest extends RedisModuleCommandsTestBase {

  private static final String index = "testindex";

  @BeforeAll
  public static void prepare() {
    RedisModuleCommandsTestBase.prepare();
  }

  @AfterEach
  public void cleanUp() {
    if (client.ftList().contains(index)) {
      client.ftDropIndex(index);
    }
  }

  public SearchWithParamsTest(RedisProtocol protocol) {
    super(protocol);
  }

  private void addDocument(String key, Map<String, Object> map) {
    client.hset(key, RediSearchUtil.toStringMap(map));
  }

  private static Map<String, Object> toMap(Object... values) {
    Map<String, Object> map = new HashMap<>();
    for (int i = 0; i < values.length; i += 2) {
      map.put((String) values[i], values[i + 1]);
    }
    return map;
  }

  private static Map<String, String> toMap(String... values) {
    Map<String, String> map = new HashMap<>();
    for (int i = 0; i < values.length; i += 2) {
      map.put(values[i], values[i + 1]);
    }
    return map;
  }

  @Test
  public void create() {
    assertOK(client.ftCreate(index,
        FTCreateParams.createParams()
            .filter("@age>16")
            .prefix("student:", "pupil:"),
        TextField.of("first"), TextField.of("last"), NumericField.of("age")));

    client.hset("profesor:5555", toMap("first", "Albert", "last", "Blue", "age", "55"));
    client.hset("student:1111", toMap("first", "Joe", "last", "Dod", "age", "18"));
    client.hset("pupil:2222", toMap("first", "Jen", "last", "Rod", "age", "14"));
    client.hset("student:3333", toMap("first", "El", "last", "Mark", "age", "17"));
    client.hset("pupil:4444", toMap("first", "Pat", "last", "Shu", "age", "21"));
    client.hset("student:5555", toMap("first", "Joen", "last", "Ko", "age", "20"));
    client.hset("teacher:6666", toMap("first", "Pat", "last", "Rod", "age", "20"));

    SearchResult noFilters = client.ftSearch(index);
    assertEquals(4, noFilters.getTotalResults());

    SearchResult res1 = client.ftSearch(index, "@first:Jo*");
    assertEquals(2, res1.getTotalResults());

    SearchResult res2 = client.ftSearch(index, "@first:Pat");
    assertEquals(1, res2.getTotalResults());

    SearchResult res3 = client.ftSearch(index, "@last:Rod");
    assertEquals(0, res3.getTotalResults());
  }

  @Test
  public void createNoParams() {
    assertOK(client.ftCreate(index,
        TextField.of("first").weight(1),
        TextField.of("last").weight(1),
        NumericField.of("age")));

    addDocument("student:1111", toMap("first", "Joe", "last", "Dod", "age", 18));
    addDocument("student:3333", toMap("first", "El", "last", "Mark", "age", 17));
    addDocument("pupil:4444", toMap("first", "Pat", "last", "Shu", "age", 21));
    addDocument("student:5555", toMap("first", "Joen", "last", "Ko", "age", 20));

    SearchResult noFilters = client.ftSearch(index);
    assertEquals(4, noFilters.getTotalResults());

    SearchResult res1 = client.ftSearch(index, "@first:Jo*");
    assertEquals(2, res1.getTotalResults());

    SearchResult res2 = client.ftSearch(index, "@first:Pat");
    assertEquals(1, res2.getTotalResults());

    SearchResult res3 = client.ftSearch(index, "@last:Rod");
    assertEquals(0, res3.getTotalResults());
  }

  @Test
  public void createWithFieldNames() {
    assertOK(client.ftCreate(index,
        FTCreateParams.createParams()
            .addPrefix("student:").addPrefix("pupil:"),
        TextField.of("first").as("given"),
        TextField.of(FieldName.of("last").as("family"))));

    client.hset("profesor:5555", toMap("first", "Albert", "last", "Blue", "age", "55"));
    client.hset("student:1111", toMap("first", "Joe", "last", "Dod", "age", "18"));
    client.hset("pupil:2222", toMap("first", "Jen", "last", "Rod", "age", "14"));
    client.hset("student:3333", toMap("first", "El", "last", "Mark", "age", "17"));
    client.hset("pupil:4444", toMap("first", "Pat", "last", "Shu", "age", "21"));
    client.hset("student:5555", toMap("first", "Joen", "last", "Ko", "age", "20"));
    client.hset("teacher:6666", toMap("first", "Pat", "last", "Rod", "age", "20"));

    SearchResult noFilters = client.ftSearch(index);
    assertEquals(5, noFilters.getTotalResults());

    SearchResult asGiven = client.ftSearch(index, "@given:Jo*");
    assertEquals(2, asGiven.getTotalResults());

    SearchResult nonLast = client.ftSearch(index, "@last:Rod");
    assertEquals(0, nonLast.getTotalResults());

    SearchResult asFamily = client.ftSearch(index, "@family:Rod");
    assertEquals(1, asFamily.getTotalResults());
  }

  @Test
  public void alterAdd() {
    assertOK(client.ftCreate(index, TextField.of("title")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    for (int i = 0; i < 100; i++) {
      addDocument(String.format("doc%d", i), fields);
    }
    SearchResult res = client.ftSearch(index, "hello world");
    assertEquals(100, res.getTotalResults());

    assertOK(client.ftAlter(index,
        TagField.of("tags"),
        TextField.of("name").weight(0.5)));

    for (int i = 0; i < 100; i++) {
      Map<String, Object> fields2 = new HashMap<>();
      fields2.put("name", "name" + i);
      fields2.put("tags", String.format("tagA,tagB,tag%d", i));
      addDocument(String.format("doc%d", i), fields2);
    }
    SearchResult res2 = client.ftSearch(index, "@tags:{tagA}");
    assertEquals(100, res2.getTotalResults());
  }

  @Test
  public void search() {
    assertOK(client.ftCreate(index, FTCreateParams.createParams(),
        TextField.of("title"), TextField.of("body")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    fields.put("body", "lorem ipsum");
    for (int i = 0; i < 100; i++) {
      addDocument(String.format("doc%d", i), fields);
    }

    SearchResult result = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams().limit(0, 5).withScores());
    assertEquals(100, result.getTotalResults());
    assertEquals(5, result.getDocuments().size());
    for (Document d : result.getDocuments()) {
      assertTrue(d.getId().startsWith("doc"));
      assertTrue(d.getScore() < 100);
    }

    client.del("doc0");

    result = client.ftSearch(index, "hello world");
    assertEquals(99, result.getTotalResults());

    assertEquals("OK", client.ftDropIndex(index));
    try {
      client.ftSearch(index, "hello world");
      fail();
    } catch (JedisDataException e) {
    }
  }

  @Test
  public void textFieldParams() {
    assertOK(client.ftCreate("testindex", TextField.of("title")
        .weight(2.5).noStem().phonetic("dm:en").withSuffixTrie().sortable()));

    assertOK(client.ftCreate("testunfindex", TextField.of("title")
        .weight(2.5).noStem().phonetic("dm:en").withSuffixTrie().sortableUNF()));

    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      assertOK(client.ftCreate("testindex-missing",
          TextField.of("title").indexMissing().indexEmpty().weight(2.5).noStem().phonetic("dm:en")
              .withSuffixTrie().sortable()));

      assertOK(client.ftCreate("testunfindex-missing",
          TextField.of("title").indexMissing().indexEmpty().weight(2.5).noStem().phonetic("dm:en")
              .withSuffixTrie().sortableUNF()));
    }

    assertOK(client.ftCreate("testnoindex", TextField.of("title").sortable().noIndex()));

    assertOK(client.ftCreate("testunfnoindex", TextField.of("title").sortableUNF().noIndex()));
  }

  @Test
  @SinceRedisVersion(value = "7.4.0", message = "optional params since 7.4.0 are being tested")
  public void searchTextFieldsCondition() {
    assertOK(client.ftCreate(index, FTCreateParams.createParams(), TextField.of("title"),
        TextField.of("body").indexMissing().indexEmpty()));

    Map<String, String> regular = new HashMap<>();
    regular.put("title", "hello world");
    regular.put("body", "lorem ipsum");
    client.hset("regular-doc", regular);

    Map<String, String> empty = new HashMap<>();
    empty.put("title", "hello world");
    empty.put("body", "");
    client.hset("empty-doc", empty);

    Map<String, String> missing = new HashMap<>();
    missing.put("title", "hello world");
    client.hset("missing-doc", missing);

    SearchResult result = client.ftSearch(index, "", FTSearchParams.searchParams().dialect(2));
    assertEquals(0, result.getTotalResults());
    assertEquals(0, result.getDocuments().size());

    result = client.ftSearch(index, "*", FTSearchParams.searchParams().dialect(2));
    assertEquals(3, result.getTotalResults());
    assertEquals(3, result.getDocuments().size());

    result = client.ftSearch(index, "@body:''", FTSearchParams.searchParams().dialect(2));
    assertEquals(1, result.getTotalResults());
    assertEquals(1, result.getDocuments().size());
    assertEquals("empty-doc", result.getDocuments().get(0).getId());

    result = client.ftSearch(index, "ismissing(@body)", FTSearchParams.searchParams().dialect(2));
    assertEquals(1, result.getTotalResults());
    assertEquals(1, result.getDocuments().size());
    assertEquals("missing-doc", result.getDocuments().get(0).getId());
  }

  @Test
  public void numericFilter() {
    assertOK(client.ftCreate(index, TextField.of("title"), NumericField.of("price")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");

    for (int i = 0; i < 100; i++) {
      fields.put("price", i);
      addDocument(String.format("doc%d", i), fields);
    }

    SearchResult res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams().filter("price", 0, 49));
    assertEquals(50, res.getTotalResults());
    assertEquals(10, res.getDocuments().size());
    for (Document d : res.getDocuments()) {
      long price = Long.valueOf((String) d.get("price"));
      assertTrue(price >= 0);
      assertTrue(price <= 49);
    }

    res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams().filter("price", 0, true, 49, true));
    assertEquals(48, res.getTotalResults());
    assertEquals(10, res.getDocuments().size());
    for (Document d : res.getDocuments()) {
      long price = Long.valueOf((String) d.get("price"));
      assertTrue(price > 0);
      assertTrue(price < 49);
    }
    res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams().filter("price", 50, 100));
    assertEquals(50, res.getTotalResults());
    assertEquals(10, res.getDocuments().size());
    for (Document d : res.getDocuments()) {
      long price = Long.valueOf((String) d.get("price"));
      assertTrue(price >= 50);
      assertTrue(price <= 100);
    }

    res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams()
            .filter("price", 20, Double.POSITIVE_INFINITY));
    assertEquals(80, res.getTotalResults());
    assertEquals(10, res.getDocuments().size());

    res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams()
            .filter("price", Double.NEGATIVE_INFINITY, 10));
    assertEquals(11, res.getTotalResults());
    assertEquals(10, res.getDocuments().size());
  }

  @Test
  public void numericFieldParams() {
    assertOK(client.ftCreate("testindex", TextField.of("title"),
        NumericField.of("price").as("px").sortable()));

    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      assertOK(client.ftCreate("testindex-missing", TextField.of("title"),
          NumericField.of("price").as("px").indexMissing().sortable()));
    }

    assertOK(client.ftCreate("testnoindex", TextField.of("title"),
        NumericField.of("price").as("px").sortable().noIndex()));
  }

  @Test
  public void stopwords() {
    assertOK(client.ftCreate(index,
        FTCreateParams.createParams()
            .stopwords("foo", "bar", "baz"),
        TextField.of("title")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world foo bar");
    addDocument("doc1", fields);
    SearchResult res = client.ftSearch(index, "hello world");
    assertEquals(1, res.getTotalResults());
    res = client.ftSearch(index, "foo bar");
    assertEquals(0, res.getTotalResults());
  }

  @Test
  public void noStopwords() {
    assertOK(client.ftCreate(index,
        FTCreateParams.createParams().noStopwords(),
        TextField.of("title")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world foo bar");
    fields.put("title", "hello world foo bar to be or not to be");
    addDocument("doc1", fields);

    assertEquals(1, client.ftSearch(index, "hello world").getTotalResults());
    assertEquals(1, client.ftSearch(index, "foo bar").getTotalResults());
    assertEquals(1, client.ftSearch(index, "to be or not to be").getTotalResults());
  }

  @Test
  public void geoFilter() {
    assertOK(client.ftCreate(index, TextField.of("title"), GeoField.of("loc")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    fields.put("loc", "-0.441,51.458");
    addDocument("doc1", fields);

    fields.put("loc", "-0.1,51.2");
    addDocument("doc2", fields);

    SearchResult res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams().
            geoFilter("loc", -0.44, 51.45, 10, GeoUnit.KM));
    assertEquals(1, res.getTotalResults());

    res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams().
            geoFilter("loc", -0.44, 51.45, 100, GeoUnit.KM));
    assertEquals(2, res.getTotalResults());
  }

  @Test
  public void geoFilterAndGeoCoordinateObject() {
    assertOK(client.ftCreate(index, TextField.of("title"), GeoField.of("loc")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    fields.put("loc", new GeoCoordinate(-0.441, 51.458));
    addDocument("doc1", fields);

    fields.put("loc", new GeoCoordinate(-0.1, 51.2));
    addDocument("doc2", fields);

    SearchResult res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams()
            .geoFilter(new FTSearchParams.GeoFilter("loc", -0.44, 51.45, 10, GeoUnit.KM)));
    assertEquals(1, res.getTotalResults());

    res = client.ftSearch(index, "hello world",
        FTSearchParams.searchParams()
            .geoFilter(new FTSearchParams.GeoFilter("loc", -0.44, 51.45, 100, GeoUnit.KM)));
    assertEquals(2, res.getTotalResults());
  }

  @Test
  public void geoFieldParams() {
    assertOK(client.ftCreate("testindex", TextField.of("title"),
        GeoField.of("location").as("loc").sortable()));

    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      assertOK(client.ftCreate("testindex-missing", TextField.of("title"),
          GeoField.of("location").as("loc").indexMissing().sortable()));
    }

    assertOK(client.ftCreate("testnoindex", TextField.of("title"),
        GeoField.of("location").as("loc").sortable().noIndex()));
  }

  @Test
  public void geoShapeFilterSpherical() throws ParseException {
    final WKTReader reader = new WKTReader();
    final GeometryFactory factory = new GeometryFactory();

    assertOK(client.ftCreate(index, GeoShapeField.of("geom", CoordinateSystem.SPHERICAL)));

    // polygon type
    final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001),
        new Coordinate(34.9001, 29.7100), new Coordinate(34.9100, 29.7100),
        new Coordinate(34.9100, 29.7001), new Coordinate(34.9001, 29.7001)});
    client.hsetObject("small", "geom", small);

    final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001),
        new Coordinate(34.9001, 29.7200), new Coordinate(34.9200, 29.7200),
        new Coordinate(34.9200, 29.7001), new Coordinate(34.9001, 29.7001)});
    client.hsetObject("large", "geom", large);

    // within condition
    final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(34.9000, 29.7000),
        new Coordinate(34.9000, 29.7150), new Coordinate(34.9150, 29.7150),
        new Coordinate(34.9150, 29.7000), new Coordinate(34.9000, 29.7000)});

    SearchResult result = client.ftSearch(index, "@geom:[within $poly]",
        FTSearchParams.searchParams().addParam("poly", within).dialect(3));
    assertEquals(1, result.getTotalResults());
    assertEquals(1, result.getDocuments().size());
    assertEquals(small, reader.read(result.getDocuments().get(0).getString("geom")));

    // contains condition
    final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(34.9002, 29.7002),
        new Coordinate(34.9002, 29.7050), new Coordinate(34.9050, 29.7050),
        new Coordinate(34.9050, 29.7002), new Coordinate(34.9002, 29.7002)});

    result = client.ftSearch(index, "@geom:[contains $poly]",
        FTSearchParams.searchParams().addParam("poly", contains).dialect(3));
    assertEquals(2, result.getTotalResults());
    assertEquals(2, result.getDocuments().size());

    // point type
    final Point point = factory.createPoint(new Coordinate(34.9010, 29.7010));
    client.hset("point", "geom", point.toString());

    result = client.ftSearch(index, "@geom:[within $poly]",
        FTSearchParams.searchParams().addParam("poly", within).dialect(3));
    assertEquals(2, result.getTotalResults());
    assertEquals(2, result.getDocuments().size());
  }

  @Test
  public void geoShapeFilterFlat() throws ParseException {
    final WKTReader reader = new WKTReader();
    final GeometryFactory factory = new GeometryFactory();

    assertOK(client.ftCreate(index, GeoShapeField.of("geom", CoordinateSystem.FLAT)));

    // polygon type
    final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(20, 20),
        new Coordinate(20, 100), new Coordinate(100, 100), new Coordinate(100, 20), new Coordinate(20, 20)});
    client.hsetObject("small", "geom", small);

    final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(10, 10),
        new Coordinate(10, 200), new Coordinate(200, 200), new Coordinate(200, 10), new Coordinate(10, 10)});
    client.hsetObject("large", "geom", large);

    // within condition
    final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(0, 0),
        new Coordinate(0, 150), new Coordinate(150, 150), new Coordinate(150, 0), new Coordinate(0, 0)});

    SearchResult result = client.ftSearch(index, "@geom:[within $poly]",
        FTSearchParams.searchParams().addParam("poly", within).dialect(3));
    assertEquals(1, result.getTotalResults());
    assertEquals(1, result.getDocuments().size());
    assertEquals(small, reader.read(result.getDocuments().get(0).getString("geom")));

    // contains condition
    final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(25, 25),
        new Coordinate(25, 50), new Coordinate(50, 50), new Coordinate(50, 25), new Coordinate(25, 25)});

    result = client.ftSearch(index, "@geom:[contains $poly]",
        FTSearchParams.searchParams().addParam("poly", contains).dialect(3));
    assertEquals(2, result.getTotalResults());
    assertEquals(2, result.getDocuments().size());

    // intersects and disjoint
    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      final Polygon disjointersect = factory.createPolygon(new Coordinate[]{new Coordinate(150, 150),
        new Coordinate(150, 250), new Coordinate(250, 250), new Coordinate(250, 150), new Coordinate(150, 150)});

      result = client.ftSearch(index, "@geom:[intersects $poly]",
          FTSearchParams.searchParams().addParam("poly", disjointersect).dialect(3));
      assertEquals(1, result.getTotalResults());
      assertEquals(1, result.getDocuments().size());
      assertEquals(large, reader.read(result.getDocuments().get(0).getString("geom")));

      result = client.ftSearch(index, "@geom:[disjoint $poly]",
          FTSearchParams.searchParams().addParam("poly", disjointersect).dialect(3));
      assertEquals(1, result.getTotalResults());
      assertEquals(1, result.getDocuments().size());
      assertEquals(small, reader.read(result.getDocuments().get(0).getString("geom")));
    }

    // point type
    final Point point = factory.createPoint(new Coordinate(30, 30));
    client.hsetObject("point", "geom", point);

    result = client.ftSearch(index, "@geom:[within $poly]",
        FTSearchParams.searchParams().addParam("poly", within).dialect(3));
    assertEquals(2, result.getTotalResults());
    assertEquals(2, result.getDocuments().size());
  }

  @Test
  public void geoShapeFieldParams() {
    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      assertOK(client.ftCreate("testindex-missing",
          GeoShapeField.of("geometry", CoordinateSystem.SPHERICAL).as("geom").indexMissing()));
    }

    assertOK(client.ftCreate("testnoindex",
        GeoShapeField.of("geometry", CoordinateSystem.SPHERICAL).as("geom").noIndex()));
  }

  @Test
  public void testQueryFlags() {
    assertOK(client.ftCreate(index, TextField.of("title")));

    Map<String, Object> fields = new HashMap<>();
    for (int i = 0; i < 100; i++) {
      fields.put("title", i % 2 != 0 ? "hello worlds" : "hello world");
      addDocument(String.format("doc%d", i), fields);
    }

    SearchResult res = client.ftSearch(index, "hello",
        FTSearchParams.searchParams().withScores());
    assertEquals(100, res.getTotalResults());
    assertEquals(10, res.getDocuments().size());

    for (Document d : res.getDocuments()) {
      assertTrue(d.getId().startsWith("doc"));
      assertTrue(((String) d.get("title")).startsWith("hello world"));
    }
//
//    res = client.ftSearch(index, "hello",
//        FTSearchParams.searchParams().withScores().explainScore());
//    assertEquals(100, res.getTotalResults());
//    assertEquals(10, res.getDocuments().size());
//
//    for (Document d : res.getDocuments()) {
//      assertTrue(d.getId().startsWith("doc"));
//      assertTrue(((String) d.get("title")).startsWith("hello world"));
//    }

    res = client.ftSearch(index, "hello",
        FTSearchParams.searchParams().noContent());
    for (Document d : res.getDocuments()) {
      assertTrue(d.getId().startsWith("doc"));
      if (protocol != RedisProtocol.RESP3) {
        assertEquals(1.0, d.getScore(), 0);
        assertNull(d.get("title"));
      } else {
        assertNull(d.getScore());
        assertThrows(NullPointerException.class, () -> d.get("title"));
      }
    }

    // test verbatim vs. stemming
    res = client.ftSearch(index, "hello worlds");
    assertEquals(100, res.getTotalResults());
    res = client.ftSearch(index, "hello worlds", FTSearchParams.searchParams().verbatim());
    assertEquals(50, res.getTotalResults());
    res = client.ftSearch(index, "hello a world", FTSearchParams.searchParams().verbatim());
    assertEquals(50, res.getTotalResults());
    res = client.ftSearch(index, "hello a worlds", FTSearchParams.searchParams().verbatim());
    assertEquals(50, res.getTotalResults());
    res = client.ftSearch(index, "hello a world", FTSearchParams.searchParams().verbatim().noStopwords());
    assertEquals(0, res.getTotalResults());
  }

  @Test
  public void testQueryParams() {
    assertOK(client.ftCreate(index, NumericField.of("numval")));

    client.hset("1", "numval", "1");
    client.hset("2", "numval", "2");
    client.hset("3", "numval", "3");

    assertEquals(2, client.ftSearch(index, "@numval:[$min $max]",
        FTSearchParams.searchParams().addParam("min", 1).addParam("max", 2)
            .dialect(2)).getTotalResults());

    Map<String, Object> paramValues = new HashMap<>();
    paramValues.put("min", 1);
    paramValues.put("max", 2);
    assertEquals(2, client.ftSearch(index, "@numval:[$min $max]",
        FTSearchParams.searchParams().params(paramValues)
            .dialect(2)).getTotalResults());

    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4) ) {
      assertEquals(1, client.ftSearch(index, "@numval:[$eq]",
          FTSearchParams.searchParams().addParam("eq", 2).dialect(4)).getTotalResults());
    }
  }

  @Test
  public void testSortQueryFlags() {
    assertOK(client.ftCreate(index, TextField.of("title").sortable()));

    Map<String, Object> fields = new HashMap<>();

    fields.put("title", "b title");
    addDocument("doc1", fields);

    fields.put("title", "a title");
    addDocument("doc2", fields);

    fields.put("title", "c title");
    addDocument("doc3", fields);

    SearchResult res = client.ftSearch(index, "title",
        FTSearchParams.searchParams().sortBy("title", SortingOrder.ASC));

    assertEquals(3, res.getTotalResults());
    Document doc1 = res.getDocuments().get(0);
    assertEquals("a title", doc1.get("title"));

    doc1 = res.getDocuments().get(1);
    assertEquals("b title", doc1.get("title"));

    doc1 = res.getDocuments().get(2);
    assertEquals("c title", doc1.get("title"));
  }

  @Test
  public void testJsonWithAlias() {
    assertOK(client.ftCreate(index,
        FTCreateParams.createParams()
            .on(IndexDataType.JSON)
            .prefix("king:"),
        TextField.of("$.name").as("name"),
        NumericField.of("$.num").as("num")));

    Map<String, Object> king1 = new HashMap<>();
    king1.put("name", "henry");
    king1.put("num", 42);
    client.jsonSet("king:1", Path.ROOT_PATH, king1);

    Map<String, Object> king2 = new HashMap<>();
    king2.put("name", "james");
    king2.put("num", 3.14);
    client.jsonSet("king:2", Path.ROOT_PATH, king2);

    SearchResult res = client.ftSearch(index, "@name:henry");
    assertEquals(1, res.getTotalResults());
    assertEquals("king:1", res.getDocuments().get(0).getId());

    res = client.ftSearch(index, "@num:[0 10]");
    assertEquals(1, res.getTotalResults());
    assertEquals("king:2", res.getDocuments().get(0).getId());

    res = client.ftSearch(index, "@num:[42 42]", FTSearchParams.searchParams());
    assertEquals(1, res.getTotalResults());
    assertEquals("king:1", res.getDocuments().get(0).getId());

    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      res = client.ftSearch(index, "@num:[42]", FTSearchParams.searchParams().dialect(4));
      assertEquals(1, res.getTotalResults());
      assertEquals("king:1", res.getDocuments().get(0).getId());
    }
  }

  @Test
  public void dropIndex() {
    assertOK(client.ftCreate(index, TextField.of("title")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    for (int i = 0; i < 100; i++) {
      addDocument(String.format("doc%d", i), fields);
    }

    SearchResult res = client.ftSearch(index, "hello world");
    assertEquals(100, res.getTotalResults());

    assertEquals("OK", client.ftDropIndex(index));

    try {
      client.ftSearch(index, "hello world");
      fail("Index should not exist.");
    } catch (JedisDataException de) {
      // toLowerCase - Error message updated to "No such index" with Redis 8.0.0
      assertTrue(de.getMessage().toLowerCase().contains("no such index"));
    }
    assertEquals(100, client.dbSize());
  }

  @Test
  public void dropIndexDD() {
    assertOK(client.ftCreate(index, TextField.of("title")));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    for (int i = 0; i < 100; i++) {
      addDocument(String.format("doc%d", i), fields);
    }

    SearchResult res = client.ftSearch(index, "hello world");
    assertEquals(100, res.getTotalResults());

    assertEquals("OK", client.ftDropIndexDD(index));

    Set<String> keys = client.keys("*");
    assertTrue(keys.isEmpty());
    assertEquals(0, client.dbSize());
  }

  @Test
  public void noStem() {
    assertOK(client.ftCreate(index, new TextField("stemmed").weight(1.0),
        new TextField("notStemmed").weight(1.0).noStem()));

    Map<String, Object> doc = new HashMap<>();
    doc.put("stemmed", "located");
    doc.put("notStemmed", "located");
    addDocument("doc", doc);

    // Query
    SearchResult res = client.ftSearch(index, "@stemmed:location");
    assertEquals(1, res.getTotalResults());

    res = client.ftSearch(index, "@notStemmed:location");
    assertEquals(0, res.getTotalResults());
  }

  @Test
  public void phoneticMatch() {
    assertOK(client.ftCreate(index, new TextField("noPhonetic").weight(1.0),
        new TextField("withPhonetic").weight(1.0).phonetic("dm:en")));

    Map<String, Object> doc = new HashMap<>();
    doc.put("noPhonetic", "morfix");
    doc.put("withPhonetic", "morfix");
    addDocument("doc", doc);

    // Query
    SearchResult res = client.ftSearch(index, "@withPhonetic:morphix=>{$phonetic:true}");
    assertEquals(1, res.getTotalResults());

    try {
      client.ftSearch(index, "@noPhonetic:morphix=>{$phonetic:true}");
      fail();
    } catch (JedisDataException e) {/*field does not support phonetics*/
    }

    SearchResult res3 = client.ftSearch(index, "@withPhonetic:morphix=>{$phonetic:false}");
    assertEquals(0, res3.getTotalResults());
  }

  @Test
  public void info() {
    Collection<SchemaField> sc = new ArrayList<>();
    sc.add(TextField.of("title").weight(5));
    sc.add(TextField.of("plot").sortable());
    sc.add(TagField.of("genre").separator(',').sortable());
    sc.add(NumericField.of("release_year").sortable());
    sc.add(NumericField.of("rating").sortable());
    sc.add(NumericField.of("votes").sortable());

    assertOK(client.ftCreate(index, sc));

    Map<String, Object> info = client.ftInfo(index);
    assertEquals(index, info.get("index_name"));
    assertEquals(6, ((List) info.get("attributes")).size());
    if (protocol != RedisProtocol.RESP3) {
      assertEquals("global_idle", ((List) info.get("cursor_stats")).get(0));
      assertEquals(0L, ((List) info.get("cursor_stats")).get(1));
    } else {
      assertEquals(0L, ((Map) info.get("cursor_stats")).get("global_idle"));
    }
  }

  @Test
  public void noIndexAndSortBy() {
    assertOK(client.ftCreate(index, TextField.of("f1").sortable().noIndex(), TextField.of("f2")));

    Map<String, Object> mm = new HashMap<>();

    mm.put("f1", "MarkZZ");
    mm.put("f2", "MarkZZ");
    addDocument("doc1", mm);

    mm.clear();
    mm.put("f1", "MarkAA");
    mm.put("f2", "MarkBB");
    addDocument("doc2", mm);

    SearchResult res = client.ftSearch(index, "@f1:Mark*");
    assertEquals(0, res.getTotalResults());

    res = client.ftSearch(index, "@f2:Mark*");
    assertEquals(2, res.getTotalResults());

    res = client.ftSearch(index, "@f2:Mark*",
        FTSearchParams.searchParams().sortBy("f1", SortingOrder.DESC));
    assertEquals(2, res.getTotalResults());

    assertEquals("doc1", res.getDocuments().get(0).getId());

    res = client.ftSearch(index, "@f2:Mark*",
        FTSearchParams.searchParams().sortBy("f1", SortingOrder.ASC));
    assertEquals("doc2", res.getDocuments().get(0).getId());
  }

  @Test
  public void testHighlightSummarize() {
    assertOK(client.ftCreate(index, TextField.of("text").weight(1)));

    Map<String, Object> doc = new HashMap<>();
    doc.put("text", "Redis is often referred as a data structures server. What this means is that "
        + "Redis provides access to mutable data structures via a set of commands, which are sent "
        + "using a server-client model with TCP sockets and a simple protocol. So different "
        + "processes can query and modify the same data structures in a shared way");
    // Add a document
    addDocument("foo", doc);

    SearchResult res = client.ftSearch(index, "data", FTSearchParams.searchParams().highlight().summarize());
    assertEquals("is often referred as a <b>data</b> structures server. What this means is that "
        + "Redis provides... What this means is that Redis provides access to mutable <b>data</b> "
        + "structures via a set of commands, which are sent using a... So different processes can "
        + "query and modify the same <b>data</b> structures in a shared... ",
        res.getDocuments().get(0).get("text"));

    res = client.ftSearch(index, "data", FTSearchParams.searchParams()
        .highlight(FTSearchParams.highlightParams().tags("<u>", "</u>"))
        .summarize());
    assertEquals("is often referred as a <u>data</u> structures server. What this means is that "
        + "Redis provides... What this means is that Redis provides access to mutable <u>data</u> "
        + "structures via a set of commands, which are sent using a... So different processes can "
        + "query and modify the same <u>data</u> structures in a shared... ",
        res.getDocuments().get(0).get("text"));
  }

  @Test
  public void getTagField() {
    assertOK(client.ftCreate(index, TextField.of("title"), TagField.of("category")));

    Map<String, Object> fields1 = new HashMap<>();
    fields1.put("title", "hello world");
    fields1.put("category", "red");
    addDocument("foo", fields1);

    Map<String, Object> fields2 = new HashMap<>();
    fields2.put("title", "hello world");
    fields2.put("category", "blue");
    addDocument("bar", fields2);

    Map<String, Object> fields3 = new HashMap<>();
    fields3.put("title", "hello world");
    fields3.put("category", "green,yellow");
    addDocument("baz", fields3);

    Map<String, Object> fields4 = new HashMap<>();
    fields4.put("title", "hello world");
    fields4.put("category", "orange;purple");
    addDocument("qux", fields4);

    assertEquals(1, client.ftSearch(index, "@category:{red}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "@category:{blue}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "hello @category:{red}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "hello @category:{blue}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "@category:{yellow}").getTotalResults());
    assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "@category:{orange\\;purple}").getTotalResults());
    assertEquals(4, client.ftSearch(index, "hello").getTotalResults());

    assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange;purple")),
        client.ftTagVals(index, "category"));
  }

  @Test
  public void testGetTagFieldWithNonDefaultSeparator() {
    assertOK(client.ftCreate(index,
        TextField.of("title"),
        TagField.of("category").separator(';')));

    Map<String, Object> fields1 = new HashMap<>();
    fields1.put("title", "hello world");
    fields1.put("category", "red");
    addDocument("foo", fields1);

    Map<String, Object> fields2 = new HashMap<>();
    fields2.put("title", "hello world");
    fields2.put("category", "blue");
    addDocument("bar", fields2);

    Map<String, Object> fields3 = new HashMap<>();
    fields3.put("title", "hello world");
    fields3.put("category", "green;yellow");
    addDocument("baz", fields3);

    Map<String, Object> fields4 = new HashMap<>();
    fields4.put("title", "hello world");
    fields4.put("category", "orange,purple");
    addDocument("qux", fields4);

    assertEquals(1, client.ftSearch(index, "@category:{red}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "@category:{blue}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "hello @category:{red}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "hello @category:{blue}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "hello @category:{yellow}").getTotalResults());
    assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "@category:{orange\\,purple}").getTotalResults());
    assertEquals(4, client.ftSearch(index, "hello").getTotalResults());

    assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange,purple")),
        client.ftTagVals(index, "category"));
  }

  @Test
  public void caseSensitiveTagField() {
    assertOK(client.ftCreate(index,
        TextField.of("title"),
        TagField.of("category").caseSensitive()));

    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    fields.put("category", "RedX");
    addDocument("foo", fields);

    assertEquals(0, client.ftSearch(index, "@category:{redx}").getTotalResults());
    assertEquals(0, client.ftSearch(index, "@category:{redX}").getTotalResults());
    assertEquals(0, client.ftSearch(index, "@category:{Redx}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "@category:{RedX}").getTotalResults());
    assertEquals(1, client.ftSearch(index, "hello").getTotalResults());
  }

  @Test
  public void tagFieldParams() {
    assertOK(client.ftCreate("testindex", TextField.of("title"),
        TagField.of("category").as("cat").separator(',')
            .caseSensitive().withSuffixTrie().sortable()));

    assertOK(client.ftCreate("testunfindex", TextField.of("title"),
        TagField.of("category").as("cat").separator(',')
            .caseSensitive().withSuffixTrie().sortableUNF()));

    if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V7_4)) {
      assertOK(client.ftCreate("testindex-missing", TextField.of("title"),
          TagField.of("category").as("cat").indexMissing().indexEmpty().separator(',')
              .caseSensitive().withSuffixTrie().sortable()));

      assertOK(client.ftCreate("testunfindex-missing", TextField.of("title"),
          TagField.of("category").as("cat").indexMissing().indexEmpty().separator(',')
              .caseSensitive().withSuffixTrie().sortableUNF()));
    }

    assertOK(client.ftCreate("testnoindex", TextField.of("title"),
        TagField.of("category").as("cat").sortable().noIndex()));

    assertOK(client.ftCreate("testunfnoindex", TextField.of("title"),
        TagField.of("category").as("cat").sortableUNF().noIndex()));
  }

  @Test
  public void returnFields() {
    assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2")));

    Map<String, Object> doc = new HashMap<>();
    doc.put("field1", "value1");
    doc.put("field2", "value2");
    addDocument("doc", doc);

    // Query
    SearchResult res = client.ftSearch(index, "*",
        FTSearchParams.searchParams().returnFields("field1"));
    assertEquals(1, res.getTotalResults());
    Document ret = res.getDocuments().get(0);
    assertEquals("value1", ret.get("field1"));
    assertNull(ret.get("field2"));

    res = client.ftSearch(index, "*", FTSearchParams.searchParams().returnField("field1", true));
    assertEquals("value1", res.getDocuments().get(0).get("field1"));

    res = client.ftSearch(index, "*", FTSearchParams.searchParams().returnField("field1", false));
    assertArrayEquals("value1".getBytes(), (byte[]) res.getDocuments().get(0).get("field1"));
  }

  @Test
  public void returnFieldsNames() {
    assertOK(client.ftCreate(index, TextField.of("a"), TextField.of("b"), TextField.of("c")));

    Map<String, Object> map = new HashMap<>();
    map.put("a", "value1");
    map.put("b", "value2");
    map.put("c", "value3");
    addDocument("doc", map);

    // Query
    SearchResult res = client.ftSearch(index, "*",
        FTSearchParams.searchParams()
            .returnFields(FieldName.of("a"),
                FieldName.of("b").as("d")));
    assertEquals(1, res.getTotalResults());
    Document doc = res.getDocuments().get(0);
    assertEquals("value1", doc.get("a"));
    assertNull(doc.get("b"));
    assertEquals("value2", doc.get("d"));
    assertNull(doc.get("c"));

    res = client.ftSearch(index, "*",
        FTSearchParams.searchParams()
            .returnField(FieldName.of("a"))
            .returnField(FieldName.of("b").as("d")));
    assertEquals(1, res.getTotalResults());
    assertEquals("value1", res.getDocuments().get(0).get("a"));
    assertEquals("value2", res.getDocuments().get(0).get("d"));

    res = client.ftSearch(index, "*",
        FTSearchParams.searchParams()
            .returnField(FieldName.of("a"), true)
            .returnField(FieldName.of("b").as("d"), true));
    assertEquals(1, res.getTotalResults());
    assertEquals("value1", res.getDocuments().get(0).get("a"));
    assertEquals("value2", res.getDocuments().get(0).get("d"));

    res = client.ftSearch(index, "*",
        FTSearchParams.searchParams()
            .returnField(FieldName.of("a"), false)
            .returnField(FieldName.of("b").as("d"), false));
    assertEquals(1, res.getTotalResults());
    assertArrayEquals("value1".getBytes(), (byte[]) res.getDocuments().get(0).get("a"));
    assertArrayEquals("value2".getBytes(), (byte[]) res.getDocuments().get(0).get("d"));
  }

  @Test
  public void inKeys() {
    assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2")));

    Map<String, Object> doc = new HashMap<>();
    doc.put("field1", "value");
    doc.put("field2", "not");
    // Store it
    addDocument("doc1", doc);
    addDocument("doc2", doc);

    // Query
    SearchResult res = client.ftSearch(index, "value",
        FTSearchParams.searchParams().inKeys("doc1"));
    assertEquals(1, res.getTotalResults());
    assertEquals("doc1", res.getDocuments().get(0).getId());
    assertEquals("value", res.getDocuments().get(0).get("field1"));
    assertNull(res.getDocuments().get(0).get("value"));
  }

  @Test
  public void alias() {
    assertOK(client.ftCreate(index, TextField.of("field1")));

    Map<String, Object> doc = new HashMap<>();
    doc.put("field1", "value");
    addDocument("doc1", doc);

    assertEquals("OK", client.ftAliasAdd("ALIAS1", index));
    SearchResult res1 = client.ftSearch("ALIAS1", "*",
        FTSearchParams.searchParams().returnFields("field1"));
    assertEquals(1, res1.getTotalResults());
    assertEquals("value", res1.getDocuments().get(0).get("field1"));

    assertEquals("OK", client.ftAliasUpdate("ALIAS2", index));
    SearchResult res2 = client.ftSearch("ALIAS2", "*",
        FTSearchParams.searchParams().returnFields("field1"));
    assertEquals(1, res2.getTotalResults());
    assertEquals("value", res2.getDocuments().get(0).get("field1"));

    try {
      client.ftAliasDel("ALIAS3");
      fail("Should throw JedisDataException");
    } catch (JedisDataException e) {
      // Alias does not exist
    }
    assertEquals("OK", client.ftAliasDel("ALIAS2"));
    try {
      client.ftAliasDel("ALIAS2");
      fail("Should throw JedisDataException");
    } catch (JedisDataException e) {
      // Alias does not exist
    }
  }

  @Test
  public void synonym() {
    assertOK(client.ftCreate(index, TextField.of("name").weight(1), TextField.of("addr").weight(1)));

    long group1 = 345L;
    long group2 = 789L;
    String group1_str = Long.toString(group1);
    String group2_str = Long.toString(group2);
    assertEquals("OK", client.ftSynUpdate(index, group1_str, "girl", "baby"));
    assertEquals("OK", client.ftSynUpdate(index, group1_str, "child"));
    assertEquals("OK", client.ftSynUpdate(index, group2_str, "child"));

    Map<String, List<String>> dump = client.ftSynDump(index);

    Map<String, List<String>> expected = new HashMap<>();
    expected.put("girl", Arrays.asList(group1_str));
    expected.put("baby", Arrays.asList(group1_str));
    expected.put("child", Arrays.asList(group1_str, group2_str));
    assertEquals(expected, dump);
  }

  @Test
  public void slop() {
    assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2")));

    Map<String, Object> doc = new HashMap<>();
    doc.put("field1", "ok hi jedis");
    addDocument("doc1", doc);

    SearchResult res = client.ftSearch(index, "ok jedis", FTSearchParams.searchParams().slop(0));
    assertEquals(0, res.getTotalResults());

    res = client.ftSearch(index, "ok jedis", FTSearchParams.searchParams().slop(1));
    assertEquals(1, res.getTotalResults());
    assertEquals("doc1", res.getDocuments().get(0).getId());
    assertEquals("ok hi jedis", res.getDocuments().get(0).get("field1"));
  }

  @Test
  public void timeout() {
    assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2")));

    Map<String, String> map = new HashMap<>();
    map.put("field1", "value");
    map.put("field2", "not");
    client.hset("doc1", map);

    SearchResult res = client.ftSearch(index, "value", FTSearchParams.searchParams().timeout(1000));
    assertEquals(1, res.getTotalResults());
    assertEquals("doc1", res.getDocuments().get(0).getId());
    assertEquals("value", res.getDocuments().get(0).get("field1"));
    assertEquals("not", res.getDocuments().get(0).get("field2"));
  }

  @Test
  public void inOrder() {
    assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2")));

    Map<String, String> map = new HashMap<>();
    map.put("field1", "value");
    map.put("field2", "not");
    client.hset("doc2", map);
    client.hset("doc1", map);

    SearchResult res = client.ftSearch(index, "value", FTSearchParams.searchParams().inOrder());
    assertEquals(2, res.getTotalResults());
    assertEquals("doc2", res.getDocuments().get(0).getId());
    assertEquals("value", res.getDocuments().get(0).get("field1"));
    assertEquals("not", res.getDocuments().get(0).get("field2"));
  }

  @Test
  public void testHNSWVectorSimilarity() {
    Map<String, Object> attr = new HashMap<>();
    attr.put("TYPE", "FLOAT32");
    attr.put("DIM", 2);
    attr.put("DISTANCE_METRIC", "L2");

    assertOK(client.ftCreate(index, VectorField.builder().fieldName("v")
        .algorithm(VectorAlgorithm.HNSW).attributes(attr).build()));

    client.hset("a", "v", "aaaaaaaa");
    client.hset("b", "v", "aaaabaaa");
    client.hset("c", "v", "aaaaabaa");

    FTSearchParams searchParams = FTSearchParams.searchParams()
        .addParam("vec", "aaaaaaaa")
        .sortBy("__v_score", SortingOrder.ASC)
        .returnFields("__v_score")
        .dialect(2);
    Document doc1 = client.ftSearch(index, "*=>[KNN 2 @v $vec]", searchParams).getDocuments().get(0);
    assertEquals("a", doc1.getId());
    assertEquals("0", doc1.get("__v_score"));
  }

  @Test
  public void testFlatVectorSimilarity() {
    assertOK(client.ftCreate(index,
        VectorField.builder().fieldName("v")
            .algorithm(VectorAlgorithm.FLAT)
            .addAttribute("TYPE", "FLOAT32")
            .addAttribute("DIM", 2)
            .addAttribute("DISTANCE_METRIC", "L2")
            .build()
    ));

    client.hset("a", "v", "aaaaaaaa");
    client.hset("b", "v", "aaaabaaa");
    client.hset("c", "v", "aaaaabaa");

    FTSearchParams searchParams = FTSearchParams.searchParams()
        .addParam("vec", "aaaaaaaa")
        .sortBy("__v_score", SortingOrder.ASC)
        .returnFields("__v_score")
        .dialect(2);

    Document doc1 = client.ftSearch(index, "*=>[KNN 2 @v $vec]", searchParams).getDocuments().get(0);
    assertEquals("a", doc1.getId());
    assertEquals("0", doc1.get("__v_score"));
  }

  @Test
  public void testFlatVectorSimilarityInt8() {
    assumeTrue(RedisConditions.of(client).moduleVersionIsGreaterThanOrEqual(SEARCH_MOD_VER_80M3),
        "INT8");
    assertOK(client.ftCreate(index,
        VectorField.builder().fieldName("v").algorithm(VectorAlgorithm.FLAT)
            .addAttribute("TYPE", "INT8").addAttribute("DIM", 2)
            .addAttribute("DISTANCE_METRIC", "L2").build()));

    byte[] a = { 127, 1 };
    byte[] b = { 127, 10 };
    byte[] c = { 127, 100 };

    client.hset("a".getBytes(), "v".getBytes(), a);
    client.hset("b".getBytes(), "v".getBytes(), b);
    client.hset("c".getBytes(), "v".getBytes(), c);

    FTSearchParams searchParams = FTSearchParams.searchParams().addParam("vec", a)
        .sortBy("__v_score", SortingOrder.ASC).returnFields("__v_score");

    Document doc1 = client.ftSearch(index, "*=>[KNN 2 @v $vec]", searchParams).getDocuments()
        .get(0);
    assertEquals("a", doc1.getId());
    assertEquals("0", doc1.get("__v_score"));
  }

  @Test
  @SinceRedisVersion(value = "7.4.0", message = "no optional params before 7.4.0")
  public void vectorFieldParams() {
    Map<String, Object> attr = new HashMap<>();
    attr.put("TYPE", "FLOAT32");
    attr.put("DIM", 2);
    attr.put("DISTANCE_METRIC", "L2");

    assertOK(client.ftCreate("testindex-missing", new VectorField("vector", VectorAlgorithm.HNSW, attr).as("vec").indexMissing()));

    // assertOK(client.ftCreate("testnoindex", new VectorField("vector", VectorAlgorithm.HNSW, attr).as("vec").noIndex()));
    // throws Field `NOINDEX` does not have a type
  }

  @Test
  @SinceRedisVersion(value = "7.4.0", message = "FLOAT16")
  public void float16StorageType() {
    assertOK(client.ftCreate(index,
        VectorField.builder().fieldName("v")
            .algorithm(VectorAlgorithm.HNSW)
            .addAttribute("TYPE", "FLOAT16")
            .addAttribute("DIM", 4)
            .addAttribute("DISTANCE_METRIC", "L2")
            .build()));
  }

  @Test
  @SinceRedisVersion(value = "7.4.0", message = "BFLOAT16")
  public void bfloat16StorageType() {
    assertOK(client.ftCreate(index,
        VectorField.builder().fieldName("v")
            .algorithm(VectorAlgorithm.HNSW)
            .addAttribute("TYPE", "BFLOAT16")
            .addAttribute("DIM", 4)
            .addAttribute("DISTANCE_METRIC", "L2")
            .build()));
  }

  @Test
  public void int8StorageType() {
    assumeTrue(RedisConditions.of(client).moduleVersionIsGreaterThanOrEqual(SEARCH_MOD_VER_80M3),
        "INT8");
    assertOK(client.ftCreate(index,
        VectorField.builder().fieldName("v").algorithm(VectorAlgorithm.HNSW)
            .addAttribute("TYPE", "INT8").addAttribute("DIM", 4)
            .addAttribute("DISTANCE_METRIC", "L2").build()));
  }

  @Test
  public void uint8StorageType() {
    assumeTrue(RedisConditions.of(client).moduleVersionIsGreaterThanOrEqual(SEARCH_MOD_VER_80M3),
        "UINT8");
    assertOK(client.ftCreate(index,
        VectorField.builder().fieldName("v").algorithm(VectorAlgorithm.HNSW)
            .addAttribute("TYPE", "UINT8").addAttribute("DIM", 4)
            .addAttribute("DISTANCE_METRIC", "L2").build()));
  }

  @Test
  public void searchProfile() {
    assertOK(client.ftCreate(index, TextField.of("t1"), TextField.of("t2")));

    Map<String, String> hash = new HashMap<>();
    hash.put("t1", "foo");
    hash.put("t2", "bar");
    client.hset("doc1", hash);

    Map.Entry<SearchResult, ProfilingInfo> reply = client.ftProfileSearch(index,
        FTProfileParams.profileParams(), "foo", FTSearchParams.searchParams());

    SearchResult result = reply.getKey();
    assertEquals(1, result.getTotalResults());
    assertEquals(Collections.singletonList("doc1"), result.getDocuments().stream().map(Document::getId).collect(Collectors.toList()));

    // profile
    Object profileObject = reply.getValue().getProfilingInfo();
    if (protocol != RedisProtocol.RESP3) {
      assertThat(profileObject, Matchers.isA(List.class));
      if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V8_0_0)) {
        assertThat((List<Object>) profileObject, Matchers.hasItems("Shards", "Coordinator"));
      }
    } else {
      assertThat(profileObject, Matchers.isA(Map.class));
      if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V8_0_0)) {
        assertThat(((Map<String, Object>) profileObject).keySet(), Matchers.hasItems("Shards", "Coordinator"));
      }
    }
  }

  @Test
  public void vectorSearchProfile() {
    assertOK(client.ftCreate(index, VectorField.builder().fieldName("v")
        .algorithm(VectorAlgorithm.FLAT).addAttribute("TYPE", "FLOAT32")
        .addAttribute("DIM", 2).addAttribute("DISTANCE_METRIC", "L2").build(),
        TextField.of("t")));

    client.hset("1", toMap("v", "bababaca", "t", "hello"));
    client.hset("2", toMap("v", "babababa", "t", "hello"));
    client.hset("3", toMap("v", "aabbaabb", "t", "hello"));
    client.hset("4", toMap("v", "bbaabbaa", "t", "hello world"));
    client.hset("5", toMap("v", "aaaabbbb", "t", "hello world"));

    FTSearchParams searchParams = FTSearchParams.searchParams().addParam("vec", "aaaaaaaa")
        .sortBy("__v_score", SortingOrder.ASC).noContent().dialect(2);
    Map.Entry<SearchResult, ProfilingInfo> reply = client.ftProfileSearch(index,
        FTProfileParams.profileParams(), "*=>[KNN 3 @v $vec]", searchParams);
    assertEquals(3, reply.getKey().getTotalResults());

    assertEquals(Arrays.asList(4, 2, 1).toString(), reply.getKey().getDocuments()
        .stream().map(Document::getId).collect(Collectors.toList()).toString());

    // profile
    Object profileObject = reply.getValue().getProfilingInfo();
    if (protocol != RedisProtocol.RESP3) {
      assertThat(profileObject, Matchers.isA(List.class));
      if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V8_0_0)) {
        assertThat((List<Object>) profileObject, Matchers.hasItems("Shards", "Coordinator"));
      }
    } else {
      assertThat(profileObject, Matchers.isA(Map.class));
      if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V8_0_0)) {
        assertThat(((Map<String, Object>) profileObject).keySet(), Matchers.hasItems("Shards", "Coordinator"));
      }
    }
  }

  @Test
  public void limitedSearchProfile() {
    assertOK(client.ftCreate(index, TextField.of("t")));
    client.hset("1", "t", "hello");
    client.hset("2", "t", "hell");
    client.hset("3", "t", "help");
    client.hset("4", "t", "helowa");

    Map.Entry<SearchResult, ProfilingInfo> reply = client.ftProfileSearch(index,
        FTProfileParams.profileParams().limited(), "%hell% hel*", FTSearchParams.searchParams().noContent());

    // profile
    Object profileObject = reply.getValue().getProfilingInfo();
    if (protocol != RedisProtocol.RESP3) {
      assertThat(profileObject, Matchers.isA(List.class));
      if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V8_0_0)) {
        assertThat((List<Object>) profileObject, Matchers.hasItems("Shards", "Coordinator"));
      }
    } else {
      assertThat(profileObject, Matchers.isA(Map.class));
      if (RedisVersionUtil.getRedisVersion(client).isGreaterThanOrEqualTo(RedisVersion.V8_0_0)) {
        assertThat(((Map<String, Object>) profileObject).keySet(), Matchers.hasItems("Shards", "Coordinator"));
      }
    }
  }

  @Test
  public void list() {
    assertEquals(Collections.emptySet(), client.ftList());

    final int count = 20;
    Set<String> names = new HashSet<>();
    for (int i = 0; i < count; i++) {
      final String name = "idx" + i;
      assertOK(client.ftCreate(name, TextField.of("t" + i)));
      names.add(name);
    }
    assertEquals(names, client.ftList());
  }

  @Test
  public void broadcast() {
    String reply = client.ftCreate(index, TextField.of("t"));
    assertOK(reply);
  }

  @Test
  public void searchIteration() {
    assertOK(client.ftCreate(index, FTCreateParams.createParams(),
        TextField.of("first"), TextField.of("last"), NumericField.of("age")));

    client.hset("profesor:5555", toMap("first", "Albert", "last", "Blue", "age", "55"));
    client.hset("student:1111", toMap("first", "Joe", "last", "Dod", "age", "18"));
    client.hset("pupil:2222", toMap("first", "Jen", "last", "Rod", "age", "14"));
    client.hset("student:3333", toMap("first", "El", "last", "Mark", "age", "17"));
    client.hset("pupil:4444", toMap("first", "Pat", "last", "Shu", "age", "21"));
    client.hset("student:5555", toMap("first", "Joen", "last", "Ko", "age", "20"));
    client.hset("teacher:6666", toMap("first", "Pat", "last", "Rod", "age", "20"));

    FtSearchIteration search = client.ftSearchIteration(3, index, "*", FTSearchParams.searchParams());
    int total = 0;
    while (!search.isIterationCompleted()) {
      SearchResult result = search.nextBatch();
      int count = result.getDocuments().size();
      assertThat(count, Matchers.lessThanOrEqualTo(3));
      total += count;
    }
    assertEquals(7, total);
  }

  @Test
  public void searchIterationCollect() {
    assertOK(client.ftCreate(index, FTCreateParams.createParams(),
        TextField.of("first"), TextField.of("last"), NumericField.of("age")));

    client.hset("profesor:5555", toMap("first", "Albert", "last", "Blue", "age", "55"));
    client.hset("student:1111", toMap("first", "Joe", "last", "Dod", "age", "18"));
    client.hset("pupil:2222", toMap("first", "Jen", "last", "Rod", "age", "14"));
    client.hset("student:3333", toMap("first", "El", "last", "Mark", "age", "17"));
    client.hset("pupil:4444", toMap("first", "Pat", "last", "Shu", "age", "21"));
    client.hset("student:5555", toMap("first", "Joen", "last", "Ko", "age", "20"));
    client.hset("teacher:6666", toMap("first", "Pat", "last", "Rod", "age", "20"));

    ArrayList<Document> collect = new ArrayList<>();
    client.ftSearchIteration(3, index, "*", FTSearchParams.searchParams()).collect(collect);
    assertEquals(7, collect.size());
    assertEquals(Arrays.asList("profesor:5555", "student:1111", "pupil:2222", "student:3333",
        "pupil:4444", "student:5555", "teacher:6666").stream().collect(Collectors.toSet()),
        collect.stream().map(Document::getId).collect(Collectors.toSet()));
  }

  @Test
  public void escapeUtil() {
    assertOK(client.ftCreate(index, TextField.of("txt")));

    client.hset("doc1", "txt", RediSearchUtil.escape("hello-world"));
    assertNotEquals("hello-world", client.hget("doc1", "txt"));
    assertEquals("hello-world", RediSearchUtil.unescape(client.hget("doc1", "txt")));

    SearchResult resultNoEscape = client.ftSearch(index, "hello-world");
    assertEquals(0, resultNoEscape.getTotalResults());

    SearchResult resultEscaped = client.ftSearch(index, RediSearchUtil.escapeQuery("hello-world"));
    assertEquals(1, resultEscaped.getTotalResults());
  }

  @Test
  public void escapeMapUtil() {
    client.hset("doc2", RediSearchUtil.toStringMap(Collections.singletonMap("txt", "hello-world"), true));
    assertNotEquals("hello-world", client.hget("doc2", "txt"));
    assertEquals("hello-world", RediSearchUtil.unescape(client.hget("doc2", "txt")));
  }

  @Test
  public void hsetObject() {
    float[] floats = new float[]{0.2f};
    assertEquals(1L, client.hsetObject("obj", "floats", floats));
    assertArrayEquals(RediSearchUtil.toByteArray(floats),
        client.hget("obj".getBytes(), "floats".getBytes()));

    GeoCoordinate geo = new GeoCoordinate(-0.441, 51.458);
    Map<String, Object> fields = new HashMap<>();
    fields.put("title", "hello world");
    fields.put("loc", geo);
    assertEquals(2L, client.hsetObject("obj", fields));
    Map<String, String> stringMap = client.hgetAll("obj");
    assertEquals(3, stringMap.size());
    assertEquals("hello world", stringMap.get("title"));
    assertEquals(geo.getLongitude() + "," + geo.getLatitude(), stringMap.get("loc"));
  }
}