Loading the builder…

Build your config, step by step

Fill in the form below and watch the file build itself on the right. Nothing here leaves your browser — there is no server, no upload, and nothing is saved until you press Download. Every setting shows its plain-English purpose plus the exact key it writes, so this page stays useful for developers, managers and testers alike.

Never paste a real API key here. The key-variable box takes the name of an environment variable (e.g. QF_API_KEY) — the key itself stays wherever you run the service. Pasting an actual key is rejected, same as the loader does.
Step 1

Name your data

entity

What is a single record here called? This name has to match exactly what your code asks for later, so pick one word you'll be comfortable using everywhere — like Order or Customer.

{{ modelCaret }} Step 2

Choose your Model

model optional

Pick which AI service turns a typed question into a real query. Skip this whole step if you'll only ever send ready-made queries — that path never calls a model at all.

Picking one only fills the boxes below — edit anything afterwards.

Set creativity to 0 so the same question always compiles the same way. Only turn on "Force JSON replies" for an endpoint you've tested — some providers return broken JSON with it on.

More connection settings
Backup models · models[]
i
{{ info_chain.t }}{{ info_chain.what }}{{ info_chain.ex }}

Optional. If the main model is unreachable or rate-limited, QueryForge tries these in order and uses whichever answers first.

Backup #{{ fb.n }}
Step 3

Choose your DB

backends

Point your data's name at the real table or collection in each database you use. Skip this and the name from Step 1 is used as-is. Note that sql specifically means PostgreSQL — MySQL is its own option because the two speak slightly different SQL.

can't mix with other databases

Elasticsearch/OpenSearch can't be combined with other databases — remove the backends above to enable it.

Step 4

Describe your fields

fields {{ fieldCountLabel }}

This is the whole vocabulary. Anything listed here can be asked about; anything left out cannot be invented — the model is told to say so rather than guess. At least one field is required.

{{ item.caret }} {{ item.cardTitle }} {{ item.type }} {{ item.badgeLine }}

Shown to the AI so it understands a field name like this one. Required — the loader rejects a custom field with no description.

The AI can't see real values in a free-text field the way it can an enum's list — this stands in for that. Required for a custom free-text field.

This is an enum field, so its required context is the Allowed values list below, not a value hint.

Leave empty unless this field is both full-text searched (contains) and exactly matched, sorted, or aggregated — a single ES field can't do both.

Anything outside this list is refused before a query is even built.

Use this when the column always stores one case but people type another.

Put in what people actually type — this is what makes ordinary questions resolve to this field.

{{ item.advCaret }}Advanced · {{ item.advSummary }}
Allowed comparisons · operators
i
{{ item.info_operators.t }}{{ item.info_operators.what }}{{ item.info_operators.ex }}

Marked with a dot: no value type on a {{ item.type }} field can ever satisfy that comparison — it's listed so you can see what's ruled out.

Nested inside a Mongo list? · elemMatch
i
{{ item.info_elemMatch.t }}{{ item.info_elemMatch.what }}{{ item.info_elemMatch.ex }}

Pick the array only when it truly is a list. Declaring it makes two conditions apply to one element; leaving it off lets them match two different elements.

Nested inside an Elasticsearch object? · nestedPath
i
{{ item.info_nestedPath.t }}{{ item.info_nestedPath.what }}{{ item.info_nestedPath.ex }}

Only for an ES mapping type of "nested" (not "object"). Declaring it makes two conditions apply to one element; leaving it off lets them match two different elements.

What this field allows
Hints & bounds

Indexed and priority never reject a query — they only decide what's checked first.

A tenancy / scope field (subscriptionId, userId…) is one your app forces onto every query. It's added as not-askable so the model never sees it — you pass its value yourself at call time.

i
{{ info_scopeField.t }}{{ info_scopeField.what }}{{ info_scopeField.ex }}

Import fields from a schema — paste your database's own schema instead of adding fields one by one

{{ schemaImportHint }}

{{ schemaImportError }}
name type notes
{{ row.name }}already in your fields {{ row.notes }}
{{ schemaImportMsg }}

Nothing here leaves your browser — the pasted text is parsed on this page only, exactly like the JSON import below. Every imported row still opens as an ordinary field card afterward, so review anything flagged before downloading.

Step 5

Choose your search target

backends.{{ esProduct }}

Which index/indexes {{ esProduct }} searches. QueryForge only ever returns the generated query DSL and the resolved index/path — nothing here executes a request.

No field is marked "Use for Elasticsearch index routing" yet — add that in a field's Advanced > Hints & bounds (Step 4) first.

The token inside {} is decorative — any name — and is replaced with the routing field's literal value.

No date field is marked "Use for Elasticsearch index routing" yet — add that in a field's Advanced > Hints & bounds (Step 4) first.

Filled in automatically when you change granularity — the token must exactly match it. A date range expands to every partition it spans.

No field is marked "Use for Elasticsearch index routing" yet — add that in a field's Advanced > Hints & bounds (Step 4) first.

WHEN priority
THEN indexes:

Every matching rule is a candidate; the highest priority wins. Two matches tied at the same priority is rejected as ambiguous rather than guessed.

No field is marked "Use for Elasticsearch index routing" yet — add that in a field's Advanced > Hints & bounds (Step 4) first.

Evaluated in order — the first matching branch wins.

IF
THEN indexes:
ELSE indexes:

Used when the routing field is absent from the query, unresolvable, or nothing matches. Required for every strategy — QueryForge never searches an unrestricted wildcard.

{{ esGeneratedPath }}
{{ policyCaret }} Step 6

Guardrails

defaults · policy optional

The result window and a few deterministic safety limits. All optional, all cheap insurance.

Page size applies when a question names none; the ceiling can't be exceeded even if asked; nesting bounds how deep "and / or" groups can go — 0 means unlimited.

Always require a tenant / user filter New (optional) · policy.requiredScope
i
{{ info_requiredScope.t }}{{ info_requiredScope.what }}{{ info_requiredScope.ex }}

Your app passes the signed-in user's tenant or account id with every query, and QueryForge locks the query to it. This makes passing it compulsory — forget it once and the query is refused instead of quietly returning everyone's rows.

QueryForge will refuse any query missing {{ rc.name }}

Add at least one name, or nothing is enforced.

A blank value is refused too — an empty tenant id matches no rows, which looks exactly like an account with no data.

Block pattern-search on these fields · policy.denyRegexOn
i
{{ info_denyRegexOn.t }}{{ info_denyRegexOn.what }}{{ info_denyRegexOn.ex }}

Worth checking on any free-text field a user can steer — an unchecked pattern can be a denial-of-service risk.

Add a field first.

Field rules (optional) · policy.requires
i
{{ info_policyRequires.t }}{{ info_policyRequires.what }}{{ info_policyRequires.ex }}

Require one field whenever another is used — e.g. passport expiry always needs a country. Leave this empty if every field here already stands on its own.

Add a field first.

When is filtered, also require
Step 7

Start from something existing

Load a file to edit it, or start from a working example. This replaces everything currently in the form.

{{ importMsg }}