Add full text search support using TSV indexes

This commit is contained in:
Vikram Rangnekar 2019-04-05 01:44:30 -04:00
parent 907ada9263
commit d1bf87e19c
8 changed files with 134 additions and 8 deletions

View File

@ -132,6 +132,16 @@ query {
}
```
Postgres also supports full text search using a TSV index. Super Graph makes it easy to use this full text search capability using the `search` argument.
```graphql
query {
products(seasrch "amazing") {
name
}
}
```
Super Graph support complex queries where you can add filters, ordering,offsets and limits on the query.
#### Logical Operators

View File

@ -0,0 +1,33 @@
class AddSearchColumn < ActiveRecord::Migration[5.1]
def self.up
add_column :products, :tsv, :tsvector
add_index :products, :tsv, using: "gin"
say_with_time("Adding trigger to update the ts_vector column") do
execute <<-SQL
CREATE FUNCTION products_tsv_trigger() RETURNS trigger AS $$
begin
new.tsv :=
setweight(to_tsvector('pg_catalog.english', coalesce(new.name,'')), 'A') ||
setweight(to_tsvector('pg_catalog.english', coalesce(new.description,'')), 'B');
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON products FOR EACH ROW EXECUTE PROCEDURE products_tsv_trigger();
SQL
end
end
def self.down
say_with_time("Removing trigger to update the tsv column") do
execute <<-SQL
DROP TRIGGER tsvectorupdate
ON products
SQL
end
remove_index :products, :tsv
remove_column :products, :tsv
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_03_22_200743) do
ActiveRecord::Schema.define(version: 2019_04_05_042247) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -36,6 +36,8 @@ ActiveRecord::Schema.define(version: 2019_03_22_200743) do
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.tsvector "tsv"
t.index ["tsv"], name: "index_products_on_tsv", using: :gin
t.index ["user_id"], name: "index_products_on_user_id"
end

1
go.mod
View File

@ -13,5 +13,6 @@ require (
github.com/sirupsen/logrus v1.4.0
github.com/spf13/viper v1.3.1
github.com/valyala/fasttemplate v1.0.1
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a
mellium.im/sasl v0.2.1 // indirect
)

View File

@ -397,12 +397,28 @@ func (v *selectBlock) renderRelationship(w io.Writer, schema *DBSchema) {
func (v *selectBlock) renderWhere(w io.Writer) error {
if v.sel.Where.Op == qcode.OpEqID {
col, ok := v.schema.PCols[v.sel.Table]
if !ok {
return fmt.Errorf("no primary key defined for %s", v.sel.Table)
t, err := v.schema.GetTable(v.sel.Table)
if err != nil {
return err
}
if len(t.PrimaryCol) == 0 {
return fmt.Errorf("no primary key column defined for %s", v.sel.Table)
}
fmt.Fprintf(w, `(("%s") = ('%s'))`, col.Name, v.sel.Where.Val)
fmt.Fprintf(w, `(("%s") = ('%s'))`, t.PrimaryCol, v.sel.Where.Val)
return nil
}
if v.sel.Where.Op == qcode.OpTsQuery {
t, err := v.schema.GetTable(v.sel.Table)
if err != nil {
return err
}
if len(t.TSVCol) == 0 {
return fmt.Errorf("no tsv column defined for %s", v.sel.Table)
}
fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, t.TSVCol, v.sel.Where.Val)
return nil
}

View File

@ -337,6 +337,26 @@ func fetchByID(t *testing.T) {
}
}
func searchQuery(t *testing.T) {
gql := `query {
products(search: "Amazing") {
id
name
}
}`
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "products" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("tsv") @@ to_tsquery('Amazing'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "products_0") AS "done_1337";`
resSQL, err := compileGQLToPSQL(gql)
if err != nil {
t.Fatal(err)
}
if resSQL != sql {
t.Fatal(errNotExpected)
}
}
func aggFunction(t *testing.T) {
gql := `query {
products {
@ -383,6 +403,7 @@ func TestCompileGQL(t *testing.T) {
t.Run("withWhereIsNull", withWhereIsNull)
t.Run("withWhereMultiOr", withWhereMultiOr)
t.Run("fetchByID", fetchByID)
t.Run("searchQuery", searchQuery)
t.Run("belongsTo", belongsTo)
t.Run("oneToMany", oneToMany)
t.Run("manyToMany", manyToMany)

View File

@ -18,11 +18,16 @@ type TTKey struct {
type DBSchema struct {
ColMap map[TCKey]*DBColumn
ColIDMap map[int]*DBColumn
PCols map[string]*DBColumn
Tables map[string]*DBTableInfo
RelMap map[TTKey]*DBRel
}
type DBTableInfo struct {
PrimaryCol string
TSVCol string
}
type RelType int
const (
@ -63,7 +68,7 @@ func initSchema() *DBSchema {
return &DBSchema{
ColMap: make(map[TCKey]*DBColumn),
ColIDMap: make(map[int]*DBColumn),
PCols: make(map[string]*DBColumn),
Tables: make(map[string]*DBTableInfo),
RelMap: make(map[TTKey]*DBRel),
}
}
@ -71,6 +76,7 @@ func initSchema() *DBSchema {
func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
// Current table
ct := strings.ToLower(t.Name)
schema.Tables[ct] = &DBTableInfo{}
// Foreign key columns in current table
var jcols []*DBColumn
@ -82,8 +88,11 @@ func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) {
for _, c := range cols {
switch {
case c.Type == "tsvector":
schema.Tables[ct].TSVCol = c.Name
case c.PrimaryKey:
schema.PCols[ct] = c
schema.Tables[ct].PrimaryCol = c.Name
case len(c.FKeyTable) != 0:
if len(c.FKeyColID) == 0 {
@ -244,3 +253,11 @@ WHERE c.relkind = 'r'::char
return t, nil
}
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
t, ok := s.Tables[table]
if !ok {
return nil, fmt.Errorf("table info not found '%s'", table)
}
return t, nil
}

View File

@ -85,6 +85,7 @@ const (
OpHasKeyAll
OpIsNull
OpEqID
OpTsQuery
)
func (t ExpOp) String() string {
@ -139,6 +140,8 @@ func (t ExpOp) String() string {
v = "op-is-null"
case OpEqID:
v = "op-eq-id"
case OpTsQuery:
v = "op-ts-query"
}
return fmt.Sprintf("<%s>", v)
}
@ -341,6 +344,10 @@ func (com *Compiler) compileArgs(sel *Select, args []*Arg) error {
if sel.ID == int16(0) {
err = com.compileArgID(sel, args[i])
}
case "search":
if sel.ID == int16(0) {
err = com.compileArgSearch(sel, args[i])
}
case "where":
err = com.compileArgWhere(sel, args[i])
case "orderby", "order_by", "order":
@ -438,6 +445,25 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
return nil
}
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error {
if sel.Where != nil && sel.Where.Op == OpTsQuery {
return nil
}
ex := &Exp{
Op: OpTsQuery,
Type: ValStr,
Val: arg.Val.Val,
}
if sel.Where != nil {
sel.Where = &Exp{Op: OpAnd, Children: []*Exp{ex, sel.Where}}
} else {
sel.Where = ex
}
return nil
}
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) error {
var err error