A GitHub search inspired interface to DataFrames.
Powered by narwhals.
Install from PyPI:
uv add frame-search
Importing frame_search adds a search properties to pandas and polars objects.
# Import to add `search` property to DataFrames
import frame_search # noqa: F401
import polars as pl
df = pl.DataFrame({
"name": ["Alice Smith", "Bob J. Dawkins", "Charlie Brown"],
"age": [25, 30, 35],
"hometown": ["New York", "New York", "Chicago"]
})
df.search('age:<30 hometown:"New York"')shape: (1, 3)
┌─────────────┬─────┬──────────┐
│ name ┆ age ┆ hometown │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ str │
╞═════════════╪═════╪══════════╡
│ Alice Smith ┆ 25 ┆ New York │
└─────────────┴─────┴──────────┘
Use with marimo to create a search interface for DataFrames:
import marimo as mo
search = mo.ui.text(label="DataFrame Search Query:")
searchThen use on a DataFrame:
import polars as pl
import frame_search # noqa: F401
df = pl.DataFrame({
"name": ["Alice Smith", "Bob J. Dawkins", "Charlie Brown"],
"age": [25, 30, 35],
"hometown": ["New York", "Los Angeles", "Chicago"]
})
df_filter = df.search(search.value)
df_filterHere is another example in a Marimo notebook:
| Syntax | Description | Example |
|---|---|---|
key:value |
Case-insensitive substring match | name:alice |
key:value1,value2 |
Match any of the values (isin) | hobby:Reading,Sports |
key:low..high |
Range match (inclusive) | age:20..40 |
key:>value |
Comparator (<, <=, >, >=, ==, !=) |
age:>30 |
"quoted value" |
Exact string with spaces | city:"New York" |
is:col |
Boolean column is True |
is:active |
has:col |
Column is not null | has:nickname |
no:col |
Column is null | no:nickname |
term1 term2 |
Implicit AND (space-separated) | name:alice age:>20 |
term1 AND term2 / term1 & term2 |
Explicit AND | name:alice AND age:>20 |
term1 OR term2 / term1 | term2 |
OR operator | hobby:Reading OR hobby:Sports |
NOT term / -term / ~term |
Negation | -name:alice, NOT age:>30 |
(expr) |
Grouping with parentheses | (name:alice OR name:bob) AND age:>20 |
`col name`:value |
Backtick-quoted column with spaces | `first name`:alice |
This repository defines a small, expressive query language for filtering and searching structured data. The grammar is implemented in Lark and supports boolean logic, comparisons, ranges, set membership, dates, numbers, strings, and quoted identifiers.
The language is designed to be:
- Readable for humans
- Unambiguous for the parser
- Flexible enough to express common filtering patterns
A query is an expression composed of:
- Boolean operators:
AND,OR,NOT(with symbolic aliases&,|,~) - Comparisons between a key and a value
- Range expressions (
..) - Set membership (
value,value,...) - Parentheses for grouping
At a high level:
<key> <comparator> <search_rhs>
Where a key identifies the field being queried. A comparator represents an operator that compares two sides (e.g. <, >, ==). search_rhs can take on many different values including dates/datetimes, integers/floats, booleans, strings, and other columns.
Keys identify the field being queried, and are always inferred to exist on the left side of any comparison. To refer to column names that have spaces in them, one needs to surround the name in backticks.
name:Alice # queries the name column for the string Alice
`first name`:Alice # queries the `first name` column for the string Alice
`first.name`:Alice # queries the `first.name` column for the string Alice
In the above example, both "first name" and "first.name" must be surrounded in backticks.
| Operator | Meaning |
|---|---|
== |
equality |
!= |
inequality |
< |
less than |
<= |
less than or equal |
> |
greater than |
>= |
greater than or equal |
: |
value dependent |
The : operator is special in that it allows for a flexible supset of expressions to be parsed and used
str:name:Alicequeries the name column for any value that starts with Alice or alice (case-insensitive prefix matching)isin:name:Alice,Bobqueries the name column for either Alice or Bob (same as case-insensitive prefix matching for strings)range:age:20..40queries the age column for any value between 20 and 40 (inclusive)
