How to Implement Complex Boolean Queries Using the FlexSearch Resolver
To implement complex Boolean queries in FlexSearch, initiate an unresolved search with resolve: false, chain the .and(), .or(), .xor(), or .not() methods to compose your logic, and finalize with .resolve() to execute the set operations.
The FlexSearch library provides a chainable Resolver API that enables sophisticated Boolean algebra across search indices. This architecture allows you to combine multiple sub-queries using intersection, union, symmetric difference, and exclusion operations before final result resolution.
Understanding the FlexSearch Resolver Architecture
The Resolver system operates on unresolved result sets—intermediate data structures that store document IDs and metadata without final ranking or formatting.
The Resolver Class and State Management
At the core of this system is the Resolver class defined in [src/resolver.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolver.js). This class maintains:
- The current result set (document IDs)
- Pending promises for asynchronous sub-queries
- Boost values for relevance weighting
- Optional highlight and query state
When you chain Boolean methods, the Resolver stores the operation in an internal graph rather than executing immediately. The actual set algebra occurs only when you invoke .resolve().
Operator Handlers and Aggregation Logic
Each Boolean operator delegates to a generic handler in [src/resolve/handler.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/handler.js), which normalizes arguments and manages cached results. The handler then routes to operator-specific aggregation functions:
- AND: Uses
intersectlogic from [src/intersect.js](https://github.com/nextapps-de/flexsearch/blob/master/src/intersect.js) - OR: Uses union logic from [
src/resolve/or.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/or.js) - XOR: Uses symmetric difference logic from [
src/resolve/xor.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/xor.js) - NOT: Uses set subtraction logic from [
src/resolve/not.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/not.js)
Boolean Operators Explained
The FlexSearch Resolver implements four fundamental set operations that correspond to Boolean logic.
AND (Intersection)
The .and() method computes the intersection of the current result set and the new sub-query. Only document IDs present in both operands survive this operation.
const results = index
.search('javascript', { resolve: false })
.and({ query: 'performance' }) // Must contain both terms
.resolve();
OR (Union)
The .or() method computes the union of sets. Document IDs present in either the current result set or the new sub-query are included.
const results = index
.search('python', { resolve: false })
.or({ query: 'ruby' }) // Include either term
.or({ query: 'javascript' }) // Chain multiple ORs
.resolve();
XOR (Symmetric Difference)
The .xor() method returns document IDs that appear in exactly one of the operand sets, excluding any overlap.
const results = index
.search('mobile', { resolve: false })
.xor({ query: 'desktop' }) // Mobile or desktop, not both
.resolve();
NOT (Exclusion)
The .not() method performs set subtraction, removing document IDs found in the sub-query from the current result set.
const results = index
.search('cloud', { resolve: false })
.not({ query: 'marketing' }) // Exclude marketing content
.resolve();
Implementing Complex Boolean Queries
The true power of the Resolver emerges when you combine operators, nest expressions, and configure execution parameters.
Basic AND and OR Chains
Start with an unresolved search and chain operators to build Boolean expressions. The Resolver executes operations in the order chained, creating a left-to-right evaluation graph.
const raw = index.search('apple', { resolve: false });
const result = raw
.and({ query: 'pie' }) // Intersection: apple AND pie
.or({ query: 'crumble' }) // Union: (apple AND pie) OR crumble
.resolve({ limit: 20 });
console.log(result);
Using XOR for Exclusive Matches
Use .xor() when you need mutually exclusive categories—content tagged with one term but definitively not tagged with another.
const raw = index.search('red', { resolve: false });
const exclusive = raw
.xor({ query: 'blue' }) // Symmetric difference: red XOR blue
.resolve({ limit: 50 });
console.log(exclusive);
Subtracting Results with NOT
The .not() operator functions as a filter pipeline, progressively narrowing results by excluding unwanted matches.
const raw = index.search('technology', { resolve: false });
const filtered = raw
.not({ query: 'advertisement' }) // Exclusion: technology NOT advertisement
.resolve({ limit: 30 });
Nested Boolean Expressions and Async Pipelines
For complex logic, pass nested objects containing and, or, xor, or not keys. The Resolver recursively processes these subgraphs. Combine with async: true for parallel execution of independent sub-queries.
const resolver = new Resolver({
index,
query: 'javascript',
async: true // Execute sub-queries in parallel
});
resolver
.and({ query: 'performance', boost: 2 })
.or({
and: [
{ query: 'web' },
{ query: 'worker' }
]
})
.not({ query: 'legacy' })
.limit(100)
.resolve();
resolver.await.then(result => {
console.log('Async complex query result:', result);
});
Caching Sub-Queries for Performance
Enable cache: true on individual sub-queries to store intermediate results and avoid redundant computation when the same query appears multiple times in a complex graph.
const raw = index.search('node', { resolve: false, cache: true });
const result = raw
.and({ query: 'express', cache: true }) // Cached sub-query
.or({ query: 'koa' })
.resolve();
Key Source Files and Implementation Details
Understanding the underlying implementation helps debug complex queries and optimize performance.
| File | Purpose | Key Implementation Detail |
|---|---|---|
[src/resolver.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolver.js) |
Core Resolver class, state management, async orchestration |
Implements execute() method that processes pending promises before running set operations |
[src/resolve/handler.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/handler.js) |
Argument normalization and sub-query execution | Normalizes nested objects and manages cached result retrieval |
[src/resolve/and.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/and.js) |
Intersection logic | Delegates to intersect function for set intersection |
[src/resolve/or.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/or.js) |
Union logic | Merges result arrays while preserving relevance scores |
[src/resolve/xor.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/xor.js) |
Symmetric difference | Filters IDs appearing in both sets |
[src/resolve/not.js](https://github.com/nextapps-de/flexsearch/blob/master/src/resolve/not.js) |
Set subtraction | Removes operand IDs from current result set |
[src/intersect.js](https://github.com/nextapps-de/flexsearch/blob/master/src/intersect.js) |
Low-level set intersection algorithms | Optimized for sorted integer arrays (document IDs) |
Summary
- Start unresolved: Begin every complex query with
resolve: falseto obtain aResolverinstance instead of final results. - Chain operators: Use
.and(),.or(),.xor(), and.not()to build Boolean expressions; each returns the Resolver for further chaining. - Nest expressions: Pass objects containing
and,or,xor, ornotkeys to create nested Boolean subgraphs. - Configure execution: Apply
async: truefor parallel sub-queries,cache: trueto reuse intermediate results, andboostto adjust relevance weighting. - Resolve finally: Call
.resolve()(or await the promise) to execute the set operations defined insrc/resolve/*.jsand return the final document list.
Frequently Asked Questions
How do I combine AND and OR operators in the same query?
Chain the methods sequentially or use nested objects. For sequential chaining, index.search('a', { resolve: false }).and({ query: 'b' }).or({ query: 'c' }) produces (a AND b) OR c. For nested logic, pass an object to .or() containing an and array: .or({ and: [{ query: 'x' }, { query: 'y' }] }) to create a OR (x AND y).
What is the difference between XOR and NOT in FlexSearch?
XOR (exclusive or) returns documents that match exactly one of the two queries, removing any overlap. NOT subtracts one set from another, keeping only documents from the original result set that do not appear in the negated query. Use XOR when you want mutually exclusive categories; use NOT when filtering out unwanted matches from an existing set.
Can I execute boolean queries asynchronously?
Yes. Initialize the Resolver with async: true or enable async on individual sub-queries. When async mode is active, the Resolver stores pending promises and executes sub-queries in parallel, deferring set operations until all promises resolve. Access the final result via resolver.await.then(...) or by awaiting resolver.resolve() if using native async/await syntax.
How does caching improve boolean query performance?
When you pass cache: true to a sub-query, the Resolver stores the intermediate result set after the first execution. If the same query appears again in a complex boolean graph—whether through repeated chaining or nested references—the Resolver reuses the cached IDs instead of re-running the search. This significantly reduces redundant computation in deeply nested or repetitive boolean expressions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →