Skip to content

Instantly share code, notes, and snippets.

@Mister-Odd
Created June 5, 2025 22:14
Show Gist options
  • Save Mister-Odd/0f0dd7117401ec7414aa0805d2e112b6 to your computer and use it in GitHub Desktop.
Save Mister-Odd/0f0dd7117401ec7414aa0805d2e112b6 to your computer and use it in GitHub Desktop.
Enable Boolean clauses in the Elm frontend for Elasticsearch
### This is 100% Copilot responses to my queries - Expect lot's of errors and random WTF moments ###
### Copilot was given my fork of nixos-search to evaluate for it's replies ###
### Enable Boolean clauses in the Elm frontend for Elasticsearch ###
# Extend the Elm model to represent Boolean logic.
# Update the query encoder to generate the appropriate JSON for Elasticsearch.
# Adjust the update logic if needed to support new query types.
### Extend the Elm Model ###
# In the Search.elm, define a new type to represent Boolean clauses:
type BoolClause
= Must MatchClause
| Should MatchClause
| MustNot MatchClause
| Filter MatchClause
type alias MatchClause =
{ field : String
, value : String
}
# Update the Model to include a list of these clauses:
type alias Model =
    { ...
    , boolClauses : List BoolClause
    }
# Update the JSON Encoder
import Json.Encode exposing (..)
encodeBoolClause : BoolClause -> ( String, Value )
encodeBoolClause clause =
case clause of
Must match ->
( "must", list [ encodeMatch match ] )
Should match ->
( "should", list [ encodeMatch match ] )
MustNot match ->
( "must_not", list [ encodeMatch match ] )
Filter match ->
( "filter", list [ encodeMatch match ] )
encodeMatch : MatchClause -> Value
encodeMatch match =
object
[ ( "match", object [ ( match.field, string match.value ) ] ) ]
### Add a function to encode the BoolClause list into a JSON object that Elasticsearch understands ###
# In the main query encoder:
encodeBoolQuery : List BoolClause -> Value
encodeBoolQuery clauses =
let
grouped =
Dict.fromListWith (++) (List.map (\c -> let (k, v) = encodeBoolClause c in (k, [v])) clauses)
in
object
[ ( "query"
, object
[ ( "bool"
, object (Dict.toList (Dict.map (\_ vs -> list vs) grouped))
)
]
)
]
# Update the View and Update Logic
### Allow users to add clauses via the UI.###
# For example, buttons to add a Must, Should, etc., clause.
# In the update function, handle those actions by appending to model.boolClauses.
### To build a UI in Elm that allows users to add Boolean clauses (must, should, must_not, filter) to an Elasticsearch query ###
Define messages for user actions.
Update the model to track input fields.
Create view elements for input and buttons.
Handle updates to modify the model and trigger query encoding
# Define Messages
# Add these to the Msg type:
type Msg
= UpdateField String
| UpdateValue String
| AddClause String
# Extend the Model
# Add temporary fields for user input:
type alias Model =
{ ...
, currentField : String
, currentValue : String
, boolClauses : List BoolClause
}
# Initialize them in the init function:
init : Model
init =
{ ...
, currentField = ""
, currentValue = ""
, boolClauses = []
}
### Create the View ###
# Add a form to input field/value and buttons to add clauses:
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Field", onInput UpdateField ] []
, input [ placeholder "Value", onInput UpdateValue ] []
, button [ onClick (AddClause "must") ] [ text "Add MUST" ]
, button [ onClick (AddClause "should") ] [ text "Add SHOULD" ]
, button [ onClick (AddClause "must_not") ] [ text "Add MUST NOT" ]
, button [ onClick (AddClause "filter") ] [ text "Add FILTER" ]
, div [] (List.map viewClause model.boolClauses)
]
# Add a helper to display added clauses:
viewClause : BoolClause -> Html msg
viewClause clause =
case clause of
Must m -> text ("MUST: " ++ m.field ++ " = " ++ m.value)
Should m -> text ("SHOULD: " ++ m.field ++ " = " ++ m.value)
MustNot m -> text ("MUST NOT: " ++ m.field ++ " = " ++ m.value)
Filter m -> text ("FILTER: " ++ m.field ++ " = " ++ m.value)
### Handle Updates ###
# Update the update function:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateField field ->
( { model | currentField = field }, Cmd.none )
UpdateValue value ->
( { model | currentValue = value }, Cmd.none )
AddClause clauseType ->
let
match = { field = model.currentField, value = model.currentValue }
newClause =
case clauseType of
"must" -> Must match
"should" -> Should match
"must_not" -> MustNot match
"filter" -> Filter match
_ -> Must match
in
( { model
| boolClauses = model.boolClauses ++ [ newClause ]
, currentField = ""
, currentValue = ""
}
, Cmd.none
)
### To integrate the updated Boolean clause UI with the Elasticsearch backend ###
Encode the boolClauses into a JSON query.
Send the query to the backend using an HTTP request.
Handle the response and update the model with results.
# Encode the Query
# Use the encodeBoolQuery function (from earlier). Use it to convert model.boolClauses into a JSON payload:
import Json.Encode exposing (encode)
sendQuery : Model -> Cmd Msg
sendQuery model =
let
body =
encode 0 (encodeBoolQuery model.boolClauses)
in
Http.post
{ url = "/the-elasticsearch-endpoint"
, body = Http.jsonBody (encodeBoolQuery model.boolClauses)
, expect = Http.expectJson GotSearchResults searchResultsDecoder
}
#Replace "/the-elasticsearch-endpoint" with the actual URL the backend exposes for search.
### Define the Response Decoder ###
# Assuming the Elasticsearch response includes a list of hits:
type alias SearchResult =
{ title : String
, snippet : String
}
searchResultsDecoder : Decoder (List SearchResult)
searchResultsDecoder =
Decode.at [ "hits", "hits" ]
(Decode.list
(Decode.map2 SearchResult
(Decode.at [ "_source", "title" ] Decode.string)
(Decode.at [ "_source", "snippet" ] Decode.string)
)
)
### Add a Message and Update Logic ###
# Extend the Msg type:
type Msg
= ...
| GotSearchResults (Result Http.Error (List SearchResult))
# Update the update function:
update msg model =
case msg of
...
AddClause clauseType ->
let
...
in
( updatedModel, sendQuery updatedModel )
GotSearchResults result ->
case result of
Ok results ->
( { model | searchResults = results }, Cmd.none )
Err _ ->
( model, Cmd.none ) -- You can add error handling here
### Display the Results ###
# Add a searchResults field to the Model:
type alias Model =
{ ...
, searchResults : List SearchResult
}
# Initialize it as an empty list, and render it in the view:
view model =
div []
[ ...
, div [] (List.map viewResult model.searchResults)
]
viewResult : SearchResult -> Html msg
viewResult result =
div []
[ h3 [] [ text result.title ]
, p [] [ text result.snippet ]
]
### Define the Elasticsearch schema and integrate it with the Elm frontend ###
# Elasticsearch Document Schema (for Nix Packages)
# Example of each document in the Elasticsearch index might look like:
{
"name": "firefox",
"version": "123.0",
"description": "A fast, privacy-focused web browser",
"homepage": "https://www.mozilla.org/firefox",
"source": "https://github.com/mozilla/firefox",
"programs": ["firefox"],
"install": {
"nix-shell": "nix-shell -p firefox",
"nixos": "services.firefox.enable = true;",
"nix-env": "nix-env -iA nixos.firefox"
},
"license": "MPL-2.0",
"maintainers": ["alice", "bob"],
"platforms": ["x86_64-linux", "aarch64-linux"]
}
### Search Fields ###
# To search by:
# Name (e.g., "firefox")
# Version (e.g., "123.0")
# The Elasticsearch query should look like this:
{
"query": {
"bool": {
"must": [
{ "match": { "name": "firefox" } },
{ "match": { "version": "123.0" } }
]
}
}
}
### Elm Decoder for Search Results ###
# Here’s how to decode the response in Elm:
type alias SearchResult =
{ name : String
, version : String
, description : String
, homepage : String
, license : String
}
searchResultsDecoder : Decoder (List SearchResult)
searchResultsDecoder =
Decode.at [ "hits", "hits" ]
(Decode.list
(Decode.map5 SearchResult
(Decode.at [ "_source", "name" ] Decode.string)
(Decode.at [ "_source", "version" ] Decode.string)
(Decode.at [ "_source", "description" ] Decode.string)
(Decode.at [ "_source", "homepage" ] Decode.string)
(Decode.at [ "_source", "license" ] Decode.string)
)
)
# It's posible to expand this decoder to include maintainers, platforms, and install instructions if needed.
# Here’s a sample Elasticsearch index mapping tailored to the Nix packages schema.
# This mapping ensures that fields like name and version are searchable, while others like platforms and maintainers are stored appropriately for filtering or display.
### Elasticsearch Index Mapping for Nix Packages ###
PUT /nixpkgs-packages
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
},
"version": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
},
"description": {
"type": "text"
},
"homepage": {
"type": "keyword"
},
"source": {
"type": "keyword"
},
"programs": {
"type": "keyword"
},
"install": {
"properties": {
"nix-shell": { "type": "text" },
" }
}
},
"license": {
"type": "keyword"
},
"maintainers": {
"type": "keyword"
},
"platforms": {
"type": "keyword"
}
}
}
}
### Notes ###
# text is used for full-text search (e.g., name, description).
# keyword is used for exact matches and aggregations (e.g., license, platforms).
# The fields.keyword subfield allows you to do both full-text and exact match queries on name and version.
### A script to bulk index the Nix package data into this schema ###
# Bulk index the Nix package data into Elasticsearch:
# Prepare the data in the Elasticsearch Bulk API format.
# Use a script to send the data to the Elasticsearch instance.
# Format The Data
# Each document should be preceded by a metadata line like this:
{ "index": { "_index": "nixpkgs-packages" } }
{ "name": "firefox", "version": "123.0", ... }
{ "index": { "_index": "nixpkgs-packages" } }
{ "name": "vim", "version": "9.0", ... }
# Python Script to Bulk Index
# Here's a Python script that:
# Reads a JSON file with the package data.
# Converts it to the correct bulk format.
# Sends it to the Elasticsearch instance.
import json
import requests
# Load the JSON file containing Nix package data
with open('nix_packages.json', 'r') as file:
packages = json.load(file)
# Prepare the bulk indexing payload
bulk_payload = ""
for package in packages:
action = json.dumps({ "index": { "_index": "nixpkgs-packages" } })
data = json.dumps(package)
bulk_payload += f"{action}\n{data}\n"
# Send the bulk request to Elasticsearch
response = requests.post(
'http://localhost:9200/_bulk',
headers={ 'Content-Type': 'application/x-ndjson' },
data=bulk_payload
)
### To test the full query flow from Elm to Elasticsearch ###
# The Elm frontend correctly builds and sends the query.
# The Elasticsearch backend receives and processes the query.
# The response is correctly decoded and displayed in the UI.
# Test the Elm Query Encoder
# In Elm, log the encoded query before sending it:
# This helps to verify that the JSON structure matches what Elasticsearch expects.
Debug.log "Encoded Query" (encode 2 (encodeBoolQuery model.boolClauses))
# Use browser dev tools or a proxy like mitmproxy or Charles Proxy to inspect the actual HTTP request sent from Elm.
#Check:
# URL is correct (e.g., http://localhost:9200/nixpkgs-packages/_search)
# Method is POST
# Headers include Content-Type: application/json
# Body matches the expected Elasticsearch query
# To test Elasticsearch directly, use curl or Postman to test the query manually:
curl -X POST "http://localhost:9200/nixpkgs-packages/_search" \
-H "Content-Type: application/json" \
-d '{
"query": {
"bool": {
"must": [
{ "match": { "name": "firefox" } },
{ "match": { "version": "123.0" } }
]
}
}
}'
# If this returns results, the index and mapping are working.
# Validate Elm Decoder
# In Elm, log the decoded results:
Debug.log "Search Results" model.searchResults
# If decoding fails, check the structure of the Elasticsearch response and adjust the decoder accordingly.
### Troubleshooting Tips ###
# If there are no results, try simplifying the query (e.g., just match on name).
# If decoding fails, use Http.expectString temporarily to inspect the raw response.
# Make sure CORS is enabled on the Elasticsearch server if the frontend is served from a different origin.
### Mock Elasticsearch response to use to test the Elm decoder ###
#This simulates what Elasticsearch would return for a query on the nixpkgs-packages index:
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "nixpkgs-packages",
"_id": "firefox-123.0",
"_score": 1.0,
"_source": {
"name": "firefox",
"version": "123.0",
"description": "A fast, privacy-focused web browser",
"homepage": "https://www.mozilla.org/firefox",
"license": "MPL-2.0"
}
}
]
}
}
# How to Use This
# Save it as a file (e.g., mock-response.json).
# In Elm, temporarily replace the Http.expectJson with Http.expectString to log the raw response.
# Use a local server or mock HTTP library to serve this file when testing.
### Mock HTTP server script (e.g., in Python or Node.js) to serve this JSON locally for testing ###
import http.server
import socketserver
import json
PORT = 8000
class MockElasticsearchHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/mock-response.json':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
with open('mock-response.json', 'r') as file:
response_content = file.read()
self.wfile.write(response_content.encode())
else:
self.send_response(404)
self.end_headers()
Handler = MockElasticsearchHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"Serving at port {PORT}")
httpd.serve_forever()
### Inspection of the current repository shows a few areas to double-check ###
# 1. Frontend Query Construction (Elm)
# The file frontend/src/Search.elm appears to build queries manually.
# If adding Boolean logic, ensure the encodeBoolQuery function is correctly integrated and replaces any older query logic.
# 2. Backend Proxy or API Layer
# If the frontend is not querying Elasticsearch directly, there may be a backend proxy that transforms or filters requests.
# Check for any middleware that might strip or alter the query payload.
# 3. CORS Configuration
# If running the frontend separately from Elasticsearch, ensure CORS is enabled on the Elasticsearch server:
# 4. Index Name and Mapping
# Confirm that the index name used in the frontend matches the one in Elasticsearch (nixpkgs-packages or similar).
# Ensure the mapping supports the fields you're querying (name, version, etc.).
# 5. Data Availability
# If the queries return no results, verify that the data is actually indexed and matches the query terms.
# Try a simple match_all query to confirm data presence.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment