I have a web page where a user can search through documents in a mongoDB collection.
I get the user's input through #q = params[:search].to_s
I then run a mongoid query:
#story = Story.any_of( { :Tags => /#{#q}/i}, {:Name => /#{#q}/i}, {:Genre => {/#{#q}/i}} )
This works fine if the user looks for something like 'humor' 'romantic comedy' or 'mystery'. But if looking for 'romance fiction', nothing comes up. Basically I'd like to add 'and' 'or' functionality to my search so that it will find documents in the database that are related to all strings that a user types into the input field.
How can this be done while still maintaining the substring search capabilties I currently have?Thanks in advance for help!
UPDATE:
Per Eugene's comment below...
I tried converting to case insensitive with #q.map! { |x| x="/#{x}/i"}. It does save it properly as ["/romantic/i","/comedy/i"]. But the query Story.any_of({:Tags.in => #q}, {:Story.in => #q})finds nothing.
When I change the array to be ["Romantic","Comedy"]. Then it does.
How can I properly make it case insensitive?
Final:
Removing the quotes worked.
However there is now no way to use an .and() search to find a book that has both words in all these fields.
to create an OR statement, you can convert the string into an array of strings, and then convert the array of strings into an array of regex and then use the '$in' option. So first, pick a delimeter - perhaps commas or space or you can set up a custom like ||. Let's say you do comma seperated. When user enters:
romantic, comedy
you split that into ['romantic', 'comedy'], then convert that to [/romantic/i, /comedy/i] then do
#story = Story.any_of( { :Tags.in => [/romantic/i, /comedy/i]}....
To create an AND query, it can get a little more complicated. There is an elemMatch function you could use.
I don't think you could do {:Tags => /romantic/i, :Tags => /comedy/i }
So my best thought would be to do sequential queries, even though there would be a performance hit, but if your DB isn't that big, it shouldn't be a big issue. So if you want Romantic AND Comedy you can do
query 1: find all collections that match /romantic/i
query 2: take results of query 1, find all collections that match /comedy/i
And so on by iterating through your array of selectors.
Related
So I have a nested activerecord which contains an array of hashes. I am trying to get the country in an app I am making using a country code that is stored in one of the elements in the array.
the record is described:
user.rules.first.countries.first["country_code"]
user has_many rules,
rules contains a jsonb column called countries
countries is a jsonb array of hashes
at the moment I am iterating through all of them to find the record. e.g.
country_code_to_find = "US"
user.rules.each do |r|
r.countries.each do |c|
if c["country_code"] == "US"
# Do some stuff
end
end
end
Is there a way I can access that country with a single line using a .where() or scope or something like that? I am using rails 4, activerecord and postgres.
Without knowing more about the JSON structure, I'm not confident you can access "that country" with a single query, since a "country" is an element in an array. You can query for the Rule objects that contain the desired "country". Something like this might work
user.rules.where("
countries #> '[{\"country_code\": \"US\"}]'
")
Depending on your business logic, it might be enough to know that this user has at least one rule with country=US.
country_code_to_find = "US"
if user.rules.where("countries #> '[{\"country_code\": \"#{country_code_to_find}\"}]'").exists?
# Do some stuff
end
More on Postgres' JSONB functions.
These questions seem related, but are not Rails-specific:
Postgresql query array of objects in JSONB field.
Query for array elements inside JSON type
Using the answer from messenjah I was able to get a solution that worked. Had to find the index of the array so I could use it. To give some more information that messenjar was after here is the json:
countries: [{"code"=>"US", "name"=>"United States", "states"=>{"NY" => "New York"}}, {"code"=>"MX", "name"=>"Mexico", "states"=>{"YUC" => "Yucatán"}}]
Then to get an array of the states I used:
country_code = "MX"
rule = #user.rules.where("countries #> '[{\"code\": \"#{country_code}\"}]'").first
country_index = rule.countries.index {|h| h["code"] == country_code }
states = rule.countries[country_index]["states"]
Basically this get the index of the array of hashes that I want. Not sure if this is better or worse than what I was using to begin with. But it works. Happy to consider other answers if they can clean this up.
I'm experimenting with a few concepts (actually playing and learning by building a RoR version of the 1978 database WHATSIT?).
It basically is a has_many :through structure with Subject -> Tags <- Value. I've tried to replicate a little of the command line structure by using a query text field to enter the commands. Basically things like: What's steve's phone.
Anyhow, with that interface most of the searches use ILIKE. I though about enhancing it by allowing OR conditions using some form of an array. Something like What's steve's [son,daugher]. I got it working by creating the ILIKE clause directly, but not with string replacement.
def bracket_to_ilike(arrel,name,bracket)
bracket_array = bracket.match(/\[([^\]]+)\]/)[1].split(',')
like_clause = bracket_array.map {|i| "#{name} ILiKE '#{i}' "}.join(" OR ")
arrel.where(like_clause)
end
bracket_to_ilike(tags,'tags.name','[son,daughter]') produces the like clause tags.name ILiKE 'son' OR tags.name ILiKE 'daughter'
And it get the relations, but with all the talk about using the form ("tags.name ILiKE ? OR tags.name ? ",v1,v2,vN..)., I though I'd ask if anyone has any ideas on how to do that.
Creating variables on the fly is doable from what I've searched, but not in favor. I just wondered if anyone has tried creating a method that can add a where clause that has a variable number parameters.I tried sending the where clause to the relation, but it didn't like that.
Steve
Couple of things to watch out for in your code...
What will happen when one of the elements of bracket_array contains a single quote?
What will happen if I take it step farther and set an element to say "'; drop tables..."?
My first stab at refactoring your code would be to see if Arel can do it. Or Sequeel, or whatever they call the "metawhere" gem these days. My second stab would be something like this:
arrel.where( [ bracket_array.size.times.map{"#{name} ILIKE ?"}.join(' OR '), *bracket_array ])
I didn't test it, but the idea is to use the size of bracket_array to generate a string of OR'd conditions, then use the splat operator to pass in all the values.
Thanks to Phillip for pointing me in the right direction.
I didn't know you could pass an array to a where clause - that opened up some options
I had used the splat operator a few times, but it didn't hit me that it actually creates an object(variable)
The [son,daughter] stuff was just a console exercise to see what I could do, but not sure what I was going to do with it. I ended up taking the model association and creating the array out of the picture and implemented OR searches.
def array_to_ilike(col_name,keys)
ilike = [keys.map {|i| "#{col_name} ILiKE ? "}.join(" OR "), *keys ]
#ilike = [keys.size.times.map{"#{col_name} ILIKE ?"}.join(' OR '), *keys ]
#both work, guess its just what you are use to.
end
I then allowed a pipe(|) character in my subject,tag,values searches, so a WHATSIT style question
What's Steve's Phone Home|Work => displays home and work phone
steve phone home|work The 's stuff is just for show
steve son|daughter => displays children
phone james%|lori% => displays phone number for anyone who's name starts with james or lori
james%|lori% => dumps all information on anyone who's name starts with james or lori
The query then parses the command and if it encounters a | in any of the words, it will do things like:
t_ilike = array_to_ilike('tags.name',name.split("|"))
# or I actually stored it off on the inital parse
t_ilike = #tuple[:tag][:ilike] ||= ['tags.name ilike ?',tag]
Again this is just a learning exercise in creating a non-CRUD class to deal with the parsing and searching.
Steve
No matter what language I'm using I always need to display a list of strings separated by some delimiter.
Let's say, I have a collection of products and need to display its names separated by ', '.
So I have a collection of Products, where each one has a 'name' attribute. I'm looking for some Rails method/helper (if it doesn't exist, maybe you can give me ideas to build it in a rails way) that will receive a collection, an attribute/method that will be called on each collection item and a string for the separator.
But I want something that does not include the separator at the end, because I will end with "Notebook, Computer, Keyboard, Mouse, " that 2 last characters should not be there.
Ex:
concat_ws(#products, :title, ", ")
#displays: Notebook, Computer, Keyboard, Mouse
Supposing #products has 4 products with that names of course.
Thanks!
you should try the helper to_sentence.
If you have an array, you can do something like
array.to_sentence. If your array has the data banana, apple, chocolate it will become:
banana, apple and chocolate.
So now if you have your AR Model with a field named, you could do something like
MyModel.all.map { |r| r.name }.to_sentence
#products.map(&:title).join(', ')
As #VP mentioned, Array#to_sentence does this job well in rails. The code for it is here:
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/conversions.rb
Saying that, its use of the Oxford Comma is questionable :-)
I am using Sphinx with the Thinking Sphinx plugin to search my data. I am using MySQL.
My data contains accented chars ("á", "é", "ã") and I want them to be equivalent to their non-accented counterparts ("a", "e", "a", for example) when searching and ordering.
I got the search working using a charset table (pastie.org/204316), and a search for "AGUA" returns "ÁGUA", but the ordering of the results is not working properly. In a search for "AGUA", "ÁGUA" cames after "MUITA ÁGUA", for example, but I wanted it to be sorted as if it were written with an "A", not an "Á".
The only solution I can think is index a new column containing the non-accented chars and using it for sortering, using the REPLACE (http://dev.mysql.com/doc/refman/5.4/en/string-functions.html#function_replace) mysql function to strip the accented chars, but I would need one call to REPLACE for each possible accented char (and there are many) and it seems to me a not very maintanable workaround.
Anybody know some better way to handle this issue?
Thanks!
Sphinx handles sorting on string fields by storing all the values in a list, sorting the list and then storing the index of each string as an int attribute. According to the docs the sorting of this list is done at a byte level and currently isn't configurable.
Ideally the strings should be sorted differently, depending on the encoding and locale. For instance, if the strings are known to be Russian text in KOI8R encoding, sorting the bytes 0xE0, 0xE1, and 0xE2 should produce 0xE1, 0xE2 and 0xE0, because in KOI8R value 0xE0 encodes a character that is (noticeably) after characters encoded by 0xE1 and 0xE2. Unfortunately, Sphinx does not support that at the moment and will simply sort the strings bytewise.
-- from http://www.sphinxsearch.com/docs/current.html
So, no easy way to achieve this within Sphinx. A modification to your REPLACE() based idea would be to have a separate column and populate it using a callback in your model. This would let you handle the replace in Ruby instead of MySQL, an arguably more maintainable solution.
# save an unaccented copy of your title. Normalise method borrowed from
# http://stackoverflow.com/questions/522715/removing-accents-diacritics-from-string-while-preserving-other-special-chars-tri
class MyModel < ActiveRecord::Base
before_validation :update_sort_col
private
def update_sort_col
sort_col = self.title.to_s.mb_chars.normalize(:kd).gsub(/[^-x00-\x7F]/n, '').to_s
end
end
you can also use a special index for that you dont even need a new column on your db
indexes "LOWER(title)", :as => :title, :sortable => true
its raw sql so you can call your replace method.
Just build index on lower case version with following syntax. Its very simple and elegant solution for case insensitive search using Sphinx.
indexes title, as: :title, sortable: :insensitive
So I have two separate queries:
tagged_items = Item.tagged_with(params[:s], :on => :tags)
searched_items = Item.find(:all, :conditions => ["MATCH(title) AGAINST (? IN BOOLEAN MODE)", "*#{params[:s]}*"])
The first tagged_items is using the acts_as_taggable_on plugin to find all the items tagged with XYZ.
The second, searched_items, is used to search the items table for the search term.
So, how could I combine (and avoid duplicates) the results of these two?
Check out named_scope. The second query can be converted to named_scope easily, I'm not sure about the first one, but if you can rewrite it using find, you're home.
http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html
items = (tagged_items + searched_items).unique
But it would be much better if you could fetch them with single query.
This approach...
#items = tagged_items | searched_items
...would make more sense if you're looking to use the results of these queries in a View, instead of working with an Array, and accomplishes the de-duplication as well.