liqe
A lightweight, dependency-minimal Lucene-like query parser, serializer, and in-memory search engine for TypeScript and JavaScript.
Repository Health
Technical Analysis
Liqe implements a Lucene-inspired query language — LQL — as a full parser, AST, serializer, and filter/test/highlight engine in a single small TypeScript package. Instead of shipping yet another bespoke filter-string mini-language, it gives applications a well-specified, extensible syntax that supports field targeting (including nested paths like name.first:foo), regex and wildcard matching, boolean/comparison ranges, and AND/OR/NOT logical composition with grouping.
Beyond parsing, Liqe treats the produced AST as a public API: consumers can filter() collections, test() a single object against a query, or highlight() the matching fields and substrings for UI display, all from the same parsed query. This makes it a natural fit anywhere an app wants to expose a search box or filter bar backed by a real, composable query language rather than ad hoc string matching — search UIs, log filtering (it originated to power Roarr’s CLI log filtering), and table/data-grid filtering being the most common uses.
What You Get
- A
parse()function that turns an LQL query string into a structured, typed AST (built on a nearley-generated grammar) filter()andtest()functions that run a parsed query against a collection or a single object, including deep/nested field pathshighlight()support that returns the matching field paths and matched substrings/regexes for building search-result highlighting UIsserialize()to turn an AST back into an LQL query string, useful for round-tripping or programmatically constructed queries- Support for keyword, phrase, regex, and wildcard (
*/?) matching, numeric comparison operators (:=,:>,:>=,:<,:<=), inclusive/exclusive ranges ([100 TO 200]/{100 TO 200}), boolean/null literals, and AND/OR/NOT boolean logic with parenthesized grouping - A fully typed AST (
LiqeQuery,TagToken,LogicalExpressionToken, etc.) that is itself treated as a stable public API for building custom search implementations
Common Use Cases
- Powering a search box or filter bar in an admin UI or data table where users type Lucene-style queries against structured records
- Filtering and highlighting matches in structured log output (Liqe’s original use case, for Roarr log filtering)
- Building saved-search or alert features where a user-authored query string needs to be parsed once and evaluated repeatedly against streaming data
- Implementing an in-memory search/filter layer over JSON documents without standing up a separate search index (Elasticsearch/Meilisearch) for small-to-medium datasets
- Validating or normalizing user-supplied filter syntax before passing semantics on to a downstream query (e.g., serializing a parsed/validated AST back into a canonical query string)
Under The Hood
Architecture
Liqe is organized as a small pipeline of single-purpose modules rather than one large parser class: src/grammar.ne (compiled via nearleyc into src/grammar.ts) defines the LQL context-free grammar, parse.ts feeds a query string through the compiled nearley.Parser and converts nearley’s raw parse tree into a stable public AST shape via hydrateAst.ts, catching nearley’s internal errors and re-throwing a typed SyntaxError with line/column/offset information. From there, filter.ts/test.ts delegate to internalFilter.ts, which recursively walks the AST (Tag, UnaryOperator, ParenthesizedExpression, LogicalExpression nodes) and, for Tag leaf nodes, lazily compiles a per-tag InternalTest closure (memoized onto the AST node itself via ast.test) so repeated evaluation against many rows doesn’t re-derive the test function each time. Nested/nonexistent-field access is handled either by walking field.path directly or, for dynamic paths, by generating a small return subject.a?.b accessor function body (createGetValueFunctionBody.ts) gated by an isSafePath regex allowlist to avoid unsafe property access. The AST itself is exported as a public type, so callers can build entirely custom evaluators on top of it instead of using filter/test.
Tech Stack
The package is pure TypeScript compiled with tsc (targeting CommonJS, main/typings in package.json), with nearley as its only non-trivial runtime dependency (for grammar parsing) plus ts-error for a typed error base class — a deliberately minimal dependency footprint for a parser library. The grammar itself is authored in nearley’s .ne DSL and compiled to JS ahead of time via a compile-parser script, so the shipped package doesn’t need the nearley compiler at runtime, only its small parser runtime. Releases are fully automated via semantic-release with npm and GitHub plugins driven by conventional commits.
Code Quality
Tests are written with ava (test/liqe/*.ts, run via ts-node/register/transpile-only) and cover each module individually — parser AST shapes, wildcard-to-regex conversion, range/comparison testing, safe-path checks, and highlighting — with coverage tracked via nyc/@istanbuljs/nyc-config-typescript and reported to Coveralls. Linting uses eslint-config-canonical’s strict auto-config plus an ava-specific ruleset, enforced in CI (npm run lint combines ESLint and a tsc --noEmit type check) alongside npm run test and npm run build on every PR via GitHub Actions, so the type-check, lint, and test suite all gate merges. Error handling favors explicit, typed errors (LiqeError, SyntaxError extending ts-error’s ExtendableError) over generic throws, and internal functions assert AST invariants (e.g. “Expected a tag expression”) rather than silently continuing on unexpected shapes.