> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Filters and boolean operators

> How to combine filters and `facetFilters` using boolean operators.

export const FilterValidator = () => {
  const [filter, setFilter] = useState("");
  const [parsedHtml, setParsedHtml] = useState("");
  const [statusMessage, setStatusMessage] = useState("");
  const [hasError, setHasError] = useState(false);
  const textareaRef = useRef(null);
  const highlightRef = useRef(null);
  const parseFilter = useMemo(() => {
    const tokensList = {
      Token_Empty_Str: "empty string",
      Token_Incomplete_Str: "incomplete string",
      Token_Error: "!",
      Token_Term: "filter",
      Token_String: "string",
      Token_Num: "numeric",
      Token_Facet_Separator: "':'",
      Token_Range: "'TO'",
      Token_Open_Paren: "'('",
      Token_Close_Paren: "')'",
      Token_OR: "'OR'",
      Token_AND: "'AND'",
      Token_NOT: "'NOT'",
      Token_EOF: "end of filter",
      Token_Open_Angled_Bracket: "<",
      Token_Close_Angled_Bracket: ">",
      Token_Comma: ",",
      Token_Operator: "numeric operator",
      Term_Tag: "tag filter",
      Term_Numeric: "numeric filter",
      Term_Facet: "tag filter"
    };
    const createToken = (type, string, rawString, pos) => {
      const rawValue = rawString === null ? string : rawString;
      return {
        type,
        value: string,
        raw_value: rawValue,
        pos,
        errorStart: false,
        errorStop: false,
        unexpectedMessage: "",
        afterSeparators: "",
        cssClasses: ["token", type],
        afterSeparatorsCssClasses: ["token-spaces"]
      };
    };
    const tokenLabel = type => tokensList[type];
    const createLexer = (withHighlight = false) => {
      const state = {
        tokens: [],
        currentPos: 0,
        lastToken: null
      };
      const resetState = () => {
        state.tokens.length = 0;
        state.currentPos = 0;
        state.lastToken = null;
      };
      const addToken = token => {
        state.tokens.push(token);
        state.lastToken = token;
      };
      function isSeparator(character) {
        return character === " " || character === "(" || character === ")" || character === "<" || character === ">" || character === "=" || character === "!" || character === ":" || character === "'" || character === '"';
      }
      function highlightSize(input, index) {
        if (!withHighlight) {
          return 0;
        }
        if (index <= input.length - 3 && input[index] === "<" && input[index + 1] === "b" && input[index + 2] === ">") {
          return 3;
        }
        if (index <= input.length - 4 && input[index] === "<" && input[index + 1] === "/" && input[index + 2] === "b" && input[index + 3] === ">") {
          return 4;
        }
        return 0;
      }
      function readTokenValue(input, index) {
        let onlyNum = true;
        let dotCount = 0;
        let cursor = index;
        for (; cursor < input.length && (!isSeparator(input[cursor]) || highlightSize(input, cursor) > 0); cursor += 1) {
          const size = highlightSize(input, cursor);
          onlyNum = onlyNum && (size > 0 || input[cursor] >= "0" && input[cursor] <= "9" || cursor === index && input[cursor] === "-" || cursor > index && input[cursor] === "." && ++dotCount === 1);
          if (size > 0) {
            cursor += size - 1;
          }
        }
        const rawValue = input.slice(index, cursor);
        if (cursor - index === 2 && input[index] === "O" && input[index + 1] === "R") {
          return createToken("Token_OR", rawValue, null, index);
        }
        if (cursor - index === 2 && input[index] === "T" && input[index + 1] === "O") {
          return createToken("Token_Range", rawValue, null, index);
        }
        if (cursor - index === 3 && input[index] === "N" && input[index + 1] === "O" && input[index + 2] === "T") {
          return createToken("Token_NOT", rawValue, null, index);
        }
        if (cursor - index === 3 && input[index] === "A" && input[index + 1] === "N" && input[index + 2] === "D") {
          return createToken("Token_AND", rawValue, null, index);
        }
        return createToken(onlyNum ? "Token_Num" : "Token_String", rawValue, null, index);
      }
      function readQuotedString(input, index) {
        const quoteType = input[index];
        let escape = false;
        let cursor;
        for (cursor = index + 1; cursor < input.length; cursor += 1) {
          if (input[cursor] === quoteType && !escape) {
            if (cursor - index <= 1) {
              return createToken("Token_Empty_Str", quoteType + quoteType, null, cursor);
            }
            return createToken("Token_String", input.slice(index + 1, cursor), input.slice(index, cursor + 1), index);
          }
          if (input[cursor] === "\\") {
            escape = !escape;
          } else {
            escape = false;
          }
        }
        return createToken("Token_Incomplete_Str", input.slice(index + 1, cursor), input.slice(index, cursor), index);
      }
      function readToken(input, index) {
        if (input[index] === "=") {
          return createToken("Token_Operator", "=", null, index);
        }
        if (input[index] === "<" && highlightSize(input, index) <= 0) {
          if (input.length > index + 1 && input[index + 1] === "=") {
            return createToken("Token_Operator", "<=", null, index);
          }
          return createToken("Token_Open_Angled_Bracket", "<", null, index);
        }
        if (input[index] === ">") {
          if (input.length > index + 1 && input[index + 1] === "=") {
            return createToken("Token_Operator", ">=", null, index);
          }
          return createToken("Token_Close_Angled_Bracket", ">", null, index);
        }
        if (input[index] === "!") {
          if (input.length > index + 1 && input[index + 1] === "=") {
            return createToken("Token_Operator", "!=", null, index);
          }
          return createToken("Token_Error", input[index], null, index);
        }
        if (input[index] === "(") {
          return createToken("Token_Open_Paren", "(", null, index);
        }
        if (input[index] === ")") {
          return createToken("Token_Close_Paren", ")", null, index);
        }
        if (input[index] === ":") {
          return createToken("Token_Facet_Separator", ":", null, index);
        }
        if (input[index] === ",") {
          return createToken("Token_Comma", ",", null, index);
        }
        if (input[index] === '"' || input[index] === "'") {
          return readQuotedString(input, index);
        }
        return readTokenValue(input, index);
      }
      function get(offset = 0) {
        const target = state.currentPos + offset;
        if (state.tokens.length === 0) {
          return undefined;
        }
        if (target < 0) {
          return state.tokens[0];
        }
        if (target >= state.tokens.length) {
          return state.tokens[state.tokens.length - 1];
        }
        return state.tokens[target];
      }
      function next() {
        if (state.currentPos > state.tokens.length) {
          return null;
        }
        state.currentPos += 1;
        return undefined;
      }
      function lex(input) {
        resetState();
        let cursor = 0;
        addToken(createToken("First_Token", "", null, 0));
        while (cursor < input.length && input[cursor] === " ") {
          state.lastToken.afterSeparators += input[cursor];
          cursor += 1;
        }
        while (cursor < input.length) {
          const token = readToken(input, cursor);
          addToken(token);
          if (token.type === "Token_Error") {
            token.afterSeparators = input.slice(cursor + 1);
            state.currentPos = state.tokens.length - 1;
            return false;
          }
          cursor += token.raw_value.length;
          while (cursor < input.length && input[cursor] === " ") {
            state.lastToken.afterSeparators += input[cursor];
            cursor += 1;
          }
        }
        addToken(createToken("Token_EOF", "E", null, input.length));
        return true;
      }
      const getTokens = () => state.tokens;
      return {
        lex,
        get,
        next,
        getTokens
      };
    };
    const parse = input => {
      const lexer = createLexer();
      let termType = "Term_None";
      let firstTermToken = null;
      const tags = [];
      const numericsFilters = [];
      const facetFilters = [];
      let foundAND = false;
      let foundOR = false;
      const response = {
        html: "",
        errorMessage: ""
      };
      if (input.length === 0) {
        return response;
      }
      if (!lexer.lex(input)) {
        const invalidToken = lexer.get(0);
        if (invalidToken) {
          response.errorMessage = `Not allowed ${tokenLabel(invalidToken.type)} at col ${invalidToken.pos}`;
        }
      }
      lexer.next();
      function error(token, errorMessage) {
        token.unexpectedMessage = errorMessage;
        token.errorStop = true;
      }
      function unexpectedToken(token, expected) {
        let message = `Unexpected token ${tokenLabel(token.type)}`;
        if (token.value.length > 0 && (token.type === "Token_String" || token.type === "Token_Num")) {
          message += `(${token.value.replace(/\n/g, "\u21b5")})`;
        }
        message += ` expected ${tokenLabel(expected)} at col ${token.pos}`;
        error(token, message);
      }
      function getSameOrError(expectedType) {
        let message = "Different types are not allowed in the same OR.";
        if (expectedType === "Term_Numeric") {
          message += "\nExpected a numeric filter which needs to have one of the following form:";
          message += "\n - numeric_attr_name=10";
          message += "\n - numeric_attr_name>10";
          message += "\n - numeric_attr_name>=10";
          message += "\n - numeric_attr_name<10";
          message += "\n - numeric_attr_name<=10";
          message += "\n - numeric_attr_name!=10";
          message += "\n - numeric_attr_name:10 TO 20";
        } else if (expectedType === "Term_Facet") {
          message += "\nExpected a facet filter which needs to have this form:";
          message += "\n - facet_name:facet_value";
        } else if (expectedType === "Term_Tag") {
          message += "\nExpected a tag filter which needs to have this form:";
          message += "\n - _tags:tag_value";
          message += "\n - tag_value";
        }
        return message;
      }
      function isOption(token) {
        return token.value === "score";
      }
      function negateOperator() {}
      function parseOption(score) {
        void score;
        if (lexer.get().type !== "Token_String") {
          unexpectedToken(lexer.get(), "Token_String");
          return false;
        }
        const optionNameToken = lexer.get();
        lexer.next();
        if (optionNameToken.value === "score") {
          if (lexer.get().type !== "Token_Operator" || lexer.get().value !== "=") {
            unexpectedToken(lexer.get(), '"="');
            return false;
          }
          lexer.next();
          if (lexer.get().type !== "Token_Num") {
            unexpectedToken(lexer.get(), "Token_Num");
            return false;
          }
          const newScore = lexer.get().value;
          lexer.next();
          return newScore;
        }
        unexpectedToken(optionNameToken, '"score"');
        return false;
      }
      function parseOptions(score) {
        if (lexer.get().type !== "Token_Open_Angled_Bracket") {
          return true;
        }
        lexer.next();
        let hasNext = true;
        do {
          if (!parseOption(score)) {
            return false;
          }
          hasNext = false;
          if (lexer.get().type === "Token_Comma") {
            hasNext = true;
          }
        } while (hasNext);
        if (lexer.get().type !== "Token_Close_Angled_Bracket") {
          unexpectedToken(lexer.get(), "Token_Close_Angled_Bracket");
        }
        lexer.next();
        return true;
      }
      function parseTerm() {
        const score = 1;
        if (lexer.get().type === "Token_Open_Paren") {
          lexer.next();
          if (!parseIntermediate(true)) return false;
          if (lexer.get().type !== "Token_Close_Paren") {
            unexpectedToken(lexer.get(), "Token_Close_Paren");
            return false;
          }
          lexer.next();
          return true;
        }
        termType = "Term_None";
        let negative = false;
        if (lexer.get().type === "Token_NOT") {
          lexer.next();
          negative = true;
        }
        if (lexer.get().type === "Token_String" || lexer.get().type === "Token_Num") {
          firstTermToken = lexer.get();
          const attributeNameToken = lexer.get();
          lexer.next();
          if (lexer.get().type === "Token_Operator" || lexer.get().type === "Token_Open_Angled_Bracket" || lexer.get().type === "Token_Close_Angled_Bracket") {
            const operatorToken = lexer.get();
            if (lexer.get().type === "Token_Open_Angled_Bracket" && isOption(lexer.get(1))) {
              if (!parseOptions(score)) return false;
              tags.push(`${attributeNameToken.value}<${score}>`);
              return true;
            }
            lexer.next();
            if (lexer.get().type !== "Token_Num") {
              unexpectedToken(lexer.get(), "Token_Num");
              return false;
            }
            const valToken = lexer.get();
            lexer.next();
            termType = "Term_Numeric";
            if (negative) {
              negateOperator(operatorToken);
            }
            numericsFilters.push(`${attributeNameToken.value} ${operatorToken.value} ${valToken.value}`);
            return true;
          }
          if (lexer.get().type === "Token_Facet_Separator") {
            lexer.next();
            if (lexer.get().type !== "Token_String" && lexer.get().type !== "Token_Num") {
              unexpectedToken(lexer.get(), "Token_String");
              return false;
            }
            const valToken = lexer.get();
            lexer.next();
            if (valToken.type === "Token_Num" && lexer.get().type === "Token_Range") {
              lexer.next();
              if (lexer.get().type !== "Token_Num") {
                unexpectedToken(lexer.get(), "Token_Num");
                return false;
              }
              numericsFilters.push(`${attributeNameToken.value}:${valToken.value} TO ${lexer.get().value}`);
              lexer.next();
              termType = "Term_Numeric";
              return true;
            }
            const isTag = attributeNameToken.value === "_tags";
            termType = isTag ? "Term_Tag" : "Term_Facet";
            if (!parseOptions(score)) return false;
            if (negative) {
              if (isTag) {
                tags.push(`-${valToken.value}<${score}>`);
              } else {
                facetFilters.push(`-${attributeNameToken.value}:${valToken.value}<${score}>`);
              }
            } else if (valToken.value[0] === "-" || valToken.value[0] === "\\") {
              if (isTag) {
                tags.push(`\\${valToken.value}<${score}>`);
              } else {
                facetFilters.push(`\\${attributeNameToken.value}:${valToken.value}<${score}>`);
              }
            } else if (isTag) {
              tags.push(`${valToken.value}<${score}>`);
            } else {
              facetFilters.push(`${attributeNameToken.value}:${valToken.value}<${score}>`);
            }
            return true;
          }
          if (negative) {
            tags.push(`-${attributeNameToken.value}<${score}>`);
          } else if (attributeNameToken.value[0] === "-" || attributeNameToken.value[0] === "\\") {
            tags.push(`\\${attributeNameToken.value}<${score}>`);
          } else {
            tags.push(`${attributeNameToken.value}<${score}>`);
          }
          termType = "Term_Tag";
          return true;
        }
        unexpectedToken(lexer.get(), "Token_Term");
        return false;
      }
      function parseIntermediate(parseAnd) {
        let previousType = termType;
        if (parseTerm()) {
          do {
            if (foundOR && previousType !== "Term_None" && termType !== previousType) {
              if (firstTermToken) {
                firstTermToken.errorStart = true;
              }
              error(lexer.get(-1), getSameOrError(previousType));
              return false;
            }
            previousType = termType;
            const currentType = lexer.get().type;
            if (currentType !== "Token_OR" && (!parseAnd || currentType !== "Token_AND")) {
              break;
            }
            if (currentType === "Token_AND") {
              foundAND = true;
            } else {
              foundOR = true;
            }
            if (foundOR && foundAND) {
              error(lexer.get(), "filter (X AND Y) OR Z is not allowed, only (X OR Y) AND Z is allowed");
              return false;
            }
            lexer.next();
            if (!parseTerm()) {
              return false;
            }
          } while (true);
          return true;
        }
        return false;
      }
      function parseAndExpr() {
        termType = "Term_None";
        foundOR = false;
        foundAND = false;
        if (parseIntermediate(false)) {
          do {
            if (lexer.get().type !== "Token_AND") {
              break;
            }
            lexer.next();
            termType = "Term_None";
            foundOR = false;
            foundAND = false;
            if (!parseIntermediate(false)) {
              return false;
            }
          } while (true);
          return true;
        }
        return false;
      }
      if (parseAndExpr()) {
        if (lexer.get().type !== "Token_EOF") {
          unexpectedToken(lexer.get(), "Token_EOF");
        }
      }
      let isValid = true;
      const tokens = lexer.getTokens();
      response.tokens = tokens.map(token => {
        if (token.errorStart || !isValid) {
          isValid = false;
          token.cssClasses.push("unexpected");
        }
        if (token.errorStop) {
          token.cssClasses.push("unexpected");
          isValid = true;
        }
        if (!isValid) {
          token.afterSeparatorsCssClasses.push("unexpected");
        }
        if (token.unexpectedMessage) {
          response.errorMessage = token.unexpectedMessage;
        }
        response.html += `<span class="${token.cssClasses.join(" ")}">${token.raw_value}</span><span class="${token.afterSeparatorsCssClasses.join(" ")}">${token.afterSeparators}</span>`;
        return token;
      });
      return response;
    };
    return parse;
  }, []);
  const syncScrollPositions = useCallback(() => {
    if (!textareaRef.current || !highlightRef.current) {
      return;
    }
    const {scrollTop, scrollLeft} = textareaRef.current;
    highlightRef.current.style.transform = `translate3d(${-scrollLeft}px, ${-scrollTop}px, 0)`;
  }, []);
  useEffect(() => {
    syncScrollPositions();
  }, [filter, syncScrollPositions]);
  const handleFilterChange = event => {
    const nextFilter = event.target.value;
    const trimmedFilter = nextFilter.trim();
    setFilter(nextFilter);
    if (!trimmedFilter) {
      setParsedHtml("");
      setStatusMessage("");
      setHasError(false);
      return;
    }
    try {
      const {html, errorMessage} = parseFilter(nextFilter);
      if (errorMessage) {
        setHasError(true);
        setStatusMessage(errorMessage);
        setParsedHtml("");
        return;
      }
      setParsedHtml(html || "");
      setHasError(false);
      setStatusMessage("Filter expression is valid.");
    } catch (error) {
      setHasError(true);
      setParsedHtml("");
      if (error instanceof Error) {
        setStatusMessage(error.message);
      } else {
        setStatusMessage("Unable to parse filter expression.");
      }
    }
  };
  return <section>
      <p>Enter your filter to validate it:</p>
      <div className="relative border rounded my-6 h-32 overflow-hidden" id="filter-validator">
        <textarea className="absolute top-0 left-0 font-mono rounded h-full w-full resize-none outline-none p-2 bg-transparent" value={filter} onChange={handleFilterChange} onScroll={syncScrollPositions} ref={textareaRef} />
        <div className="absolute top-0 left-0 p-2 font-mono font-semibold rounded bg-transparent pointer-events-none whitespace-pre-wrap break-words" aria-hidden="true" ref={highlightRef} dangerouslySetInnerHTML={{
    __html: parsedHtml
  }} />
      </div>
      {statusMessage && <p role="status" aria-live="polite" className={hasError ? "text-red-600" : "text-green-600"}>
          {statusMessage}
        </p>}
    </section>;
};

