FTHybridParams.java

package redis.clients.jedis.search.hybrid;

import redis.clients.jedis.CommandArguments;
import redis.clients.jedis.annots.Experimental;
import redis.clients.jedis.params.IParams;
import redis.clients.jedis.search.Combiner;
import redis.clients.jedis.search.Combiners;
import redis.clients.jedis.util.JedisAsserts;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.*;

/**
 * Argument list builder for the Redis {@code FT.HYBRID} command. Combines text search and vector
 * similarity search with configurable combination strategies and post-processing operations.
 * <p>
 * <strong>Basic Usage:</strong>
 * </p>
 *
 * <pre>
 * FTHybridParams params = FTHybridParams.builder()
 *     .search(FTHybridSearchParams.builder().query("comfortable shoes").build())
 *     .vectorSearch(FTHybridVectorParams.builder().field("@embedding").vector("vec")
 *         .method(FTHybridVectorParams.Knn.of(10)).build())
 *     .combine(Combiners.rrf()).param("vec", vectorBlob).build();
 * </pre>
 *
 * @see FTHybridSearchParams
 * @see FTHybridVectorParams
 * @see Combiner
 * @see Combiners
 * @see FTHybridPostProcessingParams
 */
@Experimental
public class FTHybridParams implements IParams {

  private final List<FTHybridSearchParams> searchArgs = new ArrayList<>();
  private final List<FTHybridVectorParams> vectorArgs = new ArrayList<>();
  private Combiner combiner;
  private FTHybridPostProcessingParams postProcessingArgs;
  private final Map<String, Object> params = new HashMap<>();
  private Long timeout;

  private FTHybridParams() {
  }

  /**
   * @return a new {@link Builder} for {@link FTHybridParams}.
   */
  public static Builder builder() {
    return new Builder();
  }

  /**
   * Builder for {@link FTHybridParams}.
   */
  public static class Builder {
    private final FTHybridParams instance = new FTHybridParams();

    /**
     * Build the {@link FTHybridParams} instance.
     * @return the configured arguments
     */
    public FTHybridParams build() {
      // Validate that both SEARCH and VSIM are configured (per FT.HYBRID requirements)
      JedisAsserts.isTrue(!instance.searchArgs.isEmpty(),
        "At least one SEARCH clause must be configured");
      JedisAsserts.isTrue(!instance.vectorArgs.isEmpty(),
        "At least one VSIM clause must be configured");

      return instance;
    }

    /**
     * Configure the SEARCH clause using {@link FTHybridSearchParams}.
     * @param searchArgs the search arguments
     * @return this builder
     */
    public Builder search(FTHybridSearchParams searchArgs) {
      JedisAsserts.notNull(searchArgs, "Search args must not be null");

      instance.searchArgs.add(searchArgs);
      return this;
    }

    /**
     * Configure the VSIM clause using {@link FTHybridVectorParams}.
     * @param vectorArgs the vector search arguments
     * @return this builder
     */
    public Builder vectorSearch(FTHybridVectorParams vectorArgs) {
      JedisAsserts.notNull(vectorArgs, "Vector args must not be null");

      instance.vectorArgs.add(vectorArgs);
      return this;
    }

    /**
     * Configure the COMBINE clause using a {@link Combiner}.
     * @param combiner the combiner (e.g., {@code Combiners.rrf()} or {@code Combiners.linear()})
     * @return this builder
     * @see Combiners
     */
    public Builder combine(Combiner combiner) {
      JedisAsserts.notNull(combiner, "Combiner must not be null");

      instance.combiner = combiner;
      return this;
    }

    /**
     * Set the post-processing arguments.
     * @param postProcessingArgs the post-processing configuration
     * @return this builder
     */
    public Builder postProcessing(FTHybridPostProcessingParams postProcessingArgs) {
      JedisAsserts.notNull(postProcessingArgs, "PostProcessingParams must not be null");

      instance.postProcessingArgs = postProcessingArgs;
      return this;
    }

    /**
     * Add a parameter for parameterized queries.
     * <p>
     * Parameters can be referenced in queries using {@code $name} syntax.
     * </p>
     * @param name the parameter name
     * @param value the parameter value
     * @return this builder
     */
    public Builder param(String name, Object value) {
      JedisAsserts.notNull(name, "Parameter name must not be null");
      JedisAsserts.notNull(value, "Parameter value must not be null");

      instance.params.put(name, value);
      return this;
    }

    /**
     * Set the maximum time to wait for the query to complete (in milliseconds).
     * @param timeout the timeout in milliseconds
     * @return this builder
     */
    public Builder timeout(long timeout) {
      instance.timeout = timeout;
      return this;
    }
  }

  @Override
  public void addParams(CommandArguments args) {
    // SEARCH clause(s)
    for (FTHybridSearchParams searchArg : searchArgs) {
      searchArg.addParams(args);
    }

    // VSIM clause(s)
    for (FTHybridVectorParams vectorArg : vectorArgs) {
      vectorArg.addParams(args);
    }

    // COMBINE clause
    if (combiner != null) {
      args.add(COMBINE);
      combiner.addParams(args);
    }

    // Post-processing operations (LOAD, GROUPBY, APPLY, SORTBY, FILTER, LIMIT)
    if (postProcessingArgs != null) {
      postProcessingArgs.addParams(args);
    }

    // PARAMS clause
    if (!params.isEmpty()) {
      args.add(PARAMS);
      args.add(params.size() * 2);
      for (Map.Entry<String, Object> entry : params.entrySet()) {
        args.add(entry.getKey());
        args.add(entry.getValue());
      }
    }

    // TIMEOUT clause
    if (timeout != null) {
      args.add(TIMEOUT);
      args.add(timeout);
    }
  }
}