FELN RAG: Five Examples and a Spatial Query

Ask a GIS person to find oil wells within five kilometers of gas pipelines and they can start breaking the request into pieces. Which layer contains the wells? How is “oil” encoded? Which pipelines carry gas? What spatial operation connects the two?

A language model needs those same details. “Oil” might be a numeric subtype. A country might be stored as a two-letter code. And choosing intersects when the question calls for contains can produce perfectly respectable JSON that asks the wrong question.

That is the problem behind feln-rag: give the model a description of the data and a handful of relevant worked examples, then ask it to translate a new request into a structured spatial query.

The interesting part is how little machinery the retrieval needs.

Start with the query we want to produce

The output is FELN, a JSON representation with three main pieces:

  • layers: the primary layer to return, followed by any spatial filter layers.
  • where: one SQL filter per layer, in the same order.
  • relations: the spatial relationship between the primary layer and each secondary layer.

Here is a small example from the bundled NorthSea corpus. A request for discoveries in the United Kingdom whose discovery wellbore name starts with 42/30- becomes:

{
  "layers": ["Discoveries"],
  "where": ["\"country\" = 'UK' AND \"discovery_wellbore_name\" LIKE '42/30-%'"],
  "relations": []
}

One layer, two attribute conditions, and no spatial join. Add another layer and a distance constraint, and the relationship becomes something like withinDistance 5 kilometers.

The repository generates this representation. Executing it against geographic data is a separate step. You can explore the translation without having the NorthSea geodatabase on your machine.

Two companion projects supply the foundation. layers-json describes the catalog, including field names, coded values, and hints. feln supplies the output models, validation, and comparison tools.

The vector store is a NumPy array

The bundled corpus contains 1,000 question-and-answer pairs. Each question has a known FELN answer attached to it.

An embedding model converts the question text into a vector. Those vectors become rows in a NumPy array, with each row still associated with its original question and FELN. The new question goes through the same encoder, and retrieval finds the five closest examples.

Because the vectors are normalized to unit length, cosine similarity reduces to this line in the index implementation:

scores = self.embeddings @ query.reshape(-1)

At this corpus size, scanning the array is a straightforward place to start. There is no separate vector database service to operate. The embeddings are cached on disk, with the encoder name and ordered question texts contributing to the cache fingerprint.

A similarity score tells us which questions resemble the new request. Whether their answers demonstrate the right spatial logic is something we still have to inspect and measure.

Put the examples where the model can use them

The retrieved examples become a short conversation. Each question is a user message, and its known FELN answer is the following assistant message. The actual question comes last:

System:    Translation instructions and layer catalog
User:      Retrieved question 1
Assistant: Known FELN answer 1
           ...four more question/answer pairs...
User:      The new question

That is the few-shot prompt. The catalog explains the data; the examples demonstrate how to translate requests involving it. The generation code sends those messages through LiteLLM and attempts to parse the response as FELN.

The model has to produce a new answer. Copying the closest example would be a very efficient way to return somebody else's query :-)

Make the context visible

FELN Studio puts the pieces on one page: the question, five retrieved examples, an editable system prompt and catalog, and the generated JSON.

The useful first click is Find examples only. Expand a card and inspect the FELN that will become an assistant message. With the default local encoder, this step does not call the generation model. Generate FELN then uses those displayed examples in their displayed order.

FELN Studio showing the NorthSea query workspace

The repository screenshot demonstrates the workspace and retrieval; it does not show a completed generation request.

The interface is plain HTML, CSS, and JavaScript served by Python's standard-library HTTP server. It runs on loopback. Generation sends the question, catalog, and examples to the configured model provider, while credentials stay on the server. The README explains the workflow and configuration.

The VALID FELN label has a precise meaning: the output passed schema validation. You still need to read the query. Valid structure cannot tell you whether the model chose the relationship you intended.

Count the misses, too

The evaluation code separates held-out questions from the retrieval corpus and supports a zero-shot baseline using the same catalog instructions without examples. It records validity, exact match, structural and partial scores, and request failures. Studio uses the full corpus for exploration, so evaluation belongs in that separate holdout workflow. See eval.py.

The repository reports a September 15 smoke test using MPNet retrieval and azure/gpt-5.5. Retrieval was evaluated on ten held-out questions; generation used the first three:

Check Reported result
Matching layer set among the five retrieved examples 10/10
RAG exact match 3/3
Zero-shot exact match 2/3
Generation requests Six valid outputs, no request errors

The zero-shot miss selected intersects where the reference used contains. All six outputs were valid, which is a useful illustration of why validation and correctness need separate columns. These are reported smoke results, and three questions are far too few to establish general accuracy.

Try it

The current setup requires Python 3.13, uv, and sibling checkouts of feln, layers-json, and VectorlessGAIT. That last dependency is currently required by the package configuration even when using NumPy retrieval. Follow the setup instructions, configure LLM_MODEL_NAME and the provider credentials, then run from the repository:

uv sync
uv run --no-sync python -m feln_rag.web

Open http://127.0.0.1:8765/. The default embedding model may download its weights on first use.

Start with a question whose answer you understand. Inspect the retrieved examples. Generate the FELN. Then change the distance, reverse the requested relationship, or add an attribute condition and see what moves.

For me, the appeal is being able to follow the translation all the way through: the catalog, the examples, the prompt, and the resulting query. When something goes wrong, there are concrete pieces to examine.

The next useful experiment would be a larger held-out comparison, especially on questions that differ by one spatial relationship or one coded value. Those small differences are where a convincing answer has to become a correct query.

More to come :-)

Comments

Popular posts from this blog

How to use Esri Flex API on Android and iPhone

Weighted Map Feature Clustering With Attributes

Augmented Reality, iPhone and ArcGIS Server