Skip to content

Commit

Permalink
Merge pull request #127 from pacop/multiple-searches
Browse files Browse the repository at this point in the history
Multiple searches
  • Loading branch information
rtrv authored May 15, 2018
2 parents 993dbfe + fc0839d commit 63b0f83
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
AllCops:
Exclude:
- vendor/**/*
-Metrics/BlockLength:
Enabled: false

inherit_from: .rubocop_todo.yml
4 changes: 2 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2017-12-22 15:01:37 -0500 using RuboCop version 0.52.0.
# on 2017-12-27 17:26:53 +0100 using RuboCop version 0.52.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -95,7 +95,7 @@ Style/RegexpLiteral:
Exclude:
- 'lib/mongoid_search.rb'

# Offense count: 55
# Offense count: 58
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

* [#129](https://github.com/mongoid/mongoid_search/pull/128): Add CHANGELOG and Danger - [@rtrv](https://github.com/rtrv).
* [#128](https://github.com/mongoid/mongoid_search/pull/128): Escape regexp special characters - [@chiibis](https://github.com/chiibis).
* [#127](https://github.com/mongoid/mongoid_search/pull/127): Multiple searches - [@pacop](https://github.com/pacop).
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ source 'http://rubygems.org'

gemspec

case version = ENV['MONGOID_VERSION'] || '6.0'
case version = ENV['MONGOID_VERSION'] || '7.0'
when 'HEAD'
gem 'mongoid', github: 'mongodb/mongoid'
when /^7/
gem 'mongoid', github: 'mongodb/mongoid', branch: '7.0-dev'
gem 'mongoid', '~> 7.0'
when /^6/
gem 'mongoid', '~> 6.0'
when /^5/
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ class Product
include Mongoid::Search
field :brand
field :name
field :unit
field :info, type: Hash

has_many :tags
belongs_to :category

search_in :brand, :name, tags: :name, category: :name, info: %i[summary description]
search_in :unit, index: :_unit_keywords
end

class Tag
Expand All @@ -52,14 +54,16 @@ end
Now when you save a product, you get a `_keywords` field automatically:

```ruby
p = Product.new brand: 'Apple', name: 'iPhone', info: { summary: 'Info-summary', description: 'Info-description' }
p = Product.new brand: 'Apple', name: 'iPhone', unit: 'kilogram', info: { summary: 'Info-summary', description: 'Info-description' }
p.tags << Tag.new(name: 'Amazing')
p.tags << Tag.new(name: 'Awesome')
p.tags << Tag.new(name: 'Superb')
p.save
# => true
p._keywords
# => ["amazing", "apple", "awesome", "iphone", "superb", "Info-summary", "Info-description"]
p._unit_keywords
# => ["kilogram"]
```

Now you can run search, which will look in the `_keywords` field and return all matching results:
Expand All @@ -69,6 +73,13 @@ Product.full_text_search("apple iphone").size
# => 1
```

Of course, some models could have more than one index. For instance, two different searches with different fields, so you could even specify from which index should be searched:

```ruby
Product.full_text_search("kilogram", index: :_unit_keywords).size
# => 1
```

Note that the search is case insensitive, and accept partial searching too:

```ruby
Expand Down Expand Up @@ -131,6 +142,20 @@ Product.full_text_search('amazing apple', relevant_search: true)

Please note that relevant_search will return an Array and not a Criteria object. The search method should always be called in the end of the method chain.

### index

Default is `_keywords`.

```ruby
Product.full_text_search('amazing apple', index: :_keywords)
# => [#<Product _id: 5016e7d16af54efe1c000001, _type: nil, brand: "Apple", name: "iPhone", unit: "l", attrs: nil, info: nil, category_id: nil, _keywords: ["amazing", "apple", "awesome", "iphone", "superb"], _unit_keywords: ["l"], relevance: 2.0>]

Product.full_text_search('kg', index: :_unit_keywords)
# => [#<Product _id: 5016e7d16af54efe1c000001, _type: nil, brand: "Apple", name: "iPhone", unit: "kg", attrs: nil, info: nil, category_id: nil, _keywords: ["amazing", "apple", "awesome", "iphone", "superb"], _unit_keywords: ["kg"], relevance: 2.0>]
```

index enables to have two or more different searches, with different or same fields. It should be noted that indexes are exclusive per each one.

## Initializer

Alternatively, you can create an initializer to setup those options:
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid_search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module Mongoid::Search
# Ligatures to be replaced
# http://en.wikipedia.org/wiki/Typographic_ligature
mattr_accessor :ligatures
@@ligatures = { 'œ' => 'oe', 'æ' => 'ae' }
@@ligatures = { 'œ' => 'oe', 'æ' => 'ae', 'ꜵ' => 'ao' }

# Minimum word size. Words smaller than it won't be indexed
mattr_accessor :minimum_word_size
Expand Down
39 changes: 28 additions & 11 deletions lib/mongoid_search/mongoid_search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ def self.classes
module ClassMethods #:nodoc:
# Set a field or a number of fields as sources for search
def search_in(*args)
args, _options = args_and_options(args)
self.search_fields = (search_fields || []).concat args
args, options = args_and_options(args)
set_search_fields(options[:index], args)

field :_keywords, type: Array
field options[:index], type: Array

index({ _keywords: 1 }, background: true)
index({ options[:index] => 1 }, background: true)

before_save :set_keywords
end
Expand Down Expand Up @@ -49,13 +49,20 @@ def index_keywords!

private

def set_search_fields(index, fields)
self.search_fields ||= {}

(self.search_fields[index] ||= []).concat fields
end

def query(keywords, options)
keywords_hash = keywords.map do |kw|
if Mongoid::Search.regex_search
escaped_kw = Regexp.escape(kw)
kw = Mongoid::Search.regex.call(escaped_kw)
end
{ _keywords: kw }

{ options[:index] => kw }
end

criteria.send("#{(options[:match])}_of", *keywords_hash)
Expand All @@ -65,6 +72,7 @@ def args_and_options(args)
options = args.last.is_a?(Hash) &&
%i[match
allow_empty_search
index
relevant_search].include?(args.last.keys.first) ? args.pop : {}

[args, extract_options(options)]
Expand All @@ -74,7 +82,8 @@ def extract_options(options)
{
match: options[:match] || Mongoid::Search.match,
allow_empty_search: options[:allow_empty_search] || Mongoid::Search.allow_empty_search,
relevant_search: options[:relevant_search] || Mongoid::Search.relevant_search
relevant_search: options[:relevant_search] || Mongoid::Search.relevant_search,
index: options[:index] || :_keywords
}
end

Expand All @@ -99,8 +108,8 @@ def results_with_relevance(query, options)
function() {
var entries = 0;
for(i in keywords) {
for(j in this._keywords) {
if(this._keywords[j] == keywords[i]) {
for(j in this.#{options[:index]}) {
if(this.#{options[:index]}[j] == keywords[i]) {
entries++;
}
}
Expand All @@ -122,11 +131,19 @@ def results_with_relevance(query, options)
end

def index_keywords!
update_attribute(:_keywords, set_keywords)
search_fields.map do |index, fields|
update_attribute(index, get_keywords(fields))
end
end

def set_keywords
self._keywords = Mongoid::Search::Util.keywords(self, search_fields)
.flatten.reject { |k| k.nil? || k.empty? }.uniq.sort
search_fields.each do |index, fields|
send("#{index}=", get_keywords(fields))
end
end

def get_keywords(fields)
Mongoid::Search::Util.keywords(self, fields)
.flatten.reject { |k| k.nil? || k.empty? }.uniq.sort
end
end
2 changes: 1 addition & 1 deletion mongoid_search.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)

Gem::Specification.new do |s|
s.name = 'mongoid_search'
s.version = '0.3.5'
s.version = '0.3.6'
s.authors = ['Mauricio Zaffari']
s.email = ['[email protected]']
s.homepage = 'http://www.papodenerd.net/mongoid-search-full-text-search-for-your-mongoid-models/'
Expand Down
3 changes: 3 additions & 0 deletions spec/models/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ class Product

field :brand
field :name
field :unit
field :measures, type: Array
field :attrs, type: Array
field :info, type: Hash

Expand All @@ -18,4 +20,5 @@ class Product

search_in :brand, :name, :outlet, :attrs, tags: :name, category: %i[name description],
subproducts: %i[brand name], info: %i[summary description]
search_in :unit, :measures, index: :_unit_keywords
end
2 changes: 2 additions & 0 deletions spec/models/variant.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
autoload :Product, 'models/product.rb'
class Variant < Product
field :color
field :size
search_in :color
search_in :size, index: :_unit_keywords
end
26 changes: 21 additions & 5 deletions spec/mongoid_search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Mongoid::Search.stem_proc = @default_proc
@product = Product.create brand: 'Apple',
name: 'iPhone',
unit: 'mobile olé awesome',
tags: (@tags = %w[Amazing Awesome Olé].map { |tag| Tag.new(name: tag) }),
category: Category.new(name: 'Mobile', description: 'Reviews'),
subproducts: [Subproduct.new(brand: 'Apple', name: 'Craddle')],
Expand Down Expand Up @@ -52,17 +53,20 @@
Mongoid::Search.ignore_list = nil
@product = Product.create brand: 'Эльбрус',
name: 'Процессор',
unit: 'kílográm Olé',
tags: %w[Amazing Awesome Olé].map { |tag| Tag.new(name: tag) },
category: Category.new(name: 'процессоры'),
subproducts: []
end

it 'should leave utf8 characters' do
expect(@product._keywords).to eq %w[amazing awesome ole процессор процессоры эльбрус]
expect(@product._unit_keywords).to eq %w[kilogram ole]
end

it "should return results in search when case doesn't match" do
expect(Product.full_text_search('ЭЛЬБРУС').size).to eq 1
expect(Product.full_text_search('KILOGRAM', index: :_unit_keywords).size).to eq 1
end
end

Expand All @@ -74,15 +78,18 @@
end

it 'should validate keywords' do
product = Product.create brand: 'Apple', name: 'iPhone'
product = Product.create brand: 'Apple', name: 'iPhone', unit: 'box'
expect(product._keywords).to eq(%w[apple iphone])
expect(product._unit_keywords).to eq(%w[box])
end
end

it 'should set the _keywords field for array fields also' do
@product.attrs = ['lightweight', 'plastic', :red]
@product.measures = ['box', 'bunch', :bag]
@product.save!
expect(@product._keywords).to include 'lightweight', 'plastic', 'red'
expect(@product._unit_keywords).to include 'box', 'bunch', 'bag'
end

it 'should inherit _keywords field and build upon' do
Expand All @@ -91,39 +98,48 @@
tags: %w[Amazing Awesome Olé].map { |tag| Tag.new(name: tag) },
category: Category.new(name: 'Mobile'),
subproducts: [Subproduct.new(brand: 'Apple', name: 'Craddle')],
color: :white
color: :white,
size: :big
expect(variant._keywords).to include 'white'
expect(variant._unit_keywords).to include 'big'
expect(Variant.full_text_search(name: 'Apple', color: :white)).to eq [variant]
expect(Variant.full_text_search({ size: 'big' }, index: :_unit_keywords)).to eq [variant]
end

it 'should expand the ligature to ease searching' do
# ref: http://en.wikipedia.org/wiki/Typographic_ligature, only for french right now. Rules for other languages are not know
variant1 = Variant.create tags: ['œuvre'].map { |tag| Tag.new(name: tag) }
variant2 = Variant.create tags: ['æquo'].map { |tag| Tag.new(name: tag) }
variant3 = Variant.create measures: ['ꜵquo'].map { |measure| measure }

expect(Variant.full_text_search('œuvre')).to eq [variant1]
expect(Variant.full_text_search('oeuvre')).to eq [variant1]
expect(Variant.full_text_search('æquo')).to eq [variant2]
expect(Variant.full_text_search('aequo')).to eq [variant2]
expect(Variant.full_text_search('aoquo', index: :_unit_keywords)).to eq [variant3]
expect(Variant.full_text_search('ꜵquo', index: :_unit_keywords)).to eq [variant3]
end

it 'should set the _keywords field with stemmed words if stem is enabled' do
it 'should set the keywords fields with stemmed words if stem is enabled' do
Mongoid::Search.stem_keywords = true
@product.save!
expect(@product._keywords.sort).to eq %w[amaz appl awesom craddl iphon mobil review ol info descript summari].sort
expect(@product._unit_keywords.sort).to eq %w[mobil awesom ol].sort
end

it 'should set the _keywords field with custom stemmed words if stem is enabled with a custom lambda' do
it 'should set the keywords fields with custom stemmed words if stem is enabled with a custom lambda' do
Mongoid::Search.stem_keywords = true
Mongoid::Search.stem_proc = proc { |word| word.upcase }
@product.save!
expect(@product._keywords.sort).to eq %w[AMAZING APPLE AWESOME CRADDLE DESCRIPTION INFO IPHONE MOBILE OLE REVIEWS SUMMARY]
expect(@product._unit_keywords.sort).to eq %w[AWESOME MOBILE OLE]
end

it 'should ignore keywords in an ignore list' do
Mongoid::Search.ignore_list = YAML.safe_load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))['ignorelist']
@product.save!
expect(@product._keywords.sort).to eq %w[apple craddle iphone mobile reviews ole info description summary].sort
expect(@product._unit_keywords.sort).to eq %w[mobile ole].sort
end

it 'should incorporate numbers as keywords' do
Expand Down Expand Up @@ -205,7 +221,7 @@
end

it 'should have a method to index keywords' do
expect(@product.index_keywords!).to eq true
expect(@product.index_keywords!).to include(true)
end

it 'should have a class method to index all documents keywords' do
Expand Down

0 comments on commit 63b0f83

Please sign in to comment.