elvanja

elvanja

Recommendation for building search parser

Need to build a “natural” language search query parser, for later conversion to appropriate Elasticsearch query.

Basically need something like https://github.com/Financial-Times/n-search-parser, which can do:

  • Conjunction operators: AND, OR, NOT
  • Quoted phrases: "Modesty Blase"
  • Grouping with parentheses: ("Modesty Blase" OR "Willie Garvin")

I’m trying to decide which approach to take, e.g.:

As per Best way to build a parser - #3 by david_ex it would seem that Nimble Parsec is the best candidate. There is even Pegasus — pegasus v1.0.0 which could generate Nimble Parsec parsers (can’t find any boolean search PEG definitions out there though!).

But, as far as I can tell, the Leex and Yecc solution would be more concise.

First Post!

dimitarvp

dimitarvp

Sounds like an interesting project! Can you give a few examples of what should be parsed?

Most Liked

kip

kip

ex_cldr Core Team

You can get quite a long way just using the Elixir lexer. Especially now that in Elixir 1.16 its lexing errors are better for user experience.

Example

iex> Code.string_to_quoted "(\"Modesty Blase\" or \"Willie Garvin\")"
{:ok, {:or, [line: 1], ["Modesty Blase", "Willie Garvin"]}}

iex> Code.string_to_quoted "a or (b and c)"
{:ok,
 {:or, [line: 1],
  [
    {:a, [line: 1], nil},
    {:and, [line: 1], [{:b, [line: 1], nil}, {:c, [line: 1], nil}]}
  ]}}

iex> Code.string_to_quoted "a or (b and c"
{:error,
 {[opening_delimiter: :"(", line: 1, column: 14],
  "missing terminator: ) (for \"(\" starting at line 1)", ""}}

You would then need a parser to ensure you have a valid expression for your query language but I think thats really quite easy compared to the lexical part given your reasonably straight forward requirements.

elvanja

elvanja

Good point about silently ignoring grammar errors! Haven’t considered that one and it does make sense. E.g. given search for Modesty Blase OR Willie Garvin AND it makes sense to drop the last AND since it does not serve any purpose.

Would love to formalize requirements but unfortunately this will go the “let’s see how it works and then we’ll improve” way. Not that this is bad, just that it does not yield to formalization well :smile:

So for the time being I used a plain approach, scanning the input. See the gist link in reply above. This solution does that, ignores certain input errors (drops tailing operators, uses only the last operator if more than one are supplied, …). It does not say at which character e.g. unclosed parenthesis started, but it should be fairly simple to add that too (it is scanning so counting should not be an issue).

I do plan to try some/all of those solutions and see if I can get a more maintainable solution after all. Hope it will be possible to also ignore certain “invalid” input and have nice enough error reporting.

Thanks for the idea! :bowing_man:

christhekeele

christhekeele

Based on your stated requirements

Any one of your proposed solutions should work well and require a few dozen lines to tokenize input into an AST ready to be manipulated into ES query syntax.

I would definitely formalize the yet-unstated requirements of how you need to handle parse errors before proceeding to make a choice. Many of these tools produce nice AST on valid input but are ill-suited to providing rich feedback about what went wrong, where, on invalid input.

Do you need to tell users where they are missing a parenthesis, or that something must follow a NOT before a closing paren, and how well do these libraries let you do that? Is there a strategy you can implement for silently ignoring grammar errors in part of the input but accepting the rest, and which of these libraries give you tools to do that? etc.

Last Post!

akash-akya

akash-akya

EDIT: Ehh, I missed that you also want to consider words separated by space, Foo Bar AND Baz. That won’t work here too I guess.

Just realized its similarity with Erlang expression, and I couldn’t resist abusing it.

expr = ~S|("Modesty Blase" AND "X") Or not "Willie Garvin and" and Foo|

out =>

"(\"Modesty Blase\" AND \"X\") Or not \"Willie Garvin and\" and Foo"

{:ok, tokens, _EndLine} = :erl_scan.string(to_charlist(expr))
tokens

out =>

[
  {:"(", 1},
  {:string, 1, ~c"Modesty Blase"},
  {:var, 1, :AND},
  {:string, 1, ~c"X"},
  {:")", 1},
  {:var, 1, :Or},
  {:not, 1},
  {:string, 1, ~c"Willie Garvin and"},
  {:and, 1},
  {:var, 1, :Foo}
]

tokens =
  Enum.map(tokens, fn
    {:var, anno, var} ->
      term = String.downcase(to_string(var))

      if term in ~w(and or not) do
        {String.to_existing_atom(term), anno}
      else
        {:string, anno, to_string(var)}
      end

    {op, anno} when op in ~w|( )|a ->
      {op, anno}

    {type, anno, value} when type in ~w(string atom integer)a ->
      {:string, anno, to_string(value)}

    {boolean_op, _anno} = token when boolean_op in ~w(and or not)a ->
      token

    {op, anno} ->
      {:string, anno, to_string(op)}
  end)

out =>

[
  {:"(", 1},
  {:string, 1, "Modesty Blase"},
  {:and, 1},
  {:string, 1, "X"},
  {:")", 1},
  {:or, 1},
  {:not, 1},
  {:string, 1, "Willie Garvin and"},
  {:and, 1},
  {:string, 1, "Foo"}
]

IO.puts("Expression: " <> expr)

{:ok, abs_form} = :erl_parse.parse_exprs(tokens ++ [{:dot, 1}])
abs_form

out =>

Expression: ("Modesty Blase" AND "X") Or not "Willie Garvin and" and Foo

[
  {:op, 1, :or, {:op, 1, :and, {:string, 1, "Modesty Blase"}, {:string, 1, "X"}},
   {:op, 1, :and, {:op, 1, :not, {:string, 1, "Willie Garvin and"}}, {:string, 1, "Foo"}}}
]

You can then use the parsed abs_form term for further analysis.

:warning: Although the hack seems to work, but there is one major issue, it seems erl_scan dynamically creates atoms

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40165 209
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement