Initial commit
This commit is contained in:
115
spec/lib/pg_searchable_spec.rb
Normal file
115
spec/lib/pg_searchable_spec.rb
Normal file
@@ -0,0 +1,115 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe PgSearchable do
|
||||
include_examples 'pg_search', VectorModel
|
||||
include_examples 'pg_search', VectorWithCustomPrimaryKeyModel
|
||||
include_examples 'pg_search', VectorWithCustomCallback
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
include_examples 'pg_search', VectorWithoutWildcardModel
|
||||
include_examples 'pg_search', VectorModelWithCustomSearchScope, 'fulltext'
|
||||
include_examples 'pg_search', VectorModelWithTagValues
|
||||
include_examples 'pg_search', DynamicModel
|
||||
include_examples 'pg_search', DynamicModelWithTagValues
|
||||
include_examples 'pg_search', DynamicModelWithCategory
|
||||
include_examples 'pg_search', DynamicModelWithSectionsTrhough
|
||||
|
||||
describe 'pg_search' do
|
||||
describe 'joins' do
|
||||
it 'can dynamically query compound relation' do
|
||||
record = DynamicModelWithCategory.create(name: 'something', value: 'amazing')
|
||||
category = Category.create(name: 'searchable')
|
||||
Tag.create(value: 'impressive', category: category, taggable: record)
|
||||
expect(DynamicModelWithCategory.scope_search('searchable')).to include(record)
|
||||
end
|
||||
|
||||
it 'can use has_many :through relation' do
|
||||
record = DynamicModelWithSectionsTrhough.create(name: 'something', value: 'amazing')
|
||||
tag = Tag.create(value: 'impressive', taggable: record)
|
||||
Section.create(name: 'searchable', tag: tag)
|
||||
expect(DynamicModelWithSectionsTrhough.scope_search('searchable')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'properties' do
|
||||
describe 'skip_callback' do
|
||||
context 'when enabled' do
|
||||
let(:record) { VectorModel.create(name: 'something', value: 'amazing') }
|
||||
|
||||
it 'can find the record after it updates' do
|
||||
record.update(name: 'cookie')
|
||||
expect(VectorModel.scope_search('cookie')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
let(:record) { VectorModelWithoutCallback.create(name: 'something', value: 'amazing') }
|
||||
|
||||
it 'cannot find the record after it updates' do
|
||||
record.update(name: 'cookie')
|
||||
expect(VectorModelWithoutCallback.scope_search('cookie')).not_to include(record)
|
||||
end
|
||||
|
||||
it 'can find the record after manually calling .update_pg_search_cache' do
|
||||
record.update(name: 'cookie')
|
||||
record.update_pg_search_cache
|
||||
expect(VectorModelWithoutCallback.scope_search('cookie')).to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scope' do
|
||||
it 'defaults to "scope_search"' do
|
||||
expect(VectorModel).to respond_to(:scope_search)
|
||||
end
|
||||
|
||||
it 'can use a different scope name' do
|
||||
expect(VectorModelWithCustomSearchScope).to respond_to(:fulltext)
|
||||
end
|
||||
|
||||
it 'doesnt pollutes the default method name if customized' do
|
||||
expect(VectorModelWithCustomSearchScope).not_to respond_to(:scope_search)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'language' do
|
||||
it 'defaults to english lexemes' do
|
||||
record = VectorModel.create name: 'something', value: 'amazing'
|
||||
expect(VectorModel.scope_search('amaz')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be changed to simple to avoid lexeme truncation' do
|
||||
record = SimpleVectorModel.create name: 'something', value: 'amazing'
|
||||
expect(SimpleVectorModel.scope_search('amazings')).not_to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'wildcard' do
|
||||
it 'by default uses it' do
|
||||
record = VectorModel.create name: '12345', value: 'amazing'
|
||||
expect(VectorModel.scope_search('123')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be set to false' do
|
||||
record = VectorWithoutWildcardModel.create name: '12345', value: 'amazing'
|
||||
expect(VectorWithoutWildcardModel.scope_search('123')).not_to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tags' do
|
||||
it 'allow indexing fields of other associations' do
|
||||
record = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
expect(DynamicModelWithTagValues.scope_search('red')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'external_cache_data' do
|
||||
it 'can index external data using a method' do
|
||||
record = VectorModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
expect(VectorModelWithTagValues.scope_search('red')).to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
259
spec/lib/query_lexer_spec.rb
Normal file
259
spec/lib/query_lexer_spec.rb
Normal file
@@ -0,0 +1,259 @@
|
||||
require './lexer'
|
||||
|
||||
class QueryLexerTester
|
||||
describe 'Testing the Lexer' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests bracket expression' do
|
||||
@result = @evaluator.tokenize('()')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests bracket expression with spaces' do
|
||||
@result = @evaluator.tokenize(' ( ) ')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with OR operator' do
|
||||
@result = @evaluator.tokenize('() or () OR ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_OR
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with AND operator' do
|
||||
@result = @evaluator.tokenize('() AND () and ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_AND
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_AND
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with NOT OR and NOT AND operator' do
|
||||
@result = @evaluator.tokenize('() NOT or () not AND ()')
|
||||
expect(@result.length).to eq 10
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[3][0]).to eq :OPERATOR_OR
|
||||
expect(@result[4][0]).to eq :L_BRACKET
|
||||
expect(@result[5][0]).to eq :R_BRACKET
|
||||
expect(@result[6][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[7][0]).to eq :OPERATOR_AND
|
||||
expect(@result[8][0]).to eq :L_BRACKET
|
||||
expect(@result[9][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests search term under quotes' do
|
||||
@result = @evaluator.tokenize('"123-456"')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"123-456"'
|
||||
end
|
||||
|
||||
it 'tests term without quotes' do
|
||||
@result = @evaluator.tokenize('device_id')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
end
|
||||
|
||||
it 'tests integer term without quotes' do
|
||||
@result = @evaluator.tokenize('123')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests multiple terms without quotes' do
|
||||
@result = @evaluator.tokenize('device_id tag 123-456 name123')
|
||||
|
||||
expect(@result.length).to eq 4
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'tag'
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq '123-456'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'name123'
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests simple query with two columns with name and search terms without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF tag:mta')
|
||||
|
||||
expect(@result.length).to eq 6
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'JF'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'tag'
|
||||
expect(@result[4][0]).to eq :COLON
|
||||
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[5][1]).to eq 'mta'
|
||||
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term with quotes' do
|
||||
@result = @evaluator.tokenize('name:"name with space"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"name with space"'
|
||||
end
|
||||
|
||||
it 'tests search term with quotes containing non alphanumerical characters' do
|
||||
@result = @evaluator.tokenize('"|*|/\()#-!=<>&$"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"|*|/\()#-!=<>&$"'
|
||||
end
|
||||
|
||||
it 'tests simple query in brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space")')
|
||||
|
||||
expect(@result.length).to eq 5
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests multiple query wtih brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space") or (tag:mta)')
|
||||
|
||||
expect(@result.length).to eq 11
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'tag'
|
||||
expect(@result[8][0]).to eq :COLON
|
||||
expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[9][1]).to eq 'mta'
|
||||
expect(@result[10][0]).to eq :R_BRACKET
|
||||
|
||||
end
|
||||
|
||||
it 'tests complex query' do
|
||||
@result = @evaluator.tokenize('(device-id:"with space" tag:mta no-quotes-id-123)'\
|
||||
'or "id with quotes-5" and ( ("id with q 10" or "id with q 20")'\
|
||||
'and ("id with Q 30" "id with Q 40") and not id-without-Q-50)')
|
||||
|
||||
expect(@result.length).to eq 27
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'device-id'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"with space"'
|
||||
expect(@result[4][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[4][1]).to eq 'tag'
|
||||
expect(@result[5][0]).to eq :COLON
|
||||
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[6][1]).to eq 'mta'
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'no-quotes-id-123'
|
||||
expect(@result[8][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[9][0]).to eq :OPERATOR_OR
|
||||
expect(@result[10][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[10][1]).to eq '"id with quotes-5"'
|
||||
expect(@result[11][0]).to eq :OPERATOR_AND
|
||||
|
||||
expect(@result[12][0]).to eq :L_BRACKET
|
||||
expect(@result[13][0]).to eq :L_BRACKET
|
||||
expect(@result[14][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[14][1]).to eq '"id with q 10"'
|
||||
expect(@result[15][0]).to eq :OPERATOR_OR
|
||||
expect(@result[16][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[16][1]).to eq '"id with q 20"'
|
||||
expect(@result[17][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[18][0]).to eq :OPERATOR_AND
|
||||
expect(@result[19][0]).to eq :L_BRACKET
|
||||
expect(@result[20][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[20][1]).to eq '"id with Q 30"'
|
||||
expect(@result[21][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[21][1]).to eq '"id with Q 40"'
|
||||
expect(@result[22][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[23][0]).to eq :OPERATOR_AND
|
||||
expect(@result[24][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[25][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[25][1]).to eq 'id-without-Q-50'
|
||||
expect(@result[26][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with -or-, -and- and -not- words inside quoted expression' do
|
||||
@result = @evaluator.tokenize('tag:"tag with or and not inside"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'tag'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
||||
end
|
||||
end
|
||||
end
|
||||
164
spec/lib/query_parser_spec.rb
Normal file
164
spec/lib/query_parser_spec.rb
Normal file
@@ -0,0 +1,164 @@
|
||||
require './parser'
|
||||
|
||||
class QueryParserTester
|
||||
describe 'Testing the Parser' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests query with only one search term without quotes and without column name' do
|
||||
@result = @evaluator.parse('-123')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '-123'
|
||||
end
|
||||
|
||||
it 'tests query with only one search term with quotes and without column name' do
|
||||
@result = @evaluator.parse('"OR 128"')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"OR 128"'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term without quotes' do
|
||||
@result = @evaluator.parse('tag:mta')
|
||||
|
||||
expect(@result['tag']).to eq 'mta'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term with quotes' do
|
||||
@result = @evaluator.parse('tag:"tag 120"')
|
||||
|
||||
expect(@result['tag']).to eq '"tag 120"'
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta AND tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta and tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets' do
|
||||
@result = @evaluator.parse('(123)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets and with a column name' do
|
||||
@result = @evaluator.parse('(name:JF)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result['name']).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests query with OR operator in brackets' do
|
||||
@result = @evaluator.parse('(name:JF or tag:mta)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => 'mta' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two simple brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF) and (-456)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ :DEFAULT_COLUMN => '-456' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF or tag:"tag 0") and (-456)')
|
||||
|
||||
@expected_array_part_1 = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => '"tag 0"' }
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{:OPERATOR_OR => @expected_array_part_1},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
||||
end
|
||||
|
||||
it 'tests operator precedence' do
|
||||
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
||||
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
||||
|
||||
expect(@result1).to eq @result2
|
||||
|
||||
expect(@result1.length).to eq 1
|
||||
|
||||
@expected_array_part_2 = [
|
||||
{'name' => 'JF'},
|
||||
{:DEFAULT_COLUMN => '12_4'}
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{'tag' => 'mta'},
|
||||
{:OPERATOR_AND => @expected_array_part_2}
|
||||
]
|
||||
|
||||
expect(@result1[:OPERATOR_OR]).to eq @expected_array_total
|
||||
|
||||
end
|
||||
|
||||
# Tests to write :
|
||||
# * query with multiple column names and search terms without logical operators
|
||||
# * AND NOT, OR NOT tests
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
22
spec/lib/text_to_regex_query_spec.rb
Normal file
22
spec/lib/text_to_regex_query_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToRegexQuery do
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
describe '.new' do
|
||||
# just default
|
||||
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR 1<>1', '%some-default-value%']) }
|
||||
|
||||
# default and named
|
||||
it { expect(described_class.new('name:hamo id:1', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
|
||||
|
||||
# escape characters
|
||||
it { expect(described_class.new('name:hamo id:1',[:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
|
||||
|
||||
# default and explicit with underscore
|
||||
it { expect(described_class.new('device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.device_id like ? OR 1<>1", "%-123%"]) }
|
||||
|
||||
# default and explicit with underscore with multiple columns
|
||||
it { expect(described_class.new('name:bla device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.name like ? OR players.device_id like ? OR 1<>1", "%bla%", "%-123%"]) }
|
||||
|
||||
end
|
||||
end
|
||||
80
spec/lib/text_to_tsquery_spec.rb
Normal file
80
spec/lib/text_to_tsquery_spec.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToTsquery do
|
||||
describe '.new' do
|
||||
# partial match
|
||||
it { expect(described_class.new('A').tsquery).to eq('A:*') }
|
||||
it { expect(described_class.new('A', wildcard: false).tsquery).to eq('A:') }
|
||||
it { expect(described_class.new(' A ').tsquery).to eq('A:*') }
|
||||
|
||||
# AND operations
|
||||
it { expect(described_class.new('A B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A B C').tsquery).to eq('A:*&B:*&C:*') }
|
||||
it { expect(described_class.new('A and B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A AND B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A & B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A && B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A & B && C and D AND E F').tsquery).to eq('A:*&B:*&C:*&D:*&E:*&F:*') }
|
||||
|
||||
# OR operations
|
||||
it { expect(described_class.new('A or B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A or B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A OR B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A OR B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A | B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A | B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A || B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A || B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A or or B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A or or B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A | B || C or D OR E').tsquery).to eq('A:*|B:*|C:*|D:*|E:*') }
|
||||
it { expect(described_class.new('A | B || C or D OR E', wildcard: false).tsquery).to eq('A:|B:|C:|D:|E:') }
|
||||
|
||||
# () Precedence
|
||||
it { expect(described_class.new('(A)').tsquery).to eq('(A:*)') }
|
||||
it { expect(described_class.new('(A)', wildcard: false).tsquery).to eq('(A:)') }
|
||||
it { expect(described_class.new('(A B)').tsquery).to eq('(A:*&B:*)') }
|
||||
it { expect(described_class.new('(A B)', wildcard: false).tsquery).to eq('(A:&B:)') }
|
||||
it { expect(described_class.new('A (B !C)').tsquery).to eq('A:*&(B:*&!C)') }
|
||||
it { expect(described_class.new('A (B !C)', wildcard: false).tsquery).to eq('A:&(B:&!C)') }
|
||||
it { expect(described_class.new('(A AND B) OR C').tsquery).to eq('(A:*&B:*)|C:*') }
|
||||
it { expect(described_class.new('(A AND B) OR C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
|
||||
it { expect(described_class.new('A AND (B OR C)').tsquery).to eq('A:*&(B:*|C:*)') }
|
||||
it { expect(described_class.new('A AND (B OR C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
|
||||
it { expect(described_class.new('(A & B) || C').tsquery).to eq('(A:*&B:*)|C:*') }
|
||||
it { expect(described_class.new('(A & B) || C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
|
||||
it { expect(described_class.new('A && (B | C)').tsquery).to eq('A:*&(B:*|C:*)') }
|
||||
it { expect(described_class.new('A && (B | C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
|
||||
it { expect(described_class.new('A && !D (B | C | !E)').tsquery).to eq('A:*&!D&(B:*|C:*|!E)') }
|
||||
it { expect(described_class.new('A && !D (B | C | !E)', wildcard: false).tsquery).to eq('A:&!D&(B:|C:|!E)') }
|
||||
|
||||
# Exact Matches
|
||||
it { expect(described_class.new('"A"').tsquery).to eq("'A'") }
|
||||
it { expect(described_class.new('"A B"').tsquery).to eq("'A B'") }
|
||||
it { expect(described_class.new('"A&B"').tsquery).to eq("'A&B'") }
|
||||
it { expect(described_class.new('"-A|B"').tsquery).to eq("'-A|B'") }
|
||||
it { expect(described_class.new('"A-B"').tsquery).to eq("'A-B'") }
|
||||
it { expect(described_class.new('"A" B').tsquery).to eq("'A'&B:*") }
|
||||
it { expect(described_class.new('"A" B', wildcard: false).tsquery).to eq("'A'&B:") }
|
||||
it { expect(described_class.new('"A B" C').tsquery).to eq("'A B'&C:*") }
|
||||
it { expect(described_class.new('"A B" C', wildcard: false).tsquery).to eq("'A B'&C:") }
|
||||
it { expect(described_class.new('("A B" or C) and D').tsquery).to eq("('A B'|C:*)&D:*") }
|
||||
it { expect(described_class.new('("A B" or C) and D', wildcard: false).tsquery).to eq("('A B'|C:)&D:") }
|
||||
|
||||
describe 'validations' do
|
||||
it { expect { described_class.new('(') }.to raise_error(ArgumentError, /parenthesis/) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.valid_search_parenthesis?' do
|
||||
it { expect(described_class.valid_search_parenthesis?('')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('()')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('()()')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('(()())')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('((())())')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('(')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?(')(')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?('())')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?('((()())')).to eq false }
|
||||
end
|
||||
end
|
||||
45
spec/schema.rb
Normal file
45
spec/schema.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ActiveRecord::Schema.define do
|
||||
create_table :vector_models, force: true do |t|
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.tsvector :search_cache
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_index :vector_models, :search_cache, using: :gin
|
||||
|
||||
create_table :vector_with_custom_primary_key_models, id: false, force: true do |t|
|
||||
t.uuid :uuid, null: false
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.tsvector :search_vector
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_index :vector_with_custom_primary_key_models, :uuid, using: :btree
|
||||
add_index :vector_with_custom_primary_key_models, :search_vector, using: :gin
|
||||
|
||||
create_table :dynamic_models, force: true do |t|
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :tags, force: true do |t|
|
||||
t.string :value
|
||||
t.references :category, index: true
|
||||
t.references :taggable, polymorphic: true, index: true
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :categories, force: true do |t|
|
||||
t.string :name
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :sections, force: true do |t|
|
||||
t.references :tag
|
||||
t.string :name
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
52
spec/spec_helper.rb
Normal file
52
spec/spec_helper.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler'
|
||||
Bundler.require
|
||||
|
||||
require 'simplecov'
|
||||
SimpleCov.start do
|
||||
add_filter '/spec/'
|
||||
end
|
||||
|
||||
require 'active_record'
|
||||
require 'database_cleaner'
|
||||
require 'dotenv/load'
|
||||
require 'pg_searchable_regex'
|
||||
require 'pry'
|
||||
|
||||
# requiring support files
|
||||
Dir[__dir__ + '/support/**/*.rb'].each { |f| require f }
|
||||
|
||||
RSpec.configure do |config|
|
||||
env = Dotenv.parse('.env.test', '.env.test.local')
|
||||
db_config = {
|
||||
adapter: 'postgresql',
|
||||
host: ENV['DATABASE_HOST'] || env['DATABASE_HOST'],
|
||||
username: ENV['DATABASE_USERNAME'] || env['DATABASE_USERNAME'],
|
||||
password: ENV['DATABASE_PASSWORD'] || env['DATABASE_PASSWORD'],
|
||||
database: ENV['DATABASE_NAME'] || env['DATABASE_NAME'],
|
||||
pool: ENV['DATABASE_POOL'] || env['DATABASE_POOL'],
|
||||
port: ENV['DATABASE_PORT'] || env['DATABASE_PORT']
|
||||
}
|
||||
db_config_admin = db_config.merge(database: 'postgres', schema_search_path: 'public')
|
||||
|
||||
config.before(:suite) do
|
||||
ActiveRecord::Base.establish_connection(db_config_admin)
|
||||
ActiveRecord::Base.connection.create_database(db_config[:database])
|
||||
ActiveRecord::Base.establish_connection(db_config)
|
||||
ActiveRecord::Tasks::DatabaseTasks.load_schema_for(db_config, :ruby, "#{__dir__}/schema.rb")
|
||||
DatabaseCleaner.strategy = :transaction
|
||||
DatabaseCleaner.clean_with(:truncation)
|
||||
end
|
||||
|
||||
config.around do |example|
|
||||
DatabaseCleaner.cleaning do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
config.after(:suite) do
|
||||
ActiveRecord::Base.establish_connection(db_config_admin)
|
||||
ActiveRecord::Base.connection.drop_database(db_config[:database])
|
||||
end
|
||||
end
|
||||
103
spec/support/models.rb
Executable file
103
spec/support/models.rb
Executable file
@@ -0,0 +1,103 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VectorModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache
|
||||
end
|
||||
|
||||
class VectorModelWithoutCallback < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, skip_callback: true
|
||||
end
|
||||
|
||||
class VectorWithCustomCallback < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, skip_callback: true, external_cache_data: :colors
|
||||
after_save :update_pg_search_cache
|
||||
|
||||
def colors
|
||||
%w[orange blue]
|
||||
end
|
||||
end
|
||||
|
||||
class VectorWithCustomPrimaryKeyModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[uuid name value]
|
||||
self.primary_key = :uuid
|
||||
before_save { self.uuid = SecureRandom.uuid }
|
||||
end
|
||||
|
||||
class SimpleVectorModel < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, language: :simple
|
||||
end
|
||||
|
||||
class VectorWithoutWildcardModel < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, wildcard: false
|
||||
end
|
||||
|
||||
class VectorModelWithCustomSearchScope < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, scope: 'fulltext'
|
||||
end
|
||||
|
||||
class VectorModelWithTagValues < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, external_cache_data: :tag_values
|
||||
has_many :tags, as: :taggable
|
||||
|
||||
def tag_values
|
||||
tags.pluck(:value)
|
||||
end
|
||||
end
|
||||
|
||||
class DynamicModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value]
|
||||
end
|
||||
|
||||
class DynamicModelWithTagValues < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value], joins: [:tags]
|
||||
has_many :tags, as: :taggable
|
||||
end
|
||||
|
||||
class DynamicModelWithCategory < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value categories.name],
|
||||
joins: { tags: :category }
|
||||
has_many :tags, as: :taggable
|
||||
end
|
||||
|
||||
class DynamicModelWithSectionsTrhough < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value sections.name],
|
||||
joins: [{ tags: :category }, :sections]
|
||||
has_many :tags, as: :taggable
|
||||
has_many :sections, through: :tags
|
||||
end
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
belongs_to :category
|
||||
has_many :sections
|
||||
belongs_to :taggable, polymorphic: true
|
||||
after_save { taggable.update_pg_search_cache if taggable.class.ts_cache_field.present? }
|
||||
end
|
||||
|
||||
class Section < ActiveRecord::Base
|
||||
belongs_to :tag
|
||||
end
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
has_many :tags
|
||||
end
|
||||
68
spec/support/ts_search_scope_shared_examples.rb
Normal file
68
spec/support/ts_search_scope_shared_examples.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
shared_examples 'pg_search' do |mock_class, scope_method = 'scope_search'|
|
||||
describe 'searchable fields' do
|
||||
let!(:record) { mock_class.create name: 'something', value: 'amazing' }
|
||||
|
||||
it { expect(mock_class.send(scope_method, record.id)).to include(record) }
|
||||
it { expect(mock_class.send(scope_method, 'something')).to include(record) }
|
||||
it { expect(mock_class.send(scope_method, 'amazing')).to include(record) }
|
||||
it { expect(mock_class.send(scope_method, 'candy')).not_to include(record) }
|
||||
end
|
||||
|
||||
describe 'operators' do
|
||||
before do
|
||||
mock_class.create(name: 'uno', value: 'one')
|
||||
mock_class.create(name: 'dos', value: 'two')
|
||||
mock_class.create(name: 'tres', value: 'three')
|
||||
mock_class.create(name: 'cuatro', value: 'four')
|
||||
mock_class.create(name: 'cuatro', value: 'five')
|
||||
mock_class.create(name: 'cinco', value: 'one-two-three')
|
||||
end
|
||||
|
||||
it 'returns all records if search query is blank' do
|
||||
expect(mock_class.send(scope_method, ' ').first).not_to be_blank
|
||||
end
|
||||
|
||||
describe 'AND searches' do
|
||||
it { expect(mock_class.send(scope_method, 'uno one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno&one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno&&one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno & one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno and one').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'dos AND two').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, 'uno dos').count).to eq(0) }
|
||||
end
|
||||
|
||||
describe 'OR searches' do
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or or dos').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'one OR two').count).to eq(3) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or two').count).to eq(3) }
|
||||
end
|
||||
|
||||
describe 'AND and OR searches' do
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos two').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, 'uno or dos one').count).to eq(1) }
|
||||
end
|
||||
|
||||
describe 'NOT searches' do
|
||||
it { expect(mock_class.send(scope_method, '!cuatro').count).to eq(4) }
|
||||
it { expect(mock_class.send(scope_method, 'cuatro !four').count).to eq(1) }
|
||||
end
|
||||
|
||||
describe 'exact match' do
|
||||
it { expect(mock_class.send(scope_method, '"cinco"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one-two-three"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one two"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one&two"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one|two"').count).to eq(1) }
|
||||
it { expect(mock_class.send(scope_method, '"one-two-three" or two').count).to eq(2) }
|
||||
it { expect(mock_class.send(scope_method, '"one-two"').count).to eq(0) }
|
||||
it { expect(mock_class.send(scope_method, '"cinco one"').count).to eq(1) }
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user