Algolia supports the following operators, which must be in capital letters:

* **`OR`**: must match any of the combined conditions
* **`AND`**: must match all the combined conditions
* **`NOT`**: negates a filter

## Use of parentheses

Use parentheses `(` and `)` to combine several OR conditions.
These rules apply:

* **Don't group ANDs within ORs.**
  For example, `A OR (B AND C) OR (D AND E)` isn't supported.
* **Don't use parentheses with top-level AND combinations.**
  For example, `(A AND (B OR C))` isn't supported.
  (But `A AND (B OR C)` is supported)
* **Don't use parentheses to negate a group of expressions.**
  For example, `NOT (filter1 OR filter2)` isn't supported.

### Examples of valid combinations

The following examples are valid combinations of filter expressions:

* `A AND (B OR C)`
* `(A OR B) AND C`
* `A AND (B OR C) AND (D OR E)`
* `A AND B AND (C OR D) AND (E OR F)`
* `(A OR B) AND (C OR D) AND E`

## Example scenarios for filter combinations

The `filters` parameter gives precise control over search results with a SQL-like syntax.
Use the following examples to learn how to apply different filters and combine them with boolean logic.

### Basic filters

| Filter type        | Example                                                                                                                                                | Filter                           |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| Numeric comparison | Show products that cost \$13 or more                                                                                                                   | `price > 12.99`                  |
| Numeric range      | Show products with a price between $5.99 and $100                                                                                                      | `price:5.99 TO 100`              |
| Facets             | Show records categorized as "Book"                                                                                                                     | `category:Book`                  |
| Tag filters        | Show records marked with the "published" tag                                                                                                           | `_tags:published`                |
| Boolean filters    | Show records where the `isEnabled` attribute is `true`                                                                                                 | `isEnabled:true`                 |
| Filter by date     | Show records published between two dates [Unix timestamps](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#dates) | `date: 1441745506 TO 1441755506` |
| Negation           | Exclude discontinued products                                                                                                                          | `NOT is_discontinued:true`       |

### Combine filters

* Find books not written by a specific author: `(category:Book OR category:Ebook) AND NOT author:"JK Rowling"`
* Filter products by price range and category: `price:10 TO 50 AND (category:Electronics OR category:Accessories)`
* Complex filter with several conditions: `inStock:true AND (category:Phones OR category:Tablets) AND (brand:Apple OR brand:Samsung) AND price>500`

## Don't mix filter types in OR conditions

You can't compare different filter types (string, numeric, tags) with an `OR`.
For example, `num=3 OR tag1 OR facet:value` isn't allowed.

## Apps and user actions change filter expressions

If a user or app selects a filter, Algolia omits the filters that don't apply from the expression.

For example, if the original filter expression is `((category:Electronics OR category:Home) AND NOT brand:LG) AND NOT price:100 TO 500` and the user selects the Electronics category, the expression simplifies to `(category:Electronics AND NOT brand:LG) AND NOT price:100 TO 500`.

## Filter syntax validator

<FilterValidator />
