Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Bombadil

Lord of the rings, Tom Bombadil

You know, for (PostgreSQL) search

Bombadil is a tiny wrapper around some PostgreSQL search capabilities.

It supports exact match through PostgreSQL tsvector(s) and fuzzy search inside jsonb fields.

Installation

The package can be installed by adding bombadil to your list of dependencies in mix.exs:

defdepsdo[{:bombadil,"~> 0.2"}]end

Documentation is at https://hexdocs.pm/bombadil

Preparation

Assuming that you have generated a migration with a "search_index" table

Basic Usage

(Almost) Exact match of an indexed word or sequence of words

aliasBombadil.Ecto.Schema.SearchIndexiex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"book"=>"Lord of the Rings"}))# Raw SQL: INSERT INTO "search_index" ("payload") VALUES ('{"book": "Lord of the Rings"}'){:ok,struct}# Full string providediex>YourRepo.all(Bombadil.search(SearchIndex,"Lord of the Rings"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%Lord of the Rings%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# One word provided (treated as case-insensitive)iex>YourRepo.all(Bombadil.search(SearchIndex,"lord"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lord%"][%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.search(SearchIndex,"lordz"))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE ((payload::text) ~~* $1) ["%lordz%"][]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]# No resultsiex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lard of the ringz asdf"))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (payload::text %> 'lord of the ringz asdf')[]

Match a specific field inside the indexed jsonb field

(Almost) Exact match

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,payload: %{"character"=>"Tom Bombadil"})){:ok,struct}iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"book"=>"rings"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.search(SearchIndex,[%{"character"=>"bombadil"}]))# Raw SQL: SELECT s0."id", s0."test", s0."item_id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->$1)::text ~~* $2)[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]

Fuzzy match

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"lard"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'lard')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"book"=>"Lord of the Rings"},id: 1}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"character"=>"tom bomba"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'character')::text %> 'tom bomba')[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"character"=>"Tom Bombadil"},id: 3}]iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,[%{"book"=>"tom"}]))# Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR (payload->'book')::text %> 'tom bomba')[]

Matching search results with an additional context

Given a search_index that has also an item_id field as an additional context for filtering, let's suppose you index also the item_id and you might have the same item_id for another entry with different payload field like in this snippet:

iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"lord of the rings"}))iex>YourRepo.insert_or_update(Bombadil.index(SearchIndex,item_id: 42,payload: %{"ask"=>"I am hiding with the same id, don't find me!"}))

You can apply a search and a context to filter a specific id, in this case item_id is 42 and the payload that is "hiding" is filtered out.

iex>YourRepo.all(Bombadil.fuzzy_search(SearchIndex,"lord of the ringz",context: %{item_id: 42}))[%Bombadil.Ecto.Schema.SearchIndex{__meta__: #Ecto.Schema.Metadata<:loaded, "search_index">,payload: %{"ask"=>"lord of the rings"},id: 6,item_id: 42}]

Encoding to JSON

iex>Bombadil.search(SearchIndex,"rings")|>Jason.encode!()"[{\"payload\":{\"book\":\"Lord of the Rings\"}}]"

Integration with your application

Manual integration

Define your schema

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schema@fields[:field1,:field2,:map]@derive{Jason.Encoder,only: @fields}schema"your_table"dofield(:payload,:map)field(:field1,:string)field(:field2,:string)endend

(optional) Define your schema with a different column name

If you want to use a different name for the payload field, you can define your schema in this way:

defmoduleYourApp.Ecto.Schema.SearchIndexdouseEcto.Schemaschema"search_index"dofield(:data,:map,source: :payload)field(:item_id,:integer)endend

Notice how the:

field(:data,:map,source: :payload)

specifies via the field macro another name for the :payload column

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Implement the migration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.Migrationdefchangedocreatetable("search_index")doadd(:payload,:jsonb)add(:field1,:string)add(:field2,:string)endendend

Through configuration (via macros)

config:bombadil,Bombadil.Repo,database: "your_database",username: "postgres",password: "postgres",hostname: "localhost"config:bombadil,table_name: "search_index",additional_fields: [{:your_special_id,:string},[...]]

Generate an Ecto migration

mix ecto.gen.migration create_search_index # For example

Wire the DynamicMigration based on your configuration

defmoduleYourApp.Repo.Migrations.CreateSearchIndexdouseEcto.MigrationrequireBombadil.Ecto.DynamicMigrationdefchangedoBombadil.Ecto.DynamicMigration.run()endend

Run the migration

mixecto.migrate

And implement indexing and search for your use case by using the Bombadil.fuzzy_search/2 and Bombadil.index/2 functions with your Ecto.Repo

TODO

[ ] Support other fields, rather than jsonb (?)

Thank you(s)

Many thanks to Taxfix for giving me the possibility to develop and release this library.🚀

Thanks to the https://github.com/textacular/textacular ruby library for having kicked my butt to create something similar for Elixir ☯

About

You know, for (PostgreSQL) search

Topics

Resources

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages